Aktualisiere Versionsnummer auf 3.0.1 in README.md, package.json, package-lock.json und package-solution.json. Füge Unterstützung für zentrale Aktivierung von CustomBranding hinzu und verbessere die Logik zur Handhabung von UserCustomActions. Aktualisiere Tests zur Validierung der neuen Struktur.
This commit is contained in:
@@ -59,6 +59,15 @@ export interface ISettingsSection {
|
||||
export interface IProviderStorageDefinition {
|
||||
kind: SettingsStorageKind;
|
||||
propertyKey?: string;
|
||||
action?: IProviderActionDefinition;
|
||||
}
|
||||
|
||||
export interface IProviderActionDefinition {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
location: 'ClientSideExtension.ApplicationCustomizer';
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
export interface ISettingsProvider<TConfig> {
|
||||
@@ -97,6 +106,7 @@ export interface ILoadedSettingsProvider {
|
||||
config: any; // tslint:disable-line:no-any
|
||||
originalConfig: any; // tslint:disable-line:no-any
|
||||
context: IProviderLoadContext;
|
||||
registered: boolean;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ import { ISettingsProvider } from '../core/ProviderContracts';
|
||||
|
||||
export interface ICustomBrandingSettings {
|
||||
schemaVersion: number;
|
||||
portalSettings: {
|
||||
providerKey: string;
|
||||
contractVersion: number;
|
||||
minimumPortalSettingsVersion: string;
|
||||
};
|
||||
enabled: boolean;
|
||||
debug: boolean;
|
||||
allowedCssHosts: string[];
|
||||
@@ -19,7 +24,16 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
|
||||
schemaVersion: 2,
|
||||
minimumPortalSettingsVersion: '3.0.0',
|
||||
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: [
|
||||
{
|
||||
key: 'general', title: 'Allgemein', layout: 'twoColumns', iconName: 'Settings',
|
||||
@@ -70,6 +84,11 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
|
||||
],
|
||||
createDefault: (): ICustomBrandingSettings => ({
|
||||
schemaVersion: 2,
|
||||
portalSettings: {
|
||||
providerKey: 'custombranding',
|
||||
contractVersion: 1,
|
||||
minimumPortalSettingsVersion: '3.0.0'
|
||||
},
|
||||
enabled: true,
|
||||
debug: false,
|
||||
allowedCssHosts: [],
|
||||
@@ -82,6 +101,11 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
|
||||
const result: any = {}; // tslint:disable-line:no-any
|
||||
Object.keys(source).forEach((key: string): void => { result[key] = source[key]; });
|
||||
result.schemaVersion = 2;
|
||||
result.portalSettings = {
|
||||
providerKey: 'custombranding',
|
||||
contractVersion: 1,
|
||||
minimumPortalSettingsVersion: '3.0.0'
|
||||
};
|
||||
result.enabled = source.enabled !== false;
|
||||
result.debug = source.debug === true;
|
||||
result.allowedCssHosts = Array.isArray(source.allowedCssHosts) ? source.allowedCssHosts : [];
|
||||
|
||||
@@ -50,10 +50,14 @@ export class PortalSettingsDataService {
|
||||
if (errors.length > 0) { return Promise.reject(new Error(errors.join(' '))); }
|
||||
const merged: any = mergePreservingUnknown(item.originalConfig, item.provider.normalize(item.config));
|
||||
if (item.provider.storage.kind === 'siteUserCustomAction') {
|
||||
if (!item.context.action) { return Promise.reject(new Error('Die zentrale UserCustomAction wurde nicht gefunden.')); }
|
||||
return this.saveUserCustomAction(item.context.action, merged).then((): void => {
|
||||
const persist: Promise<IUserCustomActionInfo> = item.context.action
|
||||
? 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.originalConfig = cloneValue(merged);
|
||||
item.registered = true;
|
||||
item.dirty = false;
|
||||
});
|
||||
}
|
||||
@@ -181,9 +185,13 @@ export class PortalSettingsDataService {
|
||||
const context: IProviderLoadContext = {};
|
||||
if (provider.storage.kind === 'siteUserCustomAction') {
|
||||
const action: IUserCustomActionInfo = this.findAction(provider, actions);
|
||||
if (!action) { return undefined; }
|
||||
context.action = action;
|
||||
raw = this.parseJson(action.properties, {});
|
||||
if (!action && !provider.storage.action) { return undefined; }
|
||||
if (action) {
|
||||
context.action = action;
|
||||
raw = this.parseJson(action.properties, {});
|
||||
} else {
|
||||
raw = provider.createDefault();
|
||||
}
|
||||
} else {
|
||||
const propertyKey: string = provider.storage.propertyKey || '';
|
||||
const stored: any = this.getProperty(properties, propertyKey);
|
||||
@@ -202,6 +210,7 @@ export class PortalSettingsDataService {
|
||||
config: cloneValue(normalized),
|
||||
originalConfig: cloneValue(raw || normalized),
|
||||
context: context,
|
||||
registered: !!context.action || provider.storage.kind !== 'siteUserCustomAction',
|
||||
dirty: false
|
||||
};
|
||||
}
|
||||
@@ -240,6 +249,39 @@ export class PortalSettingsDataService {
|
||||
}).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> {
|
||||
if (!key) { return Promise.reject(new Error('Der Property-Bag-Schlüssel fehlt.')); }
|
||||
const body: any = { '__metadata': { 'type': 'SP.PropertyValues' } };
|
||||
@@ -251,7 +293,11 @@ export class PortalSettingsDataService {
|
||||
|
||||
private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined {
|
||||
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) {
|
||||
return actions[i];
|
||||
}
|
||||
@@ -321,12 +367,14 @@ export class PortalSettingsDataService {
|
||||
private encodePropertyName(value: string): string { return value.replace(/\./g, '_x002e_'); }
|
||||
|
||||
private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; }
|
||||
private writeHeaders(method: string): any {
|
||||
return {
|
||||
private writeHeaders(method?: string): any {
|
||||
const headers: any = {
|
||||
'Accept': '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> {
|
||||
|
||||
+31
-10
@@ -58,7 +58,7 @@ export class PortalSettingsApp {
|
||||
const hero: HTMLElement = createElement('header', 'ps3-hero');
|
||||
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('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');
|
||||
meta.appendChild(this.metric('Site Collection', this.dataService.getSiteUrl()));
|
||||
meta.appendChild(this.metric('Konfigurationsmodell', 'Providerbasiert'));
|
||||
@@ -78,7 +78,11 @@ export class PortalSettingsApp {
|
||||
this.items = items;
|
||||
this.activeKey = preferredKey && this.findItem(preferredKey) ? preferredKey : (items.length ? items[0].provider.key : '');
|
||||
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 => {
|
||||
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));
|
||||
header.appendChild(heading);
|
||||
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);
|
||||
|
||||
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);
|
||||
this.formRenderer.render(form, item, {
|
||||
changed: (): void => { this.updateSaveState(); this.renderTabs(); },
|
||||
@@ -126,7 +139,8 @@ export class PortalSettingsApp {
|
||||
const actions: HTMLElement = createElement('div', 'ps3-actionBar');
|
||||
const reset: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Änderungen verwerfen') as HTMLButtonElement;
|
||||
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());
|
||||
actions.appendChild(reset); actions.appendChild(this.saveButton); this.contentHost.appendChild(actions);
|
||||
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('p', '', failed
|
||||
? '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);
|
||||
}
|
||||
|
||||
private saveCurrent(): void {
|
||||
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);
|
||||
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.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 => {
|
||||
this.showStatus(error.message || String(error), 'danger', true); this.updateSaveState();
|
||||
});
|
||||
@@ -200,7 +221,7 @@ export class PortalSettingsApp {
|
||||
|
||||
private updateSaveState(): void {
|
||||
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 {
|
||||
@@ -284,7 +305,7 @@ export class PortalSettingsApp {
|
||||
provider.sections = siteItem.provider.sections.filter(section => section.key === 'dates' || section.key === 'rules' || section.key === 'preview');
|
||||
const editItem: ILoadedSettingsProvider = {
|
||||
provider: provider, config: provider.normalize(loaded.config), originalConfig: loaded.config,
|
||||
context: {}, dirty: false
|
||||
context: {}, registered: true, dirty: false
|
||||
};
|
||||
const editor: DynamicFormRenderer = new DynamicFormRenderer();
|
||||
editor.render(form, editItem, {
|
||||
|
||||
Reference in New Issue
Block a user