feat: Update version to 3.0.2 and add default footer elements for portal settings

This commit is contained in:
Torsten Brendgen
2026-08-18 16:09:16 +02:00
parent 6f548362e2
commit c529716a18
10 changed files with 94 additions and 15 deletions
+5 -1
View File
@@ -1,6 +1,6 @@
# CustomBranding # 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. 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`. 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 ### Sicherheitsgrenzen
Erlaubte Elemente: Erlaubte Elemente:
+2 -2
View File
@@ -3,7 +3,7 @@
Analysestand: 20.07.2026 Analysestand: 20.07.2026
Umgesetzt: 20.07.2026 Umgesetzt: 20.07.2026
Ausgangsversion: 1.0.4 Ausgangsversion: 1.0.4
Zielversion: 3.0.1 Zielversion: 3.0.2
Status: Implementierung und lokale Qualitätssicherung abgeschlossen Status: Implementierung und lokale Qualitätssicherung abgeschlossen
## Zielbild ## 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] Bestehende 1.x-Konfigurationen mit Root-Property `elements` weiter unterstützt.
- [x] `enabled`, `debug`, `allowedCssHosts`, `cssfiles`, Top und Bottom konsistent typisiert. - [x] `enabled`, `debug`, `allowedCssHosts`, `cssfiles`, Top und Bottom konsistent typisiert.
- [x] Unbekannte Properties werden ignoriert; Warnungen erscheinen nur im Debug-Modus. - [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] Versionierten, rein deklarativen PortalSettings-Vertragsmarker ergänzt.
- [x] Runtime, Standardkonfiguration und Deployment-Skript auf denselben Providervertrag gebracht. - [x] Runtime, Standardkonfiguration und Deployment-Skript auf denselben Providervertrag gebracht.
- [x] Direkte Aktivierung aus PortalSettings 3.0.1 dokumentiert. - [x] Direkte Aktivierung aus PortalSettings 3.0.1 dokumentiert.
+20 -1
View File
@@ -184,16 +184,35 @@
function normalize(raw) { function normalize(raw) {
raw = raw && typeof raw === 'object' ? raw : {}; raw = raw && typeof raw === 'object' ? raw : {};
var configuredBottom = raw.placeholderbottom && Array.isArray(raw.placeholderbottom.elements)
? raw.placeholderbottom.elements
: [];
return { return {
enabled: raw.enabled !== false, enabled: raw.enabled !== false,
debug: raw.debug === true, debug: raw.debug === true,
allowedCssHosts: normalizeHosts(raw.allowedCssHosts), allowedCssHosts: normalizeHosts(raw.allowedCssHosts),
cssfiles: Array.isArray(raw.cssfiles) ? raw.cssfiles : [], cssfiles: Array.isArray(raw.cssfiles) ? raw.cssfiles : [],
top: raw.placeholdertop && Array.isArray(raw.placeholdertop.elements) ? raw.placeholdertop.elements : (Array.isArray(raw.elements) ? raw.elements : []), 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() { function cleanup() {
for (var i = 0; i < state.hosts.length; i++) { for (var i = 0; i < state.hosts.length; i++) {
if (state.hosts[i].parentNode) { state.hosts[i].parentNode.removeChild(state.hosts[i]); } if (state.hosts[i].parentNode) { state.hosts[i].parentNode.removeChild(state.hosts[i]); }
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "custom-branding-client-side-solution", "name": "custom-branding-client-side-solution",
"id": "03a7c4de-e031-4b83-a683-5ca5c364166e", "id": "03a7c4de-e031-4b83-a683-5ca5c364166e",
"version": "3.0.1.0", "version": "3.0.2.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+20 -2
View File
@@ -39,6 +39,22 @@ $componentName = 'CustomBranding'
$componentTitle = 'Custom Branding Application Customizer' $componentTitle = 'Custom Branding Application Customizer'
$classicActionName = 'CustomBranding.Classic.ScriptLink' $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 { function New-DefaultCustomBrandingConfiguration {
return @{ return @{
schemaVersion = 2 schemaVersion = 2
@@ -52,7 +68,7 @@ function New-DefaultCustomBrandingConfiguration {
allowedCssHosts = @() allowedCssHosts = @()
cssfiles = @() cssfiles = @()
placeholdertop = @{ elements = @() } placeholdertop = @{ elements = @() }
placeholderbottom = @{ elements = @() } placeholderbottom = @{ elements = @(New-DefaultFooterElements) }
} }
} }
@@ -133,7 +149,9 @@ try {
minimumPortalSettingsVersion = '3.0.0' minimumPortalSettingsVersion = '3.0.0'
} }
if (-not $configuration.ContainsKey('placeholdertop')) { $configuration.placeholdertop = @{ elements = @() } } 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('cssfiles')) { $configuration.cssfiles = @() }
if (-not $configuration.ContainsKey('allowedCssHosts')) { $configuration.allowedCssHosts = @() } if (-not $configuration.ContainsKey('allowedCssHosts')) { $configuration.allowedCssHosts = @() }
if ($PSBoundParameters.ContainsKey('EnableDebug')) { $configuration.debug = [bool]$EnableDebug.IsPresent } if ($PSBoundParameters.ContainsKey('EnableDebug')) { $configuration.debug = [bool]$EnableDebug.IsPresent }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "custom-branding", "name": "custom-branding",
"version": "3.0.1", "version": "3.0.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "custom-branding", "name": "custom-branding",
"version": "3.0.1", "version": "3.0.2",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -46,7 +46,7 @@ export function normalizeBrandingConfig(
} }
if (serializedLength > MaxBrandingConfigurationLength) { if (serializedLength > MaxBrandingConfigurationLength) {
warnings.push('Configuration exceeds the maximum size and was ignored.'); 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); const allowedCssHosts: string[] = normalizeHosts(raw.allowedCssHosts);
@@ -54,9 +54,12 @@ export function normalizeBrandingConfig(
const topSource: any[] = raw.placeholdertop && Array.isArray(raw.placeholdertop.elements) const topSource: any[] = raw.placeholdertop && Array.isArray(raw.placeholdertop.elements)
? raw.placeholdertop.elements ? raw.placeholdertop.elements
: (Array.isArray(raw.elements) ? raw.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 ? raw.placeholderbottom.elements
: []; : [];
const bottomSource: any[] = configuredBottomSource.length > 0
? configuredBottomSource
: createDefaultFooterElements();
const config: ICustomBrandingConfig = { const config: ICustomBrandingConfig = {
schemaVersion: BrandingSchemaVersion, schemaVersion: BrandingSchemaVersion,
@@ -127,7 +130,8 @@ export function sanitizeStyle(propertyName: string, value: string): string | und
return styleValue; return styleValue;
} }
function createEmptyConfig(): ICustomBrandingConfig { function createEmptyConfig(siteCollectionUrl: string): ICustomBrandingConfig {
const state: INormalizationState = { count: 0, warnings: [], siteCollectionUrl: siteCollectionUrl || '' };
return { return {
schemaVersion: BrandingSchemaVersion, schemaVersion: BrandingSchemaVersion,
portalSettings: createPortalSettingsDescriptor(), portalSettings: createPortalSettingsDescriptor(),
@@ -136,10 +140,26 @@ function createEmptyConfig(): ICustomBrandingConfig {
allowedCssHosts: [], allowedCssHosts: [],
cssfiles: [], cssfiles: [],
placeholdertop: { elements: [] }, 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 { function createPortalSettingsDescriptor(): IPortalSettingsDescriptor {
return { return {
providerKey: 'custombranding', providerKey: 'custombranding',
+16
View File
@@ -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.providerKey === 'custombranding', 'PortalSettings Provider-Key fehlt.');
assert(empty.config.portalSettings.contractVersion === 1, 'PortalSettings Contract-Version fehlt.'); assert(empty.config.portalSettings.contractVersion === 1, 'PortalSettings Contract-Version fehlt.');
assert(empty.config.enabled === true, 'Branding muss standardmaessig aktiviert sein.'); 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); var legacy = normalize({ elements: [{ type: 'span', content: 'Alt' }] }, siteUrl);
assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.'); assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.');
+4 -2
View File
@@ -24,8 +24,8 @@ var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCus
var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts'); var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts');
var classicSource = read('classic/custom-branding-classic.js'); var classicSource = read('classic/custom-branding-classic.js');
assert(packageJson.version === '3.0.1', 'package.json hat nicht Version 3.0.1.'); assert(packageJson.version === '3.0.2', 'package.json hat nicht Version 3.0.2.');
assert(solution.version === '3.0.1.0', 'Solution-Version ist inkonsistent.'); assert(solution.version === '3.0.2.0', 'Solution-Version ist inkonsistent.');
assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.'); assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.');
assert(!solution.features, 'Die alte web-scoped Feature-Registrierung ist noch vorhanden.'); 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.'); 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(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(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('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('changedEvent.remove') >= 0, 'Lifecycle-Cleanup fuer changedEvent fehlt.');
assert(appSource.indexOf('_onTopPlaceholderDisposed') >= 0 && appSource.indexOf('_onBottomPlaceholderDisposed') >= 0, assert(appSource.indexOf('_onTopPlaceholderDisposed') >= 0 && appSource.indexOf('_onBottomPlaceholderDisposed') >= 0,
'Top- und Bottom-Placeholder brauchen unabhaengige Dispose-Handler.'); 'Top- und Bottom-Placeholder brauchen unabhaengige Dispose-Handler.');