dev-3.0 #1

Merged
Torsten merged 10 commits from dev-3.0 into main 2026-08-18 20:49:15 +00:00
11 changed files with 138 additions and 28 deletions
Showing only changes of commit 37382d1dc0 - Show all commits
+4 -2
View File
@@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server
| Eigenschaft | Wert | | Eigenschaft | Wert |
|---|---| |---|---|
| Version | 3.0.0 | | Version | 3.0.1 |
| SharePoint Framework | 1.4.1 | | SharePoint Framework | 1.4.1 |
| Build-Node | 8.17.0 | | Build-Node | 8.17.0 |
| npm | 6.x | | npm | 6.x |
@@ -29,6 +29,8 @@ Die frühere provisionierte `PortalSettings.aspx` und der leere Application Cust
### CustomBranding ### CustomBranding
- zentrale `SPSite.UserCustomAction` - zentrale `SPSite.UserCustomAction`
- Aktivierung der fehlenden Site-Collection-Action direkt aus PortalSettings
- versionierter `portalSettings`-Vertragsmarker mit Component-ID-Fallback
- Aktivierung und Debug-Modus - Aktivierung und Debug-Modus
- CSS-Dateien inklusive `media` - CSS-Dateien inklusive `media`
- Allowlist externer CSS-Hosts - Allowlist externer CSS-Hosts
@@ -153,7 +155,7 @@ sharepoint/solution/portal-settings.sppkg
## Installation ## Installation
1. `portal-settings.sppkg` im App Catalog durch Version `3.0.0.0` ersetzen. 1. `portal-settings.sppkg` im App Catalog durch Version `3.0.1.0` ersetzen.
2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren. 2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren.
3. Eine moderne Seite erstellen. 3. Eine moderne Seite erstellen.
4. Das WebPart **Portal Settings** einmal auf der Seite hinzufügen. 4. Das WebPart **Portal Settings** einmal auf der Seite hinzufügen.
+3 -1
View File
@@ -2,7 +2,7 @@
Stand: 21.07.2026 Stand: 21.07.2026
Branch: `dev-3.0` Branch: `dev-3.0`
Zielversion: 3.0.0 Zielversion: 3.0.1
## Architektur ## Architektur
@@ -35,6 +35,8 @@ Zielversion: 3.0.0
- [x] MegaMenu vollständig auf Provider-Schema umgestellt. - [x] MegaMenu vollständig auf Provider-Schema umgestellt.
- [x] Termset, Modus, Cache, Cache-Version und Debug abgedeckt. - [x] Termset, Modus, Cache, Cache-Version und Debug abgedeckt.
- [x] CustomBranding vollständig auf Schema 2 abgebildet. - [x] CustomBranding vollständig auf Schema 2 abgebildet.
- [x] Fehlende CustomBranding-Site-Action kann beim ersten Speichern angelegt werden.
- [x] CustomBranding-Vertragsmarker und Component-ID-Fallback unterstützt.
- [x] Branding-Sicherheitsprüfung vor dem Speichern. - [x] Branding-Sicherheitsprüfung vor dem Speichern.
- [x] ExpiryIndicator-Site-Collection-Standard abgebildet. - [x] ExpiryIndicator-Site-Collection-Standard abgebildet.
- [x] Expiry-Bindungsinventar dargestellt. - [x] Expiry-Bindungsinventar dargestellt.
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "portal-settings-client-side-solution", "name": "portal-settings-client-side-solution",
"id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286", "id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286",
"version": "3.0.0.0", "version": "3.0.1.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": false "skipFeatureDeployment": false
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portal-settings", "name": "portal-settings",
"version": "3.0.0", "version": "3.0.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portal-settings", "name": "portal-settings",
"version": "3.0.0", "version": "3.0.1",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
+10
View File
@@ -59,6 +59,15 @@ export interface ISettingsSection {
export interface IProviderStorageDefinition { export interface IProviderStorageDefinition {
kind: SettingsStorageKind; kind: SettingsStorageKind;
propertyKey?: string; propertyKey?: string;
action?: IProviderActionDefinition;
}
export interface IProviderActionDefinition {
name: string;
title: string;
description: string;
location: 'ClientSideExtension.ApplicationCustomizer';
sequence: number;
} }
export interface ISettingsProvider<TConfig> { export interface ISettingsProvider<TConfig> {
@@ -97,6 +106,7 @@ export interface ILoadedSettingsProvider {
config: any; // tslint:disable-line:no-any config: any; // tslint:disable-line:no-any
originalConfig: any; // tslint:disable-line:no-any originalConfig: any; // tslint:disable-line:no-any
context: IProviderLoadContext; context: IProviderLoadContext;
registered: boolean;
dirty: boolean; dirty: boolean;
} }
@@ -2,6 +2,11 @@ import { ISettingsProvider } from '../core/ProviderContracts';
export interface ICustomBrandingSettings { export interface ICustomBrandingSettings {
schemaVersion: number; schemaVersion: number;
portalSettings: {
providerKey: string;
contractVersion: number;
minimumPortalSettingsVersion: string;
};
enabled: boolean; enabled: boolean;
debug: boolean; debug: boolean;
allowedCssHosts: string[]; allowedCssHosts: string[];
@@ -19,7 +24,16 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
schemaVersion: 2, schemaVersion: 2,
minimumPortalSettingsVersion: '3.0.0', minimumPortalSettingsVersion: '3.0.0',
componentIds: ['035ba968-6488-4d42-86b3-0470ffcc95b9'], componentIds: ['035ba968-6488-4d42-86b3-0470ffcc95b9'],
storage: { kind: 'siteUserCustomAction' }, storage: {
kind: 'siteUserCustomAction',
action: {
name: 'CustomBranding',
title: 'Custom Branding Application Customizer',
description: 'MSFT-Custom-Solution:CustomBranding',
location: 'ClientSideExtension.ApplicationCustomizer',
sequence: 100
}
},
sections: [ sections: [
{ {
key: 'general', title: 'Allgemein', layout: 'twoColumns', iconName: 'Settings', key: 'general', title: 'Allgemein', layout: 'twoColumns', iconName: 'Settings',
@@ -70,6 +84,11 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
], ],
createDefault: (): ICustomBrandingSettings => ({ createDefault: (): ICustomBrandingSettings => ({
schemaVersion: 2, schemaVersion: 2,
portalSettings: {
providerKey: 'custombranding',
contractVersion: 1,
minimumPortalSettingsVersion: '3.0.0'
},
enabled: true, enabled: true,
debug: false, debug: false,
allowedCssHosts: [], allowedCssHosts: [],
@@ -82,6 +101,11 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
const result: any = {}; // tslint:disable-line:no-any const result: any = {}; // tslint:disable-line:no-any
Object.keys(source).forEach((key: string): void => { result[key] = source[key]; }); Object.keys(source).forEach((key: string): void => { result[key] = source[key]; });
result.schemaVersion = 2; result.schemaVersion = 2;
result.portalSettings = {
providerKey: 'custombranding',
contractVersion: 1,
minimumPortalSettingsVersion: '3.0.0'
};
result.enabled = source.enabled !== false; result.enabled = source.enabled !== false;
result.debug = source.debug === true; result.debug = source.debug === true;
result.allowedCssHosts = Array.isArray(source.allowedCssHosts) ? source.allowedCssHosts : []; result.allowedCssHosts = Array.isArray(source.allowedCssHosts) ? source.allowedCssHosts : [];
+57 -9
View File
@@ -50,10 +50,14 @@ export class PortalSettingsDataService {
if (errors.length > 0) { return Promise.reject(new Error(errors.join(' '))); } if (errors.length > 0) { return Promise.reject(new Error(errors.join(' '))); }
const merged: any = mergePreservingUnknown(item.originalConfig, item.provider.normalize(item.config)); const merged: any = mergePreservingUnknown(item.originalConfig, item.provider.normalize(item.config));
if (item.provider.storage.kind === 'siteUserCustomAction') { if (item.provider.storage.kind === 'siteUserCustomAction') {
if (!item.context.action) { return Promise.reject(new Error('Die zentrale UserCustomAction wurde nicht gefunden.')); } const persist: Promise<IUserCustomActionInfo> = item.context.action
return this.saveUserCustomAction(item.context.action, merged).then((): void => { ? this.saveUserCustomAction(item.context.action, merged).then((): IUserCustomActionInfo => item.context.action)
: this.createUserCustomAction(item.provider, merged);
return persist.then((action: IUserCustomActionInfo): void => {
item.context.action = action;
item.config = item.provider.normalize(merged); item.config = item.provider.normalize(merged);
item.originalConfig = cloneValue(merged); item.originalConfig = cloneValue(merged);
item.registered = true;
item.dirty = false; item.dirty = false;
}); });
} }
@@ -181,9 +185,13 @@ export class PortalSettingsDataService {
const context: IProviderLoadContext = {}; const context: IProviderLoadContext = {};
if (provider.storage.kind === 'siteUserCustomAction') { if (provider.storage.kind === 'siteUserCustomAction') {
const action: IUserCustomActionInfo = this.findAction(provider, actions); const action: IUserCustomActionInfo = this.findAction(provider, actions);
if (!action) { return undefined; } if (!action && !provider.storage.action) { return undefined; }
context.action = action; if (action) {
raw = this.parseJson(action.properties, {}); context.action = action;
raw = this.parseJson(action.properties, {});
} else {
raw = provider.createDefault();
}
} else { } else {
const propertyKey: string = provider.storage.propertyKey || ''; const propertyKey: string = provider.storage.propertyKey || '';
const stored: any = this.getProperty(properties, propertyKey); const stored: any = this.getProperty(properties, propertyKey);
@@ -202,6 +210,7 @@ export class PortalSettingsDataService {
config: cloneValue(normalized), config: cloneValue(normalized),
originalConfig: cloneValue(raw || normalized), originalConfig: cloneValue(raw || normalized),
context: context, context: context,
registered: !!context.action || provider.storage.kind !== 'siteUserCustomAction',
dirty: false dirty: false
}; };
} }
@@ -240,6 +249,39 @@ export class PortalSettingsDataService {
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Konfiguration konnte nicht gespeichert werden.')); }).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Konfiguration konnte nicht gespeichert werden.'));
} }
private createUserCustomAction(provider: ISettingsProvider<any>, config: any): Promise<IUserCustomActionInfo> {
const definition: any = provider.storage.action;
if (!definition || !provider.componentIds.length) {
return Promise.reject(new Error('Für diesen Provider ist keine zentrale Registrierung definiert.'));
}
const body: any = {
'__metadata': { 'type': 'SP.UserCustomAction' },
'Name': definition.name,
'Title': definition.title,
'Description': definition.description,
'Location': definition.location,
'Sequence': definition.sequence,
'ClientSideComponentId': provider.componentIds[0],
'ClientSideComponentProperties': JSON.stringify(config)
};
return this.context.spHttpClient.post(this.siteUrl + '/_api/site/UserCustomActions', SPHttpClient.configurations.v1, {
headers: this.writeHeaders(), body: JSON.stringify(body)
}).then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response,
'CustomBranding konnte nicht für die Site Collection registriert werden.'))
.then((payload: any): IUserCustomActionInfo => {
const source: any = payload.d || payload;
return {
id: this.normalizeGuid(source.Id),
title: source.Title || definition.title,
name: source.Name || definition.name,
location: source.Location || definition.location,
componentId: this.normalizeGuid(source.ClientSideComponentId || provider.componentIds[0]),
properties: source.ClientSideComponentProperties || JSON.stringify(config),
entityType: source.__metadata && source.__metadata.type ? source.__metadata.type : 'SP.UserCustomAction'
};
});
}
private saveRootProperty(key: string, value: string): Promise<void> { private saveRootProperty(key: string, value: string): Promise<void> {
if (!key) { return Promise.reject(new Error('Der Property-Bag-Schlüssel fehlt.')); } if (!key) { return Promise.reject(new Error('Der Property-Bag-Schlüssel fehlt.')); }
const body: any = { '__metadata': { 'type': 'SP.PropertyValues' } }; const body: any = { '__metadata': { 'type': 'SP.PropertyValues' } };
@@ -251,7 +293,11 @@ export class PortalSettingsDataService {
private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined { private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined {
for (let i: number = 0; i < actions.length; i++) { for (let i: number = 0; i < actions.length; i++) {
if (provider.componentIds.indexOf(actions[i].componentId) >= 0 && const properties: any = this.parseJson(actions[i].properties, {});
const providerKey: string = properties && properties.portalSettings
? String(properties.portalSettings.providerKey || '').toLowerCase()
: '';
if ((provider.componentIds.indexOf(actions[i].componentId) >= 0 || providerKey === provider.key.toLowerCase()) &&
actions[i].location.indexOf('ClientSideExtension.ApplicationCustomizer') >= 0) { actions[i].location.indexOf('ClientSideExtension.ApplicationCustomizer') >= 0) {
return actions[i]; return actions[i];
} }
@@ -321,12 +367,14 @@ export class PortalSettingsDataService {
private encodePropertyName(value: string): string { return value.replace(/\./g, '_x002e_'); } private encodePropertyName(value: string): string { return value.replace(/\./g, '_x002e_'); }
private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; } private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; }
private writeHeaders(method: string): any { private writeHeaders(method?: string): any {
return { const headers: any = {
'Accept': 'application/json;odata=verbose', 'Accept': 'application/json;odata=verbose',
'Content-Type': 'application/json;odata=verbose', 'Content-Type': 'application/json;odata=verbose',
'OData-Version': '3.0', 'IF-MATCH': '*', 'X-HTTP-Method': method 'OData-Version': '3.0'
}; };
if (method) { headers['IF-MATCH'] = '*'; headers['X-HTTP-Method'] = method; }
return headers;
} }
private ensureJson(response: SPHttpClientResponse, message: string): Promise<any> { private ensureJson(response: SPHttpClientResponse, message: string): Promise<any> {
+31 -10
View File
@@ -58,7 +58,7 @@ export class PortalSettingsApp {
const hero: HTMLElement = createElement('header', 'ps3-hero'); const hero: HTMLElement = createElement('header', 'ps3-hero');
hero.appendChild(createElement('span', 'ps3-eyebrow', 'PORTAL SETTINGS V3')); hero.appendChild(createElement('span', 'ps3-eyebrow', 'PORTAL SETTINGS V3'));
hero.appendChild(createElement('h1', 'ps3-title', this.configuredTitle || 'Zentrale Verwaltung der Portal-Erweiterungen')); hero.appendChild(createElement('h1', 'ps3-title', this.configuredTitle || 'Zentrale Verwaltung der Portal-Erweiterungen'));
hero.appendChild(createElement('p', 'ps3-lead', 'Installierte Solutions werden automatisch erkannt. Ihre versionierten Provider erzeugen strukturierte und validierte Einstellungsseiten.')); hero.appendChild(createElement('p', 'ps3-lead', 'Registrierte Solutions werden automatisch erkannt. Unterstützte Provider können zentral aktiviert und über strukturierte, validierte Einstellungsseiten verwaltet werden.'));
const meta: HTMLElement = createElement('div', 'ps3-heroMeta'); const meta: HTMLElement = createElement('div', 'ps3-heroMeta');
meta.appendChild(this.metric('Site Collection', this.dataService.getSiteUrl())); meta.appendChild(this.metric('Site Collection', this.dataService.getSiteUrl()));
meta.appendChild(this.metric('Konfigurationsmodell', 'Providerbasiert')); meta.appendChild(this.metric('Konfigurationsmodell', 'Providerbasiert'));
@@ -78,7 +78,11 @@ export class PortalSettingsApp {
this.items = items; this.items = items;
this.activeKey = preferredKey && this.findItem(preferredKey) ? preferredKey : (items.length ? items[0].provider.key : ''); this.activeKey = preferredKey && this.findItem(preferredKey) ? preferredKey : (items.length ? items[0].provider.key : '');
this.renderTabs(); this.renderActive(); this.renderTabs(); this.renderActive();
this.showStatus(items.length ? items.length + ' Erweiterung(en) wurden erkannt.' : 'Keine unterstützte Erweiterung wurde aktiv registriert.', items.length ? 'success' : 'warning'); let registeredCount: number = 0;
for (let i: number = 0; i < items.length; i++) { if (items[i].registered) { registeredCount++; } }
this.showStatus(items.length
? registeredCount + ' Erweiterung(en) registriert, ' + (items.length - registeredCount) + ' zur Aktivierung verfügbar.'
: 'Keine unterstützte Erweiterung wurde aktiv registriert.', registeredCount ? 'success' : 'warning');
}).catch((error: Error): void => { }).catch((error: Error): void => {
if (!this.disposed) { this.showStatus(error.message || String(error), 'danger', true); this.renderEmpty(true); } if (!this.disposed) { this.showStatus(error.message || String(error), 'danger', true); this.renderEmpty(true); }
}); });
@@ -113,9 +117,18 @@ export class PortalSettingsApp {
heading.appendChild(createElement('p', 'ps3-providerDescription', item.provider.description)); heading.appendChild(createElement('p', 'ps3-providerDescription', item.provider.description));
header.appendChild(heading); header.appendChild(heading);
const storage: HTMLElement = createElement('div', 'ps3-storageBadge', const storage: HTMLElement = createElement('div', 'ps3-storageBadge',
item.provider.storage.kind === 'siteUserCustomAction' ? 'Site-Collection-Action' : 'Root-Web-Standard'); item.provider.storage.kind === 'siteUserCustomAction'
? (item.registered ? 'Site-Collection-Action' : 'Bereit zur Aktivierung')
: 'Root-Web-Standard');
header.appendChild(storage); this.contentHost.appendChild(header); header.appendChild(storage); this.contentHost.appendChild(header);
if (!item.registered) {
const registrationNotice: HTMLElement = createElement('div', 'ps3-status ps3-status-warning');
registrationNotice.setAttribute('role', 'status');
registrationNotice.textContent = 'CustomBranding ist noch nicht zentral registriert. Beim ersten Speichern legt PortalSettings die Site-Collection-Action an. Das CustomBranding-Paket muss zuvor im App Catalog bereitgestellt sein.';
this.contentHost.appendChild(registrationNotice);
}
const form: HTMLElement = createElement('div', 'ps3-form'); this.contentHost.appendChild(form); const form: HTMLElement = createElement('div', 'ps3-form'); this.contentHost.appendChild(form);
this.formRenderer.render(form, item, { this.formRenderer.render(form, item, {
changed: (): void => { this.updateSaveState(); this.renderTabs(); }, changed: (): void => { this.updateSaveState(); this.renderTabs(); },
@@ -126,7 +139,8 @@ export class PortalSettingsApp {
const actions: HTMLElement = createElement('div', 'ps3-actionBar'); const actions: HTMLElement = createElement('div', 'ps3-actionBar');
const reset: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Änderungen verwerfen') as HTMLButtonElement; const reset: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Änderungen verwerfen') as HTMLButtonElement;
reset.type = 'button'; reset.addEventListener('click', (): void => this.resetCurrent()); reset.type = 'button'; reset.addEventListener('click', (): void => this.resetCurrent());
this.saveButton = createElement('button', 'ps3-button ps3-button-primary', 'Konfiguration speichern') as HTMLButtonElement; this.saveButton = createElement('button', 'ps3-button ps3-button-primary',
item.registered ? 'Konfiguration speichern' : 'CustomBranding aktivieren') as HTMLButtonElement;
this.saveButton.type = 'button'; this.saveButton.addEventListener('click', (): void => this.saveCurrent()); this.saveButton.type = 'button'; this.saveButton.addEventListener('click', (): void => this.saveCurrent());
actions.appendChild(reset); actions.appendChild(this.saveButton); this.contentHost.appendChild(actions); actions.appendChild(reset); actions.appendChild(this.saveButton); this.contentHost.appendChild(actions);
this.updateSaveState(); this.updateSaveState();
@@ -138,19 +152,26 @@ export class PortalSettingsApp {
empty.appendChild(createElement('h2', '', failed ? 'PortalSettings konnte nicht geladen werden' : 'Keine aktiven Provider gefunden')); empty.appendChild(createElement('h2', '', failed ? 'PortalSettings konnte nicht geladen werden' : 'Keine aktiven Provider gefunden'));
empty.appendChild(createElement('p', '', failed empty.appendChild(createElement('p', '', failed
? 'Prüfen Sie die Statusmeldung und die REST-Berechtigungen.' ? 'Prüfen Sie die Statusmeldung und die REST-Berechtigungen.'
: 'MegaMenu und CustomBranding benötigen eine zentrale Site-Collection-Action. ExpiryIndicator wird über seinen Descriptor oder vorhandene Bindungen erkannt.')); : 'MegaMenu wird über seine zentrale Site-Collection-Action erkannt. CustomBranding kann direkt aktiviert werden; ExpiryIndicator wird über seinen Descriptor oder vorhandene Bindungen erkannt.'));
this.contentHost.appendChild(empty); this.contentHost.appendChild(empty);
} }
private saveCurrent(): void { private saveCurrent(): void {
const item: ILoadedSettingsProvider = this.currentItem(); const item: ILoadedSettingsProvider = this.currentItem();
if (!item || !item.dirty) { return; } if (!item || (item.registered && !item.dirty)) { return; }
const wasRegistered: boolean = item.registered;
const errors: string[] = item.provider.validate(item.config); const errors: string[] = item.provider.validate(item.config);
if (errors.length) { this.showStatus(errors.join(' '), 'danger', true); return; } if (errors.length) { this.showStatus(errors.join(' '), 'danger', true); return; }
if (!window.confirm('Konfiguration für „' + item.provider.displayName + '“ speichern?')) { return; } const question: string = wasRegistered
? 'Konfiguration für „' + item.provider.displayName + '“ speichern?'
: 'CustomBranding zentral für diese Site Collection aktivieren?';
if (!window.confirm(question)) { return; }
this.saveButton.disabled = true; this.showStatus('Konfiguration wird gespeichert …', 'info', true); this.saveButton.disabled = true; this.showStatus('Konfiguration wird gespeichert …', 'info', true);
this.dataService.save(item).then((): void => { this.dataService.save(item).then((): void => {
this.showStatus('Die Konfiguration wurde erfolgreich gespeichert.', 'success'); this.renderTabs(); this.renderActive(); this.showStatus(wasRegistered
? 'Die Konfiguration wurde erfolgreich gespeichert.'
: 'CustomBranding wurde zentral aktiviert und gespeichert.', 'success');
this.renderTabs(); this.renderActive();
}).catch((error: Error): void => { }).catch((error: Error): void => {
this.showStatus(error.message || String(error), 'danger', true); this.updateSaveState(); this.showStatus(error.message || String(error), 'danger', true); this.updateSaveState();
}); });
@@ -200,7 +221,7 @@ export class PortalSettingsApp {
private updateSaveState(): void { private updateSaveState(): void {
const item: ILoadedSettingsProvider = this.currentItem(); const item: ILoadedSettingsProvider = this.currentItem();
if (this.saveButton) { this.saveButton.disabled = !item || !item.dirty; } if (this.saveButton) { this.saveButton.disabled = !item || (item.registered && !item.dirty); }
} }
private openExpiryBindingDialog(item: ILoadedSettingsProvider): void { private openExpiryBindingDialog(item: ILoadedSettingsProvider): void {
@@ -284,7 +305,7 @@ export class PortalSettingsApp {
provider.sections = siteItem.provider.sections.filter(section => section.key === 'dates' || section.key === 'rules' || section.key === 'preview'); provider.sections = siteItem.provider.sections.filter(section => section.key === 'dates' || section.key === 'rules' || section.key === 'preview');
const editItem: ILoadedSettingsProvider = { const editItem: ILoadedSettingsProvider = {
provider: provider, config: provider.normalize(loaded.config), originalConfig: loaded.config, provider: provider, config: provider.normalize(loaded.config), originalConfig: loaded.config,
context: {}, dirty: false context: {}, registered: true, dirty: false
}; };
const editor: DynamicFormRenderer = new DynamicFormRenderer(); const editor: DynamicFormRenderer = new DynamicFormRenderer();
editor.render(form, editItem, { editor.render(form, editItem, {
+3
View File
@@ -28,6 +28,9 @@ assert(normalizedMega.menuMode === 'megaMenu', 'MegaMenu Modus-Fallback fehlt.')
assert(normalizedMega.unknownFuture === 'keep', 'Unbekannte MegaMenu-Properties gehen verloren.'); assert(normalizedMega.unknownFuture === 'keep', 'Unbekannte MegaMenu-Properties gehen verloren.');
const branding: ISettingsProvider<any> = getSettingsProvider('custombranding'); // tslint:disable-line:no-any const branding: ISettingsProvider<any> = 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.');
assert(branding.storage.action.location === 'ClientSideExtension.ApplicationCustomizer', 'CustomBranding Action-Location ist ungültig.');
const normalizedBranding: any = branding.normalize({ elements: [{ type: 'span' }], future: 7 }); // tslint:disable-line:no-any const normalizedBranding: any = branding.normalize({ elements: [{ type: 'span' }], future: 7 }); // tslint:disable-line:no-any
assert(normalizedBranding.placeholdertop.elements.length === 1, 'Legacy-Branding-Elemente werden nicht übernommen.'); assert(normalizedBranding.placeholdertop.elements.length === 1, 'Legacy-Branding-Elemente werden nicht übernommen.');
assert(normalizedBranding.future === 7, 'Unbekannte Branding-Properties gehen verloren.'); assert(normalizedBranding.future === 7, 'Unbekannte Branding-Properties gehen verloren.');
+2 -2
View File
@@ -20,8 +20,8 @@ var sourceFiles = [
'src/ui/PortalSettingsApp.ts' 'src/ui/PortalSettingsApp.ts'
].map(read).join('\n'); ].map(read).join('\n');
assert(packageJson.version === '3.0.0', 'Package-Version ist nicht 3.0.0.'); assert(packageJson.version === '3.0.1', 'Package-Version ist nicht 3.0.1.');
assert(solution.version === '3.0.0.0', 'Solution-Version ist nicht 3.0.0.0.'); assert(solution.version === '3.0.1.0', 'Solution-Version ist nicht 3.0.1.0.');
assert(!solution.features, 'Alte ASPX-Feature-Assets sind noch registriert.'); assert(!solution.features, 'Alte ASPX-Feature-Assets sind noch registriert.');
assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.'); assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.');
assert(manifest.componentType === 'WebPart', 'Komponente ist kein WebPart.'); assert(manifest.componentType === 'WebPart', 'Komponente ist kein WebPart.');