From c0e7c316e31614e9292b69a0450b3ff850d9e725 Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Tue, 18 Aug 2026 17:13:33 +0200 Subject: [PATCH] =?UTF-8?q?Aktualisiere=20Versionsnummer=20auf=203.1.0=20i?= =?UTF-8?q?n=20README.md,=20package.json,=20package-lock.json=20und=20pack?= =?UTF-8?q?age-solution.json.=20F=C3=BCge=20CurrentNavigationSettingsProvi?= =?UTF-8?q?der=20hinzu=20und=20integriere=20zugeh=C3=B6rige=20Tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +- ToDo.md | 3 +- config/package-solution.json | 2 +- package-lock.json | 2 +- package.json | 2 +- .../CurrentNavigationSettingsProvider.ts | 135 ++++++++++++++++++ src/providers/ProviderRegistry.ts | 2 + tests/ProviderRegistry.test.ts | 19 ++- tests/validate-static-assets.js | 4 +- 9 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 src/providers/CurrentNavigationSettingsProvider.ts diff --git a/README.md b/README.md index 41d7aad..336088a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server | Eigenschaft | Wert | |---|---| -| Version | 3.0.4 | +| Version | 3.1.0 | | SharePoint Framework | 1.4.1 | | Build-Node | 8.17.0 | | npm | 6.x | @@ -26,6 +26,14 @@ Die frühere provisionierte `PortalSettings.aspx` und der leere Application Cust - Debug-Modus - schemaerhaltendes Speichern unbekannter Properties +### CurrentNavigation + +- Auswahl des Navigationstermsets mit demselben Termset-Picker wie beim MegaMenu +- Cache-Dauer, Cache-Version und Debug-Modus +- maximale Navigationstiefe und optionale Anzeige des Root-Terms +- direkte Aktivierung als zentrale SPSite.UserCustomAction +- Hinweise zu CurrentNavigation.Root und CurrentNavigation.Hidden + ### CustomBranding - zentrale `SPSite.UserCustomAction` @@ -56,6 +64,7 @@ PortalSettingsWebPart │ ├── ProviderRegistry │ ├── MegaMenuSettingsProvider +│ ├── CurrentNavigationSettingsProvider │ ├── CustomBrandingSettingsProvider │ └── ExpiryIndicatorSettingsProvider │ @@ -155,7 +164,7 @@ sharepoint/solution/portal-settings.sppkg ## Installation -1. `portal-settings.sppkg` im App Catalog durch Version `3.0.4.0` ersetzen. +1. `portal-settings.sppkg` im App Catalog durch Version `3.1.0.0` ersetzen. 2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren. 3. Eine moderne Seite erstellen. 4. Das WebPart **Portal Settings** einmal auf der Seite hinzufügen. diff --git a/ToDo.md b/ToDo.md index 7f578eb..b346713 100644 --- a/ToDo.md +++ b/ToDo.md @@ -2,7 +2,7 @@ Stand: 21.07.2026 Branch: `dev-3.0` -Zielversion: 3.0.4 +Zielversion: 3.1.0 ## Architektur @@ -33,6 +33,7 @@ Zielversion: 3.0.4 ## Provider - [x] MegaMenu vollständig auf Provider-Schema umgestellt. +- [x] CurrentNavigation mit Termset-Picker, Cache, Debug und Aktivierung integriert. - [x] Termset, Modus, Cache, Cache-Version und Debug abgedeckt. - [x] CustomBranding vollständig auf Schema 2 abgebildet. - [x] Fehlende CustomBranding-Site-Action kann beim ersten Speichern angelegt werden. diff --git a/config/package-solution.json b/config/package-solution.json index 0cd202b..a5509dd 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "portal-settings-client-side-solution", "id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286", - "version": "3.0.4.0", + "version": "3.1.0.0", "includeClientSideAssets": true, "skipFeatureDeployment": false }, diff --git a/package-lock.json b/package-lock.json index d855362..1f0ad71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "portal-settings", - "version": "3.0.4", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6bb17c6..23dde93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portal-settings", - "version": "3.0.4", + "version": "3.1.0", "private": true, "main": "lib/index.js", "engines": { diff --git a/src/providers/CurrentNavigationSettingsProvider.ts b/src/providers/CurrentNavigationSettingsProvider.ts new file mode 100644 index 0000000..8e91842 --- /dev/null +++ b/src/providers/CurrentNavigationSettingsProvider.ts @@ -0,0 +1,135 @@ +import { ISettingsProvider } from '../core/ProviderContracts'; + +export interface ICurrentNavigationSettings { + schemaVersion: number; + portalSettings: { + providerKey: string; + contractVersion: number; + minimumPortalSettingsVersion: string; + }; + termSetId: string; + termSetName: string; + cacheMinutes: number; + cacheVersion: string; + maxDepth: number; + showRoot: boolean; + debug: boolean; + [key: string]: any; // tslint:disable-line:no-any +} + +export const CurrentNavigationSettingsProvider: ISettingsProvider = { + key: 'currentnavigation', + displayName: 'Current Navigation', + description: 'Navigation für das jeweils aktuelle Root Web oder Subweb.', + iconName: 'BulletedTreeList', + schemaVersion: 1, + minimumPortalSettingsVersion: '3.1.0', + componentIds: ['e37b828f-4315-461a-b1b0-fcf991deda3d'], + storage: { + kind: 'siteUserCustomAction', + action: { + name: 'CurrentNavigation', + title: 'Current Navigation Application Customizer', + description: 'MSFT-Custom-Solution:CurrentNavigation', + location: 'ClientSideExtension.ApplicationCustomizer', + sequence: 110 + } + }, + sections: [ + { + key: 'navigation', title: 'Navigation', iconName: 'BranchFork', layout: 'twoColumns', + description: 'Termset und Darstellung der Navigation für die aktuelle Website.', + elements: [ + { + type: 'termSet', key: 'termSetId', path: 'termSetId', companionNamePath: 'termSetName', + label: 'Navigations-Termset', description: 'Das Termset enthält die Root- und Navigationsterms.', + required: true, columnSpan: 12 + }, + { + type: 'number', key: 'maxDepth', path: 'maxDepth', label: 'Maximale Tiefe', + description: 'Anzahl der gerenderten Ebenen ab dem Root-Term.', minimum: 1, maximum: 8, + suffix: 'Ebenen', columnSpan: 6 + }, + { + type: 'boolean', key: 'showRoot', path: 'showRoot', label: 'Root-Term anzeigen', + description: 'Zeigt den zugeordneten Root-Term als erste Ebene an.', columnSpan: 6 + }, + { + type: 'notice', appearance: 'info', title: 'Local Custom Properties', columnSpan: 12, + text: 'CurrentNavigation.Root = true ordnet einen Term über seine URL einer Website zu. CurrentNavigation.Hidden = true blendet einen Zweig aus.' + } + ] + }, + { + key: 'performance', title: 'Performance und Cache', iconName: 'SpeedHigh', layout: 'twoColumns', + elements: [ + { + type: 'number', key: 'cacheMinutes', path: 'cacheMinutes', label: 'Cache-Dauer', + description: 'Gültigkeitsdauer des Browser-Caches.', minimum: 1, maximum: 1440, + suffix: 'Minuten', columnSpan: 6 + }, + { + type: 'textField', key: 'cacheVersion', path: 'cacheVersion', label: 'Cache-Version', + description: 'Eine Änderung verwirft vorhandene Browser-Caches.', placeholder: '1', columnSpan: 6 + } + ] + }, + { + key: 'diagnostics', title: 'Diagnose', iconName: 'Diagnostic', layout: 'singleColumn', + collapsible: true, collapsedByDefault: true, + elements: [{ + type: 'boolean', key: 'debug', path: 'debug', label: 'Debug-Ausgaben aktivieren', + description: 'Schreibt technische Meldungen in die Browserkonsole.', columnSpan: 12 + }] + }, + { + key: 'preview', title: 'Zusammenfassung', layout: 'singleColumn', + elements: [ + { type: 'propertySummary', title: 'Aktuelle Werte', columnSpan: 12 }, + { type: 'jsonPreview', title: 'ClientSideComponentProperties', collapsedByDefault: true, columnSpan: 12 } + ] + } + ], + createDefault: (): ICurrentNavigationSettings => ({ + schemaVersion: 1, + portalSettings: { + providerKey: 'currentnavigation', contractVersion: 1, minimumPortalSettingsVersion: '3.1.0' + }, + termSetId: '', termSetName: '', cacheMinutes: 15, cacheVersion: '1', + maxDepth: 3, showRoot: false, debug: false + }), + normalize: (value: any): ICurrentNavigationSettings => { // tslint:disable-line:no-any + const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any + const result: any = {}; // tslint:disable-line:no-any + Object.keys(source).forEach((key: string): void => { result[key] = source[key]; }); + const minutes: number = Number(source.cacheMinutes); + const depth: number = Number(source.maxDepth); + result.schemaVersion = 1; + result.portalSettings = { + providerKey: 'currentnavigation', contractVersion: 1, minimumPortalSettingsVersion: '3.1.0' + }; + result.termSetId = typeof source.termSetId === 'string' ? source.termSetId.trim() : ''; + result.termSetName = typeof source.termSetName === 'string' ? source.termSetName.trim() : ''; + result.cacheMinutes = isFinite(minutes) ? Math.min(Math.max(Math.floor(minutes), 1), 1440) : 15; + result.cacheVersion = typeof source.cacheVersion === 'string' && source.cacheVersion.trim() + ? source.cacheVersion.trim() : '1'; + result.maxDepth = isFinite(depth) ? Math.min(Math.max(Math.floor(depth), 1), 8) : 3; + result.showRoot = source.showRoot === true; + result.debug = source.debug === true; + return result as ICurrentNavigationSettings; + }, + validate: (value: ICurrentNavigationSettings): string[] => { + const errors: string[] = []; + if (!value.termSetId) { errors.push('Bitte ein Navigations-Termset auswählen.'); } + if (value.termSetId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value.termSetId)) { + errors.push('Die Termset-ID ist keine gültige GUID.'); + } + if (value.cacheMinutes < 1 || value.cacheMinutes > 1440) { + errors.push('Die Cache-Dauer muss zwischen 1 und 1440 liegen.'); + } + if (value.maxDepth < 1 || value.maxDepth > 8) { + errors.push('Die maximale Tiefe muss zwischen 1 und 8 liegen.'); + } + return errors; + } +}; diff --git a/src/providers/ProviderRegistry.ts b/src/providers/ProviderRegistry.ts index 666de7e..4ed0c56 100644 --- a/src/providers/ProviderRegistry.ts +++ b/src/providers/ProviderRegistry.ts @@ -1,10 +1,12 @@ import { ISettingsProvider } from '../core/ProviderContracts'; import { CustomBrandingSettingsProvider } from './CustomBrandingSettingsProvider'; +import { CurrentNavigationSettingsProvider } from './CurrentNavigationSettingsProvider'; import { ExpiryIndicatorSettingsProvider } from './ExpiryIndicatorSettingsProvider'; import { MegaMenuSettingsProvider } from './MegaMenuSettingsProvider'; const Providers: Array> = [ // tslint:disable-line:no-any MegaMenuSettingsProvider, + CurrentNavigationSettingsProvider, CustomBrandingSettingsProvider, ExpiryIndicatorSettingsProvider ]; diff --git a/tests/ProviderRegistry.test.ts b/tests/ProviderRegistry.test.ts index 4a5bd25..3678b41 100644 --- a/tests/ProviderRegistry.test.ts +++ b/tests/ProviderRegistry.test.ts @@ -9,8 +9,9 @@ function assert(condition: boolean, message: string): void { } const providers: Array> = getSettingsProviders(); // tslint:disable-line:no-any -assert(providers.length === 3, 'Genau drei Provider werden erwartet.'); +assert(providers.length === 4, 'Genau vier Provider werden erwartet.'); assert(!!getSettingsProvider('megamenu'), 'MegaMenu-Provider fehlt.'); +assert(!!getSettingsProvider('currentnavigation'), 'CurrentNavigation-Provider fehlt.'); assert(!!getSettingsProvider('custombranding'), 'CustomBranding-Provider fehlt.'); assert(!!getSettingsProvider('expiryindicator'), 'ExpiryIndicator-Provider fehlt.'); @@ -31,6 +32,22 @@ assert(normalizedMega.menuMode === 'megaMenu', 'MegaMenu Modus-Fallback fehlt.') assert(normalizedMega.portalSettings.providerKey === 'megamenu', 'MegaMenu-Vertragsmarker fehlt.'); assert(normalizedMega.unknownFuture === 'keep', 'Unbekannte MegaMenu-Properties gehen verloren.'); +const current: ISettingsProvider = getSettingsProvider('currentnavigation'); // tslint:disable-line:no-any +assert(!!current.storage.action, 'CurrentNavigation-Aktivierungsdefinition fehlt.'); +assert(current.storage.action.name === 'CurrentNavigation', 'CurrentNavigation Action-Name ist nicht stabil.'); +assert(current.storage.action.sequence === 110, 'CurrentNavigation Action-Sequenz ist nicht stabil.'); +const normalizedCurrent: any = current.normalize({ // tslint:disable-line:no-any + cacheMinutes: 5000, cacheVersion: '', maxDepth: 99, showRoot: true, future: 'keep' +}); +assert(normalizedCurrent.cacheMinutes === 1440, 'CurrentNavigation Cache-Maximum greift nicht.'); +assert(normalizedCurrent.cacheVersion === '1', 'CurrentNavigation Cache-Fallback fehlt.'); +assert(normalizedCurrent.maxDepth === 8, 'CurrentNavigation Tiefenlimit greift nicht.'); +assert(normalizedCurrent.showRoot === true, 'CurrentNavigation Root-Anzeige geht verloren.'); +assert(normalizedCurrent.future === 'keep', 'Unbekannte CurrentNavigation-Properties gehen verloren.'); +const invalidCurrent: any = current.createDefault(); // tslint:disable-line:no-any +invalidCurrent.termSetName = 'Nur ein Name'; +assert(current.validate(invalidCurrent).length > 0, 'CurrentNavigation muss eine Termset-ID verlangen.'); + const branding: ISettingsProvider = getSettingsProvider('custombranding'); // tslint:disable-line:no-any assert(!!branding.storage.action, 'CustomBranding-Aktivierungsdefinition fehlt.'); assert(branding.storage.action.name === 'CustomBranding', 'CustomBranding Action-Name ist nicht stabil.'); diff --git a/tests/validate-static-assets.js b/tests/validate-static-assets.js index 1628d6d..5990c5e 100644 --- a/tests/validate-static-assets.js +++ b/tests/validate-static-assets.js @@ -21,8 +21,8 @@ var sourceFiles = [ 'src/ui/PortalSettingsApp.ts' ].map(read).join('\n'); -assert(packageJson.version === '3.0.4', 'Package-Version ist nicht 3.0.4.'); -assert(solution.version === '3.0.4.0', 'Solution-Version ist nicht 3.0.4.0.'); +assert(packageJson.version === '3.1.0', 'Package-Version ist nicht 3.1.0.'); +assert(solution.version === '3.1.0.0', 'Solution-Version ist nicht 3.1.0.0.'); assert(!solution.features, 'Alte ASPX-Feature-Assets sind noch registriert.'); assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.'); assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.');