Add SharePoint element manifests for PortalSettings pages

- Created elements-v1.1.xml, elements-v2.xml, and elements.xml to define the PortalSettingsPage modules for different versions.
- Added upgrade actions for v1.1 and v2 to apply the respective element manifests.
- Ensured that the PortalSettings.aspx file is included in the SitePages library with appropriate settings.
This commit is contained in:
Torsten Brendgen
2026-07-18 20:41:57 +02:00
parent 592e46805b
commit 5eedbe6ed5
15 changed files with 279 additions and 42 deletions

View File

@@ -1,7 +1,55 @@
## portal-settings
# PortalSettings
Separate Settings-App fuer bestehende SharePoint Extensions.
PortalSettings wird einmal im Root Web einer Site Collection installiert. Die Seite erkennt registrierte
Einstellungsanbieter automatisch und zeigt nur Tabs fuer tatsaechlich vorhandene Solutions.
Aktuell verwaltet sie:
Aktuell verwaltet die Seite dynamisch die aktiven Application Customizer fuer:
- Custom Branding
- Mega Menu
- ExpiryIndicator
## ExpiryIndicator 2.1
- Site-Collection-Standard im Property Bag des Root Webs,
- Inventar aller durch das Aktivierungsskript registrierten Bindungen in Root Web und Subwebs,
- Vererbung des zentralen Standards durch alle Bindungen,
- weiterhin moegliche lokale Ausnahmen in `ClientSideComponentProperties` des gebundenen Feldes,
- keine zusaetzliche Konfigurationsliste,
- Unterstuetzung alter Einzelregeln und verschachtelter V2-Regeln mit `and`/`or`,
- Validierung und Formatierung der vollstaendigen JSON-Konfiguration vor dem Speichern.
Die Prioritaet lautet: lokale Feldausnahme, Subweb-Override, Site-Collection-Standard, eingebauter Standard.
PortalSettings bleibt fuer den Betrieb des ExpiryIndicators optional.
## Erkennung
- Custom Branding und Mega Menu werden anhand ihrer aktiven, markierten Application-Customizer-UserCustomAction erkannt.
- ExpiryIndicator registriert einen Solution-Descriptor und sein Bindungsinventar im Property Bag des Root Webs.
Neue eigene Solutions werden zentral im Provider-Register in `PortalSettings.aspx` hinterlegt. Ein Provider
definiert Kennung, Anzeigename, Component-ID, Panel und Erkennungsart. So bleibt PortalSettings die gemeinsame
Admin-Oberflaeche, ohne dass nicht installierte Solutions leere Tabs erzeugen.
## Build
Die Solution verwendet die SPFx-1.4.1-Toolchain und wird reproduzierbar mit Node.js 8.17.0 gebaut:
```powershell
npm install
npm run package
```
Das Paket liegt danach unter `sharepoint/solution/portal-settings.sppkg`.
## Installation und Upgrade
1. `portal-settings.sppkg` im App Catalog durch Version `2.0.0.0` ersetzen.
2. PortalSettings im Root Web der Site Collection installieren beziehungsweise aktualisieren.
3. `expiry-indicator.sppkg` durch Version `2.1.0.0` ersetzen und die App aktualisieren.
4. `Enable-ExpiryIndicator.ps1` fuer jede verwendete Liste oder Bibliothek erneut ausfuehren. Dadurch werden
Descriptor und Bindungsinventar idempotent registriert; vorhandene lokale Konfigurationen bleiben erhalten.
5. `/SitePages/PortalSettings.aspx` mit geleertem Browsercache laden.
Der ExpiryIndicator-Tab erscheint erst, wenn mindestens eine Bindung registriert ist, ein zentraler Standard
existiert oder der Solution-Descriptor im Root Web vorhanden ist.

View File

@@ -3,7 +3,7 @@
"solution": {
"name": "portal-settings-client-side-solution",
"id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286",
"version": "1.0.0.0",
"version": "2.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"features": [
@@ -11,13 +11,16 @@
"title": "Portal Settings",
"description": "Provisioniert die zentrale Settings-Seite fuer aktive Portal-Erweiterungen.",
"id": "57bd9daf-48f9-49e2-aebd-11b15a0b5e93",
"version": "1.0.0.0",
"version": "2.0.0.0",
"assets": {
"elementManifests": [
"elements.xml"
"elements-v2.xml"
],
"elementFiles": [
"PortalSettings.aspx"
],
"upgradeActions": [
"upgrade-actions-v2.xml"
]
}
}

68
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "0.0.1",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -281,6 +281,14 @@
"sinon": "~1.17.3",
"sinon-chai": "~2.8.0",
"webpack": "~3.6.0"
},
"dependencies": {
"phantomjs-polyfill": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/phantomjs-polyfill/-/phantomjs-polyfill-0.0.2.tgz",
"integrity": "sha512-USErWAfES7rDxo6XgXkb8gt+0viV9lRb8OZNixEgjRBQjvVMyBeUZgy3HuE7vful8fv0UWb2wUD9jhkYB269wA==",
"dev": true
}
}
},
"@microsoft/gulp-core-build-sass": {
@@ -1227,6 +1235,13 @@
"react-dom": "15.6.2",
"scrollreveal": "3.3.6",
"tslib": "~1.8.0"
},
"dependencies": {
"@types/scrollreveal": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@types/scrollreveal/-/scrollreveal-0.0.2.tgz",
"integrity": "sha512-tLP/n3XGheuFYKfQsNlZbn8d6Gm7e81r9He131adAZltpYySyF99GLgfx4Qikl2ERuY+Pbp73F78PgEicZAzHA=="
}
}
},
"@microsoft/sp-webpart-workbench": {
@@ -1565,11 +1580,6 @@
"integrity": "sha512-xnLdvcPWgKF71R2DEQCZfXLutuAApHhJT+Y4/ptZ8FN610hSVT98TyLLkMjRm3VJ2BqUUXRjYtdZ12KvDXBT7A==",
"dev": true
},
"@types/scrollreveal": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/scrollreveal/-/scrollreveal-0.0.2.tgz",
"integrity": "sha512-tLP/n3XGheuFYKfQsNlZbn8d6Gm7e81r9He131adAZltpYySyF99GLgfx4Qikl2ERuY+Pbp73F78PgEicZAzHA=="
},
"@types/semver": {
"version": "5.3.33",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.3.33.tgz",
@@ -3495,7 +3505,7 @@
}
},
"charenc": {
"version": "0.0.1",
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"dev": true
@@ -4191,7 +4201,7 @@
}
},
"crypt": {
"version": "0.0.1",
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"dev": true
@@ -4816,15 +4826,6 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true
},
"duplexer2": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
"integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==",
"dev": true,
"requires": {
"readable-stream": "~1.1.9"
}
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@@ -6433,7 +6434,7 @@
}
},
"glob-escape": {
"version": "0.0.1",
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/glob-escape/-/glob-escape-0.0.2.tgz",
"integrity": "sha512-L/cXYz8x7qer1HAyUQ+mbjcUsJVdpRxpAf7CwqHoNBs9vTpABlGfNN4tzkDxt+u3Z7ZncVyKlCNPtzb0R/7WbA==",
"dev": true
@@ -11479,6 +11480,17 @@
"dev": true,
"requires": {
"duplexer2": "0.0.2"
},
"dependencies": {
"duplexer2": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
"integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==",
"dev": true,
"requires": {
"readable-stream": "~1.1.9"
}
}
}
},
"mute-stream": {
@@ -12552,12 +12564,6 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"dev": true
},
"phantomjs-polyfill": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/phantomjs-polyfill/-/phantomjs-polyfill-0.0.2.tgz",
"integrity": "sha512-USErWAfES7rDxo6XgXkb8gt+0viV9lRb8OZNixEgjRBQjvVMyBeUZgy3HuE7vful8fv0UWb2wUD9jhkYB269wA==",
"dev": true
},
"phantomjs-prebuilt": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz",
@@ -16329,6 +16335,14 @@
"center-align": "^0.1.1",
"right-align": "^0.1.1",
"wordwrap": "0.0.2"
},
"dependencies": {
"wordwrap": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
"integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==",
"dev": true
}
}
},
"uglify-js": {
@@ -16349,10 +16363,9 @@
"dev": true
},
"wordwrap": {
"version": "0.0.1",
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
"integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==",
"dev": true
"integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q=="
},
"yargs": {
"version": "3.10.0",
@@ -17615,4 +17628,3 @@
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "0.0.1",
"version": "2.0.0",
"private": true,
"main": "lib/index.js",
"engines": {
@@ -9,6 +9,7 @@
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"package": "gulp clean && gulp bundle --ship && gulp package-solution --ship",
"test": "gulp test"
},
"dependencies": {

View File

@@ -0,0 +1,86 @@
<%@ Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
<SharePoint:ScriptLink language="javascript" name="sp.js" runat="server" Defer="true" LoadAfterUI="true" Localizable="false" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
:root{--ps-bg:#f4f7fb;--ps-surface:#fff;--ps-border:#d8e1ec;--ps-ink:#16263d;--ps-muted:#607086;--ps-accent:#0f6cbd;--ps-soft:#eaf2fb;--ps-success:#107c10;--ps-warning:#a15c00;--ps-danger:#b42318;--ps-shadow:0 16px 34px rgba(20,35,59,.08);--ps-radius:18px}
.ps-page{background:linear-gradient(180deg,#f9fbfe 0%,var(--ps-bg) 100%);padding:28px 0 40px}.ps-shell{width:min(1240px,calc(100% - 32px));margin:0 auto}.ps-hero{background:linear-gradient(135deg,#123660 0%,#0f6cbd 60%,#58a3e0 100%);color:#fff;border-radius:24px;padding:28px 30px;box-shadow:var(--ps-shadow)}.ps-hero h1{margin:0 0 10px;font-size:34px;line-height:1.1}.ps-hero p{margin:0;max-width:860px;line-height:1.6;color:rgba(255,255,255,.92)}.ps-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.16em;opacity:.85;margin-bottom:12px}.ps-status-host{min-height:56px;margin-bottom:18px}.ps-status{padding:14px 16px;border-radius:12px;border:1px solid transparent;background:#fff;box-shadow:0 10px 24px rgba(20,35,59,.06)}.ps-status-info{color:var(--ps-accent);background:#f3f8fe;border-color:rgba(15,108,189,.2)}.ps-status-success{color:var(--ps-success);background:#eff8ef;border-color:rgba(16,124,16,.2)}.ps-status-warning{color:var(--ps-warning);background:#fff6eb;border-color:rgba(161,92,0,.2)}.ps-status-error{color:var(--ps-danger);background:#fff1f0;border-color:rgba(180,35,24,.2)}.ps-metrics,.ps-grid{display:grid;gap:16px;margin-top:18px}.ps-metrics{grid-template-columns:repeat(4,minmax(0,1fr))}.ps-grid{grid-template-columns:repeat(12,minmax(0,1fr))}.ps-card,.ps-panel,.ps-metric,.ps-empty{background:var(--ps-surface);border:1px solid var(--ps-border);border-radius:var(--ps-radius);box-shadow:0 10px 24px rgba(20,35,59,.05)}.ps-metric{padding:16px 18px}.ps-metric label{display:block;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--ps-muted);margin-bottom:8px}.ps-metric strong{display:block;font-size:18px;line-height:1.45;word-break:break-word}.ps-tabs{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}.ps-tab,.ps-btn{height:42px;padding:0 18px;border-radius:999px;border:1px solid var(--ps-border);background:#fff;color:var(--ps-ink);font-size:14px;font-weight:600;cursor:pointer}.ps-tab.is-active{border-color:var(--ps-accent);background:var(--ps-soft);color:var(--ps-accent)}.ps-empty{margin-top:18px;padding:24px;color:var(--ps-muted);line-height:1.7}.ps-panel{margin-top:18px;padding:22px}.ps-panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:14px;margin-bottom:18px}.ps-panel-head h2,.ps-card h3{margin:0 0 8px}.ps-panel-head p,.ps-card p,.ps-note,.ps-help{margin:0;color:var(--ps-muted);line-height:1.6}.ps-chip{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:28px;padding:0 10px;border-radius:999px;background:var(--ps-soft);color:var(--ps-accent);font-size:12px;font-weight:700}.ps-card{padding:20px}.ps-span-4{grid-column:span 4}.ps-span-6{grid-column:span 6}.ps-span-8{grid-column:span 8}.ps-span-12{grid-column:span 12}.ps-meta{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:14px 0}.ps-meta div{padding:12px;border:1px solid var(--ps-border);border-radius:12px;background:#f8fbff}.ps-meta label{display:block;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--ps-muted);margin-bottom:6px}.ps-meta strong{display:block;font-size:14px;line-height:1.55;word-break:break-word}.ps-label{display:block;margin:0 0 8px;font-size:13px;font-weight:600}.ps-input,.ps-textarea,.ps-preview{width:100%;box-sizing:border-box;border:1px solid var(--ps-border);border-radius:12px;background:#fbfdff;color:var(--ps-ink)}.ps-input{height:44px;padding:0 14px;font-size:14px}.ps-textarea,.ps-preview{min-height:260px;padding:14px;font:13px/1.6 Consolas,'Courier New',monospace;white-space:pre-wrap;overflow:auto}.ps-inline{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center}.ps-list{display:grid;gap:10px;margin-top:14px}.ps-item,.ps-list-empty{border:1px solid var(--ps-border);border-radius:12px;background:#fbfdff}.ps-item{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:14px}.ps-list-empty{padding:18px;text-align:center;color:var(--ps-muted)}.ps-path{font:13px/1.6 Consolas,'Courier New',monospace;word-break:break-all}.ps-actions{display:flex;gap:10px;flex-wrap:wrap}.ps-actions-bar{position:sticky;bottom:16px;z-index:4;display:flex;justify-content:space-between;align-items:center;gap:14px;margin-top:18px;padding:16px 18px;border:1px solid rgba(15,108,189,.14);border-radius:20px;background:rgba(255,255,255,.92);box-shadow:0 16px 32px rgba(20,35,59,.12)}.ps-btn-primary{background:linear-gradient(135deg,var(--ps-accent) 0%,#1b86e0 100%);border-color:var(--ps-accent);color:#fff}.ps-btn-quiet{background:#f7faff}.ps-btn-danger{background:#fff6f5;border-color:rgba(180,35,24,.18);color:var(--ps-danger)}.ps-check{display:flex;align-items:center;gap:10px;min-height:44px}.ps-check input{width:18px;height:18px}.ps-note{margin-top:14px;padding:14px 16px;border-radius:12px;background:#f8fbff;border:1px solid var(--ps-border)}
@media (max-width:1080px){.ps-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.ps-span-4,.ps-span-6,.ps-span-8{grid-column:span 12}.ps-meta{grid-template-columns:minmax(0,1fr)}}@media (max-width:720px){.ps-shell{width:calc(100% - 20px)}.ps-metrics{grid-template-columns:minmax(0,1fr)}.ps-inline,.ps-actions-bar{display:grid;grid-template-columns:minmax(0,1fr)}.ps-actions .ps-btn,.ps-inline .ps-btn,.ps-tab{width:100%}.ps-hero h1{font-size:28px}}
</style>
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
<div class="ps-page"><div class="ps-shell"><div id="statusMessage" class="ps-status-host"></div><section class="ps-hero"><div class="ps-eyebrow">Portal Settings</div><h1>Zentrale Verwaltung fuer aktive Portal-Erweiterungen</h1><p>Diese Seite erkennt installierte Einstellungsanbieter automatisch und zeigt nur deren Tabs. Konfigurationen werden in UserCustomActions, Feld-Properties oder im Property Bag des Root Webs gespeichert.</p></section><section class="ps-metrics"><div class="ps-metric"><label>Aktive Erweiterungen</label><strong id="activeSolutionCount">0</strong></div><div class="ps-metric"><label>Aktiver Tab</label><strong id="activeTabValue">-</strong></div><div class="ps-metric"><label>Site Collection</label><strong id="siteUrlValue">-</strong></div><div class="ps-metric"><label>Erkannte Erweiterungen</label><strong id="detectedSolutionsValue">Keine</strong></div></section><div id="tabsHost" class="ps-tabs" role="tablist" aria-label="Portal Settings Tabs"></div><section id="emptyState" class="ps-empty" hidden>Keine unterstuetzten Erweiterungen wurden aktiv gefunden. Aktuell werden <strong>Custom Branding</strong>, <strong>Mega Menu</strong> und <strong>ExpiryIndicator</strong> unterstuetzt.</section>
<section id="panel-branding" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>Custom Branding</h2><p>Pflegt die ClientSideComponentProperties des aktiven Custom Branding Application Customizers.</p></div><span id="brandingChip" class="ps-chip">0</span></div><div class="ps-grid"><div class="ps-card ps-span-4"><h3>CSS-Dateien</h3><p>Lokale SiteAssets-Pfade oder externe Stylesheets.</p><div class="ps-meta"><div><label>Component Id</label><strong id="brandingComponentId">-</strong></div><div><label>Registrierung</label><strong id="brandingLocation">-</strong></div></div><label class="ps-label" for="brandingCssPath">Neue CSS-Datei</label><div class="ps-inline"><input type="text" id="brandingCssPath" class="ps-input" placeholder="/sites/root/SiteAssets/custom-branding.css oder https://cdn.example.com/style.css" /><button type="button" class="ps-btn ps-btn-primary" onclick="addBrandingCssFile()">Hinzufuegen</button></div><div id="brandingCssList" class="ps-list"></div></div><div class="ps-card ps-span-4"><h3>Placeholder Top</h3><p>JSON-Array fuer den oberen Placeholder.</p><label class="ps-label" for="brandingTopJson">Top JSON</label><textarea id="brandingTopJson" class="ps-textarea" spellcheck="false"></textarea></div><div class="ps-card ps-span-4"><h3>Placeholder Bottom</h3><p>JSON-Array fuer den unteren Placeholder.</p><label class="ps-label" for="brandingBottomJson">Bottom JSON</label><textarea id="brandingBottomJson" class="ps-textarea" spellcheck="false"></textarea></div><div class="ps-card ps-span-12"><h3>ClientSideComponentProperties Preview</h3><p>Dieses JSON wird direkt in die aktive UserCustomAction fuer Custom Branding geschrieben.</p><pre id="brandingPreview" class="ps-preview"></pre><div class="ps-note"><strong>Schema:</strong> <code>{ cssfiles, placeholdertop: { elements: [] }, placeholderbottom: { elements: [] } }</code></div></div></div><div class="ps-actions-bar"><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="reloadSolutions('branding')">Neu laden</button><button type="button" class="ps-btn ps-btn-quiet" onclick="validateBrandingEditors()">JSON validieren</button><button type="button" class="ps-btn ps-btn-quiet" onclick="formatBrandingEditors()">JSON formatieren</button></div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-primary" onclick="applyBrandingConfig()">Konfiguration anwenden</button></div></div></section>
<section id="panel-megamenu" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>Mega Menu</h2><p>Pflegt die ClientSideComponentProperties des aktiven Mega Menu Application Customizers.</p></div><span id="megaMenuChip" class="ps-chip">OK</span></div><div class="ps-grid"><div class="ps-card ps-span-6"><h3>Navigation</h3><p>Pflegt Term Set, optionales CSS und Debug-Verhalten.</p><div class="ps-meta"><div><label>Component Id</label><strong id="megaMenuComponentId">-</strong></div><div><label>Registrierung</label><strong id="megaMenuLocation">-</strong></div></div><label class="ps-label" for="megaMenuTermSetName">Term Set Name</label><input type="text" id="megaMenuTermSetName" class="ps-input" placeholder="Global Navigation" /><label class="ps-label" for="megaMenuCssUrl" style="margin-top:16px;">Zusatz-CSS URL</label><input type="text" id="megaMenuCssUrl" class="ps-input" placeholder="/sites/root/SiteAssets/megamenu.css oder leer lassen" /><label class="ps-label" style="margin-top:16px;">Debug-Modus</label><div class="ps-check"><input type="checkbox" id="megaMenuDebug" /><label for="megaMenuDebug" style="margin:0;font-weight:600;">Debug-Ausgaben in der Browser-Konsole aktivieren</label></div></div><div class="ps-card ps-span-6"><h3>ClientSideComponentProperties Preview</h3><p>Dieses JSON wird direkt in die aktive UserCustomAction fuer Mega Menu geschrieben.</p><pre id="megaMenuPreview" class="ps-preview"></pre><div class="ps-note"><strong>Schema:</strong> <code>{ termSetName, cssUrl, debug }</code></div></div></div><div class="ps-actions-bar"><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="reloadSolutions('megamenu')">Neu laden</button></div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-primary" onclick="applyMegaMenuConfig()">Konfiguration anwenden</button></div></div></section>
<section id="panel-expiryindicator" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>ExpiryIndicator 2.1</h2><p>Zentrale Standards fuer die Site Collection und lokale Ausnahmen fuer einzelne Listen oder Bibliotheken.</p></div><span id="expiryIndicatorChip" class="ps-chip">V2.1</span></div><div class="ps-grid"><div class="ps-card ps-span-4"><h3>Site-Collection-Standard</h3><p>Gilt im Root Web und in allen Subwebs, sofern eine Bindung auf Vererbung steht.</p><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-quiet" onclick="loadExpirySiteDefaults()">Standard laden</button><button type="button" class="ps-btn ps-btn-primary" onclick="saveExpirySiteDefaults()">Standard speichern</button></div><div class="ps-note"><strong>Speicherort:</strong> Property Bag des Root Webs. Es wird keine Konfigurationsliste verwendet.</div></div><div class="ps-card ps-span-8"><h3>Registrierte Bindungen</h3><p>Listen und Bibliotheken, die mit dem Aktivierungsskript registriert wurden.</p><div id="expiryBindingsList" class="ps-list"><div class="ps-list-empty">Keine Bindungen registriert.</div></div><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-quiet" onclick="setAllExpiryBindingsToInheritance()">Alle auf Vererbung stellen</button></div></div><div class="ps-card ps-span-4"><h3>Ausgewaehltes Ziel</h3><p>Web, Liste und interner Name des gebundenen Ablaufdatumsfeldes.</p><label class="ps-label" for="expiryWebUrl" style="margin-top:16px;">Web URL</label><input type="text" id="expiryWebUrl" class="ps-input" /><label class="ps-label" for="expiryListTitle" style="margin-top:16px;">Listentitel</label><input type="text" id="expiryListTitle" class="ps-input" placeholder="Dokumente" /><label class="ps-label" for="expiryFieldName" style="margin-top:16px;">Interner Feldname</label><input type="text" id="expiryFieldName" class="ps-input" placeholder="ExpiryDate" /><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-primary" onclick="loadExpiryIndicatorConfig()">Lokale Konfiguration laden</button><button type="button" class="ps-btn ps-btn-quiet" onclick="setSelectedExpiryBindingToInheritance()">Standard erben</button></div></div><div class="ps-card ps-span-8"><h3>Konfigurationseditor</h3><p>Unterstuetzt V1-Regeln sowie verschachtelte V2-<code>condition</code>-Gruppen mit <code>and</code>/<code>or</code>.</p><textarea id="expiryIndicatorJson" class="ps-textarea" style="min-height:430px" spellcheck="false"></textarea><div class="ps-note"><strong>Prioritaet:</strong> lokale Feldkonfiguration &gt; Subweb-Override &gt; Site-Collection-Standard &gt; eingebauter Standard.</div></div></div><div class="ps-actions-bar"><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="validateExpiryIndicatorConfig()">JSON validieren</button><button type="button" class="ps-btn ps-btn-quiet" onclick="formatExpiryIndicatorConfig()">JSON formatieren</button></div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-primary" onclick="applyExpiryIndicatorConfig()">Als lokale Ausnahme speichern</button></div></div></section>
<script type="text/javascript">
var CUSTOM_SOLUTION_TAG='MSFT-Custom-Solution';var EXPIRY_FIELD_COMPONENT_ID='a555f4fc-d6a6-4421-8189-457449d9bbde';var EXPIRY_DESCRIPTOR_PROPERTY='PortalSettings.Solutions.ExpiryIndicator';var EXPIRY_DEFAULTS_PROPERTY='PortalSettings.ExpiryIndicator.Defaults';var EXPIRY_BINDINGS_PROPERTY='PortalSettings.ExpiryIndicator.Bindings';var SUPPORTED_SOLUTIONS={branding:{key:'branding',label:'Custom Branding',componentId:'035ba968-6488-4d42-86b3-0470ffcc95b9',panelId:'panel-branding',descriptionTag:'MSFT-Custom-Solution:CustomBranding',providerType:'applicationCustomizer'},megamenu:{key:'megamenu',label:'Mega Menu',componentId:'abc3361f-bb2d-491f-aba3-cd51c19a299b',panelId:'panel-megamenu',descriptionTag:'MSFT-Custom-Solution:MegaMenu',providerType:'applicationCustomizer'},expiryindicator:{key:'expiryindicator',label:'ExpiryIndicator',componentId:EXPIRY_FIELD_COMPONENT_ID,panelId:'panel-expiryindicator',providerType:'rootPropertyBag'}};
var state={booted:false,activeKey:'',solutions:{}};
document.addEventListener('DOMContentLoaded',function(){bootPage();});
function bootPage(){if(state.booted){return;}if(window.SP&&SP.ClientContext){state.booted=true;bindPage();reloadSolutions();return;}window.setTimeout(bootPage,150);}
function bindPage(){document.getElementById('siteUrlValue').textContent=getSiteUrl();document.getElementById('expiryWebUrl').value=getWebUrl();document.getElementById('expiryIndicatorJson').value=JSON.stringify(defaultExpiryIndicatorConfig('ExpiryDate'),null,2);document.getElementById('brandingCssPath').addEventListener('keypress',function(event){if(event.key==='Enter'){event.preventDefault();addBrandingCssFile();}});document.getElementById('brandingTopJson').addEventListener('input',updateBrandingPreview);document.getElementById('brandingBottomJson').addEventListener('input',updateBrandingPreview);document.getElementById('megaMenuTermSetName').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuCssUrl').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuDebug').addEventListener('change',updateMegaMenuPreview);}
function reloadSolutions(preferredKey){showStatus('Lade aktive Einstellungsanbieter...','info',true);state.solutions={};loadSolutionsSequentially(Object.keys(SUPPORTED_SOLUTIONS),0,preferredKey||state.activeKey,[],[],function(labels,errors,preferred){var keys=getAvailableSolutionKeys();if(!keys.length){state.activeKey='';renderTabs();renderPanels();updateOverview();showStatus('Keine unterstuetzten Erweiterungen wurden aktiv registriert gefunden.','warning',true);return;}state.activeKey=preferred&&state.solutions[preferred]?preferred:keys[0];hydratePanels();renderTabs();renderPanels();updateOverview();var message='Aktive Erweiterungen geladen: '+labels.join(', ')+'.';if(errors.length){showStatus(message+' Hinweise: '+errors.join(' | '),'warning',true);return;}showStatus(message,'success');});}
function loadSolutionsSequentially(keys,index,preferred,labels,errors,done){if(index>=keys.length){done(labels,errors,preferred);return;}var key=keys[index];var definition=SUPPORTED_SOLUTIONS[key];if(definition.providerType==='rootPropertyBag'){readExpiryProviderInfo(function(info){if(info){state.solutions[key]={definition:definition,info:info,config:info.siteDefaults};labels.push(definition.label);}loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);},function(message){errors.push(definition.label+': '+message);loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);});return;}readCustomizerActionInfo(definition,function(info){if(info&&info.action){state.solutions[key]={definition:definition,info:info,config:parseSolutionConfig(key,info.action.ClientSideComponentProperties)};labels.push(definition.label);}loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);},function(message){errors.push(definition.label+': '+message);loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);});}
function hydratePanels(){if(state.solutions.branding){populateBrandingPanel(state.solutions.branding);}if(state.solutions.megamenu){populateMegaMenuPanel(state.solutions.megamenu);}if(state.solutions.expiryindicator){renderExpiryBindings();}}
function renderTabs(){var host=document.getElementById('tabsHost');var keys=getAvailableSolutionKeys();if(!keys.length){host.innerHTML='';host.hidden=true;document.getElementById('emptyState').hidden=false;return;}host.hidden=false;document.getElementById('emptyState').hidden=true;var html='';for(var i=0;i<keys.length;i++){var key=keys[i];var def=SUPPORTED_SOLUTIONS[key];var isActive=state.activeKey===key;html+='<button type="button" class="ps-tab'+(isActive?' is-active':'')+'" role="tab" aria-selected="'+(isActive?'true':'false')+'" aria-controls="'+def.panelId+'" onclick="switchTab(\''+key+'\')">'+escapeHtml(def.label)+'</button>'; }host.innerHTML=html;}
function renderPanels(){document.getElementById('panel-branding').hidden=!(state.activeKey==='branding'&&state.solutions.branding);document.getElementById('panel-megamenu').hidden=!(state.activeKey==='megamenu'&&state.solutions.megamenu);document.getElementById('panel-expiryindicator').hidden=!(state.activeKey==='expiryindicator'&&state.solutions.expiryindicator);}
function switchTab(key){if(!state.solutions[key]){return;}state.activeKey=key;renderTabs();renderPanels();updateOverview();}
function updateOverview(){var keys=getAvailableSolutionKeys();document.getElementById('activeSolutionCount').textContent=String(keys.length);document.getElementById('activeTabValue').textContent=state.activeKey&&SUPPORTED_SOLUTIONS[state.activeKey]?SUPPORTED_SOLUTIONS[state.activeKey].label:'-';document.getElementById('detectedSolutionsValue').textContent=keys.length?keys.map(function(key){return SUPPORTED_SOLUTIONS[key].label;}).join(', '):'Keine';}
function populateBrandingPanel(solution){document.getElementById('brandingComponentId').textContent=solution.definition.componentId;document.getElementById('brandingLocation').textContent=getActionLocationText(solution.info);document.getElementById('brandingCssPath').value='';document.getElementById('brandingTopJson').value=JSON.stringify(solution.config.placeholdertop.elements,null,2);document.getElementById('brandingBottomJson').value=JSON.stringify(solution.config.placeholderbottom.elements,null,2);renderBrandingCssFiles();updateBrandingPreview();}
function renderBrandingCssFiles(){var host=document.getElementById('brandingCssList');var solution=state.solutions.branding;var cssFiles=solution?solution.config.cssfiles:[];if(!cssFiles||!cssFiles.length){host.innerHTML='<div class="ps-list-empty">Keine CSS-Dateien konfiguriert.</div>';document.getElementById('brandingChip').textContent='0';return;}var html='';for(var i=0;i<cssFiles.length;i++){html+='<div class="ps-item"><div class="ps-path">'+escapeHtml(cssFiles[i].path)+'</div><button type="button" class="ps-btn ps-btn-danger" onclick="removeBrandingCssFile('+i+')">Entfernen</button></div>'; }host.innerHTML=html;document.getElementById('brandingChip').textContent=String(cssFiles.length);}
function addBrandingCssFile(){var solution=state.solutions.branding;if(!solution){showStatus('Custom Branding ist aktuell nicht aktiv konfiguriert.','warning');return;}var input=document.getElementById('brandingCssPath');var path=input.value.trim();if(!path){showStatus('Bitte geben Sie einen CSS-Pfad ein.','warning');return;}var exists=solution.config.cssfiles.some(function(item){return item.path===path;});if(exists){showStatus('Diese CSS-Datei ist bereits vorhanden.','warning');return;}solution.config.cssfiles.push({path:path});input.value='';renderBrandingCssFiles();updateBrandingPreview();showStatus('CSS-Datei hinzugefuegt.','success');}
function removeBrandingCssFile(index){var solution=state.solutions.branding;if(!solution){return;}solution.config.cssfiles.splice(index,1);renderBrandingCssFiles();updateBrandingPreview();showStatus('CSS-Datei entfernt.','success');}
function buildBrandingConfig(){var solution=state.solutions.branding;if(!solution){throw new Error('Custom Branding ist aktuell nicht aktiv konfiguriert.');}return{cssfiles:solution.config.cssfiles.slice(),placeholdertop:{elements:parseJsonArray('brandingTopJson','Placeholder Top')},placeholderbottom:{elements:parseJsonArray('brandingBottomJson','Placeholder Bottom')}};}
function validateBrandingEditors(){try{state.solutions.branding.config=normalizeBrandingConfig(buildBrandingConfig());updateBrandingPreview();showStatus('Custom Branding JSON ist gueltig.','success');}catch(error){showStatus(error.message,'error');}}
function formatBrandingEditors(){try{state.solutions.branding.config=normalizeBrandingConfig(buildBrandingConfig());document.getElementById('brandingTopJson').value=JSON.stringify(state.solutions.branding.config.placeholdertop.elements,null,2);document.getElementById('brandingBottomJson').value=JSON.stringify(state.solutions.branding.config.placeholderbottom.elements,null,2);updateBrandingPreview();showStatus('Custom Branding JSON wurde formatiert.','success');}catch(error){showStatus(error.message,'error');}}
function updateBrandingPreview(){var preview=document.getElementById('brandingPreview');try{var config=buildBrandingConfig();preview.textContent=JSON.stringify(config,null,2);document.getElementById('brandingChip').textContent=String(config.cssfiles.length);}catch(error){preview.textContent='Fehler: '+error.message;}}
function applyBrandingConfig(){try{var config=buildBrandingConfig();showStatus('Aktualisiere Custom Branding...','info',true);updateSolutionAction('branding',config,function(){state.solutions.branding.config=normalizeBrandingConfig(config);populateBrandingPanel(state.solutions.branding);showStatus('Custom Branding wurde erfolgreich aktualisiert.','success');},function(message){showStatus('Custom Branding konnte nicht aktualisiert werden: '+message,'error',true);});}catch(error){showStatus(error.message,'error');}}
function populateMegaMenuPanel(solution){document.getElementById('megaMenuComponentId').textContent=solution.definition.componentId;document.getElementById('megaMenuLocation').textContent=getActionLocationText(solution.info);document.getElementById('megaMenuTermSetName').value=solution.config.termSetName||'';document.getElementById('megaMenuCssUrl').value=solution.config.cssUrl||'';document.getElementById('megaMenuDebug').checked=solution.config.debug===true;updateMegaMenuPreview();}
function buildMegaMenuConfig(){var solution=state.solutions.megamenu;if(!solution){throw new Error('Mega Menu ist aktuell nicht aktiv konfiguriert.');}var termSetName=document.getElementById('megaMenuTermSetName').value.trim();return{termSetName:termSetName,cssUrl:document.getElementById('megaMenuCssUrl').value.trim(),debug:document.getElementById('megaMenuDebug').checked===true};}
function updateMegaMenuPreview(){var preview=document.getElementById('megaMenuPreview');try{var config=buildMegaMenuConfig();preview.textContent=JSON.stringify(config,null,2);document.getElementById('megaMenuChip').textContent=config.debug?'DBG':'OK';}catch(error){preview.textContent='Fehler: '+error.message;document.getElementById('megaMenuChip').textContent='!';}}
function applyMegaMenuConfig(){try{var config=buildMegaMenuConfig();showStatus('Aktualisiere Mega Menu...','info',true);updateSolutionAction('megamenu',config,function(){state.solutions.megamenu.config=normalizeMegaMenuConfig(config);populateMegaMenuPanel(state.solutions.megamenu);showStatus('Mega Menu wurde erfolgreich aktualisiert.','success');},function(message){showStatus('Mega Menu konnte nicht aktualisiert werden: '+message,'error',true);});}catch(error){showStatus(error.message,'error');}}
function defaultExpiryIndicatorConfig(expiryField){return{baseField:'Created',expiryField:expiryField||'ExpiryDate',default:{lifeTime:{value:2,unit:'years'},columnRule:[{daysUntilExpiry:0,operator:'lessOrEqual',backgroundColor:'#a4262c',textColor:'#ffffff',label:'Abgelaufen'},{daysUntilExpiry:30,operator:'lessOrEqual',backgroundColor:'#ffaa44',textColor:'#000000',label:'Laeuft bald ab'},{daysUntilExpiry:90,operator:'lessOrEqual',backgroundColor:'#fff4ce',textColor:'#000000',label:'Beobachten'}]},rules:[],nullText:'Kein Ablaufdatum',confirmExtension:true};}
function propertyBagJson(properties,key,fallback){if(!properties){return fallback;}var value;Object.keys(properties).some(function(name){if(name.toLowerCase()===key.toLowerCase()){value=properties[name];return true;}return false;});if(typeof value==='undefined'||value===null||value===''){return fallback;}if(typeof value==='object'){return value;}try{return JSON.parse(value);}catch(ignore){return fallback;}}
function readExpiryProviderInfo(onSuccess,onError){getJson(getSiteUrl().replace(/\/$/,'')+'/_api/web/AllProperties',function(payload){var properties=payload.d||payload||{};var descriptor=propertyBagJson(properties,EXPIRY_DESCRIPTOR_PROPERTY,null);var bindings=propertyBagJson(properties,EXPIRY_BINDINGS_PROPERTY,[]);var siteDefaults=propertyBagJson(properties,EXPIRY_DEFAULTS_PROPERTY,null);if(!Array.isArray(bindings)){bindings=[];}if(!descriptor&&!siteDefaults&&!bindings.length){onSuccess(null);return;}onSuccess({descriptor:descriptor,bindings:bindings,siteDefaults:siteDefaults||defaultExpiryIndicatorConfig('ExpiryDate'),rootProperties:properties,selected:null});},onError);}
function renderExpiryBindings(){var solution=state.solutions.expiryindicator;var host=document.getElementById('expiryBindingsList');if(!solution||!solution.info||!solution.info.bindings.length){host.innerHTML='<div class="ps-list-empty">Keine Bindungen registriert. Aktivieren Sie ExpiryIndicator 2.1 erneut an den gewuenschten Listen.</div>';return;}var html='';for(var i=0;i<solution.info.bindings.length;i++){var binding=solution.info.bindings[i];html+='<div class="ps-item"><div><strong>'+escapeHtml(binding.listTitle||binding.listId)+'</strong><div class="ps-path">'+escapeHtml(binding.webUrl)+' | '+escapeHtml(binding.fieldInternalName)+'</div></div><button type="button" class="ps-btn ps-btn-quiet" onclick="selectExpiryBinding('+i+')">Auswaehlen</button></div>';}host.innerHTML=html;document.getElementById('expiryIndicatorChip').textContent=String(solution.info.bindings.length);}
function selectExpiryBinding(index){var solution=state.solutions.expiryindicator;if(!solution||!solution.info||!solution.info.bindings[index]){return;}var binding=solution.info.bindings[index];document.getElementById('expiryWebUrl').value=binding.webUrl||getWebUrl();document.getElementById('expiryListTitle').value=binding.listTitle||'';document.getElementById('expiryFieldName').value=binding.fieldInternalName||'ExpiryDate';loadExpiryIndicatorConfig();}
function loadExpirySiteDefaults(){var solution=state.solutions.expiryindicator;if(!solution||!solution.info){return;}var config=solution.info.siteDefaults||defaultExpiryIndicatorConfig('ExpiryDate');document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);showStatus('Site-Collection-Standard wurde in den Editor geladen.','success');}
function saveExpirySiteDefaults(){var config;try{config=parseExpiryIndicatorEditor();}catch(error){showStatus(error.message,'error',true);return;}showStatus('Speichere Site-Collection-Standard...','info',true);setRootPropertyBagValue(EXPIRY_DEFAULTS_PROPERTY,JSON.stringify(config),function(){state.solutions.expiryindicator.info.siteDefaults=config;state.solutions.expiryindicator.config=config;showStatus('Site-Collection-Standard wurde gespeichert. Vererbende Bindungen verwenden ihn beim naechsten Laden.','success');},function(message){showStatus('Site-Collection-Standard konnte nicht gespeichert werden: '+message,'error',true);});}
function setRootPropertyBagValue(key,value,onSuccess,onError){try{var context=new SP.ClientContext(getSiteUrl());var web=context.get_web();var properties=web.get_allProperties();context.load(properties);context.executeQueryAsync(function(){try{properties.set_item(key,value);web.update();context.executeQueryAsync(onSuccess,function(sender,args){onError(args.get_message());});}catch(error){onError(error.message||String(error));}},function(sender,args){onError(args.get_message());});}catch(error){onError(error.message||String(error));}}
function inheritancePayload(fieldName){return{__metadata:{type:'SP.Field'},ClientSideComponentProperties:JSON.stringify({schemaVersion:2,expiryField:fieldName,inheritSiteDefaults:true})};}
function setSelectedExpiryBindingToInheritance(){var target;try{target=getExpiryTarget();}catch(error){showStatus(error.message,'error');return;}showStatus('Aktiviere Vererbung fuer die ausgewaehlte Bindung...','info',true);postJsonMerge(expiryFieldUrl(target),inheritancePayload(target.fieldName),function(){if(state.solutions.expiryindicator.info){state.solutions.expiryindicator.info.selected=null;}loadExpirySiteDefaults();showStatus('Die Bindung erbt jetzt den Site-Collection-Standard.','success');},function(message){showStatus('Vererbung konnte nicht aktiviert werden: '+message,'error',true);});}
function expiryBindingFieldUrl(binding){return String(binding.webUrl||'').replace(/\/$/,'')+"/_api/web/lists(guid'"+normalizeGuid(binding.listId)+"')/fields/getbyinternalnameortitle('"+String(binding.fieldInternalName||'ExpiryDate').replace(/'/g,"''")+"')";}
function setAllExpiryBindingsToInheritance(){var solution=state.solutions.expiryindicator;var bindings=solution&&solution.info?solution.info.bindings:[];if(!bindings.length){showStatus('Es sind keine Bindungen registriert.','warning');return;}if(!window.confirm('Lokale Konfigurationen von '+bindings.length+' Bindung(en) durch Vererbung ersetzen?')){return;}showStatus('Aktiviere Vererbung fuer '+bindings.length+' Bindung(en)...','info',true);var errors=[];function next(index){if(index>=bindings.length){if(errors.length){showStatus('Vererbung wurde mit Hinweisen abgeschlossen: '+errors.join(' | '),'warning',true);}else{showStatus('Alle registrierten Bindungen erben jetzt den Site-Collection-Standard.','success');}return;}var binding=bindings[index];postJsonMerge(expiryBindingFieldUrl(binding),inheritancePayload(binding.fieldInternalName),function(){next(index+1);},function(message){errors.push((binding.listTitle||binding.listId)+': '+message);next(index+1);});}next(0);}
function getExpiryTarget(){var webUrl=document.getElementById('expiryWebUrl').value.trim().replace(/\/$/,'');var listTitle=document.getElementById('expiryListTitle').value.trim();var fieldName=document.getElementById('expiryFieldName').value.trim();if(!webUrl){throw new Error('Bitte eine Web URL angeben.');}if(!listTitle){throw new Error('Bitte einen Listentitel angeben.');}if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName)){throw new Error('Der interne Feldname ist ungueltig.');}return{webUrl:webUrl,listTitle:listTitle,fieldName:fieldName};}
function expiryFieldUrl(target){return target.webUrl+"/_api/web/lists/getbytitle('"+target.listTitle.replace(/'/g,"''")+"')/fields/getbyinternalnameortitle('"+target.fieldName+"')";}
function loadExpiryIndicatorConfig(){var target;try{target=getExpiryTarget();}catch(error){showStatus(error.message,'error');return;}showStatus('Lade ExpiryIndicator-Konfiguration...','info',true);getJson(expiryFieldUrl(target)+'?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties',function(payload){var field=payload.d||payload;if(normalizeGuid(field.ClientSideComponentId)!==normalizeGuid(EXPIRY_FIELD_COMPONENT_ID)){showStatus('Das Feld ist nicht mit dem ExpiryIndicator Field Customizer verbunden.','error',true);return;}var config=defaultExpiryIndicatorConfig(field.InternalName);var inherited=false;if(field.ClientSideComponentProperties&&field.ClientSideComponentProperties.trim()){try{var stored=JSON.parse(field.ClientSideComponentProperties);if(stored.inheritSiteDefaults===true){inherited=true;config=JSON.parse(JSON.stringify(state.solutions.expiryindicator.info.siteDefaults||config));}else{config=stored;}}catch(error){showStatus('Die gespeicherten Component Properties enthalten ungueltiges JSON: '+error.message,'error',true);return;}}config.expiryField=field.InternalName;state.solutions.expiryindicator.info.selected={target:target,field:field,updateUrl:expiryFieldUrl(target),actionType:field.__metadata&&field.__metadata.type?field.__metadata.type:'SP.Field'};state.solutions.expiryindicator.config=config;document.getElementById('expiryFieldName').value=field.InternalName;document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);document.getElementById('expiryIndicatorChip').textContent=inherited?'ERBT':'LOKAL';showStatus(inherited?'Die Bindung erbt den Site-Collection-Standard.':'Lokale ExpiryIndicator-Konfiguration wurde geladen.','success');},function(message){showStatus('ExpiryIndicator-Konfiguration konnte nicht geladen werden: '+message,'error',true);});}
function parseExpiryIndicatorEditor(){var raw=document.getElementById('expiryIndicatorJson').value.trim();if(!raw){throw new Error('Die ExpiryIndicator-Konfiguration darf nicht leer sein.');}var config;try{config=JSON.parse(raw);}catch(error){throw new Error('Ungueltiges JSON: '+error.message);}var errors=validateExpiryIndicatorObject(config);if(errors.length){throw new Error(errors.join(' | '));}return config;}
function validateExpiryIndicatorObject(config){var errors=[];function safeName(value){return/^[A-Za-z_][A-Za-z0-9_]*$/.test(value||'');}function validDuration(value,path){if(!value||typeof value.value!=='number'||!isFinite(value.value)||['days','months','years'].indexOf(value.unit)<0){errors.push(path+' muss value und unit days/months/years enthalten.');}}function validColorRules(value,path){if(!Array.isArray(value)){errors.push(path+' muss ein Array sein.');return;}for(var i=0;i<value.length;i++){var item=value[i];if(!item||typeof item.daysUntilExpiry!=='number'||['lessThan','lessOrEqual','equal','greaterOrEqual','greaterThan'].indexOf(item.operator)<0||typeof item.backgroundColor!=='string'||typeof item.textColor!=='string'){errors.push(path+'['+i+'] ist ungueltig.');}}}function validGroup(group,path,depth){if(depth>10){errors.push(path+' ist tiefer als 10 Ebenen.');return;}if(!group||(group.operator!=='and'&&group.operator!=='or')){errors.push(path+'.operator muss and oder or sein.');}if(!group||!Array.isArray(group.conditions)||!group.conditions.length){errors.push(path+'.conditions muss mindestens einen Eintrag enthalten.');return;}for(var i=0;i<group.conditions.length;i++){var condition=group.conditions[i];var child=path+'.conditions['+i+']';if(condition&&typeof condition.operator!=='undefined'){validGroup(condition,child,depth+1);}else if(!condition||!safeName(condition.columnName)||typeof condition.columnValue==='undefined'){errors.push(child+' benoetigt columnName und columnValue.');}}}if(!config||typeof config!=='object'){return['Konfiguration muss ein Objekt sein.'];}if(!safeName(config.baseField)){errors.push('baseField ist ungueltig.');}if(!safeName(config.expiryField)){errors.push('expiryField ist ungueltig.');}if(!config.default){errors.push('default fehlt.');}else{validDuration(config.default.lifeTime,'default.lifeTime');validColorRules(config.default.columnRule,'default.columnRule');}if(!Array.isArray(config.rules)){errors.push('rules muss ein Array sein.');}else{for(var r=0;r<config.rules.length;r++){var rule=config.rules[r];var path='rules['+r+']';if(rule&&rule.condition){validGroup(rule.condition,path+'.condition',0);}else if(!rule||!safeName(rule.columnName)||typeof rule.columnValue==='undefined'){errors.push(path+' benoetigt condition oder columnName/columnValue.');}if(rule){validDuration(rule.lifeTime,path+'.lifeTime');validColorRules(rule.columnRule,path+'.columnRule');}}}return errors;}
function validateExpiryIndicatorConfig(){try{parseExpiryIndicatorEditor();showStatus('ExpiryIndicator V2 JSON ist gueltig.','success');document.getElementById('expiryIndicatorChip').textContent='OK';}catch(error){showStatus(error.message,'error',true);document.getElementById('expiryIndicatorChip').textContent='!';}}
function formatExpiryIndicatorConfig(){try{var config=parseExpiryIndicatorEditor();document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);showStatus('ExpiryIndicator JSON wurde formatiert.','success');}catch(error){showStatus(error.message,'error',true);}}
function applyExpiryIndicatorConfig(){var solution=state.solutions.expiryindicator;var selected=solution&&solution.info?solution.info.selected:null;if(!selected){showStatus('Bitte zuerst eine lokale ExpiryIndicator-Konfiguration laden.','warning',true);return;}var config;try{config=parseExpiryIndicatorEditor();}catch(error){showStatus(error.message,'error',true);return;}config.expiryField=selected.field.InternalName;var payload={__metadata:{type:selected.actionType},ClientSideComponentProperties:JSON.stringify(config)};showStatus('Aktualisiere lokale ExpiryIndicator-Ausnahme...','info',true);postJsonMerge(selected.updateUrl,payload,function(){solution.config=config;document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);document.getElementById('expiryIndicatorChip').textContent='LOKAL';showStatus('Lokale ExpiryIndicator-Konfiguration wurde erfolgreich aktualisiert.','success');},function(message){showStatus('ExpiryIndicator konnte nicht aktualisiert werden: '+message,'error',true);});}
function parseSolutionConfig(key,rawValue){var parsed={};if(typeof rawValue==='string'&&rawValue.trim().length>0){try{parsed=JSON.parse(rawValue);}catch(error){parsed={};}}if(key==='branding'){return normalizeBrandingConfig(parsed);}if(key==='megamenu'){return normalizeMegaMenuConfig(parsed);}return parsed||{};}
function normalizeBrandingConfig(config){var normalized={cssfiles:[],placeholdertop:{elements:[]},placeholderbottom:{elements:[]}};if(config&&Array.isArray(config.cssfiles)){normalized.cssfiles=config.cssfiles.filter(function(item){return item&&typeof item.path==='string'&&item.path.trim().length>0;}).map(function(item){return{path:item.path.trim()};});}if(config&&config.placeholdertop&&Array.isArray(config.placeholdertop.elements)){normalized.placeholdertop.elements=config.placeholdertop.elements;}else if(config&&Array.isArray(config.elements)){normalized.placeholdertop.elements=config.elements;}if(config&&config.placeholderbottom&&Array.isArray(config.placeholderbottom.elements)){normalized.placeholderbottom.elements=config.placeholderbottom.elements;}return normalized;}
function normalizeMegaMenuConfig(config){return{termSetName:config&&typeof config.termSetName==='string'?config.termSetName.trim():'',cssUrl:config&&typeof config.cssUrl==='string'?config.cssUrl.trim():'',debug:!!(config&&config.debug===true)};}
function parseJsonArray(textareaId,label){var textarea=document.getElementById(textareaId);var rawValue=textarea.value.trim();if(!rawValue){return [];}var parsedValue;try{parsedValue=JSON.parse(rawValue);}catch(error){throw new Error(label+' enthaelt kein gueltiges JSON: '+error.message);}if(!Array.isArray(parsedValue)){throw new Error(label+' muss ein JSON-Array sein.');}return parsedValue;}
function updateSolutionAction(key,config,onSuccess,onError){var solution=state.solutions[key];if(!solution||!solution.info||!solution.info.action){onError('Die passende UserCustomAction wurde nicht gefunden.');return;}var actionId=solution.info.action.Id;if(!actionId){onError('Die UserCustomAction hat keine gueltige Id.');return;}var updateUrl=solution.info.updateBaseUrl+'(guid\''+actionId+'\')';var payload={__metadata:{type:solution.info.actionType||'SP.UserCustomAction'},ClientSideComponentProperties:JSON.stringify(config)};postJsonMerge(updateUrl,payload,onSuccess,onError);}
function readCustomizerActionInfo(definition,onSuccess,onError){var baseSiteUrl=getSiteUrl().replace(/\/$/,'');var endpoints=[{scopeLabel:'Site Collection',readUrl:baseSiteUrl+'/_api/site/UserCustomActions',updateBaseUrl:baseSiteUrl+'/_api/site/UserCustomActions'},{scopeLabel:'Web',readUrl:baseSiteUrl+'/_api/site/rootweb/UserCustomActions',updateBaseUrl:baseSiteUrl+'/_api/site/rootweb/UserCustomActions'}];readCustomizerActionInfoFromEndpoints(definition,endpoints,0,[],onSuccess,onError);}
function readCustomizerActionInfoFromEndpoints(definition,endpoints,index,errors,onSuccess,onError){if(index>=endpoints.length){if(errors.length){onError(errors.join(' | '));return;}onSuccess(null);return;}var endpoint=endpoints[index];getJson(endpoint.readUrl,function(payload){try{var actions=getResultArray(payload);var action=findActiveCustomizerAction(actions,definition);if(action){onSuccess({action:action,updateBaseUrl:endpoint.updateBaseUrl,actionType:action.__metadata&&action.__metadata.type?action.__metadata.type:'SP.UserCustomAction',scopeLabel:endpoint.scopeLabel});return;}readCustomizerActionInfoFromEndpoints(definition,endpoints,index+1,errors,onSuccess,onError);}catch(error){errors.push(error.message||String(error));readCustomizerActionInfoFromEndpoints(definition,endpoints,index+1,errors,onSuccess,onError);}},function(message){errors.push(message);readCustomizerActionInfoFromEndpoints(definition,endpoints,index+1,errors,onSuccess,onError);});}
function findActiveCustomizerAction(actions,definition){if(!Array.isArray(actions)||!definition){return null;}var expected=normalizeGuid(definition.componentId);var fallback=null;for(var i=0;i<actions.length;i++){var action=actions[i];if(normalizeGuid(action&&action.ClientSideComponentId)!==expected){continue;}if(!matchesSolutionTag(action,definition)){continue;}if(!fallback){fallback=action;}if(typeof action.Location==='string'&&action.Location.indexOf('ClientSideExtension.ApplicationCustomizer')!==-1){return action;}}return fallback;}function matchesSolutionTag(action,definition){var description=action&&typeof action.Description==='string'?action.Description:'';if(description.indexOf(CUSTOM_SOLUTION_TAG)===-1){return false;}if(definition&&typeof definition.descriptionTag==='string'&&definition.descriptionTag){return description.indexOf(definition.descriptionTag)!==-1||description===CUSTOM_SOLUTION_TAG;}return true;}
function getJson(url,onSuccess,onError){try{var request=new XMLHttpRequest();request.open('GET',url,true);request.setRequestHeader('Accept','application/json;odata=verbose');request.onreadystatechange=function(){if(request.readyState!==4){return;}if(request.status>=200&&request.status<300){try{onSuccess(JSON.parse(request.responseText||'{}'));}catch(error){onError('Antwort konnte nicht gelesen werden: '+(error.message||String(error)));}return;}onError(buildRequestErrorMessage(request));};request.onerror=function(){onError('Netzwerkfehler beim Lesen von '+url);};request.send();}catch(error){onError(error.message||String(error));}}
function postJsonMerge(url,payload,onSuccess,onError){try{var digest=getRequestDigest();if(!digest){onError('Kein Request Digest gefunden. Laden Sie die Seite neu und versuchen Sie es erneut.');return;}var request=new XMLHttpRequest();request.open('POST',url,true);request.setRequestHeader('Accept','application/json;odata=verbose');request.setRequestHeader('Content-Type','application/json;odata=verbose');request.setRequestHeader('X-RequestDigest',digest);request.setRequestHeader('IF-MATCH','*');request.setRequestHeader('X-HTTP-Method','MERGE');request.onreadystatechange=function(){if(request.readyState!==4){return;}if(request.status>=200&&request.status<300){onSuccess();return;}onError(buildRequestErrorMessage(request));};request.onerror=function(){onError('Netzwerkfehler beim Aktualisieren von '+url);};request.send(JSON.stringify(payload));}catch(error){onError(error.message||String(error));}}
function getRequestDigest(){var digestField=document.getElementById('__REQUESTDIGEST');return digestField&&digestField.value?digestField.value:'';}
function getResultArray(payload){if(payload&&payload.d&&Array.isArray(payload.d.results)){return payload.d.results;}if(payload&&Array.isArray(payload.value)){return payload.value;}return [];}
function normalizeGuid(value){if(typeof value!=='string'){return '';}return value.replace(/[{}]/g,'').toLowerCase();}
function buildRequestErrorMessage(request){var message=String(request.status||0);if(request.statusText){message+=' '+request.statusText;}if(request.responseText){try{var payload=JSON.parse(request.responseText);if(payload&&payload.error&&payload.error.message){message+=': '+(payload.error.message.value||payload.error.message);}}catch(ignoreError){}}return message;}
function getAvailableSolutionKeys(){return Object.keys(SUPPORTED_SOLUTIONS).filter(function(key){return !!state.solutions[key];});}
function getActionLocationText(info){if(!info||!info.action){return '-';}var scopeLabel=info.scopeLabel?info.scopeLabel+' | ':'';return scopeLabel+(info.action.Location||'ClientSideExtension.ApplicationCustomizer');}
function getWebUrl(){if(window._spPageContextInfo&&_spPageContextInfo.webAbsoluteUrl){return _spPageContextInfo.webAbsoluteUrl;}return window.location.origin||'';}
function getSiteUrl(){if(window._spPageContextInfo&&_spPageContextInfo.siteAbsoluteUrl){return _spPageContextInfo.siteAbsoluteUrl;}return getWebUrl();}
function showStatus(message,type,keepVisible){var host=document.getElementById('statusMessage');host.innerHTML='<div class="ps-status ps-status-'+type+'">'+escapeHtml(message)+'</div>';if(!keepVisible){window.clearTimeout(showStatus._timer);showStatus._timer=window.setTimeout(function(){host.innerHTML='';},4200);}}
function escapeHtml(text){var div=document.createElement('div');div.textContent=text===null||text===undefined?'':String(text);return div.innerHTML;}
</script></div></div>
</asp:Content>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="PortalSettingsPageV11" Url="SitePages" RootWebOnly="TRUE">
<File
Path="PortalSettings.aspx"
Url="PortalSettings.aspx"
Type="GhostableInLibrary"
IgnoreIfAlreadyExists="FALSE"
ReplaceContent="TRUE" />
</Module>
</Elements>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="PortalSettingsPageV2" Url="SitePages" RootWebOnly="TRUE">
<File
Path="PortalSettings.aspx"
Url="PortalSettings.aspx"
Type="GhostableInLibrary"
IgnoreIfAlreadyExists="FALSE"
ReplaceContent="TRUE" />
</Module>
</Elements>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="PortalSettingsPage" Url="SitePages" RootWebOnly="TRUE">
<File
Path="PortalSettings.aspx"
Url="PortalSettings.aspx"
Type="GhostableInLibrary"
IgnoreIfAlreadyExists="FALSE" />
</Module>
</Elements>

View File

@@ -0,0 +1,3 @@
<ApplyElementManifests>
<ElementManifest Location="57bd9daf-48f9-49e2-aebd-11b15a0b5e93\elements-v1.1.xml" />
</ApplyElementManifests>

View File

@@ -0,0 +1,3 @@
<ApplyElementManifests>
<ElementManifest Location="57bd9daf-48f9-49e2-aebd-11b15a0b5e93\elements-v2.xml" />
</ApplyElementManifests>

View File

@@ -12,20 +12,21 @@
</style>
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
<div class="ps-page"><div class="ps-shell"><div id="statusMessage" class="ps-status-host"></div><section class="ps-hero"><div class="ps-eyebrow">Portal Settings</div><h1>Zentrale Verwaltung fuer aktive Portal-Erweiterungen</h1><p>Diese Seite erkennt automatisch unterstuetzte Application Customizer und blendet pro aktiver Loesung einen eigenen Tab ein. Die Aenderungen werden direkt in die jeweilige UserCustomAction geschrieben.</p></section><section class="ps-metrics"><div class="ps-metric"><label>Aktive Erweiterungen</label><strong id="activeSolutionCount">0</strong></div><div class="ps-metric"><label>Aktiver Tab</label><strong id="activeTabValue">-</strong></div><div class="ps-metric"><label>Site Collection</label><strong id="siteUrlValue">-</strong></div><div class="ps-metric"><label>Erkannte Erweiterungen</label><strong id="detectedSolutionsValue">Keine</strong></div></section><div id="tabsHost" class="ps-tabs" role="tablist" aria-label="Portal Settings Tabs"></div><section id="emptyState" class="ps-empty" hidden>Keine unterstuetzten Erweiterungen wurden aktiv gefunden. Aktuell werden <strong>Custom Branding</strong> und <strong>Mega Menu</strong> unterstuetzt.</section>
<div class="ps-page"><div class="ps-shell"><div id="statusMessage" class="ps-status-host"></div><section class="ps-hero"><div class="ps-eyebrow">Portal Settings</div><h1>Zentrale Verwaltung fuer aktive Portal-Erweiterungen</h1><p>Diese Seite erkennt installierte Einstellungsanbieter automatisch und zeigt nur deren Tabs. Konfigurationen werden in UserCustomActions, Feld-Properties oder im Property Bag des Root Webs gespeichert.</p></section><section class="ps-metrics"><div class="ps-metric"><label>Aktive Erweiterungen</label><strong id="activeSolutionCount">0</strong></div><div class="ps-metric"><label>Aktiver Tab</label><strong id="activeTabValue">-</strong></div><div class="ps-metric"><label>Site Collection</label><strong id="siteUrlValue">-</strong></div><div class="ps-metric"><label>Erkannte Erweiterungen</label><strong id="detectedSolutionsValue">Keine</strong></div></section><div id="tabsHost" class="ps-tabs" role="tablist" aria-label="Portal Settings Tabs"></div><section id="emptyState" class="ps-empty" hidden>Keine unterstuetzten Erweiterungen wurden aktiv gefunden. Aktuell werden <strong>Custom Branding</strong>, <strong>Mega Menu</strong> und <strong>ExpiryIndicator</strong> unterstuetzt.</section>
<section id="panel-branding" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>Custom Branding</h2><p>Pflegt die ClientSideComponentProperties des aktiven Custom Branding Application Customizers.</p></div><span id="brandingChip" class="ps-chip">0</span></div><div class="ps-grid"><div class="ps-card ps-span-4"><h3>CSS-Dateien</h3><p>Lokale SiteAssets-Pfade oder externe Stylesheets.</p><div class="ps-meta"><div><label>Component Id</label><strong id="brandingComponentId">-</strong></div><div><label>Registrierung</label><strong id="brandingLocation">-</strong></div></div><label class="ps-label" for="brandingCssPath">Neue CSS-Datei</label><div class="ps-inline"><input type="text" id="brandingCssPath" class="ps-input" placeholder="/sites/root/SiteAssets/custom-branding.css oder https://cdn.example.com/style.css" /><button type="button" class="ps-btn ps-btn-primary" onclick="addBrandingCssFile()">Hinzufuegen</button></div><div id="brandingCssList" class="ps-list"></div></div><div class="ps-card ps-span-4"><h3>Placeholder Top</h3><p>JSON-Array fuer den oberen Placeholder.</p><label class="ps-label" for="brandingTopJson">Top JSON</label><textarea id="brandingTopJson" class="ps-textarea" spellcheck="false"></textarea></div><div class="ps-card ps-span-4"><h3>Placeholder Bottom</h3><p>JSON-Array fuer den unteren Placeholder.</p><label class="ps-label" for="brandingBottomJson">Bottom JSON</label><textarea id="brandingBottomJson" class="ps-textarea" spellcheck="false"></textarea></div><div class="ps-card ps-span-12"><h3>ClientSideComponentProperties Preview</h3><p>Dieses JSON wird direkt in die aktive UserCustomAction fuer Custom Branding geschrieben.</p><pre id="brandingPreview" class="ps-preview"></pre><div class="ps-note"><strong>Schema:</strong> <code>{ cssfiles, placeholdertop: { elements: [] }, placeholderbottom: { elements: [] } }</code></div></div></div><div class="ps-actions-bar"><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="reloadSolutions('branding')">Neu laden</button><button type="button" class="ps-btn ps-btn-quiet" onclick="validateBrandingEditors()">JSON validieren</button><button type="button" class="ps-btn ps-btn-quiet" onclick="formatBrandingEditors()">JSON formatieren</button></div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-primary" onclick="applyBrandingConfig()">Konfiguration anwenden</button></div></div></section>
<section id="panel-megamenu" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>Mega Menu</h2><p>Pflegt die ClientSideComponentProperties des aktiven Mega Menu Application Customizers.</p></div><span id="megaMenuChip" class="ps-chip">OK</span></div><div class="ps-grid"><div class="ps-card ps-span-6"><h3>Navigation</h3><p>Pflegt Term Set, optionales CSS und Debug-Verhalten.</p><div class="ps-meta"><div><label>Component Id</label><strong id="megaMenuComponentId">-</strong></div><div><label>Registrierung</label><strong id="megaMenuLocation">-</strong></div></div><label class="ps-label" for="megaMenuTermSetName">Term Set Name</label><input type="text" id="megaMenuTermSetName" class="ps-input" placeholder="Global Navigation" /><label class="ps-label" for="megaMenuCssUrl" style="margin-top:16px;">Zusatz-CSS URL</label><input type="text" id="megaMenuCssUrl" class="ps-input" placeholder="/sites/root/SiteAssets/megamenu.css oder leer lassen" /><label class="ps-label" style="margin-top:16px;">Debug-Modus</label><div class="ps-check"><input type="checkbox" id="megaMenuDebug" /><label for="megaMenuDebug" style="margin:0;font-weight:600;">Debug-Ausgaben in der Browser-Konsole aktivieren</label></div></div><div class="ps-card ps-span-6"><h3>ClientSideComponentProperties Preview</h3><p>Dieses JSON wird direkt in die aktive UserCustomAction fuer Mega Menu geschrieben.</p><pre id="megaMenuPreview" class="ps-preview"></pre><div class="ps-note"><strong>Schema:</strong> <code>{ termSetName, cssUrl, debug }</code></div></div></div><div class="ps-actions-bar"><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="reloadSolutions('megamenu')">Neu laden</button></div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-primary" onclick="applyMegaMenuConfig()">Konfiguration anwenden</button></div></div></section>
<section id="panel-expiryindicator" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>ExpiryIndicator 2.1</h2><p>Zentrale Standards fuer die Site Collection und lokale Ausnahmen fuer einzelne Listen oder Bibliotheken.</p></div><span id="expiryIndicatorChip" class="ps-chip">V2.1</span></div><div class="ps-grid"><div class="ps-card ps-span-4"><h3>Site-Collection-Standard</h3><p>Gilt im Root Web und in allen Subwebs, sofern eine Bindung auf Vererbung steht.</p><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-quiet" onclick="loadExpirySiteDefaults()">Standard laden</button><button type="button" class="ps-btn ps-btn-primary" onclick="saveExpirySiteDefaults()">Standard speichern</button></div><div class="ps-note"><strong>Speicherort:</strong> Property Bag des Root Webs. Es wird keine Konfigurationsliste verwendet.</div></div><div class="ps-card ps-span-8"><h3>Registrierte Bindungen</h3><p>Listen und Bibliotheken, die mit dem Aktivierungsskript registriert wurden.</p><div id="expiryBindingsList" class="ps-list"><div class="ps-list-empty">Keine Bindungen registriert.</div></div><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-quiet" onclick="setAllExpiryBindingsToInheritance()">Alle auf Vererbung stellen</button></div></div><div class="ps-card ps-span-4"><h3>Ausgewaehltes Ziel</h3><p>Web, Liste und interner Name des gebundenen Ablaufdatumsfeldes.</p><label class="ps-label" for="expiryWebUrl" style="margin-top:16px;">Web URL</label><input type="text" id="expiryWebUrl" class="ps-input" /><label class="ps-label" for="expiryListTitle" style="margin-top:16px;">Listentitel</label><input type="text" id="expiryListTitle" class="ps-input" placeholder="Dokumente" /><label class="ps-label" for="expiryFieldName" style="margin-top:16px;">Interner Feldname</label><input type="text" id="expiryFieldName" class="ps-input" placeholder="ExpiryDate" /><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-primary" onclick="loadExpiryIndicatorConfig()">Lokale Konfiguration laden</button><button type="button" class="ps-btn ps-btn-quiet" onclick="setSelectedExpiryBindingToInheritance()">Standard erben</button></div></div><div class="ps-card ps-span-8"><h3>Konfigurationseditor</h3><p>Unterstuetzt V1-Regeln sowie verschachtelte V2-<code>condition</code>-Gruppen mit <code>and</code>/<code>or</code>.</p><textarea id="expiryIndicatorJson" class="ps-textarea" style="min-height:430px" spellcheck="false"></textarea><div class="ps-note"><strong>Prioritaet:</strong> lokale Feldkonfiguration &gt; Subweb-Override &gt; Site-Collection-Standard &gt; eingebauter Standard.</div></div></div><div class="ps-actions-bar"><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="validateExpiryIndicatorConfig()">JSON validieren</button><button type="button" class="ps-btn ps-btn-quiet" onclick="formatExpiryIndicatorConfig()">JSON formatieren</button></div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-primary" onclick="applyExpiryIndicatorConfig()">Als lokale Ausnahme speichern</button></div></div></section>
<script type="text/javascript">
var CUSTOM_SOLUTION_TAG='MSFT-Custom-Solution';var SUPPORTED_SOLUTIONS={branding:{key:'branding',label:'Custom Branding',componentId:'035ba968-6488-4d42-86b3-0470ffcc95b9',panelId:'panel-branding',descriptionTag:'MSFT-Custom-Solution:CustomBranding'},megamenu:{key:'megamenu',label:'Mega Menu',componentId:'abc3361f-bb2d-491f-aba3-cd51c19a299b',panelId:'panel-megamenu',descriptionTag:'MSFT-Custom-Solution:MegaMenu'}};
var CUSTOM_SOLUTION_TAG='MSFT-Custom-Solution';var EXPIRY_FIELD_COMPONENT_ID='a555f4fc-d6a6-4421-8189-457449d9bbde';var EXPIRY_DESCRIPTOR_PROPERTY='PortalSettings.Solutions.ExpiryIndicator';var EXPIRY_DEFAULTS_PROPERTY='PortalSettings.ExpiryIndicator.Defaults';var EXPIRY_BINDINGS_PROPERTY='PortalSettings.ExpiryIndicator.Bindings';var SUPPORTED_SOLUTIONS={branding:{key:'branding',label:'Custom Branding',componentId:'035ba968-6488-4d42-86b3-0470ffcc95b9',panelId:'panel-branding',descriptionTag:'MSFT-Custom-Solution:CustomBranding',providerType:'applicationCustomizer'},megamenu:{key:'megamenu',label:'Mega Menu',componentId:'abc3361f-bb2d-491f-aba3-cd51c19a299b',panelId:'panel-megamenu',descriptionTag:'MSFT-Custom-Solution:MegaMenu',providerType:'applicationCustomizer'},expiryindicator:{key:'expiryindicator',label:'ExpiryIndicator',componentId:EXPIRY_FIELD_COMPONENT_ID,panelId:'panel-expiryindicator',providerType:'rootPropertyBag'}};
var state={booted:false,activeKey:'',solutions:{}};
document.addEventListener('DOMContentLoaded',function(){bootPage();});
function bootPage(){if(state.booted){return;}if(window.SP&&SP.ClientContext){state.booted=true;bindPage();reloadSolutions();return;}window.setTimeout(bootPage,150);}
function bindPage(){document.getElementById('siteUrlValue').textContent=getSiteUrl();document.getElementById('brandingCssPath').addEventListener('keypress',function(event){if(event.key==='Enter'){event.preventDefault();addBrandingCssFile();}});document.getElementById('brandingTopJson').addEventListener('input',updateBrandingPreview);document.getElementById('brandingBottomJson').addEventListener('input',updateBrandingPreview);document.getElementById('megaMenuTermSetName').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuCssUrl').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuDebug').addEventListener('change',updateMegaMenuPreview);}
function reloadSolutions(preferredKey){showStatus('Lade aktive Application Customizer...','info',true);state.solutions={};loadSolutionsSequentially(Object.keys(SUPPORTED_SOLUTIONS),0,preferredKey||state.activeKey,[],[],function(labels,errors,preferred){var keys=getAvailableSolutionKeys();if(!keys.length){state.activeKey='';renderTabs();renderPanels();updateOverview();showStatus('Keine unterstuetzten Erweiterungen mit dem Tag MSFT-Custom-Solution wurden aktiv konfiguriert gefunden.','warning',true);return;}state.activeKey=preferred&&state.solutions[preferred]?preferred:keys[0];hydratePanels();renderTabs();renderPanels();updateOverview();var message='Aktive Erweiterungen geladen: '+labels.join(', ')+'.';if(errors.length){showStatus(message+' Hinweise: '+errors.join(' | '),'warning',true);return;}showStatus(message,'success');});}
function loadSolutionsSequentially(keys,index,preferred,labels,errors,done){if(index>=keys.length){done(labels,errors,preferred);return;}var key=keys[index];var definition=SUPPORTED_SOLUTIONS[key];readCustomizerActionInfo(definition,function(info){if(info&&info.action){state.solutions[key]={definition:definition,info:info,config:parseSolutionConfig(key,info.action.ClientSideComponentProperties)};labels.push(definition.label);}loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);},function(message){errors.push(definition.label+': '+message);loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);});}
function hydratePanels(){if(state.solutions.branding){populateBrandingPanel(state.solutions.branding);}if(state.solutions.megamenu){populateMegaMenuPanel(state.solutions.megamenu);}}
function bindPage(){document.getElementById('siteUrlValue').textContent=getSiteUrl();document.getElementById('expiryWebUrl').value=getWebUrl();document.getElementById('expiryIndicatorJson').value=JSON.stringify(defaultExpiryIndicatorConfig('ExpiryDate'),null,2);document.getElementById('brandingCssPath').addEventListener('keypress',function(event){if(event.key==='Enter'){event.preventDefault();addBrandingCssFile();}});document.getElementById('brandingTopJson').addEventListener('input',updateBrandingPreview);document.getElementById('brandingBottomJson').addEventListener('input',updateBrandingPreview);document.getElementById('megaMenuTermSetName').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuCssUrl').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuDebug').addEventListener('change',updateMegaMenuPreview);}
function reloadSolutions(preferredKey){showStatus('Lade aktive Einstellungsanbieter...','info',true);state.solutions={};loadSolutionsSequentially(Object.keys(SUPPORTED_SOLUTIONS),0,preferredKey||state.activeKey,[],[],function(labels,errors,preferred){var keys=getAvailableSolutionKeys();if(!keys.length){state.activeKey='';renderTabs();renderPanels();updateOverview();showStatus('Keine unterstuetzten Erweiterungen wurden aktiv registriert gefunden.','warning',true);return;}state.activeKey=preferred&&state.solutions[preferred]?preferred:keys[0];hydratePanels();renderTabs();renderPanels();updateOverview();var message='Aktive Erweiterungen geladen: '+labels.join(', ')+'.';if(errors.length){showStatus(message+' Hinweise: '+errors.join(' | '),'warning',true);return;}showStatus(message,'success');});}
function loadSolutionsSequentially(keys,index,preferred,labels,errors,done){if(index>=keys.length){done(labels,errors,preferred);return;}var key=keys[index];var definition=SUPPORTED_SOLUTIONS[key];if(definition.providerType==='rootPropertyBag'){readExpiryProviderInfo(function(info){if(info){state.solutions[key]={definition:definition,info:info,config:info.siteDefaults};labels.push(definition.label);}loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);},function(message){errors.push(definition.label+': '+message);loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);});return;}readCustomizerActionInfo(definition,function(info){if(info&&info.action){state.solutions[key]={definition:definition,info:info,config:parseSolutionConfig(key,info.action.ClientSideComponentProperties)};labels.push(definition.label);}loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);},function(message){errors.push(definition.label+': '+message);loadSolutionsSequentially(keys,index+1,preferred,labels,errors,done);});}
function hydratePanels(){if(state.solutions.branding){populateBrandingPanel(state.solutions.branding);}if(state.solutions.megamenu){populateMegaMenuPanel(state.solutions.megamenu);}if(state.solutions.expiryindicator){renderExpiryBindings();}}
function renderTabs(){var host=document.getElementById('tabsHost');var keys=getAvailableSolutionKeys();if(!keys.length){host.innerHTML='';host.hidden=true;document.getElementById('emptyState').hidden=false;return;}host.hidden=false;document.getElementById('emptyState').hidden=true;var html='';for(var i=0;i<keys.length;i++){var key=keys[i];var def=SUPPORTED_SOLUTIONS[key];var isActive=state.activeKey===key;html+='<button type="button" class="ps-tab'+(isActive?' is-active':'')+'" role="tab" aria-selected="'+(isActive?'true':'false')+'" aria-controls="'+def.panelId+'" onclick="switchTab(\''+key+'\')">'+escapeHtml(def.label)+'</button>'; }host.innerHTML=html;}
function renderPanels(){document.getElementById('panel-branding').hidden=!(state.activeKey==='branding'&&state.solutions.branding);document.getElementById('panel-megamenu').hidden=!(state.activeKey==='megamenu'&&state.solutions.megamenu);}
function renderPanels(){document.getElementById('panel-branding').hidden=!(state.activeKey==='branding'&&state.solutions.branding);document.getElementById('panel-megamenu').hidden=!(state.activeKey==='megamenu'&&state.solutions.megamenu);document.getElementById('panel-expiryindicator').hidden=!(state.activeKey==='expiryindicator'&&state.solutions.expiryindicator);}
function switchTab(key){if(!state.solutions[key]){return;}state.activeKey=key;renderTabs();renderPanels();updateOverview();}
function updateOverview(){var keys=getAvailableSolutionKeys();document.getElementById('activeSolutionCount').textContent=String(keys.length);document.getElementById('activeTabValue').textContent=state.activeKey&&SUPPORTED_SOLUTIONS[state.activeKey]?SUPPORTED_SOLUTIONS[state.activeKey].label:'-';document.getElementById('detectedSolutionsValue').textContent=keys.length?keys.map(function(key){return SUPPORTED_SOLUTIONS[key].label;}).join(', '):'Keine';}
function populateBrandingPanel(solution){document.getElementById('brandingComponentId').textContent=solution.definition.componentId;document.getElementById('brandingLocation').textContent=getActionLocationText(solution.info);document.getElementById('brandingCssPath').value='';document.getElementById('brandingTopJson').value=JSON.stringify(solution.config.placeholdertop.elements,null,2);document.getElementById('brandingBottomJson').value=JSON.stringify(solution.config.placeholderbottom.elements,null,2);renderBrandingCssFiles();updateBrandingPreview();}
@@ -41,6 +42,26 @@ function populateMegaMenuPanel(solution){document.getElementById('megaMenuCompon
function buildMegaMenuConfig(){var solution=state.solutions.megamenu;if(!solution){throw new Error('Mega Menu ist aktuell nicht aktiv konfiguriert.');}var termSetName=document.getElementById('megaMenuTermSetName').value.trim();return{termSetName:termSetName,cssUrl:document.getElementById('megaMenuCssUrl').value.trim(),debug:document.getElementById('megaMenuDebug').checked===true};}
function updateMegaMenuPreview(){var preview=document.getElementById('megaMenuPreview');try{var config=buildMegaMenuConfig();preview.textContent=JSON.stringify(config,null,2);document.getElementById('megaMenuChip').textContent=config.debug?'DBG':'OK';}catch(error){preview.textContent='Fehler: '+error.message;document.getElementById('megaMenuChip').textContent='!';}}
function applyMegaMenuConfig(){try{var config=buildMegaMenuConfig();showStatus('Aktualisiere Mega Menu...','info',true);updateSolutionAction('megamenu',config,function(){state.solutions.megamenu.config=normalizeMegaMenuConfig(config);populateMegaMenuPanel(state.solutions.megamenu);showStatus('Mega Menu wurde erfolgreich aktualisiert.','success');},function(message){showStatus('Mega Menu konnte nicht aktualisiert werden: '+message,'error',true);});}catch(error){showStatus(error.message,'error');}}
function defaultExpiryIndicatorConfig(expiryField){return{baseField:'Created',expiryField:expiryField||'ExpiryDate',default:{lifeTime:{value:2,unit:'years'},columnRule:[{daysUntilExpiry:0,operator:'lessOrEqual',backgroundColor:'#a4262c',textColor:'#ffffff',label:'Abgelaufen'},{daysUntilExpiry:30,operator:'lessOrEqual',backgroundColor:'#ffaa44',textColor:'#000000',label:'Laeuft bald ab'},{daysUntilExpiry:90,operator:'lessOrEqual',backgroundColor:'#fff4ce',textColor:'#000000',label:'Beobachten'}]},rules:[],nullText:'Kein Ablaufdatum',confirmExtension:true};}
function propertyBagJson(properties,key,fallback){if(!properties){return fallback;}var value;Object.keys(properties).some(function(name){if(name.toLowerCase()===key.toLowerCase()){value=properties[name];return true;}return false;});if(typeof value==='undefined'||value===null||value===''){return fallback;}if(typeof value==='object'){return value;}try{return JSON.parse(value);}catch(ignore){return fallback;}}
function readExpiryProviderInfo(onSuccess,onError){getJson(getSiteUrl().replace(/\/$/,'')+'/_api/web/AllProperties',function(payload){var properties=payload.d||payload||{};var descriptor=propertyBagJson(properties,EXPIRY_DESCRIPTOR_PROPERTY,null);var bindings=propertyBagJson(properties,EXPIRY_BINDINGS_PROPERTY,[]);var siteDefaults=propertyBagJson(properties,EXPIRY_DEFAULTS_PROPERTY,null);if(!Array.isArray(bindings)){bindings=[];}if(!descriptor&&!siteDefaults&&!bindings.length){onSuccess(null);return;}onSuccess({descriptor:descriptor,bindings:bindings,siteDefaults:siteDefaults||defaultExpiryIndicatorConfig('ExpiryDate'),rootProperties:properties,selected:null});},onError);}
function renderExpiryBindings(){var solution=state.solutions.expiryindicator;var host=document.getElementById('expiryBindingsList');if(!solution||!solution.info||!solution.info.bindings.length){host.innerHTML='<div class="ps-list-empty">Keine Bindungen registriert. Aktivieren Sie ExpiryIndicator 2.1 erneut an den gewuenschten Listen.</div>';return;}var html='';for(var i=0;i<solution.info.bindings.length;i++){var binding=solution.info.bindings[i];html+='<div class="ps-item"><div><strong>'+escapeHtml(binding.listTitle||binding.listId)+'</strong><div class="ps-path">'+escapeHtml(binding.webUrl)+' | '+escapeHtml(binding.fieldInternalName)+'</div></div><button type="button" class="ps-btn ps-btn-quiet" onclick="selectExpiryBinding('+i+')">Auswaehlen</button></div>';}host.innerHTML=html;document.getElementById('expiryIndicatorChip').textContent=String(solution.info.bindings.length);}
function selectExpiryBinding(index){var solution=state.solutions.expiryindicator;if(!solution||!solution.info||!solution.info.bindings[index]){return;}var binding=solution.info.bindings[index];document.getElementById('expiryWebUrl').value=binding.webUrl||getWebUrl();document.getElementById('expiryListTitle').value=binding.listTitle||'';document.getElementById('expiryFieldName').value=binding.fieldInternalName||'ExpiryDate';loadExpiryIndicatorConfig();}
function loadExpirySiteDefaults(){var solution=state.solutions.expiryindicator;if(!solution||!solution.info){return;}var config=solution.info.siteDefaults||defaultExpiryIndicatorConfig('ExpiryDate');document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);showStatus('Site-Collection-Standard wurde in den Editor geladen.','success');}
function saveExpirySiteDefaults(){var config;try{config=parseExpiryIndicatorEditor();}catch(error){showStatus(error.message,'error',true);return;}showStatus('Speichere Site-Collection-Standard...','info',true);setRootPropertyBagValue(EXPIRY_DEFAULTS_PROPERTY,JSON.stringify(config),function(){state.solutions.expiryindicator.info.siteDefaults=config;state.solutions.expiryindicator.config=config;showStatus('Site-Collection-Standard wurde gespeichert. Vererbende Bindungen verwenden ihn beim naechsten Laden.','success');},function(message){showStatus('Site-Collection-Standard konnte nicht gespeichert werden: '+message,'error',true);});}
function setRootPropertyBagValue(key,value,onSuccess,onError){try{var context=new SP.ClientContext(getSiteUrl());var web=context.get_web();var properties=web.get_allProperties();context.load(properties);context.executeQueryAsync(function(){try{properties.set_item(key,value);web.update();context.executeQueryAsync(onSuccess,function(sender,args){onError(args.get_message());});}catch(error){onError(error.message||String(error));}},function(sender,args){onError(args.get_message());});}catch(error){onError(error.message||String(error));}}
function inheritancePayload(fieldName){return{__metadata:{type:'SP.Field'},ClientSideComponentProperties:JSON.stringify({schemaVersion:2,expiryField:fieldName,inheritSiteDefaults:true})};}
function setSelectedExpiryBindingToInheritance(){var target;try{target=getExpiryTarget();}catch(error){showStatus(error.message,'error');return;}showStatus('Aktiviere Vererbung fuer die ausgewaehlte Bindung...','info',true);postJsonMerge(expiryFieldUrl(target),inheritancePayload(target.fieldName),function(){if(state.solutions.expiryindicator.info){state.solutions.expiryindicator.info.selected=null;}loadExpirySiteDefaults();showStatus('Die Bindung erbt jetzt den Site-Collection-Standard.','success');},function(message){showStatus('Vererbung konnte nicht aktiviert werden: '+message,'error',true);});}
function expiryBindingFieldUrl(binding){return String(binding.webUrl||'').replace(/\/$/,'')+"/_api/web/lists(guid'"+normalizeGuid(binding.listId)+"')/fields/getbyinternalnameortitle('"+String(binding.fieldInternalName||'ExpiryDate').replace(/'/g,"''")+"')";}
function setAllExpiryBindingsToInheritance(){var solution=state.solutions.expiryindicator;var bindings=solution&&solution.info?solution.info.bindings:[];if(!bindings.length){showStatus('Es sind keine Bindungen registriert.','warning');return;}if(!window.confirm('Lokale Konfigurationen von '+bindings.length+' Bindung(en) durch Vererbung ersetzen?')){return;}showStatus('Aktiviere Vererbung fuer '+bindings.length+' Bindung(en)...','info',true);var errors=[];function next(index){if(index>=bindings.length){if(errors.length){showStatus('Vererbung wurde mit Hinweisen abgeschlossen: '+errors.join(' | '),'warning',true);}else{showStatus('Alle registrierten Bindungen erben jetzt den Site-Collection-Standard.','success');}return;}var binding=bindings[index];postJsonMerge(expiryBindingFieldUrl(binding),inheritancePayload(binding.fieldInternalName),function(){next(index+1);},function(message){errors.push((binding.listTitle||binding.listId)+': '+message);next(index+1);});}next(0);}
function getExpiryTarget(){var webUrl=document.getElementById('expiryWebUrl').value.trim().replace(/\/$/,'');var listTitle=document.getElementById('expiryListTitle').value.trim();var fieldName=document.getElementById('expiryFieldName').value.trim();if(!webUrl){throw new Error('Bitte eine Web URL angeben.');}if(!listTitle){throw new Error('Bitte einen Listentitel angeben.');}if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName)){throw new Error('Der interne Feldname ist ungueltig.');}return{webUrl:webUrl,listTitle:listTitle,fieldName:fieldName};}
function expiryFieldUrl(target){return target.webUrl+"/_api/web/lists/getbytitle('"+target.listTitle.replace(/'/g,"''")+"')/fields/getbyinternalnameortitle('"+target.fieldName+"')";}
function loadExpiryIndicatorConfig(){var target;try{target=getExpiryTarget();}catch(error){showStatus(error.message,'error');return;}showStatus('Lade ExpiryIndicator-Konfiguration...','info',true);getJson(expiryFieldUrl(target)+'?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties',function(payload){var field=payload.d||payload;if(normalizeGuid(field.ClientSideComponentId)!==normalizeGuid(EXPIRY_FIELD_COMPONENT_ID)){showStatus('Das Feld ist nicht mit dem ExpiryIndicator Field Customizer verbunden.','error',true);return;}var config=defaultExpiryIndicatorConfig(field.InternalName);var inherited=false;if(field.ClientSideComponentProperties&&field.ClientSideComponentProperties.trim()){try{var stored=JSON.parse(field.ClientSideComponentProperties);if(stored.inheritSiteDefaults===true){inherited=true;config=JSON.parse(JSON.stringify(state.solutions.expiryindicator.info.siteDefaults||config));}else{config=stored;}}catch(error){showStatus('Die gespeicherten Component Properties enthalten ungueltiges JSON: '+error.message,'error',true);return;}}config.expiryField=field.InternalName;state.solutions.expiryindicator.info.selected={target:target,field:field,updateUrl:expiryFieldUrl(target),actionType:field.__metadata&&field.__metadata.type?field.__metadata.type:'SP.Field'};state.solutions.expiryindicator.config=config;document.getElementById('expiryFieldName').value=field.InternalName;document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);document.getElementById('expiryIndicatorChip').textContent=inherited?'ERBT':'LOKAL';showStatus(inherited?'Die Bindung erbt den Site-Collection-Standard.':'Lokale ExpiryIndicator-Konfiguration wurde geladen.','success');},function(message){showStatus('ExpiryIndicator-Konfiguration konnte nicht geladen werden: '+message,'error',true);});}
function parseExpiryIndicatorEditor(){var raw=document.getElementById('expiryIndicatorJson').value.trim();if(!raw){throw new Error('Die ExpiryIndicator-Konfiguration darf nicht leer sein.');}var config;try{config=JSON.parse(raw);}catch(error){throw new Error('Ungueltiges JSON: '+error.message);}var errors=validateExpiryIndicatorObject(config);if(errors.length){throw new Error(errors.join(' | '));}return config;}
function validateExpiryIndicatorObject(config){var errors=[];function safeName(value){return/^[A-Za-z_][A-Za-z0-9_]*$/.test(value||'');}function validDuration(value,path){if(!value||typeof value.value!=='number'||!isFinite(value.value)||['days','months','years'].indexOf(value.unit)<0){errors.push(path+' muss value und unit days/months/years enthalten.');}}function validColorRules(value,path){if(!Array.isArray(value)){errors.push(path+' muss ein Array sein.');return;}for(var i=0;i<value.length;i++){var item=value[i];if(!item||typeof item.daysUntilExpiry!=='number'||['lessThan','lessOrEqual','equal','greaterOrEqual','greaterThan'].indexOf(item.operator)<0||typeof item.backgroundColor!=='string'||typeof item.textColor!=='string'){errors.push(path+'['+i+'] ist ungueltig.');}}}function validGroup(group,path,depth){if(depth>10){errors.push(path+' ist tiefer als 10 Ebenen.');return;}if(!group||(group.operator!=='and'&&group.operator!=='or')){errors.push(path+'.operator muss and oder or sein.');}if(!group||!Array.isArray(group.conditions)||!group.conditions.length){errors.push(path+'.conditions muss mindestens einen Eintrag enthalten.');return;}for(var i=0;i<group.conditions.length;i++){var condition=group.conditions[i];var child=path+'.conditions['+i+']';if(condition&&typeof condition.operator!=='undefined'){validGroup(condition,child,depth+1);}else if(!condition||!safeName(condition.columnName)||typeof condition.columnValue==='undefined'){errors.push(child+' benoetigt columnName und columnValue.');}}}if(!config||typeof config!=='object'){return['Konfiguration muss ein Objekt sein.'];}if(!safeName(config.baseField)){errors.push('baseField ist ungueltig.');}if(!safeName(config.expiryField)){errors.push('expiryField ist ungueltig.');}if(!config.default){errors.push('default fehlt.');}else{validDuration(config.default.lifeTime,'default.lifeTime');validColorRules(config.default.columnRule,'default.columnRule');}if(!Array.isArray(config.rules)){errors.push('rules muss ein Array sein.');}else{for(var r=0;r<config.rules.length;r++){var rule=config.rules[r];var path='rules['+r+']';if(rule&&rule.condition){validGroup(rule.condition,path+'.condition',0);}else if(!rule||!safeName(rule.columnName)||typeof rule.columnValue==='undefined'){errors.push(path+' benoetigt condition oder columnName/columnValue.');}if(rule){validDuration(rule.lifeTime,path+'.lifeTime');validColorRules(rule.columnRule,path+'.columnRule');}}}return errors;}
function validateExpiryIndicatorConfig(){try{parseExpiryIndicatorEditor();showStatus('ExpiryIndicator V2 JSON ist gueltig.','success');document.getElementById('expiryIndicatorChip').textContent='OK';}catch(error){showStatus(error.message,'error',true);document.getElementById('expiryIndicatorChip').textContent='!';}}
function formatExpiryIndicatorConfig(){try{var config=parseExpiryIndicatorEditor();document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);showStatus('ExpiryIndicator JSON wurde formatiert.','success');}catch(error){showStatus(error.message,'error',true);}}
function applyExpiryIndicatorConfig(){var solution=state.solutions.expiryindicator;var selected=solution&&solution.info?solution.info.selected:null;if(!selected){showStatus('Bitte zuerst eine lokale ExpiryIndicator-Konfiguration laden.','warning',true);return;}var config;try{config=parseExpiryIndicatorEditor();}catch(error){showStatus(error.message,'error',true);return;}config.expiryField=selected.field.InternalName;var payload={__metadata:{type:selected.actionType},ClientSideComponentProperties:JSON.stringify(config)};showStatus('Aktualisiere lokale ExpiryIndicator-Ausnahme...','info',true);postJsonMerge(selected.updateUrl,payload,function(){solution.config=config;document.getElementById('expiryIndicatorJson').value=JSON.stringify(config,null,2);document.getElementById('expiryIndicatorChip').textContent='LOKAL';showStatus('Lokale ExpiryIndicator-Konfiguration wurde erfolgreich aktualisiert.','success');},function(message){showStatus('ExpiryIndicator konnte nicht aktualisiert werden: '+message,'error',true);});}
function parseSolutionConfig(key,rawValue){var parsed={};if(typeof rawValue==='string'&&rawValue.trim().length>0){try{parsed=JSON.parse(rawValue);}catch(error){parsed={};}}if(key==='branding'){return normalizeBrandingConfig(parsed);}if(key==='megamenu'){return normalizeMegaMenuConfig(parsed);}return parsed||{};}
function normalizeBrandingConfig(config){var normalized={cssfiles:[],placeholdertop:{elements:[]},placeholderbottom:{elements:[]}};if(config&&Array.isArray(config.cssfiles)){normalized.cssfiles=config.cssfiles.filter(function(item){return item&&typeof item.path==='string'&&item.path.trim().length>0;}).map(function(item){return{path:item.path.trim()};});}if(config&&config.placeholdertop&&Array.isArray(config.placeholdertop.elements)){normalized.placeholdertop.elements=config.placeholdertop.elements;}else if(config&&Array.isArray(config.elements)){normalized.placeholdertop.elements=config.elements;}if(config&&config.placeholderbottom&&Array.isArray(config.placeholderbottom.elements)){normalized.placeholderbottom.elements=config.placeholderbottom.elements;}return normalized;}
function normalizeMegaMenuConfig(config){return{termSetName:config&&typeof config.termSetName==='string'?config.termSetName.trim():'',cssUrl:config&&typeof config.cssUrl==='string'?config.cssUrl.trim():'',debug:!!(config&&config.debug===true)};}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="PortalSettingsPageV11" Url="SitePages" RootWebOnly="TRUE">
<File
Path="PortalSettings.aspx"
Url="PortalSettings.aspx"
Type="GhostableInLibrary"
IgnoreIfAlreadyExists="FALSE"
ReplaceContent="TRUE" />
</Module>
</Elements>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="PortalSettingsPageV2" Url="SitePages" RootWebOnly="TRUE">
<File
Path="PortalSettings.aspx"
Url="PortalSettings.aspx"
Type="GhostableInLibrary"
IgnoreIfAlreadyExists="FALSE"
ReplaceContent="TRUE" />
</Module>
</Elements>

View File

@@ -0,0 +1,3 @@
<ApplyElementManifests>
<ElementManifest Location="57bd9daf-48f9-49e2-aebd-11b15a0b5e93\elements-v1.1.xml" />
</ApplyElementManifests>

View File

@@ -0,0 +1,3 @@
<ApplyElementManifests>
<ElementManifest Location="57bd9daf-48f9-49e2-aebd-11b15a0b5e93\elements-v2.xml" />
</ApplyElementManifests>