feat: update to version 2.1.0 with central site collection configuration and inheritance support
This commit is contained in:
32
README.md
32
README.md
@@ -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
16
ToDo.md
@@ -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.
|
||||
|
||||
@@ -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
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "expiry-indicator",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"alias": "ExpiryIndicatorFieldCustomizer",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "FieldCustomizer",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"alias": "ExpiryIndicatorApplicationCustomizer",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"alias": "ExpiryIndicatorCommandSet",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ListViewCommandSet",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"items": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user