diff --git a/README.md b/README.md index a757ad2..99fb01d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Beispiel der intern gespeicherten Gesamtstruktur (nicht vollständig in eines de } ``` -## Konfiguration über "Expiry-Einstellungen" +## Konfiguration über den Application Customizer ### Voraussetzungen @@ -67,9 +67,18 @@ Beispiel der intern gespeicherten Gesamtstruktur (nicht vollständig in eines de 3. Der angemeldete Benutzer benötigt die SharePoint-Berechtigung `Listen verwalten`. 4. Das konfigurierte Ablaufdatumsfeld muss bereits vorhanden sein. Die Solution legt dieses Feld nicht an. -Nach der Installation erscheint in modernen Listen und Dokumentbibliotheken der Befehl -**Expiry-Einstellungen** in der Befehlsleiste. Falls der Befehl nicht sichtbar ist, zunächst prüfen, ob -die App in der Site installiert und die Berechtigung `Listen verwalten` vorhanden ist. +Der Einstellungsdialog wird ohne dauerhaftes Element in der Befehlsleiste durch den Application Customizer +bereitgestellt. Dazu an die URL einer modernen Listen- oder Bibliotheksansicht den Parameter +`expiryIndicatorSettings=1` anhängen: + +```text +/Shared Documents/Forms/AllItems.aspx?expiryIndicatorSettings=1 +``` + +Sind bereits andere Query-Parameter vorhanden, wird der Parameter mit `&expiryIndicatorSettings=1` ergänzt. +Nach dem Speichern wird der Parameter entfernt und die Ansicht mit der neuen Konfiguration geladen. Ohne den +Parameter bleibt der Application Customizer unsichtbar. Nur Benutzer mit `Listen verwalten` können den Dialog +öffnen und speichern. ### Interne Feldnamen ermitteln @@ -208,8 +217,16 @@ Die Regeln werden von oben nach unten geprüft. Dabei gilt: 3. Die erste passende Regel liefert Laufzeit und Farbregeln. 4. Existiert das Feld nicht oder passt kein Wert, werden Standardlaufzeit und Standard-Farbregeln verwendet. -Nach **Speichern** wird die Konfiguration pro Liste beziehungsweise Bibliothek in der ausgeblendeten Liste -`_ExpiryIndicatorConfiguration` der aktuellen Site gespeichert. Anschließend die Ansicht neu laden. +Nach **Speichern** wird die Konfiguration als JSON direkt in `ClientSideComponentProperties` des mit dem +ExpiryIndicator gebundenen lokalen Listenfeldes gespeichert. Damit verwendet die App denselben +Konfigurationsmechanismus wie andere SPFx-Extensions und benötigt keine zusätzliche Konfigurationsliste. +PortalSettings kann optional dieselben Component Properties verwalten, ist aber keine Voraussetzung für den +Betrieb des ExpiryIndicators. + +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 +nicht automatisch gelöscht. ### Anzeige aktivieren @@ -223,8 +240,9 @@ an der Ablaufdatumsspalte registriert: -ExpiryFieldInternalName 'CustomerExpiryDate' ``` -Der Wert von `ExpiryFieldInternalName` muss mit **Internes Ablaufdatumsfeld** in den Einstellungen -übereinstimmen. Das Command Set wird durch die Installation der App registriert. Danach die Listenansicht +Der Field Customizer und seine Component Properties werden am mit `ExpiryFieldInternalName` angegebenen lokalen +Listenfeld registriert. Der interne Name wird anschließend aus dieser Bindung ermittelt und im Einstellungsdialog +nur lesbar angezeigt. Das Command Set wird durch die Installation der App registriert. Danach die Listenansicht neu laden. ### Ablaufdatum um ein Jahr verlängern @@ -252,7 +270,7 @@ Das Paket wird als `sharepoint/solution/expiry-indicator.sppkg` erzeugt. 1. `expiry-indicator.sppkg` in den App Catalog der On-Premises-Farm laden. 2. Die App in der gewünschten Site installieren. 3. Eine moderne Liste/Bibliothek öffnen und wie im Abschnitt - **Konfiguration über "Expiry-Einstellungen"** beschrieben konfigurieren. + **Konfiguration über den Application Customizer** beschrieben konfigurieren. 4. Den Field Customizer mit der vorhandenen Ablaufdatumsspalte verbinden: ```powershell diff --git a/ToDo.md b/ToDo.md index e41ebd5..6f3e6d2 100644 --- a/ToDo.md +++ b/ToDo.md @@ -8,7 +8,7 @@ Eine SharePoint-Framework-Solution für SharePoint Server Subscription Edition a - Berechnung eines effektiven Ablaufdatums aus `Created`, wenn `ExpiryDate` leer ist, - frei konfigurierbare Laufzeit-, Schwellenwert- und Farbregeln pro Liste/Bibliothek, - einen Befehl **„Ablaufdatum +1 Jahr“** für ein oder mehrere ausgewählte Elemente, -- einen nur für Listenverwalter sichtbaren Befehl **„Expiry-Einstellungen“**. +- einen unsichtbar laufenden Application Customizer, der den Einstellungsdialog per URL-Parameter für Listenverwalter öffnet. ## Wichtige Randbedingung @@ -33,7 +33,7 @@ Die Berechnung arbeitet mit Kalenderjahren/-monaten/-tagen, nicht mit pauschalen ### 2. ListView Command Set -`ExpiryIndicatorCommandSet` stellt in der modernen Befehlsleiste zwei Aktionen bereit: +`ExpiryIndicatorCommandSet` stellt in der modernen Befehlsleiste eine fachliche Aktion bereit: - **Ablaufdatum +1 Jahr** - sichtbar, wenn mindestens ein Element gewählt wurde und `ExpiryDate` vorhanden ist, @@ -42,13 +42,16 @@ Die Berechnung arbeitet mit Kalenderjahren/-monaten/-tagen, nicht mit pauschalen - verwendet bei leerem `ExpiryDate` zunächst das berechnete effektive Ablaufdatum, - addiert ein Kalenderjahr und schreibt das Ergebnis nach `ExpiryDate`, - zeigt eine Zusammenfassung über erfolgreiche und fehlgeschlagene Änderungen. -- **Expiry-Einstellungen** - - nur sichtbar für Benutzer mit `ManageLists`-Berechtigung, - - öffnet einen Konfigurationsdialog für die aktuelle Liste/Bibliothek. -### 3. Konfiguration +### 3. Application Customizer -Die Konfiguration wird pro Liste anhand ihrer GUID gespeichert. Dafür verwendet die App eine eigene, versteckte Konfigurationsliste im aktuellen Web. Diese technische Liste darf von der Solution beziehungsweise beim ersten Speichern der Einstellungen erzeugt werden; die fachliche Spalte `ExpiryDate` bleibt davon unberührt. +`ExpiryIndicatorApplicationCustomizer` läuft ohne permanente Oberfläche. Auf einer modernen Listen- oder +Bibliotheksansicht öffnet `expiryIndicatorSettings=1` den Konfigurationsdialog für Benutzer mit +`ManageLists`-Berechtigung. Die Befehlsleiste enthält dadurch keinen Einstellungsbefehl. + +### 4. Konfiguration + +Die Konfiguration wird pro Liste als JSON in `ClientSideComponentProperties` des gebundenen lokalen Listenfeldes gespeichert. Eine zusätzliche Konfigurationsliste wird bei neuen Installationen nicht erzeugt. Für Upgrades darf eine bestehende `_ExpiryIndicatorConfiguration` ausschließlich als Lese-Fallback verwendet werden, bis die Konfiguration beim nächsten Speichern in die Component Properties migriert wurde. Die fachliche Spalte `ExpiryDate` bleibt davon unberührt. Konfigurierbar sind mindestens: @@ -90,10 +93,10 @@ Beispiel einer Laufzeitkonfiguration: } ``` -### 4. Aktivierung und Deployment +### 5. Aktivierung und Deployment - Erstellung eines `.sppkg`-Pakets mit eingebetteten Client-Side Assets. -- Registrierung des Command Sets für moderne generische Listen und Dokumentbibliotheken. +- Registrierung des Command Sets und des Settings-Application-Customizers. - Keine Provisionierung fachlicher Ablaufdatumsspalten, Site Columns oder Content Types. - Bereitstellung eines PowerShell-/CSOM-Aktivierungsskripts, das den Field Customizer mit einer als Parameter angegebenen vorhandenen Ablaufdatumsspalte verknüpft. - Bereitstellung eines entsprechenden Deaktivierungsskripts, das nur diese Verknüpfung und App-Custom-Actions entfernt, nicht aber fachliche Daten oder Spalten. @@ -106,7 +109,8 @@ Beispiel einer Laufzeitkonfiguration: - [ ] Regel-Engine für feldwertabhängige Laufzeiten und Farbschwellen implementieren. - [ ] REST-Service für Konfiguration und Elementaktualisierungen implementieren. - [ ] Field Customizer implementieren. -- [ ] ListView Command Set und Einstellungsdialog implementieren. +- [x] ListView Command Set und Einstellungsdialog implementieren. +- [x] Settings-Application-Customizer mit URL-Aktivierung implementieren. - [ ] Feature-XML für Command-Bar-Registrierung erstellen. - [ ] CSOM-Aktivierungs- und Deaktivierungsskripte für die vorhandene `ExpiryDate`-Spalte erstellen. - [ ] Lokalisierung Deutsch/Englisch ergänzen. diff --git a/config/config.json b/config/config.json index 76f3be3..c48ece7 100644 --- a/config/config.json +++ b/config/config.json @@ -17,6 +17,14 @@ "manifest": "./src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.manifest.json" } ] + }, + "expiry-indicator-application-customizer": { + "components": [ + { + "entrypoint": "./lib/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.js", + "manifest": "./src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.manifest.json" + } + ] } }, "externals": {}, @@ -25,4 +33,3 @@ "ExpiryIndicatorCommandSetStrings": "lib/extensions/expiryIndicatorCommandSet/loc/{locale}.js" } } - diff --git a/config/package-solution.json b/config/package-solution.json index 2b02331..e25e546 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -3,15 +3,15 @@ "solution": { "name": "expiry-indicator-client-side-solution", "id": "68bb6d2d-9895-45b4-88e8-b98835faa981", - "version": "1.0.8.0", + "version": "1.0.10.0", "includeClientSideAssets": true, "skipFeatureDeployment": false, "features": [ { - "title": "ExpiryIndicator command set registration", - "description": "Registers ExpiryIndicator commands for modern lists and document libraries. Does not provision ExpiryDate.", + "title": "ExpiryIndicator extension registration", + "description": "Registers ExpiryIndicator commands and its settings Application Customizer. Does not provision ExpiryDate.", "id": "913402af-ab9a-4974-9f86-5c2159ae41db", - "version": "1.0.8.0", + "version": "1.0.10.0", "assets": { "elementManifests": [ "elements.xml" diff --git a/package.json b/package.json index 9190091..4684a82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expiry-indicator", - "version": "1.0.8", + "version": "1.0.10", "private": true, "engines": { "node": ">=6.9.0 <9.0.0" @@ -13,6 +13,7 @@ "test": "gulp test" }, "dependencies": { + "@microsoft/sp-application-base": "1.4.1", "@microsoft/sp-core-library": "1.4.1", "@microsoft/sp-dialog": "1.4.1", "@microsoft/sp-http": "1.4.1", diff --git a/scripts/Enable-ExpiryIndicator.ps1 b/scripts/Enable-ExpiryIndicator.ps1 index ba98714..bb97acc 100644 --- a/scripts/Enable-ExpiryIndicator.ps1 +++ b/scripts/Enable-ExpiryIndicator.ps1 @@ -32,8 +32,13 @@ try { Write-Warning "Das Feld wurde über seinen Anzeigenamen gefunden. Verwende künftig den internen Namen '$($field.InternalName)'." } + $alreadyBound = $field.ClientSideComponentId -eq $FieldCustomizerComponentId $field.ClientSideComponentId = $FieldCustomizerComponentId - $field.ClientSideComponentProperties = '{}' + if (-not $alreadyBound -or [string]::IsNullOrWhiteSpace($field.ClientSideComponentProperties)) { + $field.ClientSideComponentProperties = (@{ + expiryField = $field.InternalName + } | ConvertTo-Json -Compress) + } $field.Update() # Remove the temporary list-scoped command registration used by older diff --git a/sharepoint/assets/elements.xml b/sharepoint/assets/elements.xml index 41f6dd0..be4a0e2 100644 --- a/sharepoint/assets/elements.xml +++ b/sharepoint/assets/elements.xml @@ -15,4 +15,9 @@ Location="ClientSideExtension.ListViewCommandSet.CommandBar" ClientSideComponentId="cd58f5d9-ffc8-4df7-a910-3054842d7abf" ClientSideComponentProperties="{}" /> + diff --git a/src/common/ExpiryConfigService.ts b/src/common/ExpiryConfigService.ts index 7abd7e9..456fbb1 100644 --- a/src/common/ExpiryConfigService.ts +++ b/src/common/ExpiryConfigService.ts @@ -17,8 +17,16 @@ interface IConfigurationItem { ExpiryIndicatorJson: string; } +interface IBoundFieldInfo { + id: string; + internalName: string; + entityType: string; + properties: string; +} + const CONFIGURATION_LIST_TITLE: string = '_ExpiryIndicatorConfiguration'; const CONFIGURATION_FIELD: string = 'ExpiryIndicatorJson'; +const FIELD_CUSTOMIZER_COMPONENT_ID: string = 'a555f4fc-d6a6-4421-8189-457449d9bbde'; export class ExpiryConfigService { private readonly _spHttpClient: SPHttpClient; @@ -30,59 +38,119 @@ export class ExpiryConfigService { } public getConfig(listId: string): Promise { - return this._getConfigurationList(false).then((list: IConfigurationListInfo | undefined): Promise => { - if (!list) { - return Promise.resolve(createDefaultConfig()); + return this._getBoundField(listId).then((field: IBoundFieldInfo): Promise => { + const propertyConfig: IExpiryConfig | undefined = this._parseProperties(field.properties, field.internalName); + if (propertyConfig) { + return Promise.resolve(propertyConfig); } - return this._getConfigurationItem(listId).then((item: IConfigurationItem | undefined): IExpiryConfig => { - if (!item || !item[CONFIGURATION_FIELD]) { - return createDefaultConfig(); - } - - try { - return normalizeConfig(JSON.parse(item[CONFIGURATION_FIELD])); - } catch (error) { - return createDefaultConfig(); + // Upgrade compatibility: read an existing hidden-list configuration, + // 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; } + const defaults: IExpiryConfig = createDefaultConfig(); + defaults.expiryField = field.internalName; + return defaults; }); }); } public saveConfig(listId: string, config: IExpiryConfig): Promise { - const normalized: IExpiryConfig = normalizeConfig(config); - return this._getConfigurationList(true).then((list: IConfigurationListInfo): Promise => { - return this._getConfigurationItem(listId).then((item: IConfigurationItem | undefined): Promise => { - const body: any = { - '__metadata': { 'type': list.entityType }, - 'Title': listId, - 'ExpiryIndicatorJson': JSON.stringify(normalized) - }; + return this._getBoundField(listId).then((field: IBoundFieldInfo): Promise => { + const normalized: IExpiryConfig = normalizeConfig(config); + normalized.expiryField = field.internalName; + const body: any = { + '__metadata': { 'type': field.entityType }, + 'ClientSideComponentProperties': JSON.stringify(normalized) + }; + const options: ISPHttpClientOptions = { + headers: this._headers('MERGE'), + body: JSON.stringify(body) + }; + const fieldId: string = field.id.replace(/[{}]/g, ''); + const url: string = this._listUrl(listId) + "/fields(guid'" + fieldId + "')"; + return this._spHttpClient.post(url, SPHttpClient.configurations.v1, options) + .then((response: SPHttpClientResponse): Promise => { + if (!response.ok) { + return responseError(response, 'Konfiguration konnte nicht am Field Customizer gespeichert werden') + .then((error: Error): Promise => Promise.reject(error)); + } + return Promise.resolve(); + }); + }); + } - const options: ISPHttpClientOptions = { - headers: this._headers(item ? 'MERGE' : undefined), - body: JSON.stringify(body) - }; - const url: string = this._configurationListUrl() + '/items' + (item ? '(' + item.Id + ')' : ''); + private _getLegacyConfig(listId: string): Promise { + return this._getConfigurationList().then((list: IConfigurationListInfo | undefined): Promise => { + if (!list) { + return Promise.resolve(undefined); + } - return this._spHttpClient.post(url, SPHttpClient.configurations.v1, options) - .then((response: SPHttpClientResponse): Promise => { - if (!response.ok) { - return responseError(response, 'Konfiguration konnte nicht gespeichert werden') - .then((error: Error): Promise => Promise.reject(error)); - } - return Promise.resolve(); - }); + return this._getConfigurationItem(listId).then((item: IConfigurationItem | undefined): IExpiryConfig | undefined => { + if (!item || !item[CONFIGURATION_FIELD]) { + return undefined; + } + + try { + return normalizeConfig(JSON.parse(item[CONFIGURATION_FIELD])); + } catch (error) { + return undefined; + } }); }); } - private _getConfigurationList(createIfMissing: boolean): Promise { + private _getBoundField(listId: string): Promise { + const url: string = this._listUrl(listId) + '/fields' + + '?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties'; + return this._spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this._headers() }) + .then((response: SPHttpClientResponse): Promise => { + if (!response.ok) { + return responseError(response, 'Field-Customizer-Bindung konnte nicht gelesen werden') + .then((error: Error): Promise => Promise.reject(error)); + } + return response.json().then((data: any): IBoundFieldInfo => { + const values: any[] = data.value || (data.d && data.d.results) || []; + for (let index: number = 0; index < values.length; index++) { + const source: any = values[index]; + const componentId: string = String(source.ClientSideComponentId || '') + .replace(/[{}]/g, '').toLowerCase(); + if (componentId === FIELD_CUSTOMIZER_COMPONENT_ID) { + return { + id: source.Id, + internalName: source.InternalName, + entityType: source.__metadata && source.__metadata.type ? source.__metadata.type : 'SP.Field', + properties: source.ClientSideComponentProperties || '' + }; + } + } + throw new Error('In dieser Liste ist kein ExpiryIndicator-Field-Customizer gebunden.'); + }); + }); + } + + private _parseProperties(value: string, internalName: string): IExpiryConfig | undefined { + if (!value || value.trim() === '' || value.trim() === '{}') { + return undefined; + } + try { + const config: IExpiryConfig = normalizeConfig(JSON.parse(value)); + config.expiryField = internalName; + return config; + } catch (error) { + return undefined; + } + } + + private _getConfigurationList(): Promise { const url: string = this._configurationListUrl() + '?$select=Id,ListItemEntityTypeFullName'; return this._spHttpClient.get(url, SPHttpClient.configurations.v1) .then((response: SPHttpClientResponse): Promise => { if (response.status === 404) { - return createIfMissing ? this._createConfigurationList() : Promise.resolve(undefined); + return Promise.resolve(undefined); } if (!response.ok) { return responseError(response, 'Konfigurationsliste konnte nicht gelesen werden') @@ -92,85 +160,6 @@ export class ExpiryConfigService { }); } - private _createConfigurationList(): Promise { - const body: any = { - '__metadata': { 'type': 'SP.List' }, - 'BaseTemplate': 100, - 'Description': 'Technische Konfiguration der ExpiryIndicator-App.', - 'Title': CONFIGURATION_LIST_TITLE - }; - const options: ISPHttpClientOptions = { - headers: this._headers(), - body: JSON.stringify(body) - }; - - return this._spHttpClient.post(this._webUrl + '/_api/web/lists', SPHttpClient.configurations.v1, options) - .then((response: SPHttpClientResponse): Promise => { - if (!response.ok) { - // Another request may have created the list in parallel. - return this._getConfigurationList(false).then((existing: IConfigurationListInfo | undefined): any => { - if (existing) { - return existing; - } - return responseError(response, 'Konfigurationsliste konnte nicht erstellt werden') - .then((error: Error): Promise => Promise.reject(error)); - }); - } - - return this._createConfigurationField() - .then((): Promise => this._hideConfigurationList()) - .then((): Promise => this._getConfigurationList(false)); - }); - } - - private _createConfigurationField(): Promise { - const body: any = { - '__metadata': { 'type': 'SP.Field' }, - 'FieldTypeKind': 3, - 'Required': false, - 'Title': CONFIGURATION_FIELD - }; - const options: ISPHttpClientOptions = { - headers: this._headers(), - body: JSON.stringify(body) - }; - - return this._spHttpClient.post( - this._configurationListUrl() + '/fields', - SPHttpClient.configurations.v1, - options - ).then((response: SPHttpClientResponse): Promise => { - if (!response.ok) { - return responseError(response, 'Konfigurationsfeld konnte nicht erstellt werden') - .then((error: Error): Promise => Promise.reject(error)); - } - return Promise.resolve(); - }); - } - - private _hideConfigurationList(): Promise { - const options: ISPHttpClientOptions = { - headers: this._headers('MERGE'), - body: JSON.stringify({ - '__metadata': { 'type': 'SP.List' }, - 'Hidden': true, - 'OnQuickLaunch': false - }) - }; - - return this._spHttpClient.post( - this._configurationListUrl(), - SPHttpClient.configurations.v1, - options - ).then((response: SPHttpClientResponse): Promise => { - if (!response.ok) { - return responseError(response, 'Konfigurationsliste konnte nicht ausgeblendet werden') - .then((error: Error): Promise => Promise.reject(error)); - } - return Promise.resolve(); - }); - } - private _getConfigurationItem(listId: string): Promise { const filter: string = escapeODataString(listId.toLowerCase()); const url: string = this._configurationListUrl() + '/items' + @@ -195,6 +184,10 @@ export class ExpiryConfigService { return this._webUrl + "/_api/web/lists/getbytitle('" + CONFIGURATION_LIST_TITLE + "')"; } + private _listUrl(listId: string): string { + return this._webUrl + "/_api/web/lists(guid'" + listId.replace(/[{}]/g, '') + "')"; + } + private _mapListInfo(data: any): IConfigurationListInfo { const source: any = data.d || data; return { diff --git a/src/extensions/expiryIndicator/ExpiryIndicatorFieldCustomizer.manifest.json b/src/extensions/expiryIndicator/ExpiryIndicatorFieldCustomizer.manifest.json index a5bd1fe..22f4608 100644 --- a/src/extensions/expiryIndicator/ExpiryIndicatorFieldCustomizer.manifest.json +++ b/src/extensions/expiryIndicator/ExpiryIndicatorFieldCustomizer.manifest.json @@ -4,7 +4,7 @@ "alias": "ExpiryIndicatorFieldCustomizer", "componentType": "Extension", "extensionType": "FieldCustomizer", - "version": "1.0.8", + "version": "1.0.10", "manifestVersion": 2, "requiresCustomScript": false } diff --git a/src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.manifest.json b/src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.manifest.json new file mode 100644 index 0000000..e5a448d --- /dev/null +++ b/src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.manifest.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", + "id": "95691218-bcb1-4fad-adda-02bb12014d15", + "alias": "ExpiryIndicatorApplicationCustomizer", + "componentType": "Extension", + "extensionType": "ApplicationCustomizer", + "version": "1.0.10", + "manifestVersion": 2, + "requiresCustomScript": false +} diff --git a/src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.ts b/src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.ts new file mode 100644 index 0000000..6e23f94 --- /dev/null +++ b/src/extensions/expiryIndicatorApplicationCustomizer/ExpiryIndicatorApplicationCustomizer.ts @@ -0,0 +1,79 @@ +import { override } from '@microsoft/decorators'; +import { BaseApplicationCustomizer } from '@microsoft/sp-application-base'; +import { Dialog } from '@microsoft/sp-dialog'; +import { SPPermission } from '@microsoft/sp-page-context'; +import { ExpiryConfigService } from '../../common/ExpiryConfigService'; +import { IExpiryConfig } from '../../common/ExpiryModels'; +import { ExpirySettingsDialog } from '../expiryIndicatorCommandSet/ExpirySettingsDialog'; +import * as strings from 'ExpiryIndicatorCommandSetStrings'; + +export interface IExpiryIndicatorApplicationCustomizerProperties { +} + +export default class ExpiryIndicatorApplicationCustomizer + extends BaseApplicationCustomizer { + + @override + public onInit(): Promise { + if (!this._isSettingsRequested()) { + return Promise.resolve(); + } + + if (!this.context.pageContext.list) { + Dialog.alert(strings.SettingsListRequired); + return Promise.resolve(); + } + + if (!this.context.pageContext.list.permissions.hasPermission(SPPermission.manageLists)) { + Dialog.alert(strings.SettingsPermissionRequired); + return Promise.resolve(); + } + + const listId: string = this.context.pageContext.list.id.toString(); + const service: ExpiryConfigService = new ExpiryConfigService( + this.context.spHttpClient, + this.context.pageContext.web.absoluteUrl + ); + + service.getConfig(listId).then((config: IExpiryConfig): void => { + const dialog: ExpirySettingsDialog = new ExpirySettingsDialog( + config, + (newConfig: IExpiryConfig): Promise => service.saveConfig(listId, newConfig) + ); + dialog.show().then((): void => { + const cleanUrl: string = this._removeSettingsParameter(); + if (dialog.wasSaved) { + window.location.replace(cleanUrl); + } else if (window.history && window.history.replaceState) { + window.history.replaceState(undefined, document.title, cleanUrl); + } + }); + }).catch((error: Error): void => { + const message: string = error && error.message ? error.message : String(error); + Dialog.alert(strings.SettingsLoadFailed + ': ' + message); + }); + + return Promise.resolve(); + } + + private _isSettingsRequested(): boolean { + const values: string[] = window.location.search.replace(/^\?/, '').split('&'); + for (let index: number = 0; index < values.length; index++) { + const pair: string[] = values[index].split('='); + if (decodeURIComponent(pair[0] || '').toLowerCase() === 'expiryindicatorsettings') { + const value: string = decodeURIComponent(pair.slice(1).join('=') || '1').toLowerCase(); + return value === '1' || value === 'true' || value === 'yes'; + } + } + return false; + } + + private _removeSettingsParameter(): string { + const values: string[] = window.location.search.replace(/^\?/, '').split('&'); + const remaining: string[] = values.filter((value: string): boolean => { + const name: string = decodeURIComponent((value.split('=')[0] || '')).toLowerCase(); + return name !== 'expiryindicatorsettings' && value !== ''; + }); + return window.location.pathname + (remaining.length ? '?' + remaining.join('&') : '') + window.location.hash; + } +} diff --git a/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.manifest.json b/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.manifest.json index 586d6c7..c4204a6 100644 --- a/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.manifest.json +++ b/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.manifest.json @@ -4,7 +4,7 @@ "alias": "ExpiryIndicatorCommandSet", "componentType": "Extension", "extensionType": "ListViewCommandSet", - "version": "1.0.8", + "version": "1.0.10", "manifestVersion": 2, "requiresCustomScript": false, "items": { @@ -14,13 +14,6 @@ "de-de": "Ablaufdatum +1 Jahr" }, "type": "command" - }, - "SETTINGS": { - "title": { - "default": "Expiry settings", - "de-de": "Expiry-Einstellungen" - }, - "type": "command" } } } diff --git a/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.ts b/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.ts index 950e27e..39cd05b 100644 --- a/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.ts +++ b/src/extensions/expiryIndicatorCommandSet/ExpiryIndicatorCommandSet.ts @@ -10,7 +10,6 @@ import { import { ExpiryConfigService } from '../../common/ExpiryConfigService'; import { ExpiryItemService } from '../../common/ExpiryItemService'; import { createDefaultConfig, IExpiryConfig, IUpdateResult } from '../../common/ExpiryModels'; -import { ExpirySettingsDialog } from './ExpirySettingsDialog'; import * as strings from 'ExpiryIndicatorCommandSetStrings'; export interface IExpiryIndicatorCommandSetProperties { @@ -24,7 +23,6 @@ export default class ExpiryIndicatorCommandSet private _itemService: ExpiryItemService; private _expiryFieldExists: boolean = false; private _canEdit: boolean = false; - private _canManage: boolean = false; @override public onInit(): Promise { @@ -37,7 +35,6 @@ export default class ExpiryIndicatorCommandSet this._configService = new ExpiryConfigService(this.context.spHttpClient, webUrl); this._itemService = new ExpiryItemService(this.context.spHttpClient, webUrl, listId); this._canEdit = this.context.pageContext.list.permissions.hasPermission(SPPermission.editListItems); - this._canManage = this.context.pageContext.list.permissions.hasPermission(SPPermission.manageLists); return this._configService.getConfig(listId).then((config: IExpiryConfig): Promise => { this._config = config; @@ -51,14 +48,9 @@ export default class ExpiryIndicatorCommandSet @override public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void { const extendCommand: Command = this.tryGetCommand('EXTEND_ONE_YEAR'); - const settingsCommand: Command = this.tryGetCommand('SETTINGS'); - if (extendCommand) { extendCommand.visible = this._expiryFieldExists && this._canEdit && event.selectedRows.length > 0; } - if (settingsCommand) { - settingsCommand.visible = this._canManage; - } } @override @@ -68,11 +60,6 @@ export default class ExpiryIndicatorCommandSet return; } - if (event.itemId === 'SETTINGS') { - this._openSettings(); - return; - } - throw new Error('Unknown command: ' + event.itemId); } @@ -115,20 +102,6 @@ export default class ExpiryIndicatorCommandSet } } - private _openSettings(): void { - const listId: string = this.context.pageContext.list.id.toString(); - const dialog: ExpirySettingsDialog = new ExpirySettingsDialog( - this._config, - (config: IExpiryConfig): Promise => { - return this._configService.saveConfig(listId, config).then((): Promise => { - this._config = config; - return this._refreshFieldState(); - }); - } - ); - dialog.show(); - } - private _refreshFieldState(): Promise { return this._itemService.fieldExists(this._config.expiryField).then((exists: boolean): void => { this._expiryFieldExists = exists; diff --git a/src/extensions/expiryIndicatorCommandSet/ExpirySettingsDialog.ts b/src/extensions/expiryIndicatorCommandSet/ExpirySettingsDialog.ts index 37a1532..d0167ba 100644 --- a/src/extensions/expiryIndicatorCommandSet/ExpirySettingsDialog.ts +++ b/src/extensions/expiryIndicatorCommandSet/ExpirySettingsDialog.ts @@ -10,6 +10,7 @@ import * as strings from 'ExpiryIndicatorCommandSetStrings'; export class ExpirySettingsDialog extends BaseDialog { private _expiryConfig: IExpiryConfig; private _save: (config: IExpiryConfig) => Promise; + private _wasSaved: boolean = false; public constructor(config: IExpiryConfig, save: (config: IExpiryConfig) => Promise) { super(); @@ -51,6 +52,7 @@ export class ExpirySettingsDialog extends BaseDialog { this._setValue('baseField', this._expiryConfig.baseField); this._setValue('expiryField', this._expiryConfig.expiryField); + (this._field('expiryField') as HTMLInputElement).readOnly = true; this._setValue('lifetimeValue', String(this._expiryConfig.default.lifeTime.value)); this._setValue('lifetimeUnit', this._expiryConfig.default.lifeTime.unit); this._setValue('defaultColumnRule', JSON.stringify(this._expiryConfig.default.columnRule, undefined, 2)); @@ -70,6 +72,10 @@ export class ExpirySettingsDialog extends BaseDialog { return { isBlocking: true }; } + public get wasSaved(): boolean { + return this._wasSaved; + } + private _saveForm(): void { const saveButton: HTMLButtonElement = this.domElement.querySelector('[data-action="save"]') as HTMLButtonElement; const errorElement: HTMLElement = this._field('error') as HTMLElement; @@ -96,6 +102,7 @@ export class ExpirySettingsDialog extends BaseDialog { saveButton.disabled = true; this._save(config).then((): void => { this._expiryConfig = config; + this._wasSaved = true; this.close(); }).catch((error: Error): void => { saveButton.disabled = false; diff --git a/src/extensions/expiryIndicatorCommandSet/loc/de-de.js b/src/extensions/expiryIndicatorCommandSet/loc/de-de.js index d19daef..3f5d727 100644 --- a/src/extensions/expiryIndicatorCommandSet/loc/de-de.js +++ b/src/extensions/expiryIndicatorCommandSet/loc/de-de.js @@ -15,5 +15,8 @@ define([], function() { "NoItemsSelected": "Es wurden keine gültigen Elemente ausgewählt.", "UpdateSummary": "Erfolgreich aktualisiert: {0}; Fehler: {1}", "ConfirmMessage": "Ablaufdatum für {0} Element(e) um ein Kalenderjahr verlängern?" + ,"SettingsListRequired": "Die ExpiryIndicator-Einstellungen m\u00fcssen aus einer modernen Listen- oder Bibliotheksansicht ge\u00f6ffnet werden." + ,"SettingsPermissionRequired": "Zum Bearbeiten der ExpiryIndicator-Einstellungen ist die Berechtigung zum Verwalten dieser Liste erforderlich." + ,"SettingsLoadFailed": "Die ExpiryIndicator-Einstellungen konnten nicht geladen werden" }; }); diff --git a/src/extensions/expiryIndicatorCommandSet/loc/en-us.js b/src/extensions/expiryIndicatorCommandSet/loc/en-us.js index 174a09c..ccaf56e 100644 --- a/src/extensions/expiryIndicatorCommandSet/loc/en-us.js +++ b/src/extensions/expiryIndicatorCommandSet/loc/en-us.js @@ -15,5 +15,8 @@ define([], function() { "NoItemsSelected": "No valid items were selected.", "UpdateSummary": "Successfully updated: {0}; errors: {1}", "ConfirmMessage": "Extend the expiry date for {0} item(s) by one calendar year?" + ,"SettingsListRequired": "Open the ExpiryIndicator settings from a modern list or document library view." + ,"SettingsPermissionRequired": "You need permission to manage this list to edit the ExpiryIndicator settings." + ,"SettingsLoadFailed": "The ExpiryIndicator settings could not be loaded" }; }); diff --git a/src/extensions/expiryIndicatorCommandSet/loc/mystrings.d.ts b/src/extensions/expiryIndicatorCommandSet/loc/mystrings.d.ts index acd5985..a7add12 100644 --- a/src/extensions/expiryIndicatorCommandSet/loc/mystrings.d.ts +++ b/src/extensions/expiryIndicatorCommandSet/loc/mystrings.d.ts @@ -14,6 +14,9 @@ declare interface IExpiryIndicatorCommandSetStrings { NoItemsSelected: string; UpdateSummary: string; ConfirmMessage: string; + SettingsListRequired: string; + SettingsPermissionRequired: string; + SettingsLoadFailed: string; } declare module 'ExpiryIndicatorCommandSetStrings' {