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:
@@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
|---|---|
|
||||
| Version | 3.0.0 |
|
||||
| Version | 3.0.1 |
|
||||
| SharePoint Framework | 1.4.1 |
|
||||
| Build-Node | 8.17.0 |
|
||||
| npm | 6.x |
|
||||
@@ -29,6 +29,8 @@ Die frühere provisionierte `PortalSettings.aspx` und der leere Application Cust
|
||||
### CustomBranding
|
||||
|
||||
- zentrale `SPSite.UserCustomAction`
|
||||
- Aktivierung der fehlenden Site-Collection-Action direkt aus PortalSettings
|
||||
- versionierter `portalSettings`-Vertragsmarker mit Component-ID-Fallback
|
||||
- Aktivierung und Debug-Modus
|
||||
- CSS-Dateien inklusive `media`
|
||||
- Allowlist externer CSS-Hosts
|
||||
@@ -153,7 +155,7 @@ sharepoint/solution/portal-settings.sppkg
|
||||
|
||||
## 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.
|
||||
3. Eine moderne Seite erstellen.
|
||||
4. Das WebPart **Portal Settings** einmal auf der Seite hinzufügen.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Stand: 21.07.2026
|
||||
Branch: `dev-3.0`
|
||||
Zielversion: 3.0.0
|
||||
Zielversion: 3.0.1
|
||||
|
||||
## Architektur
|
||||
|
||||
@@ -35,6 +35,8 @@ Zielversion: 3.0.0
|
||||
- [x] MegaMenu vollständig auf Provider-Schema umgestellt.
|
||||
- [x] Termset, Modus, Cache, Cache-Version und Debug abgedeckt.
|
||||
- [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] ExpiryIndicator-Site-Collection-Standard abgebildet.
|
||||
- [x] Expiry-Bindungsinventar dargestellt.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"solution": {
|
||||
"name": "portal-settings-client-side-solution",
|
||||
"id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286",
|
||||
"version": "3.0.0.0",
|
||||
"version": "3.0.1.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": false
|
||||
},
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portal-settings",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portal-settings",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
|
||||
@@ -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; }
|
||||
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, {
|
||||
|
||||
@@ -28,6 +28,9 @@ assert(normalizedMega.menuMode === 'megaMenu', 'MegaMenu Modus-Fallback fehlt.')
|
||||
assert(normalizedMega.unknownFuture === 'keep', 'Unbekannte MegaMenu-Properties gehen verloren.');
|
||||
|
||||
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
|
||||
assert(normalizedBranding.placeholdertop.elements.length === 1, 'Legacy-Branding-Elemente werden nicht übernommen.');
|
||||
assert(normalizedBranding.future === 7, 'Unbekannte Branding-Properties gehen verloren.');
|
||||
|
||||
@@ -20,8 +20,8 @@ var sourceFiles = [
|
||||
'src/ui/PortalSettingsApp.ts'
|
||||
].map(read).join('\n');
|
||||
|
||||
assert(packageJson.version === '3.0.0', 'Package-Version ist nicht 3.0.0.');
|
||||
assert(solution.version === '3.0.0.0', 'Solution-Version ist nicht 3.0.0.0.');
|
||||
assert(packageJson.version === '3.0.1', 'Package-Version ist nicht 3.0.1.');
|
||||
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(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.');
|
||||
assert(manifest.componentType === 'WebPart', 'Komponente ist kein WebPart.');
|
||||
|
||||
Reference in New Issue
Block a user