dev-3.0 #1

Merged
Torsten merged 5 commits from dev-3.0 into main 2026-08-18 20:48:16 +00:00
10 changed files with 94 additions and 15 deletions
Showing only changes of commit c529716a18 - Show all commits
+5 -1
View File
@@ -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:
+2 -2
View File
@@ -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.
+20 -1
View File
@@ -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]); }
+1 -1
View File
@@ -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
},
+20 -2
View File
@@ -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 }
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "custom-branding",
"version": "3.0.1",
"version": "3.0.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "custom-branding",
"version": "3.0.1",
"version": "3.0.2",
"private": true,
"main": "lib/index.js",
"engines": {
@@ -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',
+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.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.');
+4 -2
View File
@@ -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.');