3 Commits
9 changed files with 71 additions and 17 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server
| Eigenschaft | Wert |
|---|---|
| Version | 3.2.4 |
| Version | 3.2.7 |
| SharePoint Framework | 1.4.1 |
| Build-Node | 8.17.0 |
| npm | 6.x |
@@ -171,7 +171,7 @@ sharepoint/solution/portal-settings.sppkg
## Installation
1. `portal-settings.sppkg` im App Catalog durch Version `3.2.4.0` ersetzen.
1. `portal-settings.sppkg` im App Catalog durch Version `3.2.7.0` ersetzen.
2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren.
3. Die Seite mit dem PnP-PowerShell-Skript erzeugen beziehungsweise reparieren:
+2 -2
View File
@@ -2,7 +2,7 @@
Stand: 21.07.2026
Branch: `dev-3.0`
Zielversion: 3.2.4
Zielversion: 3.2.7
## Architektur
@@ -79,7 +79,7 @@ Zielversion: 3.2.4
- [ ] Per PnP-Skript provisionierte `PortalSettings.aspx` und enthaltenes WebPart prüfen.
- [ ] MegaMenu-Termsets laden und speichern.
- [ ] CustomBranding laden und schemaerhaltend speichern.
- [ ] ExpiryIndicator-Standard speichern.
- [x] ExpiryIndicator-Standard über SharePoint-SE-kompatibles CSOM `ProcessQuery` speichern.
- [ ] Expiry-Bindung hinzufügen, lokal konfigurieren und wieder auf Vererbung stellen.
- [ ] Bedienung mit Tastatur, 200-Prozent-Zoom und Windows-Hochkontrast prüfen.
- [ ] Alte V2-Seite nach erfolgreicher Parallelabnahme archivieren oder entfernen.
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": {
"name": "portal-settings-client-side-solution",
"id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286",
"version": "3.2.4.0",
"version": "3.2.7.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "3.2.4",
"version": "3.2.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "3.2.4",
"version": "3.2.7",
"private": true,
"main": "lib/index.js",
"engines": {
@@ -13,6 +13,7 @@ export interface ICurrentNavigationSettings {
cacheVersion: string;
maxDepth: number;
showRoot: boolean;
initialExpansion: 'activePath' | 'expanded' | 'collapsed';
debug: boolean;
[key: string]: any; // tslint:disable-line:no-any
}
@@ -54,6 +55,15 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
type: 'boolean', key: 'showRoot', path: 'showRoot', label: 'Root-Term anzeigen',
description: 'Zeigt den zugeordneten Root-Term als erste Ebene an.', columnSpan: 6
},
{
type: 'choice', key: 'initialExpansion', path: 'initialExpansion', label: 'Initialer Aufklappzustand',
description: 'Legt fest, welche Zweige beim Laden der Navigation geöffnet sind.', columnSpan: 6,
options: [
{ key: 'activePath', text: 'Aktuellen Pfad aufklappen' },
{ key: 'expanded', text: 'Alles aufklappen' },
{ key: 'collapsed', text: 'Alles zuklappen' }
]
},
{
type: 'notice', appearance: 'info', title: 'Local Custom Properties', columnSpan: 12,
text: 'CurrentNavigation.Root = true ordnet einen Term über seine URL einer Website zu. CurrentNavigation.Hidden = true blendet einen Zweig aus.'
@@ -96,7 +106,7 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
providerKey: 'currentnavigation', contractVersion: 1, minimumPortalSettingsVersion: '3.1.0'
},
termSetId: '', termSetName: '', cacheMinutes: 15, cacheVersion: '1',
maxDepth: 3, showRoot: false, debug: false
maxDepth: 3, showRoot: false, initialExpansion: 'activePath', debug: false
}),
normalize: (value: any): ICurrentNavigationSettings => { // tslint:disable-line:no-any
const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any
@@ -115,6 +125,8 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
? source.cacheVersion.trim() : '1';
result.maxDepth = isFinite(depth) ? Math.min(Math.max(Math.floor(depth), 1), 8) : 3;
result.showRoot = source.showRoot === true;
result.initialExpansion = source.initialExpansion === 'expanded' || source.initialExpansion === 'collapsed'
? source.initialExpansion : 'activePath';
result.debug = source.debug === true;
return result as ICurrentNavigationSettings;
},
@@ -130,6 +142,9 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
if (value.maxDepth < 1 || value.maxDepth > 8) {
errors.push('Die maximale Tiefe muss zwischen 1 und 8 liegen.');
}
if (['activePath', 'expanded', 'collapsed'].indexOf(value.initialExpansion) < 0) {
errors.push('Der initiale Aufklappzustand ist ungültig.');
}
return errors;
}
};
+33 -6
View File
@@ -299,11 +299,35 @@ export class PortalSettingsDataService {
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' } };
body[this.encodePropertyName(key)] = value;
return this.context.spHttpClient.post(this.siteUrl + '/_api/web/AllProperties', SPHttpClient.configurations.v1, {
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body)
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Der Site-Collection-Standard konnte nicht gespeichert werden.'));
const requestXml: string = '<Request SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="PortalSettingsV3" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">' +
'<Actions>' +
'<Method Name="SetFieldValue" Id="5" ObjectPathId="4"><Parameters>' +
'<Parameter Type="String">' + this.escapeXml(key) + '</Parameter>' +
'<Parameter Type="String">' + this.escapeXml(value) + '</Parameter>' +
'</Parameters></Method>' +
'<Method Name="Update" Id="6" ObjectPathId="2" />' +
'</Actions>' +
'<ObjectPaths>' +
'<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />' +
'<Property Id="2" ParentId="0" Name="Web" />' +
'<Property Id="4" ParentId="2" Name="AllProperties" />' +
'</ObjectPaths>' +
'</Request>';
return this.context.spHttpClient.post(this.siteUrl + '/_vti_bin/client.svc/ProcessQuery', SPHttpClient.configurations.v1, {
headers: { 'Accept': 'application/json', 'Content-Type': 'text/xml;charset="UTF-8"' }, body: requestXml
}).then((response: SPHttpClientResponse): Promise<string> => {
if (!response.ok) {
return this.responseError(response, 'Der Site-Collection-Standard konnte nicht gespeichert werden.')
.then((error: Error): Promise<string> => Promise.reject(error));
}
return response.text();
}).then((text: string): void => {
const payload: any[] = text ? JSON.parse(text) : [];
if (payload.length && payload[0] && payload[0].ErrorInfo) {
throw new Error('Der Site-Collection-Standard konnte nicht gespeichert werden. ' +
(payload[0].ErrorInfo.ErrorMessage || 'SharePoint hat den CSOM-Aufruf abgelehnt.'));
}
});
}
private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined {
@@ -379,7 +403,10 @@ export class PortalSettingsDataService {
return url;
}
private normalizePropertyName(value: string): string { return String(value || '').replace(/_x002e_/gi, '.').toLowerCase(); }
private encodePropertyName(value: string): string { return value.replace(/\./g, '_x002e_'); }
private escapeXml(value: string): string {
return String(value || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}
private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; }
private writeHeaders(method?: string): any {
+5 -1
View File
@@ -38,16 +38,20 @@ assert(!!current.storage.action, 'CurrentNavigation-Aktivierungsdefinition fehlt
assert(current.storage.action.name === 'CurrentNavigation', 'CurrentNavigation Action-Name ist nicht stabil.');
assert(current.storage.action.sequence === 110, 'CurrentNavigation Action-Sequenz ist nicht stabil.');
const normalizedCurrent: any = current.normalize({ // tslint:disable-line:no-any
cacheMinutes: 5000, cacheVersion: '', maxDepth: 99, showRoot: true, future: 'keep'
cacheMinutes: 5000, cacheVersion: '', maxDepth: 99, showRoot: true,
initialExpansion: 'expanded', future: 'keep'
});
assert(normalizedCurrent.cacheMinutes === 1440, 'CurrentNavigation Cache-Maximum greift nicht.');
assert(normalizedCurrent.cacheVersion === '1', 'CurrentNavigation Cache-Fallback fehlt.');
assert(normalizedCurrent.maxDepth === 8, 'CurrentNavigation Tiefenlimit greift nicht.');
assert(normalizedCurrent.showRoot === true, 'CurrentNavigation Root-Anzeige geht verloren.');
assert(normalizedCurrent.initialExpansion === 'expanded', 'CurrentNavigation Aufklappzustand geht verloren.');
assert(normalizedCurrent.future === 'keep', 'Unbekannte CurrentNavigation-Properties gehen verloren.');
const invalidCurrent: any = current.createDefault(); // tslint:disable-line:no-any
invalidCurrent.termSetName = 'Nur ein Name';
assert(current.validate(invalidCurrent).length > 0, 'CurrentNavigation muss eine Termset-ID verlangen.');
assert(current.normalize({ initialExpansion: 'invalid' }).initialExpansion === 'activePath',
'CurrentNavigation benötigt einen kompatiblen Fallback für den Aufklappzustand.');
const branding: ISettingsProvider<any> = getSettingsProvider('custombranding'); // tslint:disable-line:no-any
assert(!!branding.storage.action, 'CustomBranding-Aktivierungsdefinition fehlt.');
+10 -2
View File
@@ -21,9 +21,13 @@ var sourceFiles = [
'src/ui/PortalSettingsApp.ts'
].map(read).join('\n');
var dataService = read('src/services/PortalSettingsDataService.ts');
var rootPropertyWriter = dataService.substring(
dataService.indexOf('private saveRootProperty'),
dataService.indexOf('private findAction')
);
assert(packageJson.version === '3.2.4', 'Package-Version ist nicht 3.2.4.');
assert(solution.version === '3.2.4.0', 'Solution-Version ist nicht 3.2.4.0.');
assert(packageJson.version === '3.2.7', 'Package-Version ist nicht 3.2.7.');
assert(solution.version === '3.2.7.0', 'Solution-Version ist nicht 3.2.7.0.');
assert(!solution.features, 'Das inkompatible Seiten-Provisioning-Feature darf nicht paketiert werden.');
assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.');
assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.');
@@ -36,6 +40,10 @@ assert(dataService.indexOf('/_api/site/rootweb/UserCustomActions') >= 0 &&
'Root-Web-Actions werden nicht für die Provider-Erkennung berücksichtigt.');
assert(dataService.indexOf('body.Sequence = provider.storage.action.sequence') >= 0,
'Bestehende Actions werden nicht auf die Provider-Sequence aktualisiert.');
assert(rootPropertyWriter.indexOf('/_vti_bin/client.svc/ProcessQuery') >= 0 &&
rootPropertyWriter.indexOf('SetFieldValue') >= 0 && rootPropertyWriter.indexOf('Name="Update"') >= 0 &&
rootPropertyWriter.indexOf('/_api/web/AllProperties') < 0,
'Root-Web-Properties müssen über den SharePoint-CSOM-Endpunkt gespeichert werden.');
['themePrimary', 'neutralPrimary', 'neutralLight', 'white'].forEach(function (slot) {
assert(styles.indexOf('[theme: ' + slot + ', default:') >= 0, 'Themeslot fehlt: ' + slot);
});