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`.
|
- Verschachtelte V2-Bedingungsgruppen mit den logischen Verknüpfungen `AND` und `OR`.
|
||||||
- Command-Bar-Befehl zum Verlängern eines oder mehrerer Elemente um ein Kalenderjahr.
|
- Command-Bar-Befehl zum Verlängern eines oder mehrerer Elemente um ein Kalenderjahr.
|
||||||
- CSR/JSLink-Darstellung und Ribbon-Befehl für klassische SharePoint-Ansichten.
|
- 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.
|
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
|
PortalSettings kann optional dieselben Component Properties verwalten, ist aber keine Voraussetzung für den
|
||||||
Betrieb des ExpiryIndicators.
|
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
|
Bei einem Upgrade wird eine vorhandene Konfiguration aus der früheren ausgeblendeten Liste
|
||||||
`_ExpiryIndicatorConfiguration` weiterhin gelesen, solange noch keine Component Properties gespeichert wurden.
|
`_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
|
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
|
### 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:
|
`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,
|
- 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
|
-SkipClassic
|
||||||
```
|
```
|
||||||
|
|
||||||
Die klassische Laufzeit verwendet dieselbe JSON-Konfiguration aus den
|
Die klassische Laufzeit verwendet dieselbe Vererbungskette und dieselben Feld-Properties wie die moderne
|
||||||
`ClientSideComponentProperties` des gebundenen Ablaufdatumsfeldes. Es gibt keine zweite Konfiguration.
|
Laufzeit. Es gibt keine zweite Konfiguration.
|
||||||
|
|
||||||
### Ablaufdatum um ein Jahr verlängern
|
### 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.
|
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
|
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
|
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
|
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.
|
4. Modern- und Classic-Ansicht mit geleertem Browsercache neu laden und prüfen.
|
||||||
|
|
||||||
## Deaktivierung
|
## Deaktivierung
|
||||||
|
|||||||
16
ToDo.md
16
ToDo.md
@@ -150,6 +150,22 @@ Beispiel einer Laufzeitkonfiguration:
|
|||||||
- [ ] Feature-Upgrade in SharePoint Server Subscription Edition testen.
|
- [ ] Feature-Upgrade in SharePoint Server Subscription Edition testen.
|
||||||
- [ ] Modern-/Classic-Abnahmetest auf einer Testliste und einer Test-Dokumentbibliothek durchführen.
|
- [ ] 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
|
## Abnahmekriterien
|
||||||
|
|
||||||
- Die Solution enthält und provisioniert keine Definition für eine fachliche Ablaufdatumsspalte.
|
- Die Solution enthält und provisioniert keine Definition für eine fachliche Ablaufdatumsspalte.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"solution": {
|
"solution": {
|
||||||
"name": "expiry-indicator-client-side-solution",
|
"name": "expiry-indicator-client-side-solution",
|
||||||
"id": "68bb6d2d-9895-45b4-88e8-b98835faa981",
|
"id": "68bb6d2d-9895-45b4-88e8-b98835faa981",
|
||||||
"version": "2.0.1.0",
|
"version": "2.1.0.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": false,
|
"skipFeatureDeployment": false,
|
||||||
"features": [
|
"features": [
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"title": "ExpiryIndicator extension registration",
|
"title": "ExpiryIndicator extension registration",
|
||||||
"description": "Registers ExpiryIndicator commands and its settings Application Customizer. Does not provision ExpiryDate.",
|
"description": "Registers ExpiryIndicator commands and its settings Application Customizer. Does not provision ExpiryDate.",
|
||||||
"id": "913402af-ab9a-4974-9f86-5c2159ae41db",
|
"id": "913402af-ab9a-4974-9f86-5c2159ae41db",
|
||||||
"version": "2.0.1.0",
|
"version": "2.1.0.0",
|
||||||
"assets": {
|
"assets": {
|
||||||
"elementManifests": [
|
"elementManifests": [
|
||||||
"elements.xml",
|
"elements.xml",
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "expiry-indicator",
|
"name": "expiry-indicator",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "expiry-indicator",
|
"name": "expiry-indicator",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0 <9.0.0"
|
"node": ">=6.9.0 <9.0.0"
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ $context.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
|
|||||||
try {
|
try {
|
||||||
$list = $context.Web.Lists.GetByTitle($ListTitle)
|
$list = $context.Web.Lists.GetByTitle($ListTitle)
|
||||||
$field = $list.Fields.GetByInternalNameOrTitle($ExpiryFieldInternalName)
|
$field = $list.Fields.GetByInternalNameOrTitle($ExpiryFieldInternalName)
|
||||||
|
$rootWeb = $context.Site.RootWeb
|
||||||
$customActions = $list.UserCustomActions
|
$customActions = $list.UserCustomActions
|
||||||
$webCustomActions = $context.Web.UserCustomActions
|
$webCustomActions = $context.Web.UserCustomActions
|
||||||
$context.Load($list)
|
$context.Load($list)
|
||||||
$context.Load($field)
|
$context.Load($field)
|
||||||
|
$context.Load($context.Web)
|
||||||
|
$context.Load($rootWeb)
|
||||||
|
$context.Load($rootWeb.AllProperties)
|
||||||
$context.Load($customActions)
|
$context.Load($customActions)
|
||||||
$context.Load($webCustomActions)
|
$context.Load($webCustomActions)
|
||||||
$context.ExecuteQuery()
|
$context.ExecuteQuery()
|
||||||
@@ -41,6 +45,28 @@ try {
|
|||||||
$field.ClientSideComponentProperties = ''
|
$field.ClientSideComponentProperties = ''
|
||||||
$field.Update()
|
$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 {
|
$customActions | Where-Object {
|
||||||
$_.Name -eq 'ExpiryIndicator.Classic.ScriptLink' -or
|
$_.Name -eq 'ExpiryIndicator.Classic.ScriptLink' -or
|
||||||
$_.Name -eq 'ExpiryIndicator.Classic.Ribbon'
|
$_.Name -eq 'ExpiryIndicator.Classic.Ribbon'
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ $context.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
|
|||||||
try {
|
try {
|
||||||
$list = $context.Web.Lists.GetByTitle($ListTitle)
|
$list = $context.Web.Lists.GetByTitle($ListTitle)
|
||||||
$field = $list.Fields.GetByInternalNameOrTitle($ExpiryFieldInternalName)
|
$field = $list.Fields.GetByInternalNameOrTitle($ExpiryFieldInternalName)
|
||||||
|
$rootWeb = $context.Site.RootWeb
|
||||||
$customActions = $list.UserCustomActions
|
$customActions = $list.UserCustomActions
|
||||||
$webCustomActions = $context.Web.UserCustomActions
|
$webCustomActions = $context.Web.UserCustomActions
|
||||||
$context.Load($list)
|
$context.Load($list)
|
||||||
$context.Load($field)
|
$context.Load($field)
|
||||||
|
$context.Load($context.Web)
|
||||||
|
$context.Load($rootWeb)
|
||||||
|
$context.Load($rootWeb.AllProperties)
|
||||||
$context.Load($customActions)
|
$context.Load($customActions)
|
||||||
$context.Load($webCustomActions)
|
$context.Load($webCustomActions)
|
||||||
$context.ExecuteQuery()
|
$context.ExecuteQuery()
|
||||||
@@ -41,12 +45,58 @@ try {
|
|||||||
$alreadyBound = $field.ClientSideComponentId -eq $FieldCustomizerComponentId
|
$alreadyBound = $field.ClientSideComponentId -eq $FieldCustomizerComponentId
|
||||||
$field.ClientSideComponentId = $FieldCustomizerComponentId
|
$field.ClientSideComponentId = $FieldCustomizerComponentId
|
||||||
if (-not $alreadyBound -or [string]::IsNullOrWhiteSpace($field.ClientSideComponentProperties)) {
|
if (-not $alreadyBound -or [string]::IsNullOrWhiteSpace($field.ClientSideComponentProperties)) {
|
||||||
$field.ClientSideComponentProperties = (@{
|
$field.ClientSideComponentProperties = ([ordered]@{
|
||||||
|
schemaVersion = 2
|
||||||
expiryField = $field.InternalName
|
expiryField = $field.InternalName
|
||||||
|
inheritSiteDefaults = $true
|
||||||
} | ConvertTo-Json -Compress)
|
} | ConvertTo-Json -Compress)
|
||||||
}
|
}
|
||||||
$field.Update()
|
$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
|
# Remove old diagnostic registrations and make the V2 Classic
|
||||||
# registrations idempotent.
|
# registrations idempotent.
|
||||||
$customActions | Where-Object {
|
$customActions | Where-Object {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var FIELD_CUSTOMIZER_ID = 'a555f4fc-d6a6-4421-8189-457449d9bbde';
|
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 };
|
var state = { context: null, field: null, config: null, loading: null };
|
||||||
|
|
||||||
function configuredExpiryField() {
|
function configuredExpiryField() {
|
||||||
@@ -44,6 +46,10 @@
|
|||||||
return String(pageContext().webAbsoluteUrl || '').replace(/\/$/, '');
|
return String(pageContext().webAbsoluteUrl || '').replace(/\/$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function siteUrl() {
|
||||||
|
return String(pageContext().siteAbsoluteUrl || webUrl()).replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
function listId(context) {
|
function listId(context) {
|
||||||
return String((context && context.listName) || pageContext().pageListId || '').replace(/[{}]/g, '');
|
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) {
|
function loadConfig(context) {
|
||||||
var id = listId(context);
|
var id = listId(context);
|
||||||
if (!id) { return Promise.reject(new Error('Keine Liste erkannt.')); }
|
if (!id) { return Promise.reject(new Error('Keine Liste erkannt.')); }
|
||||||
if (state.loading && state.context === id) { return state.loading; }
|
if (state.loading && state.context === id) { return state.loading; }
|
||||||
state.context = id;
|
state.context = id;
|
||||||
state.loading = request('GET', listUrl(id) +
|
var fieldRequest = request('GET', listUrl(id) +
|
||||||
'/fields?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties')
|
'/fields?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties');
|
||||||
.then(function (data) {
|
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 fields = data.d ? data.d.results : (data.value || []);
|
||||||
var field;
|
var field;
|
||||||
for (var index = 0; index < fields.length; index++) {
|
for (var index = 0; index < fields.length; index++) {
|
||||||
@@ -109,12 +156,18 @@
|
|||||||
if (componentId === FIELD_CUSTOMIZER_ID) { field = fields[index]; break; }
|
if (componentId === FIELD_CUSTOMIZER_ID) { field = fields[index]; break; }
|
||||||
}
|
}
|
||||||
if (!field) { throw new Error('Kein ExpiryIndicator-Feld gebunden.'); }
|
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 {
|
try {
|
||||||
if (field.ClientSideComponentProperties) {
|
if (field.ClientSideComponentProperties) {
|
||||||
var parsed = JSON.parse(field.ClientSideComponentProperties);
|
var parsed = JSON.parse(field.ClientSideComponentProperties);
|
||||||
for (var name in parsed) {
|
if (parsed.inheritSiteDefaults === true || parsed.override || isBindingOnly(parsed)) {
|
||||||
if (Object.prototype.hasOwnProperty.call(parsed, name)) { config[name] = parsed[name]; }
|
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) { }
|
} catch (ignore) { }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
SPHttpClient,
|
SPHttpClient,
|
||||||
SPHttpClientResponse
|
SPHttpClientResponse
|
||||||
} from '@microsoft/sp-http';
|
} from '@microsoft/sp-http';
|
||||||
import { createDefaultConfig, IExpiryConfig, normalizeConfig } from './ExpiryModels';
|
import { createDefaultConfig, IExpiryConfig, mergeConfigValues, normalizeConfig } from './ExpiryModels';
|
||||||
import { escapeODataString, responseError } from './RestError';
|
import { escapeODataString, responseError } from './RestError';
|
||||||
|
|
||||||
interface IConfigurationListInfo {
|
interface IConfigurationListInfo {
|
||||||
@@ -27,33 +27,43 @@ interface IBoundFieldInfo {
|
|||||||
const CONFIGURATION_LIST_TITLE: string = '_ExpiryIndicatorConfiguration';
|
const CONFIGURATION_LIST_TITLE: string = '_ExpiryIndicatorConfiguration';
|
||||||
const CONFIGURATION_FIELD: string = 'ExpiryIndicatorJson';
|
const CONFIGURATION_FIELD: string = 'ExpiryIndicatorJson';
|
||||||
const FIELD_CUSTOMIZER_COMPONENT_ID: string = 'a555f4fc-d6a6-4421-8189-457449d9bbde';
|
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 {
|
export class ExpiryConfigService {
|
||||||
private readonly _spHttpClient: SPHttpClient;
|
private readonly _spHttpClient: SPHttpClient;
|
||||||
private readonly _webUrl: string;
|
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._spHttpClient = spHttpClient;
|
||||||
this._webUrl = webUrl.replace(/\/$/, '');
|
this._webUrl = webUrl.replace(/\/$/, '');
|
||||||
|
this._siteUrl = (siteUrl || webUrl).replace(/\/$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConfig(listId: string): Promise<IExpiryConfig> {
|
public getConfig(listId: string): Promise<IExpiryConfig> {
|
||||||
return this._getBoundField(listId).then((field: IBoundFieldInfo): 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> => {
|
||||||
if (propertyConfig) {
|
const propertyConfig: IExpiryConfig | undefined = this._parseProperties(
|
||||||
return Promise.resolve(propertyConfig);
|
field.properties,
|
||||||
}
|
field.internalName,
|
||||||
|
inherited
|
||||||
// Upgrade compatibility: read an existing hidden-list configuration,
|
);
|
||||||
// but never create that list. The next save migrates the JSON to the field.
|
if (propertyConfig) {
|
||||||
return this._getLegacyConfig(listId).then((legacyConfig: IExpiryConfig | undefined): IExpiryConfig => {
|
return Promise.resolve(propertyConfig);
|
||||||
if (legacyConfig) {
|
|
||||||
legacyConfig.expiryField = field.internalName;
|
|
||||||
return legacyConfig;
|
|
||||||
}
|
}
|
||||||
const defaults: IExpiryConfig = createDefaultConfig();
|
|
||||||
defaults.expiryField = field.internalName;
|
// Upgrade compatibility: read an existing hidden-list configuration,
|
||||||
return defaults;
|
// but never create that list. The next save migrates the JSON to the field.
|
||||||
|
return this._getLegacyConfig(listId).then((legacyConfig: IExpiryConfig | undefined): IExpiryConfig => {
|
||||||
|
if (legacyConfig) {
|
||||||
|
legacyConfig.expiryField = field.internalName;
|
||||||
|
return legacyConfig;
|
||||||
|
}
|
||||||
|
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() === '{}') {
|
if (!value || value.trim() === '' || value.trim() === '{}') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
try {
|
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;
|
config.expiryField = internalName;
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} 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> {
|
private _getConfigurationList(): Promise<any> {
|
||||||
const url: string = this._configurationListUrl() + '?$select=Id,ListItemEntityTypeFullName';
|
const url: string = this._configurationListUrl() + '?$select=Id,ListItemEntityTypeFullName';
|
||||||
return this._spHttpClient.get(url, SPHttpClient.configurations.v1)
|
return this._spHttpClient.get(url, SPHttpClient.configurations.v1)
|
||||||
@@ -211,3 +294,15 @@ export class ExpiryConfigService {
|
|||||||
return headers;
|
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 {
|
import {
|
||||||
createDefaultConfig,
|
createDefaultConfig,
|
||||||
IExpiryConfig,
|
IExpiryConfig,
|
||||||
|
mergeConfigValues,
|
||||||
normalizeConfig,
|
normalizeConfig,
|
||||||
resolveBehavior,
|
resolveBehavior,
|
||||||
validateValueRules
|
validateValueRules
|
||||||
@@ -84,4 +85,21 @@ describe('ExpiryModels V2 rules', (): void => {
|
|||||||
}]);
|
}]);
|
||||||
expect(errors.length).to.be.greaterThan(0);
|
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[] {
|
export function getRuleFieldNames(config: IExpiryConfig): string[] {
|
||||||
const fields: string[] = [];
|
const fields: string[] = [];
|
||||||
config.rules.forEach((rule: IExpiryValueRule): void => {
|
config.rules.forEach((rule: IExpiryValueRule): void => {
|
||||||
@@ -321,6 +343,9 @@ function normalizeColumnRules(value: any, fallback: IExpiryColumnRule[]): IExpir
|
|||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const rules: IExpiryColumnRule[] = [];
|
const rules: IExpiryColumnRule[] = [];
|
||||||
value.forEach((rule: any): void => {
|
value.forEach((rule: any): void => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"alias": "ExpiryIndicatorFieldCustomizer",
|
"alias": "ExpiryIndicatorFieldCustomizer",
|
||||||
"componentType": "Extension",
|
"componentType": "Extension",
|
||||||
"extensionType": "FieldCustomizer",
|
"extensionType": "FieldCustomizer",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"manifestVersion": 2,
|
"manifestVersion": 2,
|
||||||
"requiresCustomScript": false
|
"requiresCustomScript": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export default class ExpiryIndicatorFieldCustomizer
|
|||||||
|
|
||||||
const service: ExpiryConfigService = new ExpiryConfigService(
|
const service: ExpiryConfigService = new ExpiryConfigService(
|
||||||
this.context.spHttpClient,
|
this.context.spHttpClient,
|
||||||
this.context.pageContext.web.absoluteUrl
|
this.context.pageContext.web.absoluteUrl,
|
||||||
|
this.context.pageContext.site.absoluteUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
this._itemService = new ExpiryItemService(
|
this._itemService = new ExpiryItemService(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"alias": "ExpiryIndicatorApplicationCustomizer",
|
"alias": "ExpiryIndicatorApplicationCustomizer",
|
||||||
"componentType": "Extension",
|
"componentType": "Extension",
|
||||||
"extensionType": "ApplicationCustomizer",
|
"extensionType": "ApplicationCustomizer",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"manifestVersion": 2,
|
"manifestVersion": 2,
|
||||||
"requiresCustomScript": false
|
"requiresCustomScript": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export default class ExpiryIndicatorApplicationCustomizer
|
|||||||
const listId: string = this.context.pageContext.list.id.toString();
|
const listId: string = this.context.pageContext.list.id.toString();
|
||||||
const service: ExpiryConfigService = new ExpiryConfigService(
|
const service: ExpiryConfigService = new ExpiryConfigService(
|
||||||
this.context.spHttpClient,
|
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 => {
|
service.getConfig(listId).then((config: IExpiryConfig): void => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"alias": "ExpiryIndicatorCommandSet",
|
"alias": "ExpiryIndicatorCommandSet",
|
||||||
"componentType": "Extension",
|
"componentType": "Extension",
|
||||||
"extensionType": "ListViewCommandSet",
|
"extensionType": "ListViewCommandSet",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"manifestVersion": 2,
|
"manifestVersion": 2,
|
||||||
"requiresCustomScript": false,
|
"requiresCustomScript": false,
|
||||||
"items": {
|
"items": {
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ export default class ExpiryIndicatorCommandSet
|
|||||||
|
|
||||||
const listId: string = this.context.pageContext.list.id.toString();
|
const listId: string = this.context.pageContext.list.id.toString();
|
||||||
const webUrl: string = this.context.pageContext.web.absoluteUrl;
|
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._itemService = new ExpiryItemService(this.context.spHttpClient, webUrl, listId);
|
||||||
this._canEdit = this.context.pageContext.list.permissions.hasPermission(SPPermission.editListItems);
|
this._canEdit = this.context.pageContext.list.permissions.hasPermission(SPPermission.editListItems);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user