feat: update to version 2.1.0 with central site collection configuration and inheritance support

This commit is contained in:
Torsten Brendgen
2026-07-18 20:42:00 +02:00
parent 0d6da3451d
commit 0b8b752837
17 changed files with 348 additions and 43 deletions

View File

@@ -10,7 +10,7 @@ SPFx-1.4.1-Solution für moderne und klassische Listen und Dokumentbibliotheken
- Verschachtelte V2-Bedingungsgruppen mit den logischen Verknüpfungen `AND` und `OR`.
- Command-Bar-Befehl zum Verlängern eines oder mehrerer Elemente um ein Kalenderjahr.
- CSR/JSLink-Darstellung und Ribbon-Befehl für klassische SharePoint-Ansichten.
- Pro Liste/Bibliothek gespeicherte Konfiguration.
- Zentraler Site-Collection-Standard mit optionalen Subweb- und Listen-Ausnahmen.
Die Solution provisioniert ausdrücklich keine fachliche Ablaufdatumsspalte und keinen Content Type.
@@ -309,6 +309,22 @@ Konfigurationsmechanismus wie andere SPFx-Extensions und benötigt keine zusätz
PortalSettings kann optional dieselben Component Properties verwalten, ist aber keine Voraussetzung für den
Betrieb des ExpiryIndicators.
### Zentrale Vererbung mit PortalSettings (Version 2.1)
Version 2.1 kann einen Standard aus dem Property Bag des Root Webs auf alle registrierten Bindungen der
Site Collection vererben. Die Aufloesung erfolgt in dieser Reihenfolge:
1. lokale Konfiguration am Listenfeld,
2. optionaler Override im Property Bag des aktuellen Subwebs,
3. Site-Collection-Standard im Property Bag des Root Webs,
4. eingebauter Standard der Extension.
`Enable-ExpiryIndicator.ps1` registriert jede Bindung im Root Web und verwendet fuer neue oder bisher leere
Feld-Properties eine schlanke Vererbungsreferenz. Bestehende vollstaendige Listenkonfigurationen bleiben beim
erneuten Ausfuehren des Skripts erhalten. In PortalSettings kann man den zentralen Standard bearbeiten, einzelne
Bindungen auswaehlen oder alle registrierten Bindungen bewusst auf Vererbung umstellen. Eine versteckte Liste
wird nicht verwendet.
Bei einem Upgrade wird eine vorhandene Konfiguration aus der früheren ausgeblendeten Liste
`_ExpiryIndicatorConfiguration` weiterhin gelesen, solange noch keine Component Properties gespeichert wurden.
Beim nächsten Speichern wird sie in das gebundene Feld übernommen. Die alte Liste wird aus Sicherheitsgründen
@@ -333,7 +349,7 @@ neu laden.
### Klassische SharePoint-Ansichten
Version 2.0 legt bei der App-Installation `ExpiryIndicatorClassic.js` unter
Version 2.1 legt bei der App-Installation `ExpiryIndicatorClassic.js` unter
`SiteAssets/ExpiryIndicator` ab. Das Aktivierungsskript registriert standardmäßig für die angegebene Liste:
- einen einmaligen Web-`ScriptLink` für CSR/JSLink, der das gebundene Feld der aktuellen Liste selbst erkennt,
@@ -351,8 +367,8 @@ Soll nur die moderne Ansicht aktiviert werden, kann die Classic-Registrierung au
-SkipClassic
```
Die klassische Laufzeit verwendet dieselbe JSON-Konfiguration aus den
`ClientSideComponentProperties` des gebundenen Ablaufdatumsfeldes. Es gibt keine zweite Konfiguration.
Die klassische Laufzeit verwendet dieselbe Vererbungskette und dieselben Feld-Properties wie die moderne
Laufzeit. Es gibt keine zweite Konfiguration.
### Ablaufdatum um ein Jahr verlängern
@@ -391,14 +407,14 @@ Das Paket wird als `sharepoint/solution/expiry-indicator.sppkg` erzeugt.
Das Skript ist in einer SharePoint Management Shell beziehungsweise auf einem Rechner mit den passenden SharePoint-CSOM-Assemblies und Zugriff auf die Site auszuführen.
### Upgrade von Version 1.x auf 2.0
### Upgrade auf Version 2.1
1. Das vorhandene Paket im App Catalog durch Version `2.0.1.0` ersetzen.
1. Das vorhandene Paket im App Catalog durch Version `2.1.0.0` ersetzen.
2. Die App in der Site aktualisieren. Die enthaltene Feature-UpgradeAction provisioniert ausschließlich das
neue Classic-Asset und legt keine fachliche Spalte an.
aktualisierten Assets und legt keine fachliche Spalte an.
3. `Enable-ExpiryIndicator.ps1` für jede bereits angebundene Liste erneut ausführen. Das Skript erhält die
vorhandene Feldkonfiguration und ergänzt idempotent den gemeinsamen Web-ScriptLink sowie die
listenspezifische Ribbon-Registrierung.
listenspezifische Ribbon-Registrierung sowie das zentrale Bindungsinventar.
4. Modern- und Classic-Ansicht mit geleertem Browsercache neu laden und prüfen.
## Deaktivierung

16
ToDo.md
View File

@@ -150,6 +150,22 @@ Beispiel einer Laufzeitkonfiguration:
- [ ] Feature-Upgrade in SharePoint Server Subscription Edition testen.
- [ ] Modern-/Classic-Abnahmetest auf einer Testliste und einer Test-Dokumentbibliothek durchführen.
## Release 2.1 zentrale Site-Collection-Konfiguration
- [x] Hierarchie `Liste > Subweb > Site Collection > eingebauter Standard` implementieren.
- [x] Moderne und klassische Laufzeit auf dieselbe Vererbungskette umstellen.
- [x] Solution-Descriptor und Bindungsinventar im Property Bag des Root Webs registrieren.
- [x] Bestehende vollständige Listenkonfigurationen beim erneuten Aktivieren erhalten.
- [x] Neue beziehungsweise leere Bindungen standardmäßig auf zentrale Vererbung stellen.
- [x] PortalSettings um providerbasierte Erkennung installierter Solutions erweitern.
- [x] ExpiryIndicator-Tab nur bei erkanntem Descriptor, Standard oder Bindungsinventar anzeigen.
- [x] Site-Collection-Standard in PortalSettings laden, validieren und speichern.
- [x] Registrierte Bindungen aus Root Web und Subwebs in PortalSettings anzeigen.
- [x] Einzelne oder alle registrierten Bindungen auf Vererbung umstellen können.
- [x] Versionen anheben und ExpiryIndicator- sowie PortalSettings-Pakete mit Node.js 8.17.0 bauen.
- [ ] Upgrade und zentrale Vererbung in SharePoint Server Subscription Edition testen.
- [ ] Berechtigungen mit Site-Collection-Administrator und normalem Listenverwalter prüfen.
## Abnahmekriterien
- Die Solution enthält und provisioniert keine Definition für eine fachliche Ablaufdatumsspalte.

View File

@@ -3,7 +3,7 @@
"solution": {
"name": "expiry-indicator-client-side-solution",
"id": "68bb6d2d-9895-45b4-88e8-b98835faa981",
"version": "2.0.1.0",
"version": "2.1.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"features": [
@@ -11,7 +11,7 @@
"title": "ExpiryIndicator extension registration",
"description": "Registers ExpiryIndicator commands and its settings Application Customizer. Does not provision ExpiryDate.",
"id": "913402af-ab9a-4974-9f86-5c2159ae41db",
"version": "2.0.1.0",
"version": "2.1.0.0",
"assets": {
"elementManifests": [
"elements.xml",

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "expiry-indicator",
"version": "2.0.1",
"version": "2.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "expiry-indicator",
"version": "2.0.1",
"version": "2.1.0",
"private": true,
"engines": {
"node": ">=6.9.0 <9.0.0"

View File

@@ -24,10 +24,14 @@ $context.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
try {
$list = $context.Web.Lists.GetByTitle($ListTitle)
$field = $list.Fields.GetByInternalNameOrTitle($ExpiryFieldInternalName)
$rootWeb = $context.Site.RootWeb
$customActions = $list.UserCustomActions
$webCustomActions = $context.Web.UserCustomActions
$context.Load($list)
$context.Load($field)
$context.Load($context.Web)
$context.Load($rootWeb)
$context.Load($rootWeb.AllProperties)
$context.Load($customActions)
$context.Load($webCustomActions)
$context.ExecuteQuery()
@@ -41,6 +45,28 @@ try {
$field.ClientSideComponentProperties = ''
$field.Update()
$bindingsKey = 'PortalSettings.ExpiryIndicator.Bindings'
$bindings = @()
$existingBindingsJson = [string]$rootWeb.AllProperties[$bindingsKey]
if (-not [string]::IsNullOrWhiteSpace($existingBindingsJson)) {
try {
$bindings = @($existingBindingsJson | ConvertFrom-Json)
}
catch {
$bindings = @()
}
}
$webUrl = $context.Web.Url.TrimEnd('/')
$listId = $list.Id.ToString('D')
$bindings = @($bindings | Where-Object {
-not (
([string]$_.webUrl).TrimEnd('/').Equals($webUrl, [StringComparison]::OrdinalIgnoreCase) -and
([string]$_.listId).Equals($listId, [StringComparison]::OrdinalIgnoreCase)
)
})
$rootWeb.AllProperties[$bindingsKey] = (ConvertTo-Json -InputObject @($bindings) -Compress -Depth 5)
$rootWeb.Update()
$customActions | Where-Object {
$_.Name -eq 'ExpiryIndicator.Classic.ScriptLink' -or
$_.Name -eq 'ExpiryIndicator.Classic.Ribbon'

View File

@@ -26,10 +26,14 @@ $context.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
try {
$list = $context.Web.Lists.GetByTitle($ListTitle)
$field = $list.Fields.GetByInternalNameOrTitle($ExpiryFieldInternalName)
$rootWeb = $context.Site.RootWeb
$customActions = $list.UserCustomActions
$webCustomActions = $context.Web.UserCustomActions
$context.Load($list)
$context.Load($field)
$context.Load($context.Web)
$context.Load($rootWeb)
$context.Load($rootWeb.AllProperties)
$context.Load($customActions)
$context.Load($webCustomActions)
$context.ExecuteQuery()
@@ -41,12 +45,58 @@ try {
$alreadyBound = $field.ClientSideComponentId -eq $FieldCustomizerComponentId
$field.ClientSideComponentId = $FieldCustomizerComponentId
if (-not $alreadyBound -or [string]::IsNullOrWhiteSpace($field.ClientSideComponentProperties)) {
$field.ClientSideComponentProperties = (@{
$field.ClientSideComponentProperties = ([ordered]@{
schemaVersion = 2
expiryField = $field.InternalName
inheritSiteDefaults = $true
} | ConvertTo-Json -Compress)
}
$field.Update()
$descriptorKey = 'PortalSettings.Solutions.ExpiryIndicator'
$bindingsKey = 'PortalSettings.ExpiryIndicator.Bindings'
$rootWeb.AllProperties[$descriptorKey] = ([ordered]@{
key = 'ExpiryIndicator'
displayName = 'Expiry Indicator'
version = '2.1.0'
provider = 'expiryIndicator'
componentIds = @(
'a555f4fc-d6a6-4421-8189-457449d9bbde',
'cd58f5d9-ffc8-4df7-a910-3054842d7abf',
'95691218-bcb1-4fad-adda-02bb12014d15'
)
settingsScope = @('siteCollection', 'web', 'list')
} | ConvertTo-Json -Compress -Depth 5)
$bindings = @()
$existingBindingsJson = [string]$rootWeb.AllProperties[$bindingsKey]
if (-not [string]::IsNullOrWhiteSpace($existingBindingsJson)) {
try {
$bindings = @($existingBindingsJson | ConvertFrom-Json)
}
catch {
Write-Warning 'Das vorhandene ExpiryIndicator-Bindungsinventar war ungültig und wird neu aufgebaut.'
}
}
$webUrl = $context.Web.Url.TrimEnd('/')
$listId = $list.Id.ToString('D')
$bindings = @($bindings | Where-Object {
-not (
([string]$_.webUrl).TrimEnd('/').Equals($webUrl, [StringComparison]::OrdinalIgnoreCase) -and
([string]$_.listId).Equals($listId, [StringComparison]::OrdinalIgnoreCase)
)
})
$bindings += [ordered]@{
webUrl = $webUrl
listId = $listId
listTitle = $list.Title
fieldInternalName = $field.InternalName
fieldId = $field.Id.ToString('D')
}
$rootWeb.AllProperties[$bindingsKey] = (ConvertTo-Json -InputObject @($bindings) -Compress -Depth 5)
$rootWeb.Update()
# Remove old diagnostic registrations and make the V2 Classic
# registrations idempotent.
$customActions | Where-Object {

View File

@@ -2,6 +2,8 @@
'use strict';
var FIELD_CUSTOMIZER_ID = 'a555f4fc-d6a6-4421-8189-457449d9bbde';
var SITE_DEFAULTS_PROPERTY = 'PortalSettings.ExpiryIndicator.Defaults';
var WEB_OVERRIDE_PROPERTY = 'PortalSettings.ExpiryIndicator.Override';
var state = { context: null, field: null, config: null, loading: null };
function configuredExpiryField() {
@@ -44,6 +46,10 @@
return String(pageContext().webAbsoluteUrl || '').replace(/\/$/, '');
}
function siteUrl() {
return String(pageContext().siteAbsoluteUrl || webUrl()).replace(/\/$/, '');
}
function listId(context) {
return String((context && context.listName) || pageContext().pageListId || '').replace(/[{}]/g, '');
}
@@ -94,14 +100,55 @@
};
}
function mergeConfig(baseValue, overrideValue) {
var base = baseValue || defaults();
var override = overrideValue || {};
var result = {
baseField: override.baseField || base.baseField || 'Created',
expiryField: override.expiryField || base.expiryField || 'ExpiryDate',
default: {
lifeTime: override.default && override.default.lifeTime ? override.default.lifeTime :
(base.default && base.default.lifeTime ? base.default.lifeTime : { value: 2, unit: 'years' }),
columnRule: override.default && override.default.columnRule instanceof Array ? override.default.columnRule :
(base.default && base.default.columnRule instanceof Array ? base.default.columnRule : [])
},
rules: override.rules instanceof Array ? override.rules : (base.rules instanceof Array ? base.rules : []),
nullText: typeof override.nullText === 'string' ? override.nullText : base.nullText,
confirmExtension: typeof override.confirmExtension === 'boolean' ? override.confirmExtension : base.confirmExtension
};
return result;
}
function propertyValue(payload, key) {
var properties = payload && payload.d ? payload.d : (payload || {});
for (var name in properties) {
if (Object.prototype.hasOwnProperty.call(properties, name) && name.toLowerCase() === key.toLowerCase()) {
try { return typeof properties[name] === 'string' ? JSON.parse(properties[name]) : properties[name]; }
catch (ignore) { return null; }
}
}
return null;
}
function isBindingOnly(value) {
if (!value || typeof value !== 'object') { return false; }
return typeof value.expiryField === 'string' && !value.default && !value.rules &&
!value.baseField && typeof value.nullText === 'undefined' && typeof value.confirmExtension === 'undefined';
}
function loadConfig(context) {
var id = listId(context);
if (!id) { return Promise.reject(new Error('Keine Liste erkannt.')); }
if (state.loading && state.context === id) { return state.loading; }
state.context = id;
state.loading = request('GET', listUrl(id) +
'/fields?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties')
.then(function (data) {
var fieldRequest = request('GET', listUrl(id) +
'/fields?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties');
var sitePropertiesRequest = request('GET', siteUrl() + '/_api/web/AllProperties').catch(function () { return {}; });
var webPropertiesRequest = siteUrl().toLowerCase() === webUrl().toLowerCase() ? Promise.resolve({}) :
request('GET', webUrl() + '/_api/web/AllProperties').catch(function () { return {}; });
state.loading = Promise.all([fieldRequest, sitePropertiesRequest, webPropertiesRequest])
.then(function (results) {
var data = results[0];
var fields = data.d ? data.d.results : (data.value || []);
var field;
for (var index = 0; index < fields.length; index++) {
@@ -109,12 +156,18 @@
if (componentId === FIELD_CUSTOMIZER_ID) { field = fields[index]; break; }
}
if (!field) { throw new Error('Kein ExpiryIndicator-Feld gebunden.'); }
var config = defaults(field.InternalName);
var inherited = mergeConfig(defaults(field.InternalName), propertyValue(results[1], SITE_DEFAULTS_PROPERTY));
inherited = mergeConfig(inherited, propertyValue(results[2], WEB_OVERRIDE_PROPERTY));
var config = inherited;
try {
if (field.ClientSideComponentProperties) {
var parsed = JSON.parse(field.ClientSideComponentProperties);
for (var name in parsed) {
if (Object.prototype.hasOwnProperty.call(parsed, name)) { config[name] = parsed[name]; }
if (parsed.inheritSiteDefaults === true || parsed.override || isBindingOnly(parsed)) {
config = mergeConfig(inherited, parsed.override || {});
} else if (parsed.inheritSiteDefaults === false) {
config = mergeConfig(defaults(field.InternalName), parsed.override || {});
} else {
config = mergeConfig(defaults(field.InternalName), parsed);
}
}
} catch (ignore) { }

View File

@@ -3,7 +3,7 @@ import {
SPHttpClient,
SPHttpClientResponse
} from '@microsoft/sp-http';
import { createDefaultConfig, IExpiryConfig, normalizeConfig } from './ExpiryModels';
import { createDefaultConfig, IExpiryConfig, mergeConfigValues, normalizeConfig } from './ExpiryModels';
import { escapeODataString, responseError } from './RestError';
interface IConfigurationListInfo {
@@ -27,19 +27,29 @@ interface IBoundFieldInfo {
const CONFIGURATION_LIST_TITLE: string = '_ExpiryIndicatorConfiguration';
const CONFIGURATION_FIELD: string = 'ExpiryIndicatorJson';
const FIELD_CUSTOMIZER_COMPONENT_ID: string = 'a555f4fc-d6a6-4421-8189-457449d9bbde';
const SITE_DEFAULTS_PROPERTY: string = 'PortalSettings.ExpiryIndicator.Defaults';
const WEB_OVERRIDE_PROPERTY: string = 'PortalSettings.ExpiryIndicator.Override';
export class ExpiryConfigService {
private readonly _spHttpClient: SPHttpClient;
private readonly _webUrl: string;
private readonly _siteUrl: string;
private _inheritedConfigPromise: Promise<IExpiryConfig> | undefined;
public constructor(spHttpClient: SPHttpClient, webUrl: string) {
public constructor(spHttpClient: SPHttpClient, webUrl: string, siteUrl?: string) {
this._spHttpClient = spHttpClient;
this._webUrl = webUrl.replace(/\/$/, '');
this._siteUrl = (siteUrl || webUrl).replace(/\/$/, '');
}
public getConfig(listId: string): Promise<IExpiryConfig> {
return this._getBoundField(listId).then((field: IBoundFieldInfo): Promise<IExpiryConfig> => {
const propertyConfig: IExpiryConfig | undefined = this._parseProperties(field.properties, field.internalName);
return this._getInheritedConfig().then((inherited: IExpiryConfig): Promise<IExpiryConfig> => {
const propertyConfig: IExpiryConfig | undefined = this._parseProperties(
field.properties,
field.internalName,
inherited
);
if (propertyConfig) {
return Promise.resolve(propertyConfig);
}
@@ -51,9 +61,9 @@ export class ExpiryConfigService {
legacyConfig.expiryField = field.internalName;
return legacyConfig;
}
const defaults: IExpiryConfig = createDefaultConfig();
defaults.expiryField = field.internalName;
return defaults;
inherited.expiryField = field.internalName;
return inherited;
});
});
});
}
@@ -132,12 +142,29 @@ export class ExpiryConfigService {
});
}
private _parseProperties(value: string, internalName: string): IExpiryConfig | undefined {
private _parseProperties(
value: string,
internalName: string,
inherited: IExpiryConfig
): IExpiryConfig | undefined {
if (!value || value.trim() === '' || value.trim() === '{}') {
return undefined;
}
try {
const config: IExpiryConfig = normalizeConfig(JSON.parse(value));
const properties: any = JSON.parse(value);
let config: IExpiryConfig;
if (properties.inheritSiteDefaults === true || properties.override || this._isBindingOnly(properties)) {
config = properties.inheritSiteDefaults === false
? createDefaultConfig()
: mergeConfigValues(createDefaultConfig(), inherited);
if (properties.override) {
config = mergeConfigValues(config, properties.override);
}
} else {
// Existing V1/V2 field configurations remain list-local until an
// administrator explicitly enables inheritance.
config = normalizeConfig(properties);
}
config.expiryField = internalName;
return config;
} catch (error) {
@@ -145,6 +172,62 @@ export class ExpiryConfigService {
}
}
private _getInheritedConfig(): Promise<IExpiryConfig> {
if (!this._inheritedConfigPromise) {
this._inheritedConfigPromise = Promise.all([
this._getPropertyBagValue(this._siteUrl, SITE_DEFAULTS_PROPERTY),
this._webUrl.toLowerCase() === this._siteUrl.toLowerCase()
? Promise.resolve(undefined)
: this._getPropertyBagValue(this._webUrl, WEB_OVERRIDE_PROPERTY)
]).then((values: any[]): IExpiryConfig => {
let config: IExpiryConfig = createDefaultConfig();
if (values[0]) {
config = mergeConfigValues(config, values[0]);
}
if (values[1]) {
config = mergeConfigValues(config, values[1]);
}
return config;
});
}
return this._inheritedConfigPromise;
}
private _getPropertyBagValue(webUrl: string, key: string): Promise<any | undefined> {
return this._spHttpClient.get(webUrl + '/_api/web/AllProperties', SPHttpClient.configurations.v1, {
headers: this._headers()
}).then((response: SPHttpClientResponse): Promise<any | undefined> => {
if (!response.ok) {
return Promise.resolve(undefined);
}
return response.json().then((data: any): any | undefined => {
const source: any = data.d || data;
let rawValue: any = source[key];
if (typeof rawValue === 'undefined') {
Object.keys(source).some((propertyName: string): boolean => {
if (propertyName.toLowerCase() === key.toLowerCase()) {
rawValue = source[propertyName];
return true;
}
return false;
});
}
if (typeof rawValue !== 'string' || rawValue.trim() === '') {
return undefined;
}
try {
return JSON.parse(rawValue);
} catch (error) {
return undefined;
}
});
});
}
private _isBindingOnly(properties: any): boolean {
return properties && isOnlyKnownBindingProperties(properties);
}
private _getConfigurationList(): Promise<any> {
const url: string = this._configurationListUrl() + '?$select=Id,ListItemEntityTypeFullName';
return this._spHttpClient.get(url, SPHttpClient.configurations.v1)
@@ -211,3 +294,15 @@ export class ExpiryConfigService {
return headers;
}
}
function isOnlyKnownBindingProperties(properties: any): boolean {
if (!properties || typeof properties.expiryField !== 'string') {
return false;
}
const configurationKeys: string[] = [
'baseField', 'default', 'rules', 'nullText', 'confirmExtension'
];
return !configurationKeys.some((key: string): boolean =>
typeof properties[key] !== 'undefined'
);
}

View File

@@ -4,6 +4,7 @@ import { expect } from 'chai';
import {
createDefaultConfig,
IExpiryConfig,
mergeConfigValues,
normalizeConfig,
resolveBehavior,
validateValueRules
@@ -84,4 +85,21 @@ describe('ExpiryModels V2 rules', (): void => {
}]);
expect(errors.length).to.be.greaterThan(0);
});
it('merges partial site and web defaults without losing inherited values', (): void => {
const siteConfig: IExpiryConfig = mergeConfigValues(createDefaultConfig(), {
baseField: 'DocumentCreated',
default: { lifeTime: { value: 3, unit: 'years' } },
nullText: 'Nicht berechenbar'
});
const webConfig: IExpiryConfig = mergeConfigValues(siteConfig, {
default: { columnRule: [] },
confirmExtension: false
});
expect(webConfig.baseField).to.equal('DocumentCreated');
expect(webConfig.default.lifeTime.value).to.equal(3);
expect(webConfig.default.columnRule.length).to.equal(0);
expect(webConfig.nullText).to.equal('Nicht berechenbar');
expect(webConfig.confirmExtension).to.equal(false);
});
});

View File

@@ -101,6 +101,28 @@ export function normalizeConfig(value: any): IExpiryConfig {
};
}
export function mergeConfigValues(baseValue: any, overrideValue: any): IExpiryConfig {
const base: IExpiryConfig = normalizeConfig(baseValue);
const override: any = overrideValue || {};
const overrideDefault: any = override.default || {};
return normalizeConfig({
baseField: isSafeInternalName(override.baseField) ? override.baseField : base.baseField,
expiryField: isSafeInternalName(override.expiryField) ? override.expiryField : base.expiryField,
default: {
lifeTime: overrideDefault.lifeTime || base.default.lifeTime,
columnRule: Array.isArray(overrideDefault.columnRule)
? overrideDefault.columnRule
: base.default.columnRule
},
rules: Array.isArray(override.rules) ? override.rules : base.rules,
nullText: typeof override.nullText === 'string' ? override.nullText : base.nullText,
confirmExtension: typeof override.confirmExtension === 'boolean'
? override.confirmExtension
: base.confirmExtension
});
}
export function getRuleFieldNames(config: IExpiryConfig): string[] {
const fields: string[] = [];
config.rules.forEach((rule: IExpiryValueRule): void => {
@@ -321,6 +343,9 @@ function normalizeColumnRules(value: any, fallback: IExpiryColumnRule[]): IExpir
if (!Array.isArray(value)) {
return fallback;
}
if (value.length === 0) {
return [];
}
const rules: IExpiryColumnRule[] = [];
value.forEach((rule: any): void => {

View File

@@ -4,7 +4,7 @@
"alias": "ExpiryIndicatorFieldCustomizer",
"componentType": "Extension",
"extensionType": "FieldCustomizer",
"version": "2.0.1",
"version": "2.1.0",
"manifestVersion": 2,
"requiresCustomScript": false
}

View File

@@ -33,7 +33,8 @@ export default class ExpiryIndicatorFieldCustomizer
const service: ExpiryConfigService = new ExpiryConfigService(
this.context.spHttpClient,
this.context.pageContext.web.absoluteUrl
this.context.pageContext.web.absoluteUrl,
this.context.pageContext.site.absoluteUrl
);
this._itemService = new ExpiryItemService(

View File

@@ -4,7 +4,7 @@
"alias": "ExpiryIndicatorApplicationCustomizer",
"componentType": "Extension",
"extensionType": "ApplicationCustomizer",
"version": "2.0.1",
"version": "2.1.0",
"manifestVersion": 2,
"requiresCustomScript": false
}

View File

@@ -32,7 +32,8 @@ export default class ExpiryIndicatorApplicationCustomizer
const listId: string = this.context.pageContext.list.id.toString();
const service: ExpiryConfigService = new ExpiryConfigService(
this.context.spHttpClient,
this.context.pageContext.web.absoluteUrl
this.context.pageContext.web.absoluteUrl,
this.context.pageContext.site.absoluteUrl
);
service.getConfig(listId).then((config: IExpiryConfig): void => {

View File

@@ -4,7 +4,7 @@
"alias": "ExpiryIndicatorCommandSet",
"componentType": "Extension",
"extensionType": "ListViewCommandSet",
"version": "2.0.1",
"version": "2.1.0",
"manifestVersion": 2,
"requiresCustomScript": false,
"items": {

View File

@@ -32,7 +32,11 @@ export default class ExpiryIndicatorCommandSet
const listId: string = this.context.pageContext.list.id.toString();
const webUrl: string = this.context.pageContext.web.absoluteUrl;
this._configService = new ExpiryConfigService(this.context.spHttpClient, webUrl);
this._configService = new ExpiryConfigService(
this.context.spHttpClient,
webUrl,
this.context.pageContext.site.absoluteUrl
);
this._itemService = new ExpiryItemService(this.context.spHttpClient, webUrl, listId);
this._canEdit = this.context.pageContext.list.permissions.hasPermission(SPPermission.editListItems);