From c529716a18e87ea5c84d27fb723de84d33466ef3 Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Tue, 18 Aug 2026 16:09:16 +0200 Subject: [PATCH] feat: Update version to 3.0.2 and add default footer elements for portal settings --- README.md | 6 +++- ToDo.md | 4 +-- classic/custom-branding-classic.js | 21 +++++++++++++- config/package-solution.json | 2 +- deployment/add-custombranding.ps1 | 22 +++++++++++++-- package-lock.json | 2 +- package.json | 2 +- .../customBranding/BrandingConfig.ts | 28 ++++++++++++++++--- tests/BrandingConfig.test.js | 16 +++++++++++ tests/validate-static-assets.js | 6 ++-- 10 files changed, 94 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2f30e0a..9b6e12c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CustomBranding -CustomBranding 3.0.1 ist ein zentraler SPFx-1.4.1-Application-Customizer für SharePoint Server Subscription Edition. Die Solution lädt freigegebene Stylesheets und rendert eine kontrollierte Komponentenstruktur im oberen oder unteren SharePoint-Placeholder. Moderne und klassische Seiten verwenden dieselben `ClientSideComponentProperties`. +CustomBranding 3.0.2 ist ein zentraler SPFx-1.4.1-Application-Customizer für SharePoint Server Subscription Edition. Die Solution lädt freigegebene Stylesheets und rendert eine kontrollierte Komponentenstruktur im oberen oder unteren SharePoint-Placeholder. Moderne und klassische Seiten verwenden dieselben `ClientSideComponentProperties`. Die Konfiguration liegt in genau einer `SPSite.UserCustomAction` pro Site Collection. Es werden weder eine versteckte Liste noch ein Property Bag benötigt. Dadurch gilt das Branding automatisch für das Root Web, vorhandene Subwebs und später angelegte Subwebs. @@ -119,6 +119,10 @@ Das aktuelle Konfigurationsschema hat die Version 2: Bestehende 1.x-Konfigurationen mit einem Root-Array `elements` werden weiterhin als Top-Inhalt gelesen. Unbekannte Properties ignoriert die Runtime. Das vollständige Beispiel liegt unter `examples/custom-branding.example.json`. +Ist `placeholderbottom.elements` leer oder fehlt die Eigenschaft, rendert CustomBranding automatisch den bisherigen +**Portal Settings**-Link auf `~sitecollection/SitePages/PortalSettings.aspx`. Sobald eigene Footer-Elemente konfiguriert +sind, ersetzen sie diesen Standard. + ### Sicherheitsgrenzen Erlaubte Elemente: diff --git a/ToDo.md b/ToDo.md index 051cfb8..949e329 100644 --- a/ToDo.md +++ b/ToDo.md @@ -3,7 +3,7 @@ Analysestand: 20.07.2026 Umgesetzt: 20.07.2026 Ausgangsversion: 1.0.4 -Zielversion: 3.0.1 +Zielversion: 3.0.2 Status: Implementierung und lokale Qualitätssicherung abgeschlossen ## Zielbild @@ -25,7 +25,7 @@ CustomBranding wird pro Site Collection genau einmal über eine `SPSite.UserCust - [x] Bestehende 1.x-Konfigurationen mit Root-Property `elements` weiter unterstützt. - [x] `enabled`, `debug`, `allowedCssHosts`, `cssfiles`, Top und Bottom konsistent typisiert. - [x] Unbekannte Properties werden ignoriert; Warnungen erscheinen nur im Debug-Modus. -- [x] Keine feste Abhängigkeit oder Verlinkung zu PortalSettings mehr vorhanden. +- [x] Den bisherigen Portal-Settings-Link als sicheren Standard-Footer in der Runtime verankert. - [x] Versionierten, rein deklarativen PortalSettings-Vertragsmarker ergänzt. - [x] Runtime, Standardkonfiguration und Deployment-Skript auf denselben Providervertrag gebracht. - [x] Direkte Aktivierung aus PortalSettings 3.0.1 dokumentiert. diff --git a/classic/custom-branding-classic.js b/classic/custom-branding-classic.js index e44196e..63e5b8c 100644 --- a/classic/custom-branding-classic.js +++ b/classic/custom-branding-classic.js @@ -184,16 +184,35 @@ function normalize(raw) { raw = raw && typeof raw === 'object' ? raw : {}; + var configuredBottom = raw.placeholderbottom && Array.isArray(raw.placeholderbottom.elements) + ? raw.placeholderbottom.elements + : []; return { enabled: raw.enabled !== false, debug: raw.debug === true, allowedCssHosts: normalizeHosts(raw.allowedCssHosts), cssfiles: Array.isArray(raw.cssfiles) ? raw.cssfiles : [], top: raw.placeholdertop && Array.isArray(raw.placeholdertop.elements) ? raw.placeholdertop.elements : (Array.isArray(raw.elements) ? raw.elements : []), - bottom: raw.placeholderbottom && Array.isArray(raw.placeholderbottom.elements) ? raw.placeholderbottom.elements : [] + bottom: configuredBottom.length > 0 ? configuredBottom : defaultFooterElements() }; } + function defaultFooterElements() { + return [{ + type: 'div', + attributes: { role: 'contentinfo', 'aria-label': 'Portalverwaltung' }, + styles: { + 'background-color': '#323130', color: '#ffffff', padding: '12px 16px', + 'text-align': 'center', width: '100%', 'box-sizing': 'border-box' + }, + children: [{ + type: 'a', content: 'Portal Settings', + attributes: { href: '~sitecollection/SitePages/PortalSettings.aspx' }, + styles: { color: '#ffffff', 'text-decoration': 'underline' } + }] + }]; + } + function cleanup() { for (var i = 0; i < state.hosts.length; i++) { if (state.hosts[i].parentNode) { state.hosts[i].parentNode.removeChild(state.hosts[i]); } diff --git a/config/package-solution.json b/config/package-solution.json index 98f5e03..26a1f7e 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "custom-branding-client-side-solution", "id": "03a7c4de-e031-4b83-a683-5ca5c364166e", - "version": "3.0.1.0", + "version": "3.0.2.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/deployment/add-custombranding.ps1 b/deployment/add-custombranding.ps1 index d55f0c1..b8a8b07 100644 --- a/deployment/add-custombranding.ps1 +++ b/deployment/add-custombranding.ps1 @@ -39,6 +39,22 @@ $componentName = 'CustomBranding' $componentTitle = 'Custom Branding Application Customizer' $classicActionName = 'CustomBranding.Classic.ScriptLink' +function New-DefaultFooterElements { + return @(@{ + type = 'div' + attributes = @{ role = 'contentinfo'; 'aria-label' = 'Portalverwaltung' } + styles = @{ + 'background-color' = '#323130'; color = '#ffffff'; padding = '12px 16px' + 'text-align' = 'center'; width = '100%'; 'box-sizing' = 'border-box' + } + children = @(@{ + type = 'a'; content = 'Portal Settings' + attributes = @{ href = '~sitecollection/SitePages/PortalSettings.aspx' } + styles = @{ color = '#ffffff'; 'text-decoration' = 'underline' } + }) + }) +} + function New-DefaultCustomBrandingConfiguration { return @{ schemaVersion = 2 @@ -52,7 +68,7 @@ function New-DefaultCustomBrandingConfiguration { allowedCssHosts = @() cssfiles = @() placeholdertop = @{ elements = @() } - placeholderbottom = @{ elements = @() } + placeholderbottom = @{ elements = @(New-DefaultFooterElements) } } } @@ -133,7 +149,9 @@ try { minimumPortalSettingsVersion = '3.0.0' } if (-not $configuration.ContainsKey('placeholdertop')) { $configuration.placeholdertop = @{ elements = @() } } - if (-not $configuration.ContainsKey('placeholderbottom')) { $configuration.placeholderbottom = @{ elements = @() } } + if (-not $configuration.ContainsKey('placeholderbottom')) { + $configuration.placeholderbottom = @{ elements = @(New-DefaultFooterElements) } + } if (-not $configuration.ContainsKey('cssfiles')) { $configuration.cssfiles = @() } if (-not $configuration.ContainsKey('allowedCssHosts')) { $configuration.allowedCssHosts = @() } if ($PSBoundParameters.ContainsKey('EnableDebug')) { $configuration.debug = [bool]$EnableDebug.IsPresent } diff --git a/package-lock.json b/package-lock.json index 137e04d..5c418da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "custom-branding", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 57d4596..276f0f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "custom-branding", - "version": "3.0.1", + "version": "3.0.2", "private": true, "main": "lib/index.js", "engines": { diff --git a/src/extensions/customBranding/BrandingConfig.ts b/src/extensions/customBranding/BrandingConfig.ts index 5a02f27..e7bca10 100644 --- a/src/extensions/customBranding/BrandingConfig.ts +++ b/src/extensions/customBranding/BrandingConfig.ts @@ -46,7 +46,7 @@ export function normalizeBrandingConfig( } if (serializedLength > MaxBrandingConfigurationLength) { warnings.push('Configuration exceeds the maximum size and was ignored.'); - return { config: createEmptyConfig(), warnings: warnings }; + return { config: createEmptyConfig(siteCollectionUrl), warnings: warnings }; } const allowedCssHosts: string[] = normalizeHosts(raw.allowedCssHosts); @@ -54,9 +54,12 @@ export function normalizeBrandingConfig( const topSource: any[] = raw.placeholdertop && Array.isArray(raw.placeholdertop.elements) ? raw.placeholdertop.elements : (Array.isArray(raw.elements) ? raw.elements : []); - const bottomSource: any[] = raw.placeholderbottom && Array.isArray(raw.placeholderbottom.elements) + const configuredBottomSource: any[] = raw.placeholderbottom && Array.isArray(raw.placeholderbottom.elements) ? raw.placeholderbottom.elements : []; + const bottomSource: any[] = configuredBottomSource.length > 0 + ? configuredBottomSource + : createDefaultFooterElements(); const config: ICustomBrandingConfig = { schemaVersion: BrandingSchemaVersion, @@ -127,7 +130,8 @@ export function sanitizeStyle(propertyName: string, value: string): string | und return styleValue; } -function createEmptyConfig(): ICustomBrandingConfig { +function createEmptyConfig(siteCollectionUrl: string): ICustomBrandingConfig { + const state: INormalizationState = { count: 0, warnings: [], siteCollectionUrl: siteCollectionUrl || '' }; return { schemaVersion: BrandingSchemaVersion, portalSettings: createPortalSettingsDescriptor(), @@ -136,10 +140,26 @@ function createEmptyConfig(): ICustomBrandingConfig { allowedCssHosts: [], cssfiles: [], placeholdertop: { elements: [] }, - placeholderbottom: { elements: [] } + placeholderbottom: { elements: normalizeElements(createDefaultFooterElements(), 1, state) } }; } +function createDefaultFooterElements(): any[] { + return [{ + type: 'div', + attributes: { role: 'contentinfo', 'aria-label': 'Portalverwaltung' }, + styles: { + 'background-color': '#323130', color: '#ffffff', padding: '12px 16px', + 'text-align': 'center', width: '100%', 'box-sizing': 'border-box' + }, + children: [{ + type: 'a', content: 'Portal Settings', + attributes: { href: '~sitecollection/SitePages/PortalSettings.aspx' }, + styles: { color: '#ffffff', 'text-decoration': 'underline' } + }] + }]; +} + function createPortalSettingsDescriptor(): IPortalSettingsDescriptor { return { providerKey: 'custombranding', diff --git a/tests/BrandingConfig.test.js b/tests/BrandingConfig.test.js index 4ff1b03..2320f5a 100644 --- a/tests/BrandingConfig.test.js +++ b/tests/BrandingConfig.test.js @@ -21,6 +21,22 @@ assert(empty.config.schemaVersion === 2, 'Das aktuelle Schema muss Version 2 ver assert(empty.config.portalSettings.providerKey === 'custombranding', 'PortalSettings Provider-Key fehlt.'); assert(empty.config.portalSettings.contractVersion === 1, 'PortalSettings Contract-Version fehlt.'); assert(empty.config.enabled === true, 'Branding muss standardmaessig aktiviert sein.'); +assert(empty.config.placeholderbottom.elements.length === 1, 'Der Standard-Footer fehlt.'); +assert(empty.config.placeholderbottom.elements[0].children[0].content === 'Portal Settings', + 'Der Standard-Footer enthaelt nicht den Portal-Settings-Link.'); +assert(empty.config.placeholderbottom.elements[0].children[0].attributes.href === + siteUrl + '/SitePages/PortalSettings.aspx', 'Der Standard-Footer verweist nicht auf PortalSettings.'); + +var emptyBottom = normalize({ placeholderbottom: { elements: [] } }, siteUrl); +assert(emptyBottom.config.placeholderbottom.elements.length === 1, + 'Ein geleertes Footer-Array muss auf den Standard-Footer zurueckfallen.'); + +var customBottom = normalize({ + placeholderbottom: { elements: [{ type: 'span', content: 'Eigener Footer' }] } +}, siteUrl); +assert(customBottom.config.placeholderbottom.elements.length === 1 && + customBottom.config.placeholderbottom.elements[0].content === 'Eigener Footer', + 'Ein eigener Footer muss den Standard-Footer ersetzen.'); var legacy = normalize({ elements: [{ type: 'span', content: 'Alt' }] }, siteUrl); assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.'); diff --git a/tests/validate-static-assets.js b/tests/validate-static-assets.js index 80e5beb..cc32d5b 100644 --- a/tests/validate-static-assets.js +++ b/tests/validate-static-assets.js @@ -24,8 +24,8 @@ var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCus var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts'); var classicSource = read('classic/custom-branding-classic.js'); -assert(packageJson.version === '3.0.1', 'package.json hat nicht Version 3.0.1.'); -assert(solution.version === '3.0.1.0', 'Solution-Version ist inkonsistent.'); +assert(packageJson.version === '3.0.2', 'package.json hat nicht Version 3.0.2.'); +assert(solution.version === '3.0.2.0', 'Solution-Version ist inkonsistent.'); assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.'); assert(!solution.features, 'Die alte web-scoped Feature-Registrierung ist noch vorhanden.'); assert(!fs.existsSync(path.join(root, 'sharepoint/assets/elements.xml')), 'elements.xml muss entfernt sein.'); @@ -33,6 +33,8 @@ assert(serveText.indexOf('onclick') < 0, 'serve.json enthaelt ein Event-Attribut assert(serveText.indexOf('placeholdertop') >= 0, 'serve.json verwendet nicht das echte Schema.'); assert(rendererSource.indexOf('innerHTML') < 0 && appSource.indexOf('innerHTML') < 0, 'Der moderne Renderer darf innerHTML nicht verwenden.'); assert(classicSource.indexOf('innerHTML') < 0, 'Die Classic-Runtime darf innerHTML nicht verwenden.'); +assert(classicSource.indexOf('~sitecollection/SitePages/PortalSettings.aspx') >= 0, + 'Der Standard-Footer fehlt in der Classic-Runtime.'); assert(appSource.indexOf('changedEvent.remove') >= 0, 'Lifecycle-Cleanup fuer changedEvent fehlt.'); assert(appSource.indexOf('_onTopPlaceholderDisposed') >= 0 && appSource.indexOf('_onBottomPlaceholderDisposed') >= 0, 'Top- und Bottom-Placeholder brauchen unabhaengige Dispose-Handler.');