Aktualisiere Versionsnummer auf 2.2.0 in README.md, package.json, package-lock.json und package-solution.json. Verbessere Mega Menu Konfiguration durch zentrale Speicherung im Property Bag des Root Webs und Synchronisierung auf alle Unterwebs. Aktualisiere UI-Beschreibungen und füge neue Logik zur Handhabung von Runtime-Bindungen hinzu.

This commit is contained in:
Torsten Brendgen
2026-07-19 23:15:40 +02:00
parent d7d469adb0
commit 62fef65c12
5 changed files with 19 additions and 13 deletions

View File

@@ -9,7 +9,7 @@ Aktuell verwaltet sie:
- Mega Menu
- ExpiryIndicator
Fuer Mega Menu wird das Termset ueber einen durchsuchbaren Picker aus dem Default Site Collection Term Store ausgewaehlt. PortalSettings speichert automatisch Termset-ID und Namen sowie Cache-Dauer und Debug-Modus. Individuelle Stylesheets werden ausschliesslich ueber Custom Branding eingebunden.
Fuer Mega Menu wird das Termset ueber einen durchsuchbaren Picker aus dem Default Site Collection Term Store ausgewaehlt. PortalSettings speichert Termset-ID, Namen, Cache-Dauer und Debug-Modus einmal zentral im Property Bag des Root Webs. Beim Speichern werden identische Runtime-Bindungen rekursiv auf das Root Web und alle vorhandenen Unterwebs synchronisiert. Individuelle Stylesheets werden ausschliesslich ueber Custom Branding eingebunden.
## ExpiryIndicator 2.1
@@ -27,10 +27,11 @@ PortalSettings bleibt fuer den Betrieb des ExpiryIndicators optional.
## Erkennung
- Custom Branding und Mega Menu werden anhand ihrer aktiven, markierten Application-Customizer-UserCustomAction erkannt.
- Custom Branding wird anhand seiner aktiven Application-Customizer-UserCustomAction erkannt.
- Mega Menu wird anhand seiner zentralen Root-Web-Konfiguration oder einer aktiven Runtime-Bindung erkannt.
- ExpiryIndicator registriert einen Solution-Descriptor und sein Bindungsinventar im Property Bag des Root Webs.
Beim Speichern einer nur web-scoped registrierten Custom-Branding- oder MegaMenu-Konfiguration erstellt PortalSettings automatisch die site-scoped UserCustomAction und entfernt den alten Web-Eintrag. Das ist fuer die verlaessliche Ausfuehrung auf allen modernen Seiten und Subwebs der Site Collection erforderlich.
Custom Branding kann beim Speichern auf eine site-scoped UserCustomAction angehoben werden. Mega Menu verwendet wegen der On-Premises-Loader-Eigenheiten bewusst web-scoped Runtime-Bindungen. Die fachliche Konfiguration bleibt dennoch einmalig und zentral: PortalSettings speichert sie im Root Web und verteilt sie auf alle vorhandenen Webs. Fuer spaeter neu angelegte Unterwebs wird die Konfiguration einfach erneut gespeichert, wodurch die Synchronisierung idempotent nachgezogen wird.
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
@@ -49,7 +50,7 @@ Das Paket liegt danach unter `sharepoint/solution/portal-settings.sppkg`.
## Installation und Upgrade
1. `portal-settings.sppkg` im App Catalog durch Version `2.1.8.0` ersetzen.
1. `portal-settings.sppkg` im App Catalog durch Version `2.2.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. `/SitePages/PortalSettings.aspx` mit geleertem Browsercache laden.

View File

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

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "2.1.8",
"version": "2.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "2.1.8",
"version": "2.2.0",
"private": true,
"main": "lib/index.js",
"engines": {

View File

@@ -15,18 +15,18 @@
<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><div class="ps-hero-actions"><button type="button" class="ps-btn" onclick="openExpiryBindingDialog()">ExpiryIndicator anbinden</button></div></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>Waehlt das Termset aus dem Default Site Collection Term Store und pflegt Cache sowie Debug-Verhalten. Individuelle Styles werden ueber Custom Branding eingebunden.</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><input type="hidden" id="megaMenuTermSetId" /><input type="hidden" id="megaMenuTermSetName" /><label class="ps-label">Ausgewaehltes Termset</label><div class="ps-item"><div><strong id="megaMenuSelectedTermSetName">Kein Termset ausgewaehlt</strong><div id="megaMenuSelectedTermSetMeta" class="ps-path">-</div></div><div class="ps-actions"><button type="button" class="ps-btn" onclick="openMegaMenuTermSetDialog()">Auswaehlen</button><button type="button" class="ps-btn ps-btn-quiet" onclick="clearMegaMenuTermSet()">Leeren</button></div></div><label class="ps-label" for="megaMenuCacheMinutes" style="margin-top:16px;">Cache-Dauer in Minuten</label><input type="number" id="megaMenuCacheMinutes" class="ps-input" min="1" max="1440" value="15" /><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>{ termSetId, termSetName, cacheMinutes, 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-megamenu" class="ps-panel" hidden><div class="ps-panel-head"><div><h2>Mega Menu</h2><p>Eine zentrale Konfiguration fuer die gesamte Site Collection. PortalSettings synchronisiert die Runtime-Bindung auf Root Web und alle Unterwebs.</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>Waehlt das Termset aus dem Default Site Collection Term Store und pflegt Cache sowie Debug-Verhalten. Individuelle Styles werden ueber Custom Branding eingebunden.</p><div class="ps-meta"><div><label>Component Id</label><strong id="megaMenuComponentId">-</strong></div><div><label>Konfigurationsbereich</label><strong id="megaMenuLocation">Site Collection</strong></div></div><input type="hidden" id="megaMenuTermSetId" /><input type="hidden" id="megaMenuTermSetName" /><label class="ps-label">Ausgewaehltes Termset</label><div class="ps-item"><div><strong id="megaMenuSelectedTermSetName">Kein Termset ausgewaehlt</strong><div id="megaMenuSelectedTermSetMeta" class="ps-path">-</div></div><div class="ps-actions"><button type="button" class="ps-btn" onclick="openMegaMenuTermSetDialog()">Auswaehlen</button><button type="button" class="ps-btn ps-btn-quiet" onclick="clearMegaMenuTermSet()">Leeren</button></div></div><label class="ps-label" for="megaMenuCacheMinutes" style="margin-top:16px;">Cache-Dauer in Minuten</label><input type="number" id="megaMenuCacheMinutes" class="ps-input" min="1" max="1440" value="15" /><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 class="ps-note"><strong>Speicherort:</strong> Property Bag des Root Webs. Beim Speichern werden alle vorhandenen Unterwebs rekursiv synchronisiert.</div></div><div class="ps-card ps-span-6"><h3>Zentrale Konfiguration</h3><p>Dieses JSON wird einmal zentral gespeichert und identisch in die web-scoped Runtime-Bindungen geschrieben.</p><pre id="megaMenuPreview" class="ps-preview"></pre><div class="ps-note"><strong>Schema:</strong> <code>{ termSetId, termSetName, cacheMinutes, 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()">Speichern und alle Webs synchronisieren</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 per PortalSettings oder 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="openExpiryBindingDialog()">Bindung hinzufuegen</button><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>
<div id="expiryBindingDialog" class="ps-modal-backdrop" hidden><div class="ps-modal" role="dialog" aria-modal="true" aria-labelledby="expiryBindingDialogTitle"><h2 id="expiryBindingDialogTitle">ExpiryIndicator anbinden</h2><p>Verbindet ein bereits vorhandenes Datumsfeld mit dem Field Customizer und registriert die Liste zentral.</p><div id="bindExpiryStatus"></div><label class="ps-label" for="bindExpiryWebUrl">Web URL</label><input type="text" id="bindExpiryWebUrl" class="ps-input" /><label class="ps-label" for="bindExpiryListTitle" style="margin-top:16px;">Listentitel</label><input type="text" id="bindExpiryListTitle" class="ps-input" placeholder="Documents" /><label class="ps-label" for="bindExpiryFieldName" style="margin-top:16px;">Interner Name des vorhandenen Datumsfeldes</label><input type="text" id="bindExpiryFieldName" class="ps-input" placeholder="ExpiryDate" /><div class="ps-check" style="margin-top:14px;"><input type="checkbox" id="bindExpiryClassic" checked="checked" /><label for="bindExpiryClassic" style="margin:0;font-weight:600;">Classic ScriptLink und Ribbon-Befehl registrieren</label></div><div class="ps-note">Die Solution legt keine Spalte an. Bei bereits vorhandener lokaler Konfiguration bleiben die Component Properties erhalten.</div><div class="ps-actions"><button type="button" class="ps-btn ps-btn-quiet" onclick="closeExpiryBindingDialog()">Abbrechen</button><button type="button" id="bindExpirySubmit" class="ps-btn ps-btn-primary" onclick="bindExpiryIndicatorFromDialog()">Bindung anlegen</button></div></div></div>
<div id="megaMenuTermSetDialog" class="ps-modal-backdrop" hidden><div class="ps-modal" role="dialog" aria-modal="true" aria-labelledby="megaMenuTermSetDialogTitle"><h2 id="megaMenuTermSetDialogTitle">Termset auswaehlen</h2><p>Termsets aus dem Default Site Collection Term Store.</p><div id="megaMenuTermSetStatus"></div><label class="ps-label" for="megaMenuTermSetSearch">Suche</label><input type="text" id="megaMenuTermSetSearch" class="ps-input" placeholder="Gruppe oder Termset" oninput="renderMegaMenuTermSets()" /><div id="megaMenuTermSetList" style="max-height:420px;overflow:auto;margin-top:16px;"></div><div class="ps-actions" style="margin-top:16px;"><button type="button" class="ps-btn ps-btn-quiet" onclick="closeMegaMenuTermSetDialog()">Abbrechen</button></div></div></div>
<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 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 MEGAMENU_COMPONENT_ID='c0abbffb-355d-4d4e-bd38-9e15fb811506';var MEGAMENU_LEGACY_COMPONENT_ID='abc3361f-bb2d-491f-aba3-cd51c19a299b';var MEGAMENU_CONFIGURATION_PROPERTY='PortalSettings.MegaMenu.Configuration';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:MEGAMENU_COMPONENT_ID,panelId:'panel-megamenu',descriptionTag:'MSFT-Custom-Solution:MegaMenu',providerType:'siteCollectionMegaMenu'},expiryindicator:{key:'expiryindicator',label:'ExpiryIndicator',componentId:EXPIRY_FIELD_COMPONENT_ID,panelId:'panel-expiryindicator',providerType:'rootPropertyBag'}};
var state={booted:false,activeKey:'',solutions:{},megaMenuTermSets:[],megaMenuTermSetsLoaded:false};
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('megaMenuTermSetId').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuTermSetName').addEventListener('input',updateMegaMenuPreview);document.getElementById('megaMenuCacheMinutes').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 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;}if(definition.providerType==='siteCollectionMegaMenu'){readMegaMenuProviderInfo(definition,function(info){if(info){state.solutions[key]={definition:definition,info:info,config:normalizeMegaMenuConfig(info.centralConfig||{})};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);}
@@ -41,10 +41,15 @@ function validateBrandingEditors(){try{state.solutions.branding.config=normalize
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('megaMenuTermSetId').value=solution.config.termSetId||'';document.getElementById('megaMenuTermSetName').value=solution.config.termSetName||'';document.getElementById('megaMenuCacheMinutes').value=String(solution.config.cacheMinutes||15);document.getElementById('megaMenuDebug').checked=solution.config.debug===true;updateMegaMenuSelectedTermSet();updateMegaMenuPreview();}
function populateMegaMenuPanel(solution){document.getElementById('megaMenuComponentId').textContent=solution.definition.componentId;document.getElementById('megaMenuLocation').textContent='Site Collection | '+String(solution.info.webCount||0)+' Web-Bindungen';document.getElementById('megaMenuTermSetId').value=solution.config.termSetId||'';document.getElementById('megaMenuTermSetName').value=solution.config.termSetName||'';document.getElementById('megaMenuCacheMinutes').value=String(solution.config.cacheMinutes||15);document.getElementById('megaMenuDebug').checked=solution.config.debug===true;updateMegaMenuSelectedTermSet();updateMegaMenuPreview();}
function buildMegaMenuConfig(){var solution=state.solutions.megamenu;if(!solution){throw new Error('Mega Menu ist aktuell nicht aktiv konfiguriert.');}var termSetId=document.getElementById('megaMenuTermSetId').value.trim();var termSetName=document.getElementById('megaMenuTermSetName').value.trim();var cacheMinutes=Number(document.getElementById('megaMenuCacheMinutes').value);if(termSetId&&!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(termSetId)){throw new Error('Die Term Set ID ist keine gueltige GUID.');}if(!termSetId&&!termSetName){throw new Error('Bitte eine Term Set ID oder einen Term Set Name angeben.');}if(!isFinite(cacheMinutes)||cacheMinutes<1||cacheMinutes>1440){throw new Error('Die Cache-Dauer muss zwischen 1 und 1440 Minuten liegen.');}return{termSetId:termSetId,termSetName:termSetName,cacheMinutes:Math.floor(cacheMinutes),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 applyMegaMenuConfig(){try{var config=buildMegaMenuConfig();showStatus('Speichere zentrale MegaMenu-Konfiguration und ermittle alle Unterwebs...','info',true);setRootPropertyBagValue(MEGAMENU_CONFIGURATION_PROPERTY,JSON.stringify(config),function(){synchronizeMegaMenuWebs(config,function(result){showStatus('Mega Menu wurde zentral gespeichert und auf '+result.webCount+' Webs synchronisiert.','success',true);reloadSolutions('megamenu');},function(message){showStatus('Die zentrale Konfiguration wurde gespeichert, die Web-Bindungen konnten aber nicht vollstaendig synchronisiert werden: '+message,'error',true);});},function(message){showStatus('Mega Menu konnte nicht zentral gespeichert werden: '+message,'error',true);});}catch(error){showStatus(error.message,'error');}}
function readMegaMenuProviderInfo(definition,onSuccess,onError){var rootUrl=getSiteUrl().replace(/\/$/,'');getJson(rootUrl+'/_api/web/AllProperties',function(propertyPayload){var properties=propertyPayload.d||propertyPayload||{};var centralConfig=propertyBagJson(properties,MEGAMENU_CONFIGURATION_PROPERTY,null);getAllWebUrls(rootUrl,function(webUrls){readCustomizerActionInfo(definition,function(actionInfo){if(!centralConfig&&actionInfo&&actionInfo.action){centralConfig=parseSolutionConfig('megamenu',actionInfo.action.ClientSideComponentProperties);}if(!centralConfig&&!actionInfo){onSuccess(null);return;}onSuccess({action:actionInfo?actionInfo.action:null,updateBaseUrl:actionInfo?actionInfo.updateBaseUrl:'',actionType:'SP.UserCustomAction',scopeLabel:'Site Collection configuration',centralConfig:centralConfig||normalizeMegaMenuConfig({}),webCount:webUrls.length,webUrls:webUrls});},onError);},onError);},onError);}
function getAllWebUrls(rootUrl,onSuccess,onError){var urls=[];var visited={};function visit(webUrl,done){var normalized=String(webUrl||'').replace(/\/$/,'');var key=normalized.toLowerCase();if(!normalized||visited[key]){done();return;}visited[key]=true;urls.push(normalized);getJson(normalized+'/_api/web/webs?$select=Url',function(payload){var children=getResultArray(payload);var index=0;function next(){if(index>=children.length){done();return;}var child=children[index++];visit(child.Url,next);}next();},onError);}visit(rootUrl,function(){onSuccess(urls);});}
function synchronizeMegaMenuWebs(config,onSuccess,onError){getAllWebUrls(getSiteUrl().replace(/\/$/,''),function(webUrls){var index=0;var failures=[];function next(){if(index>=webUrls.length){if(failures.length){onError(failures.join(' | '));return;}onSuccess({webCount:webUrls.length});return;}var webUrl=webUrls[index++];showStatus('Synchronisiere MegaMenu-Bindung '+index+' von '+webUrls.length+': '+webUrl,'info',true);synchronizeMegaMenuWeb(webUrl,config,next,function(message){failures.push(webUrl+': '+message);next();});}next();},onError);}
function synchronizeMegaMenuWeb(webUrl,config,onSuccess,onError){var actionsUrl=webUrl.replace(/\/$/,'')+'/_api/web/UserCustomActions';getJson(actionsUrl,function(payload){var actions=getResultArray(payload);var current=null;var obsolete=[];for(var i=0;i<actions.length;i++){var componentId=normalizeGuid(actions[i].ClientSideComponentId);if(componentId===normalizeGuid(MEGAMENU_COMPONENT_ID)&&!current){current=actions[i];continue;}if(componentId===normalizeGuid(MEGAMENU_COMPONENT_ID)||componentId===normalizeGuid(MEGAMENU_LEGACY_COMPONENT_ID)){obsolete.push(actions[i]);}}function removeNext(){if(!obsolete.length){upsert();return;}var action=obsolete.shift();postJsonDelete(actionsUrl+"(guid'"+normalizeGuid(action.Id)+"')",removeNext,onError);}function upsert(){var serialized=JSON.stringify(config);if(current&&current.Id){postJsonMerge(actionsUrl+"(guid'"+normalizeGuid(current.Id)+"')",{__metadata:{type:current.__metadata&&current.__metadata.type?current.__metadata.type:'SP.UserCustomAction'},Name:current.Name||createGuidInBraces(),Title:'MegaMenu Application Customizer',Description:'MSFT-Custom-Solution:MegaMenu',Location:'ClientSideExtension.ApplicationCustomizer',ClientSideComponentId:MEGAMENU_COMPONENT_ID,ClientSideComponentProperties:serialized,Sequence:100},onSuccess,onError);return;}postJson(actionsUrl,{__metadata:{type:'SP.UserCustomAction'},Name:createGuidInBraces(),Title:'MegaMenu Application Customizer',Description:'MSFT-Custom-Solution:MegaMenu',Location:'ClientSideExtension.ApplicationCustomizer',ClientSideComponentId:MEGAMENU_COMPONENT_ID,ClientSideComponentProperties:serialized,Sequence:100},onSuccess,onError);}removeNext();},onError);}
function createGuidInBraces(){function part(length){var value='';while(value.length<length){value+=Math.floor(Math.random()*16).toString(16);}return value;}return'{'+part(8)+'-'+part(4)+'-4'+part(3)+'-'+((8+Math.floor(Math.random()*4)).toString(16))+part(3)+'-'+part(12)+'}';}
function updateMegaMenuSelectedTermSet(){var id=document.getElementById('megaMenuTermSetId').value.trim();var name=document.getElementById('megaMenuTermSetName').value.trim();document.getElementById('megaMenuSelectedTermSetName').textContent=name||'Kein Termset ausgewaehlt';document.getElementById('megaMenuSelectedTermSetMeta').textContent=id||'-';}
function clearMegaMenuTermSet(){document.getElementById('megaMenuTermSetId').value='';document.getElementById('megaMenuTermSetName').value='';updateMegaMenuSelectedTermSet();updateMegaMenuPreview();}
function openMegaMenuTermSetDialog(){document.getElementById('megaMenuTermSetDialog').hidden=false;document.getElementById('megaMenuTermSetSearch').value='';document.getElementById('megaMenuTermSetList').innerHTML='';setMegaMenuTermSetStatus('Lade Termsets...','info');if(state.megaMenuTermSetsLoaded){setMegaMenuTermSetStatus('','info');renderMegaMenuTermSets();window.setTimeout(function(){document.getElementById('megaMenuTermSetSearch').focus();},0);return;}loadMegaMenuTermSets();}