Add Portal Settings Web Part with styles, localization, and tests
- Implemented the PortalSettings Web Part with a new manifest and TypeScript file. - Created SCSS styles for the Web Part, defining various UI elements and responsive design. - Added localization support for English and German languages. - Developed tests for Provider Registry and static assets validation. - Included PowerShell script for project validation.
This commit is contained in:
@@ -1,117 +0,0 @@
|
||||
<%@ 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)}
|
||||
.ps-hero-actions{margin-top:18px}.ps-modal-backdrop{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(10,25,45,.58)}.ps-modal-backdrop[hidden]{display:none}.ps-modal{width:min(620px,100%);max-height:calc(100vh - 40px);overflow:auto;padding:24px;border-radius:20px;background:#fff;box-shadow:0 28px 70px rgba(0,0,0,.28)}.ps-modal h2{margin:0 0 8px}.ps-modal p{margin:0 0 18px;color:var(--ps-muted);line-height:1.6}.ps-modal .ps-actions{justify-content:flex-end;margin-top:22px}
|
||||
@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><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>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 > Subweb-Override > Site-Collection-Standard > 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 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;}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);}
|
||||
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='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('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&¤t.Id){postJsonMerge(actionsUrl+"(guid'"+normalizeGuid(current.Id)+"')",{__metadata:{type:current.__metadata&¤t.__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();}
|
||||
function closeMegaMenuTermSetDialog(){document.getElementById('megaMenuTermSetDialog').hidden=true;}
|
||||
function setMegaMenuTermSetStatus(message,type){var host=document.getElementById('megaMenuTermSetStatus');host.innerHTML=message?'<div class="ps-status ps-status-'+type+'">'+escapeHtml(message)+'</div>':'';}
|
||||
function loadMegaMenuTermSets(){var requestXml='<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="PortalSettings" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectIdentityQuery Id="3" ObjectPathId="1" /><ObjectPath Id="5" ObjectPathId="4" /><ObjectIdentityQuery Id="6" ObjectPathId="4" /><Query Id="7" ObjectPathId="4"><Query SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /><Property Name="Groups"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="IsSystemGroup" ScalarProperty="true" /><Property Name="TermSets"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="Description" ScalarProperty="true" /></Properties></ChildItemQuery></Property></Properties></ChildItemQuery></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="4" ParentId="1" Name="GetDefaultSiteCollectionTermStore" /></ObjectPaths></Request>';try{var request=new XMLHttpRequest();request.open('POST',getSiteUrl().replace(/\/$/,'')+'/_vti_bin/client.svc/ProcessQuery',true);request.setRequestHeader('Accept','application/json');request.setRequestHeader('Content-Type','application/xml');var digest=getRequestDigest();if(digest){request.setRequestHeader('X-RequestDigest',digest);}request.onreadystatechange=function(){if(request.readyState!==4){return;}if(request.status<200||request.status>=300){setMegaMenuTermSetStatus('Termsets konnten nicht geladen werden: '+buildRequestErrorMessage(request),'error');return;}try{var payload=JSON.parse(request.responseText||'[]');var errorInfo=payload&&payload.length?payload[0]:null;if(errorInfo&&errorInfo.ErrorInfo){throw new Error(errorInfo.ErrorInfo.ErrorMessage||'Taxonomy-Abfrage fehlgeschlagen.');}state.megaMenuTermSets=parseMegaMenuTermSets(payload);state.megaMenuTermSetsLoaded=true;setMegaMenuTermSetStatus(state.megaMenuTermSets.length?'':'Keine lesbaren Termsets gefunden.',state.megaMenuTermSets.length?'info':'warning');renderMegaMenuTermSets();window.setTimeout(function(){document.getElementById('megaMenuTermSetSearch').focus();},0);}catch(error){setMegaMenuTermSetStatus('Termsets konnten nicht gelesen werden: '+(error.message||String(error)),'error');}};request.onerror=function(){setMegaMenuTermSetStatus('Netzwerkfehler beim Laden der Termsets.','error');};request.send(requestXml);}catch(error){setMegaMenuTermSetStatus(error.message||String(error),'error');}}
|
||||
function parseMegaMenuTermSets(payload){var result=[];if(!Array.isArray(payload)){return result;}var store=null;for(var i=0;i<payload.length;i++){if(payload[i]&&payload[i]._ObjectType_==='SP.Taxonomy.TermStore'){store=payload[i];break;}}var groups=store&&store.Groups&&Array.isArray(store.Groups._Child_Items_)?store.Groups._Child_Items_:[];for(var g=0;g<groups.length;g++){var group=groups[g];if(!group||group.IsSystemGroup===true){continue;}var sets=group.TermSets&&Array.isArray(group.TermSets._Child_Items_)?group.TermSets._Child_Items_:[];for(var s=0;s<sets.length;s++){var termSet=sets[s];if(!termSet){continue;}var id=cleanTaxonomyGuid(termSet.Id);if(id){result.push({id:id,name:termSet.Name||id,groupName:group.Name||'Ohne Gruppe',description:termSet.Description||''});}}}result.sort(function(left,right){var a=(left.groupName+' '+left.name).toLowerCase();var b=(right.groupName+' '+right.name).toLowerCase();return a<b?-1:(a>b?1:0);});return result;}
|
||||
function cleanTaxonomyGuid(value){return String(value||'').replace('/Guid(','').replace(')/','').replace(')','').replace(/[{}]/g,'').toLowerCase();}
|
||||
function renderMegaMenuTermSets(){var host=document.getElementById('megaMenuTermSetList');var search=document.getElementById('megaMenuTermSetSearch').value.trim().toLowerCase();var html='';for(var i=0;i<state.megaMenuTermSets.length;i++){var item=state.megaMenuTermSets[i];var haystack=(item.groupName+' '+item.name+' '+item.description+' '+item.id).toLowerCase();if(search&&haystack.indexOf(search)===-1){continue;}html+='<div class="ps-item"><div><strong>'+escapeHtml(item.name)+'</strong><div class="ps-path">'+escapeHtml(item.groupName)+' | '+escapeHtml(item.id)+'</div>'+(item.description?'<div>'+escapeHtml(item.description)+'</div>':'')+'</div><button type="button" class="ps-btn" onclick="selectMegaMenuTermSet('+i+')">Auswaehlen</button></div>'; }host.innerHTML=html||'<div class="ps-list-empty">Keine passenden Termsets gefunden.</div>';}
|
||||
function selectMegaMenuTermSet(index){var item=state.megaMenuTermSets[index];if(!item){return;}document.getElementById('megaMenuTermSetId').value=item.id;document.getElementById('megaMenuTermSetName').value=item.name;updateMegaMenuSelectedTermSet();updateMegaMenuPreview();closeMegaMenuTermSetDialog();showStatus('Termset ausgewaehlt: '+item.name+'.','success');}
|
||||
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 openExpiryBindingDialog(){document.getElementById('bindExpiryWebUrl').value=getWebUrl();document.getElementById('bindExpiryListTitle').value='';document.getElementById('bindExpiryFieldName').value='ExpiryDate';document.getElementById('bindExpiryStatus').innerHTML='';document.getElementById('bindExpirySubmit').disabled=false;document.getElementById('expiryBindingDialog').hidden=false;window.setTimeout(function(){document.getElementById('bindExpiryListTitle').focus();},0);}
|
||||
function closeExpiryBindingDialog(){document.getElementById('expiryBindingDialog').hidden=true;}
|
||||
function getExpiryBindingInput(){var webUrl=document.getElementById('bindExpiryWebUrl').value.trim().replace(/\/$/,'');var listTitle=document.getElementById('bindExpiryListTitle').value.trim();var fieldName=document.getElementById('bindExpiryFieldName').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,classic:document.getElementById('bindExpiryClassic').checked===true};}
|
||||
function bindingListUrl(target){return target.webUrl+"/_api/web/lists/getbytitle('"+target.listTitle.replace(/'/g,"''")+"')";}
|
||||
function bindExpiryIndicatorFromDialog(){var target;try{target=getExpiryBindingInput();}catch(error){showStatus(error.message,'error',true);return;}var button=document.getElementById('bindExpirySubmit');button.disabled=true;showStatus('Pruefe Liste und Ablaufdatumsfeld...','info',true);var listUrl=bindingListUrl(target);getJson(listUrl+'?$select=Id,Title,BaseType',function(listPayload){var list=listPayload.d||listPayload;getJson(listUrl+"/fields/getbyinternalnameortitle('"+target.fieldName+"')?$select=Id,InternalName,TypeAsString,ClientSideComponentId,ClientSideComponentProperties",function(fieldPayload){var field=fieldPayload.d||fieldPayload;var currentComponent=normalizeGuid(field.ClientSideComponentId);if(currentComponent&¤tComponent!==normalizeGuid(EXPIRY_FIELD_COMPONENT_ID)){button.disabled=false;showStatus('Das Feld ist bereits mit einem anderen Field Customizer verbunden. Die Bindung wurde nicht geaendert.','error',true);return;}if(field.TypeAsString&&field.TypeAsString!=='DateTime'){button.disabled=false;showStatus('Das angegebene Feld ist kein DateTime-Feld.','error',true);return;}var properties=field.ClientSideComponentProperties;if(!properties||!String(properties).trim()){properties=JSON.stringify({schemaVersion:2,expiryField:field.InternalName,inheritSiteDefaults:true});}var payload={__metadata:{type:field.__metadata&&field.__metadata.type?field.__metadata.type:'SP.Field'},ClientSideComponentId:EXPIRY_FIELD_COMPONENT_ID,ClientSideComponentProperties:properties};postJsonMerge(listUrl+"/fields(guid'"+normalizeGuid(field.Id)+"')",payload,function(){var info={target:target,list:list,field:field,listUrl:listUrl};function finish(classicWarning){registerExpiryBinding(info,function(){button.disabled=false;closeExpiryBindingDialog();reloadSolutions('expiryindicator');if(classicWarning){showStatus('Die moderne Bindung wurde angelegt. Classic konnte nicht vollstaendig registriert werden: '+classicWarning,'warning',true);}else{showStatus('ExpiryIndicator wurde erfolgreich angebunden und zentral registriert.','success');}},function(message){button.disabled=false;showStatus('Das Feld wurde gebunden, aber das zentrale Inventar konnte nicht aktualisiert werden: '+message,'warning',true);});}if(target.classic){ensureExpiryClassicRegistration(info,function(warning){finish(warning);});}else{finish('');}},function(message){button.disabled=false;showStatus('Das Feld konnte nicht mit dem ExpiryIndicator verbunden werden: '+message,'error',true);});},function(message){button.disabled=false;showStatus('Das Ablaufdatumsfeld konnte nicht gelesen werden: '+message,'error',true);});},function(message){button.disabled=false;showStatus('Die Liste oder Bibliothek konnte nicht gelesen werden: '+message,'error',true);});}
|
||||
function registerExpiryBinding(info,onSuccess,onError){getJson(getSiteUrl().replace(/\/$/,'')+'/_api/web/AllProperties',function(payload){var properties=payload.d||payload||{};var bindings=normalizeExpiryBindings(propertyBagJson(properties,EXPIRY_BINDINGS_PROPERTY,[]));var webUrl=info.target.webUrl.replace(/\/$/,'');var listId=normalizeGuid(info.list.Id);bindings=bindings.filter(function(item){return !(String(item.webUrl||'').replace(/\/$/,'').toLowerCase()===webUrl.toLowerCase()&&normalizeGuid(item.listId)===listId);});bindings.push({webUrl:webUrl,listId:listId,listTitle:info.list.Title,fieldInternalName:info.field.InternalName,fieldId:normalizeGuid(info.field.Id)});var descriptor={key:'ExpiryIndicator',displayName:'Expiry Indicator',version:'2.1.1',provider:'expiryIndicator',componentIds:[EXPIRY_FIELD_COMPONENT_ID,'cd58f5d9-ffc8-4df7-a910-3054842d7abf','95691218-bcb1-4fad-adda-02bb12014d15'],settingsScope:['siteCollection','web','list']};setRootPropertyBagValue(EXPIRY_DESCRIPTOR_PROPERTY,JSON.stringify(descriptor),function(){setRootPropertyBagValue(EXPIRY_BINDINGS_PROPERTY,JSON.stringify(bindings),onSuccess,onError);},onError);},onError);}
|
||||
function ensureExpiryClassicRegistration(info,onComplete){var webActionsUrl=info.target.webUrl+'/_api/web/UserCustomActions';getJson(webActionsUrl,function(webPayload){var webActions=getResultArray(webPayload);var hasScript=webActions.some(function(action){return action.Name==='ExpiryIndicator.Classic.ScriptLink';});function ensureRibbon(){var listActionsUrl=info.listUrl+'/UserCustomActions';getJson(listActionsUrl,function(listPayload){var listActions=getResultArray(listPayload);var ribbonAction=null;for(var i=0;i<listActions.length;i++){if(listActions[i].Name==='ExpiryIndicator.Classic.Ribbon'){ribbonAction=listActions[i];break;}}var ribbonLocation=Number(info.list.BaseType)===1?'Ribbon.Documents.Manage.Controls._children':'Ribbon.ListItem.Actions.Controls._children';var ribbonXml='<CommandUIExtension xmlns="http://schemas.microsoft.com/sharepoint/"><CommandUIDefinitions><CommandUIDefinition Location="'+ribbonLocation+'"><Button Id="ExpiryIndicator.Classic.ExtendOneYear.Button" Command="ExpiryIndicator.Classic.ExtendOneYear" Sequence="90" LabelText="Ablaufdatum +1 Jahr" Description="Verlaengert das Ablaufdatum der ausgewaehlten Elemente um ein Kalenderjahr." TemplateAlias="o1" /></CommandUIDefinition></CommandUIDefinitions><CommandUIHandlers><CommandUIHandler Command="ExpiryIndicator.Classic.ExtendOneYear" CommandAction="javascript:ExpiryIndicatorClassic.extendSelected();" EnabledScript="javascript:ExpiryIndicatorClassic.canExtend();" /></CommandUIHandlers></CommandUIExtension>';var payload={__metadata:{type:'SP.UserCustomAction'},Name:'ExpiryIndicator.Classic.Ribbon',Title:'ExpiryIndicator Classic ribbon command',Location:'CommandUI.Ribbon',CommandUIExtension:ribbonXml,Sequence:651};if(ribbonAction){if(String(ribbonAction.CommandUIExtension||'').indexOf(ribbonLocation)>=0){onComplete('');return;}postJsonMerge(listActionsUrl+"(guid'"+normalizeGuid(ribbonAction.Id)+"')",payload,function(){onComplete('');},onComplete);return;}postJson(listActionsUrl,payload,function(){onComplete('');},onComplete);},onComplete);}if(hasScript){ensureRibbon();return;}postJson(webActionsUrl,{__metadata:{type:'SP.UserCustomAction'},Name:'ExpiryIndicator.Classic.ScriptLink',Title:'ExpiryIndicator Classic runtime',Location:'ScriptLink',ScriptSrc:'~sitecollection/SiteAssets/ExpiryIndicator/ExpiryIndicatorClassic.js',Sequence:650},ensureRibbon,onComplete);},onComplete);}
|
||||
function normalizePropertyBagKey(value){return String(value||'').replace(/_x002e_/gi,'.').toLowerCase();}
|
||||
function propertyBagJson(properties,key,fallback){if(!properties){return fallback;}var value;var expected=normalizePropertyBagKey(key);Object.keys(properties).some(function(name){if(normalizePropertyBagKey(name)===expected){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 normalizeExpiryBindings(value){var result=[];function visit(item){if(Array.isArray(item)){item.forEach(visit);return;}if(!item||typeof item!=='object'){return;}if(item.webUrl&&item.listId&&item.fieldInternalName){result.push(item);}}visit(value);return result;}
|
||||
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=normalizeExpiryBindings(propertyBagJson(properties,EXPIRY_BINDINGS_PROPERTY,[]));var siteDefaults=propertyBagJson(properties,EXPIRY_DEFAULTS_PROPERTY,null);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){var cacheMinutes=config?Number(config.cacheMinutes):15;return{termSetId:config&&typeof config.termSetId==='string'?config.termSetId.trim():'',termSetName:config&&typeof config.termSetName==='string'?config.termSetName.trim():'',cacheMinutes:isFinite(cacheMinutes)?Math.min(Math.max(Math.floor(cacheMinutes),1),1440):15,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;}if(solution.info.scopeLabel!=='Site Collection'){promoteSolutionActionToSiteCollection(key,config,onSuccess,onError);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 promoteSolutionActionToSiteCollection(key,config,onSuccess,onError){var solution=state.solutions[key];var definition=solution.definition;var oldInfo=solution.info;var oldAction=oldInfo.action;var siteActionsUrl=getSiteUrl().replace(/\/$/,'')+'/_api/site/UserCustomActions';var payload={__metadata:{type:'SP.UserCustomAction'},Name:oldAction.Name||definition.key,Title:oldAction.Title||definition.label,Description:definition.descriptionTag||CUSTOM_SOLUTION_TAG,Location:'ClientSideExtension.ApplicationCustomizer',ClientSideComponentId:definition.componentId,ClientSideComponentProperties:JSON.stringify(config)};postJsonResult(siteActionsUrl,payload,function(createdPayload){var createdAction=createdPayload&&createdPayload.d?createdPayload.d:createdPayload;var oldActionId=oldAction.Id;function finish(){solution.info={action:createdAction&&createdAction.Id?createdAction:{Id:createdAction&&createdAction.Id?createdAction.Id:'',Title:payload.Title,Name:payload.Name,Description:payload.Description,Location:payload.Location,ClientSideComponentId:payload.ClientSideComponentId,ClientSideComponentProperties:payload.ClientSideComponentProperties},updateBaseUrl:siteActionsUrl,actionType:'SP.UserCustomAction',scopeLabel:'Site Collection'};onSuccess();}if(!oldActionId||!oldInfo.updateBaseUrl){finish();return;}var oldUrl=oldInfo.updateBaseUrl+'(guid\''+oldActionId+'\')';postJsonDelete(oldUrl,finish,function(message){if(window.console&&console.warn){console.warn('Die site-scoped Registrierung wurde erstellt, der alte Web-Eintrag konnte aber nicht entfernt werden: '+message);}finish();});},function(message){onError('Die site-scoped UserCustomAction konnte nicht erstellt werden: '+message);});}
|
||||
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 tagged=null;var applicationCustomizer=null;var componentFallback=null;for(var i=0;i<actions.length;i++){var action=actions[i];if(normalizeGuid(action&&action.ClientSideComponentId)!==expected){continue;}if(!componentFallback){componentFallback=action;}var isApplicationCustomizer=typeof action.Location==='string'&&action.Location.indexOf('ClientSideExtension.ApplicationCustomizer')!==-1;if(isApplicationCustomizer&&!applicationCustomizer){applicationCustomizer=action;}if(matchesSolutionTag(action,definition)){if(isApplicationCustomizer){return action;}if(!tagged){tagged=action;}}}return tagged||applicationCustomizer||componentFallback;}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 postJson(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.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 Erstellen unter '+url);};request.send(JSON.stringify(payload));}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 postJsonResult(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.onreadystatechange=function(){if(request.readyState!==4){return;}if(request.status>=200&&request.status<300){try{onSuccess(request.responseText?JSON.parse(request.responseText):{});}catch(error){onError('Erstellte UserCustomAction konnte nicht gelesen werden: '+(error.message||String(error)));}return;}onError(buildRequestErrorMessage(request));};request.onerror=function(){onError('Netzwerkfehler beim Erstellen unter '+url);};request.send(JSON.stringify(payload));}catch(error){onError(error.message||String(error));}}
|
||||
function postJsonDelete(url,onSuccess,onError){try{var digest=getRequestDigest();if(!digest){onError('Kein Request Digest gefunden.');return;}var request=new XMLHttpRequest();request.open('POST',url,true);request.setRequestHeader('Accept','application/json;odata=verbose');request.setRequestHeader('X-RequestDigest',digest);request.setRequestHeader('IF-MATCH','*');request.setRequestHeader('X-HTTP-Method','DELETE');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 Entfernen von '+url);};request.send();}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 '';}var normalized=value.replace(/[{}]/g,'').toLowerCase();return normalized==='00000000-0000-0000-0000-000000000000'?'':normalized;}
|
||||
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 html='<div class="ps-status ps-status-'+type+'">'+escapeHtml(message)+'</div>';var host=document.getElementById('statusMessage');host.innerHTML=html;var dialog=document.getElementById('expiryBindingDialog');var dialogHost=document.getElementById('bindExpiryStatus');if(dialog&&dialog.hidden===false&&dialogHost){dialogHost.innerHTML=html;}if(!keepVisible){window.clearTimeout(showStatus._timer);showStatus._timer=window.setTimeout(function(){host.innerHTML='';if(dialogHost){dialogHost.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>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<ApplyElementManifests>
|
||||
<ElementManifest Location="57bd9daf-48f9-49e2-aebd-11b15a0b5e93\elements-v1.1.xml" />
|
||||
</ApplyElementManifests>
|
||||
@@ -1,3 +0,0 @@
|
||||
<ApplyElementManifests>
|
||||
<ElementManifest Location="57bd9daf-48f9-49e2-aebd-11b15a0b5e93\elements-v2.xml" />
|
||||
</ApplyElementManifests>
|
||||
@@ -0,0 +1,46 @@
|
||||
// tslint:disable:no-any
|
||||
export function getPathValue(source: any, path: string): any {
|
||||
if (!path) { return source; }
|
||||
const parts: string[] = path.split('.');
|
||||
let current: any = source;
|
||||
for (let i: number = 0; i < parts.length; i++) {
|
||||
if (current === undefined || current === null) { return undefined; }
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export function setPathValue(target: any, path: string, value: any): void {
|
||||
const parts: string[] = path.split('.');
|
||||
let current: any = target;
|
||||
for (let i: number = 0; i < parts.length - 1; i++) {
|
||||
if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {
|
||||
current[parts[i]] = {};
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
export function cloneValue<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
export function mergePreservingUnknown(original: any, edited: any): any {
|
||||
if (!isObject(original) || !isObject(edited)) { return cloneValue(edited); }
|
||||
const result: any = cloneValue(original);
|
||||
Object.keys(edited).forEach((key: string): void => {
|
||||
result[key] = isObject(result[key]) && isObject(edited[key])
|
||||
? mergePreservingUnknown(result[key], edited[key])
|
||||
: cloneValue(edited[key]);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isEmptyValue(value: any): boolean {
|
||||
return value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);
|
||||
}
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
export type SettingsStorageKind = 'siteUserCustomAction' | 'rootPropertyBag';
|
||||
export type SettingsLayout = 'singleColumn' | 'twoColumns' | 'threeColumns';
|
||||
export type SettingsAppearance = 'default' | 'subtle' | 'emphasized' | 'info' | 'success' | 'warning' | 'danger';
|
||||
export type SettingsElementType =
|
||||
'heading' | 'text' | 'notice' | 'divider' | 'group' | 'propertySummary' | 'jsonPreview' |
|
||||
'textField' | 'number' | 'boolean' | 'choice' | 'choiceCards' | 'termSet' | 'stringList' |
|
||||
'json' | 'bindingInventory';
|
||||
|
||||
export interface ISettingsOption {
|
||||
key: string;
|
||||
text: string;
|
||||
description?: string;
|
||||
iconName?: string;
|
||||
}
|
||||
|
||||
export interface IVisibilityCondition {
|
||||
field: string;
|
||||
operator: 'equals' | 'notEquals' | 'isEmpty' | 'isNotEmpty' | 'contains' | 'greaterThan' | 'lessThan';
|
||||
value?: any; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export interface ISettingsElement {
|
||||
type: SettingsElementType;
|
||||
key?: string;
|
||||
path?: string;
|
||||
companionNamePath?: string;
|
||||
text?: string;
|
||||
title?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
level?: 2 | 3 | 4;
|
||||
appearance?: SettingsAppearance;
|
||||
columnSpan?: 1 | 2 | 3 | 4 | 6 | 8 | 12;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
step?: number;
|
||||
suffix?: string;
|
||||
options?: ISettingsOption[];
|
||||
elements?: ISettingsElement[];
|
||||
visibleWhen?: IVisibilityCondition;
|
||||
collapsedByDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface ISettingsSection {
|
||||
key: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
iconName?: string;
|
||||
layout?: SettingsLayout;
|
||||
appearance?: SettingsAppearance;
|
||||
collapsible?: boolean;
|
||||
collapsedByDefault?: boolean;
|
||||
elements: ISettingsElement[];
|
||||
}
|
||||
|
||||
export interface IProviderStorageDefinition {
|
||||
kind: SettingsStorageKind;
|
||||
propertyKey?: string;
|
||||
}
|
||||
|
||||
export interface ISettingsProvider<TConfig> {
|
||||
key: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
iconName?: string;
|
||||
schemaVersion: number;
|
||||
minimumPortalSettingsVersion: string;
|
||||
componentIds: string[];
|
||||
storage: IProviderStorageDefinition;
|
||||
sections: ISettingsSection[];
|
||||
createDefault(): TConfig;
|
||||
normalize(value: any): TConfig; // tslint:disable-line:no-any
|
||||
validate(value: TConfig): string[];
|
||||
}
|
||||
|
||||
export interface IUserCustomActionInfo {
|
||||
id: string;
|
||||
title: string;
|
||||
name: string;
|
||||
location: string;
|
||||
componentId: string;
|
||||
properties: string;
|
||||
entityType: string;
|
||||
}
|
||||
|
||||
export interface IProviderLoadContext {
|
||||
action?: IUserCustomActionInfo;
|
||||
propertyBag?: { [key: string]: any }; // tslint:disable-line:no-any
|
||||
bindings?: any[]; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export interface ILoadedSettingsProvider {
|
||||
provider: ISettingsProvider<any>; // tslint:disable-line:no-any
|
||||
config: any; // tslint:disable-line:no-any
|
||||
originalConfig: any; // tslint:disable-line:no-any
|
||||
context: IProviderLoadContext;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
export interface ITermSetInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
groupName: string;
|
||||
description: string;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/component-manifest.schema.json",
|
||||
"id": "cd6b9e8d-07b9-4468-b230-6bb35acd9a5f",
|
||||
"alias": "PortalSettingsApplicationCustomizer",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { override } from '@microsoft/decorators';
|
||||
import { Log } from '@microsoft/sp-core-library';
|
||||
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
|
||||
|
||||
import * as strings from 'PortalSettingsApplicationCustomizerStrings';
|
||||
|
||||
const LOG_SOURCE: string = 'PortalSettingsApplicationCustomizer';
|
||||
|
||||
export interface IPortalSettingsApplicationCustomizerProperties {
|
||||
}
|
||||
|
||||
export default class PortalSettingsApplicationCustomizer
|
||||
extends BaseApplicationCustomizer<IPortalSettingsApplicationCustomizerProperties> {
|
||||
|
||||
@override
|
||||
public onInit(): Promise<void> {
|
||||
Log.info(LOG_SOURCE, 'Initialized ' + strings.Title);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"Title": "PortalSettingsApplicationCustomizer"
|
||||
}
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
declare interface IPortalSettingsApplicationCustomizerStrings {
|
||||
Title: string;
|
||||
}
|
||||
|
||||
declare module 'PortalSettingsApplicationCustomizerStrings' {
|
||||
const strings: IPortalSettingsApplicationCustomizerStrings;
|
||||
// tslint:disable-next-line:export-name
|
||||
export = strings;
|
||||
}
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
||||
export * from './core/ProviderContracts';
|
||||
export * from './providers/ProviderRegistry';
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { ISettingsProvider } from '../core/ProviderContracts';
|
||||
|
||||
export interface ICustomBrandingSettings {
|
||||
schemaVersion: number;
|
||||
enabled: boolean;
|
||||
debug: boolean;
|
||||
allowedCssHosts: string[];
|
||||
cssfiles: Array<{ path: string; media?: string }>;
|
||||
placeholdertop: { elements: any[] }; // tslint:disable-line:no-any
|
||||
placeholderbottom: { elements: any[] }; // tslint:disable-line:no-any
|
||||
[key: string]: any; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSettings> = {
|
||||
key: 'custombranding',
|
||||
displayName: 'Custom Branding',
|
||||
description: 'Stylesheets und strukturierte Inhalte für Header und Footer.',
|
||||
iconName: 'Color',
|
||||
schemaVersion: 2,
|
||||
minimumPortalSettingsVersion: '3.0.0',
|
||||
componentIds: ['035ba968-6488-4d42-86b3-0470ffcc95b9'],
|
||||
storage: { kind: 'siteUserCustomAction' },
|
||||
sections: [
|
||||
{
|
||||
key: 'general', title: 'Allgemein', layout: 'twoColumns', iconName: 'Settings',
|
||||
elements: [
|
||||
{
|
||||
type: 'boolean', key: 'enabled', path: 'enabled', label: 'CustomBranding aktivieren',
|
||||
description: 'Deaktiviert Rendering und Stylesheets, erhält aber die Konfiguration.', columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'boolean', key: 'debug', path: 'debug', label: 'Debug-Ausgaben aktivieren',
|
||||
description: 'Protokolliert Validierungs- und Ladeinformationen.', columnSpan: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'stylesheets', title: 'Stylesheets', layout: 'twoColumns', iconName: 'FileCSS',
|
||||
description: 'Lokale Stylesheets sind direkt erlaubt; externe Hosts benötigen eine Freigabe.',
|
||||
elements: [
|
||||
{
|
||||
type: 'json', key: 'cssfiles', path: 'cssfiles', label: 'CSS-Dateien', columnSpan: 8,
|
||||
description: 'Array mit path und optional media.'
|
||||
},
|
||||
{
|
||||
type: 'stringList', key: 'allowedCssHosts', path: 'allowedCssHosts', label: 'Freigegebene CSS-Hosts',
|
||||
description: 'Nur Hostnamen; externe Stylesheets müssen HTTPS verwenden.', columnSpan: 4
|
||||
},
|
||||
{
|
||||
type: 'notice', appearance: 'warning', title: 'Sicherheitsgrenze', columnSpan: 12,
|
||||
text: 'Freigaben gelten für die gesamte Site Collection. Nehmen Sie nur kontrollierte Hosts auf.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'content', title: 'Header und Footer', layout: 'twoColumns', iconName: 'WebAppBuilderFragment',
|
||||
description: 'Die Elementstrukturen werden vor dem Speichern als JSON geprüft.',
|
||||
elements: [
|
||||
{ type: 'json', key: 'placeholdertop', path: 'placeholdertop.elements', label: 'Header-Elemente', columnSpan: 6 },
|
||||
{ type: 'json', key: 'placeholderbottom', path: 'placeholderbottom.elements', label: 'Footer-Elemente', columnSpan: 6 }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'preview', title: 'Zusammenfassung', layout: 'singleColumn',
|
||||
elements: [
|
||||
{ type: 'propertySummary', title: 'Aktuelle Werte', columnSpan: 12 },
|
||||
{ type: 'jsonPreview', title: 'ClientSideComponentProperties', collapsedByDefault: true, columnSpan: 12 }
|
||||
]
|
||||
}
|
||||
],
|
||||
createDefault: (): ICustomBrandingSettings => ({
|
||||
schemaVersion: 2,
|
||||
enabled: true,
|
||||
debug: false,
|
||||
allowedCssHosts: [],
|
||||
cssfiles: [],
|
||||
placeholdertop: { elements: [] },
|
||||
placeholderbottom: { elements: [] }
|
||||
}),
|
||||
normalize: (value: any): ICustomBrandingSettings => { // tslint:disable-line:no-any
|
||||
const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any
|
||||
const result: any = {}; // tslint:disable-line:no-any
|
||||
Object.keys(source).forEach((key: string): void => { result[key] = source[key]; });
|
||||
result.schemaVersion = 2;
|
||||
result.enabled = source.enabled !== false;
|
||||
result.debug = source.debug === true;
|
||||
result.allowedCssHosts = Array.isArray(source.allowedCssHosts) ? source.allowedCssHosts : [];
|
||||
result.cssfiles = Array.isArray(source.cssfiles) ? source.cssfiles : [];
|
||||
result.placeholdertop = source.placeholdertop && Array.isArray(source.placeholdertop.elements)
|
||||
? source.placeholdertop : { elements: Array.isArray(source.elements) ? source.elements : [] };
|
||||
result.placeholderbottom = source.placeholderbottom && Array.isArray(source.placeholderbottom.elements)
|
||||
? source.placeholderbottom : { elements: [] };
|
||||
delete result.elements;
|
||||
return result as ICustomBrandingSettings;
|
||||
},
|
||||
validate: (value: ICustomBrandingSettings): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!Array.isArray(value.cssfiles)) { errors.push('CSS-Dateien müssen ein Array sein.'); }
|
||||
if (!Array.isArray(value.allowedCssHosts)) { errors.push('CSS-Hosts müssen ein Array sein.'); }
|
||||
if (!value.placeholdertop || !Array.isArray(value.placeholdertop.elements)) { errors.push('Header-Elemente sind ungültig.'); }
|
||||
if (!value.placeholderbottom || !Array.isArray(value.placeholderbottom.elements)) { errors.push('Footer-Elemente sind ungültig.'); }
|
||||
const cssFiles: Array<{ path: string; media?: string }> = Array.isArray(value.cssfiles) ? value.cssfiles : [];
|
||||
if (cssFiles.length > 20) { errors.push('Es sind maximal 20 Stylesheets erlaubt.'); }
|
||||
for (let i: number = 0; i < cssFiles.length; i++) {
|
||||
if (!cssFiles[i] || typeof cssFiles[i].path !== 'string' || !cssFiles[i].path.trim()) {
|
||||
errors.push('CSS-Datei ' + (i + 1) + ' besitzt keinen gültigen Pfad.');
|
||||
}
|
||||
}
|
||||
if (value.placeholdertop && Array.isArray(value.placeholdertop.elements)) {
|
||||
validateElements(value.placeholdertop.elements, 'Header', 1, { count: 0 }, errors);
|
||||
}
|
||||
if (value.placeholderbottom && Array.isArray(value.placeholderbottom.elements)) {
|
||||
validateElements(value.placeholderbottom.elements, 'Footer', 1, { count: 0 }, errors);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
|
||||
function validateElements(elements: any[], path: string, depth: number, state: { count: number }, errors: string[]): void { // tslint:disable-line:no-any
|
||||
if (depth > 8) { errors.push(path + ' überschreitet die maximale Tiefe von 8.'); return; }
|
||||
if (!Array.isArray(elements)) { errors.push(path + ' muss ein Array sein.'); return; }
|
||||
for (let i: number = 0; i < elements.length; i++) {
|
||||
const element: any = elements[i]; // tslint:disable-line:no-any
|
||||
state.count++; if (state.count > 200) { errors.push(path + ' enthält mehr als 200 Elemente.'); return; }
|
||||
const tag: string = String(element && element.type || '').toLowerCase();
|
||||
if (!element || AllowedTags.indexOf(tag) < 0) {
|
||||
errors.push(path + '[' + i + '] verwendet einen nicht erlaubten Elementtyp.'); continue;
|
||||
}
|
||||
if (tag === 'img' && (!element.attributes || typeof element.attributes.alt !== 'string')) {
|
||||
errors.push(path + '[' + i + '] ist ein Bild ohne alt-Attribut.');
|
||||
}
|
||||
if (element.attributes && typeof element.attributes === 'object') {
|
||||
Object.keys(element.attributes).forEach((name: string): void => {
|
||||
const lower: string = name.toLowerCase(); const attributeValue: string = String(element.attributes[name] || '');
|
||||
if (lower.indexOf('on') === 0) { errors.push(path + '[' + i + '] enthält ein Ereignisattribut.'); }
|
||||
if (lower === 'href' || lower === 'src') {
|
||||
const protocol: RegExpMatchArray = attributeValue.trim().match(/^([a-z][a-z0-9+.-]*):/i);
|
||||
const allowed: string[] = lower === 'href' ? ['http', 'https', 'mailto'] : ['http', 'https'];
|
||||
if (protocol && allowed.indexOf(protocol[1].toLowerCase()) < 0) {
|
||||
errors.push(path + '[' + i + '] enthält eine unsichere URL.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (element.children) { validateElements(element.children, path + '[' + i + '].children', depth + 1, state, errors); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { ISettingsProvider } from '../core/ProviderContracts';
|
||||
|
||||
export interface IExpiryIndicatorSettings {
|
||||
baseField: string;
|
||||
expiryField: string;
|
||||
default: {
|
||||
lifeTime: { value: number; unit: 'days' | 'months' | 'years' };
|
||||
columnRule: any[]; // tslint:disable-line:no-any
|
||||
};
|
||||
rules: any[]; // tslint:disable-line:no-any
|
||||
nullText: string;
|
||||
confirmExtension: boolean;
|
||||
[key: string]: any; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export const ExpiryIndicatorSettingsProvider: ISettingsProvider<IExpiryIndicatorSettings> = {
|
||||
key: 'expiryindicator',
|
||||
displayName: 'ExpiryIndicator',
|
||||
description: 'Zentrale Standards, Vererbung und Bindungen für Listen und Bibliotheken.',
|
||||
iconName: 'DateTime2',
|
||||
schemaVersion: 2,
|
||||
minimumPortalSettingsVersion: '3.0.0',
|
||||
componentIds: [
|
||||
'a555f4fc-d6a6-4421-8189-457449d9bbde',
|
||||
'95691218-bcb1-4fad-adda-02bb12014d15',
|
||||
'cd58f5d9-ffc8-4df7-a910-3054842d7abf'
|
||||
],
|
||||
storage: { kind: 'rootPropertyBag', propertyKey: 'PortalSettings.ExpiryIndicator.Defaults' },
|
||||
sections: [
|
||||
{
|
||||
key: 'scope', title: 'Gültigkeitsbereich', layout: 'singleColumn', iconName: 'Globe',
|
||||
elements: [
|
||||
{
|
||||
type: 'notice', appearance: 'info', title: 'Site-Collection-Standard',
|
||||
text: 'Diese Werte gelten für alle vererbenden Bindungen im Root Web und in Unterwebs.', columnSpan: 12
|
||||
},
|
||||
{ type: 'bindingInventory', key: 'bindings', title: 'Registrierte Bindungen', columnSpan: 12 }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'dates', title: 'Datumsberechnung', layout: 'twoColumns', iconName: 'Calendar',
|
||||
elements: [
|
||||
{
|
||||
type: 'textField', key: 'baseField', path: 'baseField', label: 'Basis-Datumsfeld',
|
||||
description: 'Interner Feldname, normalerweise Created.', required: true, columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'textField', key: 'expiryField', path: 'expiryField', label: 'Ablaufdatumsfeld',
|
||||
description: 'Standardwert für neue Bindungen; vorhandene Bindungen behalten ihren Feldnamen.', required: true, columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'number', key: 'lifeTimeValue', path: 'default.lifeTime.value', label: 'Standardlaufzeit',
|
||||
minimum: 1, maximum: 10000, columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'choice', key: 'lifeTimeUnit', path: 'default.lifeTime.unit', label: 'Einheit', columnSpan: 6,
|
||||
options: [
|
||||
{ key: 'days', text: 'Tage' }, { key: 'months', text: 'Monate' }, { key: 'years', text: 'Jahre' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'textField', key: 'nullText', path: 'nullText', label: 'Text ohne Ablaufdatum', columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'boolean', key: 'confirmExtension', path: 'confirmExtension',
|
||||
label: 'Verlängerung bestätigen', description: 'Zeigt vor der Verlängerung eine Sicherheitsabfrage.', columnSpan: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'rules', title: 'Farben und fachliche Regeln', layout: 'twoColumns', iconName: 'BranchFork',
|
||||
elements: [
|
||||
{
|
||||
type: 'json', key: 'columnRule', path: 'default.columnRule', label: 'Standard-Farbregeln',
|
||||
description: 'Die erste passende Schwelle gewinnt.', columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'json', key: 'rules', path: 'rules', label: 'Feldabhängige Regeln',
|
||||
description: 'Unterstützt Legacy-Regeln und verschachtelte AND-/OR-Bedingungen.', columnSpan: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'preview', title: 'Zusammenfassung', layout: 'singleColumn',
|
||||
elements: [
|
||||
{ type: 'propertySummary', title: 'Aktuelle Werte', columnSpan: 12 },
|
||||
{ type: 'jsonPreview', title: 'Zentraler Standard', collapsedByDefault: true, columnSpan: 12 }
|
||||
]
|
||||
}
|
||||
],
|
||||
createDefault: (): IExpiryIndicatorSettings => ({
|
||||
baseField: 'Created',
|
||||
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: 'Läuft bald ab' },
|
||||
{ daysUntilExpiry: 90, operator: 'lessOrEqual', backgroundColor: '#fff4ce', textColor: '#000000', label: 'Beobachten' }
|
||||
]
|
||||
},
|
||||
rules: [], nullText: 'Kein Ablaufdatum', confirmExtension: true
|
||||
}),
|
||||
normalize: (value: any): IExpiryIndicatorSettings => { // tslint:disable-line:no-any
|
||||
const defaults: IExpiryIndicatorSettings = ExpiryIndicatorSettingsProvider.createDefault();
|
||||
const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any
|
||||
const result: any = {}; // tslint:disable-line:no-any
|
||||
Object.keys(source).forEach((key: string): void => { result[key] = source[key]; });
|
||||
result.baseField = safeName(source.baseField) ? source.baseField : defaults.baseField;
|
||||
result.expiryField = safeName(source.expiryField) ? source.expiryField : defaults.expiryField;
|
||||
result.default = source.default && typeof source.default === 'object' ? source.default : defaults.default;
|
||||
result.default.lifeTime = result.default.lifeTime || defaults.default.lifeTime;
|
||||
result.default.columnRule = Array.isArray(result.default.columnRule) ? result.default.columnRule : defaults.default.columnRule;
|
||||
result.rules = Array.isArray(source.rules) ? source.rules : [];
|
||||
result.nullText = typeof source.nullText === 'string' ? source.nullText : defaults.nullText;
|
||||
result.confirmExtension = source.confirmExtension !== false;
|
||||
return result as IExpiryIndicatorSettings;
|
||||
},
|
||||
validate: (value: IExpiryIndicatorSettings): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!safeName(value.baseField)) { errors.push('Das Basis-Datumsfeld ist ungültig.'); }
|
||||
if (!safeName(value.expiryField)) { errors.push('Das Ablaufdatumsfeld ist ungültig.'); }
|
||||
if (!value.default || !value.default.lifeTime || value.default.lifeTime.value < 1 ||
|
||||
['days', 'months', 'years'].indexOf(value.default.lifeTime.unit) < 0) { errors.push('Die Standardlaufzeit ist ungültig.'); }
|
||||
if (!value.default || !Array.isArray(value.default.columnRule)) { errors.push('Die Standard-Farbregeln sind ungültig.'); }
|
||||
if (!Array.isArray(value.rules)) { errors.push('Die fachlichen Regeln sind ungültig.'); }
|
||||
if (value.default) { validateColorRules(value.default.columnRule, 'Standard-Farbregeln', errors); }
|
||||
for (let i: number = 0; i < value.rules.length; i++) {
|
||||
const rule: any = value.rules[i]; // tslint:disable-line:no-any
|
||||
if (!rule || (!rule.condition && (!safeName(rule.columnName) || rule.columnValue === undefined))) {
|
||||
errors.push('Regel ' + (i + 1) + ' benötigt eine Bedingung oder columnName/columnValue.');
|
||||
}
|
||||
if (rule && rule.condition) { validateCondition(rule.condition, 'Regel ' + (i + 1), 1, errors); }
|
||||
if (!rule || !rule.lifeTime || Number(rule.lifeTime.value) < 1 || ['days', 'months', 'years'].indexOf(rule.lifeTime.unit) < 0) {
|
||||
errors.push('Regel ' + (i + 1) + ' enthält eine ungültige Laufzeit.');
|
||||
}
|
||||
if (rule) { validateColorRules(rule.columnRule, 'Regel ' + (i + 1), errors); }
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
function safeName(value: string): boolean {
|
||||
return typeof value === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
||||
}
|
||||
|
||||
function validateColorRules(rules: any[], path: string, errors: string[]): void { // tslint:disable-line:no-any
|
||||
if (!Array.isArray(rules)) { return; }
|
||||
const operators: string[] = ['lessThan', 'lessOrEqual', 'equal', 'greaterOrEqual', 'greaterThan'];
|
||||
for (let i: number = 0; i < rules.length; i++) {
|
||||
const rule: any = rules[i]; // tslint:disable-line:no-any
|
||||
if (!rule || typeof rule.daysUntilExpiry !== 'number' || operators.indexOf(rule.operator) < 0 ||
|
||||
typeof rule.backgroundColor !== 'string' || typeof rule.textColor !== 'string') {
|
||||
errors.push(path + '[' + i + '] ist ungültig.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateCondition(condition: any, path: string, depth: number, errors: string[]): void { // tslint:disable-line:no-any
|
||||
if (depth > 10) { errors.push(path + ' überschreitet die maximale Bedingungstiefe.'); return; }
|
||||
if (!condition || (condition.operator !== 'and' && condition.operator !== 'or') || !Array.isArray(condition.conditions) || !condition.conditions.length) {
|
||||
errors.push(path + ' enthält eine ungültige AND-/OR-Bedingungsgruppe.'); return;
|
||||
}
|
||||
for (let i: number = 0; i < condition.conditions.length; i++) {
|
||||
const child: any = condition.conditions[i]; // tslint:disable-line:no-any
|
||||
if (child && child.operator) { validateCondition(child, path + '.' + i, depth + 1, errors); }
|
||||
else if (!child || !safeName(child.columnName) || child.columnValue === undefined) { errors.push(path + '.' + i + ' ist keine gültige Feldbedingung.'); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ISettingsProvider } from '../core/ProviderContracts';
|
||||
|
||||
export interface IMegaMenuSettings {
|
||||
termSetId: string;
|
||||
termSetName: string;
|
||||
cacheMinutes: number;
|
||||
cacheVersion: string;
|
||||
menuMode: 'megaMenu' | 'flyout';
|
||||
debug: boolean;
|
||||
[key: string]: any; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export const MegaMenuSettingsProvider: ISettingsProvider<IMegaMenuSettings> = {
|
||||
key: 'megamenu',
|
||||
displayName: 'Mega Menu',
|
||||
description: 'Zentrale Navigation für das Root Web und alle Unterwebs.',
|
||||
iconName: 'GlobalNavButton',
|
||||
schemaVersion: 1,
|
||||
minimumPortalSettingsVersion: '3.0.0',
|
||||
componentIds: [
|
||||
'c0abbffb-355d-4d4e-bd38-9e15fb811506',
|
||||
'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
||||
],
|
||||
storage: { kind: 'siteUserCustomAction' },
|
||||
sections: [
|
||||
{
|
||||
key: 'navigation',
|
||||
title: 'Navigation',
|
||||
description: 'Navigationsquelle und Darstellung des Menüs.',
|
||||
iconName: 'BranchFork',
|
||||
layout: 'twoColumns',
|
||||
elements: [
|
||||
{ type: 'heading', level: 3, text: 'Navigationsquelle', columnSpan: 12 },
|
||||
{
|
||||
type: 'termSet', key: 'termSetId', path: 'termSetId', companionNamePath: 'termSetName',
|
||||
label: 'Navigations-Termset', description: 'Das Termset liefert die Navigationseinträge.',
|
||||
required: true, columnSpan: 12
|
||||
},
|
||||
{ type: 'divider', columnSpan: 12 },
|
||||
{ type: 'heading', level: 3, text: 'Darstellung', columnSpan: 12 },
|
||||
{
|
||||
type: 'choiceCards', key: 'menuMode', path: 'menuMode', label: 'Darstellungsmodus', columnSpan: 12,
|
||||
options: [
|
||||
{ key: 'megaMenu', text: 'Mega Menu', description: 'Breites, mehrspaltiges Navigationspanel.' },
|
||||
{ key: 'flyout', text: 'Strukturelles Flyout', description: 'Kompaktes Menü mit seitlichen Untermenüs.' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'notice', appearance: 'info', title: 'Styling', columnSpan: 12,
|
||||
text: 'Farben und individuelle Styles werden zentral über CustomBranding verwaltet.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'performance', title: 'Performance und Cache', iconName: 'SpeedHigh', layout: 'twoColumns',
|
||||
description: 'Steuert die lokale Zwischenspeicherung der Navigation.',
|
||||
elements: [
|
||||
{
|
||||
type: 'number', key: 'cacheMinutes', path: 'cacheMinutes', label: 'Cache-Dauer',
|
||||
description: 'Gültigkeitsdauer des Browser-Caches.', minimum: 1, maximum: 1440, suffix: 'Minuten', columnSpan: 6
|
||||
},
|
||||
{
|
||||
type: 'textField', key: 'cacheVersion', path: 'cacheVersion', label: 'Cache-Version',
|
||||
description: 'Eine Änderung verwirft vorhandene Browser-Caches.', placeholder: '1', columnSpan: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'diagnostics', title: 'Diagnose', iconName: 'Diagnostic', layout: 'singleColumn',
|
||||
collapsible: true, collapsedByDefault: true,
|
||||
elements: [
|
||||
{
|
||||
type: 'boolean', key: 'debug', path: 'debug', label: 'Debug-Ausgaben aktivieren',
|
||||
description: 'Schreibt technische Meldungen in die Browserkonsole.', columnSpan: 12
|
||||
},
|
||||
{
|
||||
type: 'notice', appearance: 'warning', title: 'Nur vorübergehend aktivieren',
|
||||
text: 'Deaktivieren Sie die Debug-Ausgaben nach Abschluss der Diagnose.', columnSpan: 12,
|
||||
visibleWhen: { field: 'debug', operator: 'equals', value: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'preview', title: 'Zusammenfassung', layout: 'singleColumn',
|
||||
elements: [
|
||||
{ type: 'propertySummary', title: 'Aktuelle Werte', columnSpan: 12 },
|
||||
{ type: 'jsonPreview', title: 'ClientSideComponentProperties', collapsedByDefault: true, columnSpan: 12 }
|
||||
]
|
||||
}
|
||||
],
|
||||
createDefault: (): IMegaMenuSettings => ({
|
||||
termSetId: '', termSetName: '', cacheMinutes: 15, cacheVersion: '1', menuMode: 'megaMenu', debug: false
|
||||
}),
|
||||
normalize: (value: any): IMegaMenuSettings => { // tslint:disable-line:no-any
|
||||
const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any
|
||||
const minutes: number = Number(source.cacheMinutes);
|
||||
const result: any = {}; // tslint:disable-line:no-any
|
||||
Object.keys(source).forEach((key: string): void => { result[key] = source[key]; });
|
||||
result.termSetId = typeof source.termSetId === 'string' ? source.termSetId.trim() : '';
|
||||
result.termSetName = typeof source.termSetName === 'string' ? source.termSetName.trim() : '';
|
||||
result.cacheMinutes = isFinite(minutes) ? Math.min(Math.max(Math.floor(minutes), 1), 1440) : 15;
|
||||
result.cacheVersion = typeof source.cacheVersion === 'string' && source.cacheVersion.trim() ? source.cacheVersion.trim() : '1';
|
||||
result.menuMode = source.menuMode === 'flyout' ? 'flyout' : 'megaMenu';
|
||||
result.debug = source.debug === true;
|
||||
return result as IMegaMenuSettings;
|
||||
},
|
||||
validate: (value: IMegaMenuSettings): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!value.termSetId && !value.termSetName) { errors.push('Bitte ein Navigations-Termset auswählen.'); }
|
||||
if (value.termSetId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value.termSetId)) {
|
||||
errors.push('Die Termset-ID ist keine gültige GUID.');
|
||||
}
|
||||
if (value.cacheMinutes < 1 || value.cacheMinutes > 1440) { errors.push('Die Cache-Dauer muss zwischen 1 und 1440 liegen.'); }
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ISettingsProvider } from '../core/ProviderContracts';
|
||||
import { CustomBrandingSettingsProvider } from './CustomBrandingSettingsProvider';
|
||||
import { ExpiryIndicatorSettingsProvider } from './ExpiryIndicatorSettingsProvider';
|
||||
import { MegaMenuSettingsProvider } from './MegaMenuSettingsProvider';
|
||||
|
||||
const Providers: Array<ISettingsProvider<any>> = [ // tslint:disable-line:no-any
|
||||
MegaMenuSettingsProvider,
|
||||
CustomBrandingSettingsProvider,
|
||||
ExpiryIndicatorSettingsProvider
|
||||
];
|
||||
|
||||
export function getSettingsProviders(): Array<ISettingsProvider<any>> { // tslint:disable-line:no-any
|
||||
return Providers.slice();
|
||||
}
|
||||
|
||||
export function getSettingsProvider(key: string): ISettingsProvider<any> | undefined { // tslint:disable-line:no-any
|
||||
for (let i: number = 0; i < Providers.length; i++) {
|
||||
if (Providers[i].key === key) { return Providers[i]; }
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
// tslint:disable:no-any max-line-length
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import {
|
||||
ILoadedSettingsProvider,
|
||||
IProviderLoadContext,
|
||||
ISettingsProvider,
|
||||
IUserCustomActionInfo
|
||||
} from '../core/ProviderContracts';
|
||||
import { cloneValue, mergePreservingUnknown } from '../core/ObjectPath';
|
||||
|
||||
const BindingsProperty: string = 'PortalSettings.ExpiryIndicator.Bindings';
|
||||
const DescriptorProperty: string = 'PortalSettings.Solutions.ExpiryIndicator';
|
||||
const ExpiryFieldComponentId: string = 'a555f4fc-d6a6-4421-8189-457449d9bbde';
|
||||
|
||||
export interface IListOption { id: string; title: string; }
|
||||
export interface IFieldOption {
|
||||
id: string;
|
||||
title: string;
|
||||
internalName: string;
|
||||
componentId: string;
|
||||
properties: string;
|
||||
entityType: string;
|
||||
}
|
||||
export interface IExpiryBindingLoadResult { config: any; inherited: boolean; field: IFieldOption; }
|
||||
|
||||
export class PortalSettingsDataService {
|
||||
private readonly siteUrl: string;
|
||||
|
||||
public constructor(private context: WebPartContext) {
|
||||
this.siteUrl = context.pageContext.site.absoluteUrl.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
public load(providers: Array<ISettingsProvider<any>>): Promise<ILoadedSettingsProvider[]> {
|
||||
return Promise.all([this.getSiteActions(), this.getRootProperties()])
|
||||
.then((values: any[]): ILoadedSettingsProvider[] => {
|
||||
const actions: IUserCustomActionInfo[] = values[0];
|
||||
const properties: any = values[1];
|
||||
const loaded: ILoadedSettingsProvider[] = [];
|
||||
for (let i: number = 0; i < providers.length; i++) {
|
||||
const result: ILoadedSettingsProvider = this.loadProvider(providers[i], actions, properties);
|
||||
if (result) { loaded.push(result); }
|
||||
}
|
||||
return loaded;
|
||||
});
|
||||
}
|
||||
|
||||
public save(item: ILoadedSettingsProvider): Promise<void> {
|
||||
const errors: string[] = item.provider.validate(item.config);
|
||||
if (errors.length > 0) { return Promise.reject(new Error(errors.join(' '))); }
|
||||
const merged: any = mergePreservingUnknown(item.originalConfig, item.provider.normalize(item.config));
|
||||
if (item.provider.storage.kind === 'siteUserCustomAction') {
|
||||
if (!item.context.action) { return Promise.reject(new Error('Die zentrale UserCustomAction wurde nicht gefunden.')); }
|
||||
return this.saveUserCustomAction(item.context.action, merged).then((): void => {
|
||||
item.config = item.provider.normalize(merged);
|
||||
item.originalConfig = cloneValue(merged);
|
||||
item.dirty = false;
|
||||
});
|
||||
}
|
||||
const propertyKey: string = item.provider.storage.propertyKey || '';
|
||||
return this.saveRootProperty(propertyKey, JSON.stringify(merged)).then((): void => {
|
||||
item.config = item.provider.normalize(merged);
|
||||
item.originalConfig = cloneValue(merged);
|
||||
item.dirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
public getSiteUrl(): string { return this.siteUrl; }
|
||||
|
||||
public getLists(webUrl: string): Promise<IListOption[]> {
|
||||
let base: string;
|
||||
try { base = this.normalizeScopedWebUrl(webUrl); } catch (error) { return Promise.reject(error); }
|
||||
const url: string = base + '/_api/web/lists?$select=Id,Title,Hidden&$filter=Hidden eq false&$orderby=Title';
|
||||
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.readHeaders() })
|
||||
.then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response, 'Listen konnten nicht geladen werden.'))
|
||||
.then((payload: any): IListOption[] => this.results(payload).map((source: any): IListOption => ({
|
||||
id: this.normalizeGuid(source.Id), title: source.Title || source.Id
|
||||
})));
|
||||
}
|
||||
|
||||
public getDateFields(webUrl: string, listId: string): Promise<IFieldOption[]> {
|
||||
let base: string;
|
||||
try { base = this.normalizeScopedWebUrl(webUrl); } catch (error) { return Promise.reject(error); }
|
||||
const url: string = base + "/_api/web/lists(guid'" + this.normalizeGuid(listId) + "')/fields" +
|
||||
'?$select=Id,Title,InternalName,TypeAsString,Hidden,ReadOnlyField,ClientSideComponentId,ClientSideComponentProperties';
|
||||
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.readHeaders() })
|
||||
.then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response, 'Felder konnten nicht geladen werden.'))
|
||||
.then((payload: any): IFieldOption[] => this.results(payload)
|
||||
.filter((source: any): boolean => source.TypeAsString === 'DateTime' && source.Hidden !== true && source.ReadOnlyField !== true)
|
||||
.map((source: any): IFieldOption => ({
|
||||
id: this.normalizeGuid(source.Id), title: source.Title || source.InternalName, internalName: source.InternalName,
|
||||
componentId: this.normalizeGuid(source.ClientSideComponentId), properties: source.ClientSideComponentProperties || '',
|
||||
entityType: source.__metadata && source.__metadata.type ? source.__metadata.type : 'SP.Field'
|
||||
})));
|
||||
}
|
||||
|
||||
public bindExpiryField(
|
||||
webUrl: string,
|
||||
list: IListOption,
|
||||
field: IFieldOption,
|
||||
currentBindings: any[]
|
||||
): Promise<any[]> {
|
||||
if (field.componentId && field.componentId !== '00000000-0000-0000-0000-000000000000' && field.componentId !== ExpiryFieldComponentId) {
|
||||
return Promise.reject(new Error('Das Feld ist bereits mit einem anderen Field Customizer verbunden.'));
|
||||
}
|
||||
let base: string;
|
||||
try { base = this.normalizeScopedWebUrl(webUrl); } catch (error) { return Promise.reject(error); }
|
||||
const fieldUrl: string = base + "/_api/web/lists(guid'" + this.normalizeGuid(list.id) + "')/fields(guid'" + field.id + "')";
|
||||
const properties: string = field.properties && field.properties.trim()
|
||||
? field.properties
|
||||
: JSON.stringify({ schemaVersion: 2, expiryField: field.internalName, inheritSiteDefaults: true });
|
||||
const body: any = {
|
||||
'__metadata': { 'type': field.entityType || 'SP.Field' },
|
||||
'ClientSideComponentId': ExpiryFieldComponentId,
|
||||
'ClientSideComponentProperties': properties
|
||||
};
|
||||
return this.context.spHttpClient.post(fieldUrl, SPHttpClient.configurations.v1, {
|
||||
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body)
|
||||
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Das Feld konnte nicht angebunden werden.'))
|
||||
.then((): Promise<any[]> => {
|
||||
const bindings: any[] = (currentBindings || []).filter((binding: any): boolean =>
|
||||
!(String(binding.webUrl || '').replace(/\/$/, '').toLowerCase() === base.toLowerCase() && this.normalizeGuid(binding.listId) === this.normalizeGuid(list.id)));
|
||||
bindings.push({ webUrl: base, listId: this.normalizeGuid(list.id), listTitle: list.title, fieldInternalName: field.internalName, fieldId: field.id });
|
||||
const descriptor: any = {
|
||||
key: 'ExpiryIndicator', displayName: 'Expiry Indicator', version: '2.1.2', provider: 'expiryIndicator',
|
||||
componentIds: [ExpiryFieldComponentId, 'cd58f5d9-ffc8-4df7-a910-3054842d7abf', '95691218-bcb1-4fad-adda-02bb12014d15'],
|
||||
settingsScope: ['siteCollection', 'web', 'list']
|
||||
};
|
||||
return this.saveRootProperty(DescriptorProperty, JSON.stringify(descriptor))
|
||||
.then((): Promise<void> => this.saveRootProperty(BindingsProperty, JSON.stringify(bindings)))
|
||||
.then((): any[] => bindings);
|
||||
});
|
||||
}
|
||||
|
||||
public loadExpiryBinding(binding: any, inheritedConfig: any): Promise<IExpiryBindingLoadResult> {
|
||||
let baseUrl: string;
|
||||
try { baseUrl = this.expiryBindingFieldUrl(binding); } catch (error) { return Promise.reject(error); }
|
||||
const url: string = baseUrl + '?$select=Id,Title,InternalName,ClientSideComponentId,ClientSideComponentProperties';
|
||||
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.readHeaders() })
|
||||
.then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response, 'Die lokale Bindung konnte nicht geladen werden.'))
|
||||
.then((payload: any): IExpiryBindingLoadResult => {
|
||||
const source: any = payload.d || payload;
|
||||
const field: IFieldOption = {
|
||||
id: this.normalizeGuid(source.Id), title: source.Title || source.InternalName, internalName: source.InternalName,
|
||||
componentId: this.normalizeGuid(source.ClientSideComponentId), properties: source.ClientSideComponentProperties || '',
|
||||
entityType: source.__metadata && source.__metadata.type ? source.__metadata.type : 'SP.Field'
|
||||
};
|
||||
if (field.componentId !== ExpiryFieldComponentId) { throw new Error('Das Feld ist nicht mit dem ExpiryIndicator verbunden.'); }
|
||||
const properties: any = this.parseJson(field.properties, {});
|
||||
let config: any; let inherited: boolean = false;
|
||||
if (!field.properties || Object.keys(properties).length === 0 || properties.inheritSiteDefaults === true || this.isBindingOnly(properties)) {
|
||||
config = cloneValue(inheritedConfig); inherited = true;
|
||||
if (properties.override) { config = mergePreservingUnknown(config, properties.override); inherited = false; }
|
||||
} else { config = properties; }
|
||||
config.expiryField = field.internalName;
|
||||
return { config: config, inherited: inherited, field: field };
|
||||
});
|
||||
}
|
||||
|
||||
public saveExpiryBinding(binding: any, field: IFieldOption, config: any, inherit: boolean): Promise<void> {
|
||||
let fieldUrl: string;
|
||||
try { fieldUrl = this.expiryBindingFieldUrl(binding); } catch (error) { return Promise.reject(error); }
|
||||
const properties: any = inherit
|
||||
? { schemaVersion: 2, expiryField: field.internalName, inheritSiteDefaults: true }
|
||||
: mergePreservingUnknown(config, { expiryField: field.internalName });
|
||||
const body: any = {
|
||||
'__metadata': { 'type': field.entityType || 'SP.Field' },
|
||||
'ClientSideComponentProperties': JSON.stringify(properties)
|
||||
};
|
||||
return this.context.spHttpClient.post(fieldUrl, SPHttpClient.configurations.v1, {
|
||||
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body)
|
||||
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Die lokale Konfiguration konnte nicht gespeichert werden.'));
|
||||
}
|
||||
|
||||
private loadProvider(
|
||||
provider: ISettingsProvider<any>,
|
||||
actions: IUserCustomActionInfo[],
|
||||
properties: any
|
||||
): ILoadedSettingsProvider | undefined {
|
||||
let raw: any;
|
||||
const context: IProviderLoadContext = {};
|
||||
if (provider.storage.kind === 'siteUserCustomAction') {
|
||||
const action: IUserCustomActionInfo = this.findAction(provider, actions);
|
||||
if (!action) { return undefined; }
|
||||
context.action = action;
|
||||
raw = this.parseJson(action.properties, {});
|
||||
} else {
|
||||
const propertyKey: string = provider.storage.propertyKey || '';
|
||||
const stored: any = this.getProperty(properties, propertyKey);
|
||||
const descriptor: any = this.getProperty(properties, DescriptorProperty);
|
||||
const bindingsValue: any = this.getProperty(properties, BindingsProperty);
|
||||
const registration: IUserCustomActionInfo = this.findAnyAction(provider, actions);
|
||||
if (!stored && !descriptor && !bindingsValue && !registration) { return undefined; }
|
||||
if (registration) { context.action = registration; }
|
||||
raw = this.parseJson(stored, provider.createDefault());
|
||||
context.propertyBag = properties;
|
||||
context.bindings = this.normalizeBindings(this.parseJson(bindingsValue, []));
|
||||
}
|
||||
const normalized: any = provider.normalize(raw || provider.createDefault());
|
||||
return {
|
||||
provider: provider,
|
||||
config: cloneValue(normalized),
|
||||
originalConfig: cloneValue(raw || normalized),
|
||||
context: context,
|
||||
dirty: false
|
||||
};
|
||||
}
|
||||
|
||||
private getSiteActions(): Promise<IUserCustomActionInfo[]> {
|
||||
const url: string = this.siteUrl + '/_api/site/UserCustomActions' +
|
||||
'?$select=Id,Title,Name,Location,ClientSideComponentId,ClientSideComponentProperties';
|
||||
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.readHeaders() })
|
||||
.then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response, 'UserCustomActions konnten nicht gelesen werden.'))
|
||||
.then((payload: any): IUserCustomActionInfo[] => this.results(payload).map((source: any): IUserCustomActionInfo => ({
|
||||
id: this.normalizeGuid(source.Id),
|
||||
title: source.Title || '',
|
||||
name: source.Name || '',
|
||||
location: source.Location || '',
|
||||
componentId: this.normalizeGuid(source.ClientSideComponentId),
|
||||
properties: source.ClientSideComponentProperties || '{}',
|
||||
entityType: source.__metadata && source.__metadata.type ? source.__metadata.type : 'SP.UserCustomAction'
|
||||
})));
|
||||
}
|
||||
|
||||
private getRootProperties(): Promise<any> {
|
||||
return this.context.spHttpClient.get(this.siteUrl + '/_api/web/AllProperties', SPHttpClient.configurations.v1, {
|
||||
headers: this.readHeaders()
|
||||
}).then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response, 'Root-Web-Properties konnten nicht gelesen werden.'))
|
||||
.then((payload: any): any => payload.d || payload);
|
||||
}
|
||||
|
||||
private saveUserCustomAction(action: IUserCustomActionInfo, config: any): Promise<void> {
|
||||
const url: string = this.siteUrl + "/_api/site/UserCustomActions(guid'" + action.id + "')";
|
||||
const body: any = {
|
||||
'__metadata': { 'type': action.entityType || 'SP.UserCustomAction' },
|
||||
'ClientSideComponentProperties': JSON.stringify(config)
|
||||
};
|
||||
return this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
|
||||
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body)
|
||||
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Konfiguration konnte nicht gespeichert werden.'));
|
||||
}
|
||||
|
||||
private saveRootProperty(key: string, value: string): Promise<void> {
|
||||
if (!key) { return Promise.reject(new Error('Der Property-Bag-Schlüssel fehlt.')); }
|
||||
const body: any = { '__metadata': { 'type': 'SP.PropertyValues' } };
|
||||
body[this.encodePropertyName(key)] = value;
|
||||
return this.context.spHttpClient.post(this.siteUrl + '/_api/web/AllProperties', SPHttpClient.configurations.v1, {
|
||||
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body)
|
||||
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Der Site-Collection-Standard konnte nicht gespeichert werden.'));
|
||||
}
|
||||
|
||||
private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined {
|
||||
for (let i: number = 0; i < actions.length; i++) {
|
||||
if (provider.componentIds.indexOf(actions[i].componentId) >= 0 &&
|
||||
actions[i].location.indexOf('ClientSideExtension.ApplicationCustomizer') >= 0) {
|
||||
return actions[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private findAnyAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined {
|
||||
for (let i: number = 0; i < actions.length; i++) {
|
||||
if (provider.componentIds.indexOf(actions[i].componentId) >= 0) { return actions[i]; }
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getProperty(properties: any, key: string): any {
|
||||
if (!properties) { return undefined; }
|
||||
const normalizedExpected: string = this.normalizePropertyName(key);
|
||||
const names: string[] = Object.keys(properties);
|
||||
for (let i: number = 0; i < names.length; i++) {
|
||||
if (this.normalizePropertyName(names[i]) === normalizedExpected) { return properties[names[i]]; }
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private parseJson(value: any, fallback: any): any {
|
||||
if (typeof value !== 'string' || !value.trim()) { return cloneValue(fallback); }
|
||||
try { return JSON.parse(value); } catch (error) { return cloneValue(fallback); }
|
||||
}
|
||||
|
||||
private normalizeBindings(value: any): any[] {
|
||||
if (Array.isArray(value)) { return value; }
|
||||
if (!value || typeof value !== 'object') { return []; }
|
||||
const result: any[] = [];
|
||||
Object.keys(value).forEach((key: string): void => {
|
||||
if (Array.isArray(value[key])) { Array.prototype.push.apply(result, value[key]); }
|
||||
else if (value[key] && typeof value[key] === 'object') { result.push(value[key]); }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private expiryBindingFieldUrl(binding: any): string {
|
||||
const webUrl: string = this.normalizeScopedWebUrl(binding.webUrl);
|
||||
const listId: string = this.normalizeGuid(binding.listId);
|
||||
const fieldId: string = this.normalizeGuid(binding.fieldId);
|
||||
if (!webUrl || !listId || !fieldId) { throw new Error('Die Bindung enthält keine vollständigen Web-, Listen- oder Feldinformationen.'); }
|
||||
return webUrl + "/_api/web/lists(guid'" + listId + "')/fields(guid'" + fieldId + "')";
|
||||
}
|
||||
|
||||
private isBindingOnly(properties: any): boolean {
|
||||
if (!properties || typeof properties.expiryField !== 'string') { return false; }
|
||||
const keys: string[] = ['baseField', 'default', 'rules', 'nullText', 'confirmExtension'];
|
||||
for (let i: number = 0; i < keys.length; i++) { if (properties[keys[i]] !== undefined) { return false; } }
|
||||
return true;
|
||||
}
|
||||
|
||||
private results(payload: any): any[] { return payload.value || (payload.d && payload.d.results) || []; }
|
||||
private normalizeGuid(value: any): string { return String(value || '').replace(/[{}]/g, '').toLowerCase(); }
|
||||
private normalizeScopedWebUrl(value: any): string {
|
||||
const url: string = String(value || '').trim().replace(/\/$/, '');
|
||||
const expected: string = this.siteUrl.toLowerCase(); const normalized: string = url.toLowerCase();
|
||||
if (!url || (normalized !== expected && normalized.indexOf(expected + '/') !== 0)) {
|
||||
throw new Error('Die Web-URL muss innerhalb der aktuellen Site Collection liegen.');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
private normalizePropertyName(value: string): string { return String(value || '').replace(/_x002e_/gi, '.').toLowerCase(); }
|
||||
private encodePropertyName(value: string): string { return value.replace(/\./g, '_x002e_'); }
|
||||
|
||||
private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; }
|
||||
private writeHeaders(method: string): any {
|
||||
return {
|
||||
'Accept': 'application/json;odata=verbose',
|
||||
'Content-Type': 'application/json;odata=verbose',
|
||||
'OData-Version': '3.0', 'IF-MATCH': '*', 'X-HTTP-Method': method
|
||||
};
|
||||
}
|
||||
|
||||
private ensureJson(response: SPHttpClientResponse, message: string): Promise<any> {
|
||||
if (!response.ok) { return this.responseError(response, message).then((error: Error): Promise<any> => Promise.reject(error)); }
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private ensureOk(response: SPHttpClientResponse, message: string): Promise<void> {
|
||||
return response.ok ? Promise.resolve() : this.responseError(response, message).then((error: Error): Promise<void> => Promise.reject(error));
|
||||
}
|
||||
|
||||
private responseError(response: SPHttpClientResponse, prefix: string): Promise<Error> {
|
||||
return response.text().then((text: string): Error => {
|
||||
let detail: string = text;
|
||||
try {
|
||||
const parsed: any = JSON.parse(text);
|
||||
detail = parsed.error && parsed.error.message ? (parsed.error.message.value || parsed.error.message) : text;
|
||||
} catch (error) { /* Textantwort verwenden. */ }
|
||||
return new Error(prefix + ' (' + response.status + '): ' + detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// tslint:disable:no-any max-line-length
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { ITermSetInfo } from '../core/ProviderContracts';
|
||||
|
||||
export class TaxonomyService {
|
||||
public constructor(private context: WebPartContext) { }
|
||||
|
||||
public getTermSets(): Promise<ITermSetInfo[]> {
|
||||
const requestXml: string = '<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="PortalSettingsV3" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectIdentityQuery Id="3" ObjectPathId="1" /><ObjectPath Id="5" ObjectPathId="4" /><ObjectIdentityQuery Id="6" ObjectPathId="4" /><Query Id="7" ObjectPathId="4"><Query SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /><Property Name="Groups"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="IsSystemGroup" ScalarProperty="true" /><Property Name="TermSets"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="Description" ScalarProperty="true" /></Properties></ChildItemQuery></Property></Properties></ChildItemQuery></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="4" ParentId="1" Name="GetDefaultSiteCollectionTermStore" /></ObjectPaths></Request>';
|
||||
const url: string = this.context.pageContext.site.absoluteUrl.replace(/\/$/, '') + '/_vti_bin/client.svc/ProcessQuery';
|
||||
return this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/xml' }, body: requestXml
|
||||
}).then((response: SPHttpClientResponse): Promise<string> => {
|
||||
if (!response.ok) { return Promise.reject(new Error('Termsets konnten nicht geladen werden (' + response.status + ').')); }
|
||||
return response.text();
|
||||
}).then((text: string): ITermSetInfo[] => this.parse(JSON.parse(text || '[]')));
|
||||
}
|
||||
|
||||
private parse(payload: any[]): ITermSetInfo[] {
|
||||
if (!Array.isArray(payload)) { return []; }
|
||||
if (payload.length && payload[0] && payload[0].ErrorInfo) {
|
||||
throw new Error(payload[0].ErrorInfo.ErrorMessage || 'Taxonomy-Abfrage fehlgeschlagen.');
|
||||
}
|
||||
let store: any;
|
||||
for (let i: number = 0; i < payload.length; i++) {
|
||||
if (payload[i] && payload[i]._ObjectType_ === 'SP.Taxonomy.TermStore') { store = payload[i]; break; }
|
||||
}
|
||||
const groups: any[] = store && store.Groups && Array.isArray(store.Groups._Child_Items_)
|
||||
? store.Groups._Child_Items_ : [];
|
||||
const result: ITermSetInfo[] = [];
|
||||
for (let g: number = 0; g < groups.length; g++) {
|
||||
if (!groups[g] || groups[g].IsSystemGroup === true) { continue; }
|
||||
const sets: any[] = groups[g].TermSets && Array.isArray(groups[g].TermSets._Child_Items_)
|
||||
? groups[g].TermSets._Child_Items_ : [];
|
||||
for (let s: number = 0; s < sets.length; s++) {
|
||||
const id: string = this.cleanGuid(sets[s] && sets[s].Id);
|
||||
if (id) {
|
||||
result.push({ id: id, name: sets[s].Name || id, groupName: groups[g].Name || 'Ohne Gruppe', description: sets[s].Description || '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.sort((left: ITermSetInfo, right: ITermSetInfo): number =>
|
||||
(left.groupName + left.name).localeCompare(right.groupName + right.name));
|
||||
}
|
||||
|
||||
private cleanGuid(value: any): string {
|
||||
return String(value || '').replace('/Guid(', '').replace(')/', '').replace(')', '').replace(/[{}]/g, '').toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export function createElement(
|
||||
tag: string,
|
||||
className?: string,
|
||||
text?: string
|
||||
): HTMLElement {
|
||||
const element: HTMLElement = document.createElement(tag);
|
||||
if (className) { element.className = className; }
|
||||
if (text !== undefined) { element.textContent = text; }
|
||||
return element;
|
||||
}
|
||||
|
||||
export function clearElement(element: HTMLElement): void {
|
||||
while (element.firstChild) { element.removeChild(element.firstChild); }
|
||||
}
|
||||
|
||||
export function appendText(parent: HTMLElement, tag: 'p' | 'span' | 'strong', className: string, text: string): HTMLElement {
|
||||
const element: HTMLElement = createElement(tag, className, text);
|
||||
parent.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// tslint:disable:no-any max-line-length
|
||||
import { ILoadedSettingsProvider, ISettingsElement, ISettingsOption, ISettingsSection } from '../core/ProviderContracts';
|
||||
import { getPathValue, isEmptyValue, setPathValue } from '../core/ObjectPath';
|
||||
import { appendText, clearElement, createElement } from './Dom';
|
||||
|
||||
export interface IDynamicFormCallbacks {
|
||||
changed: () => void;
|
||||
selectTermSet: (element: ISettingsElement) => void;
|
||||
addBinding: () => void;
|
||||
editBinding: (binding: any) => void; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export class DynamicFormRenderer {
|
||||
private host: HTMLElement;
|
||||
private item: ILoadedSettingsProvider;
|
||||
private callbacks: IDynamicFormCallbacks;
|
||||
|
||||
public render(host: HTMLElement, item: ILoadedSettingsProvider, callbacks: IDynamicFormCallbacks): void {
|
||||
this.host = host;
|
||||
this.item = item;
|
||||
this.callbacks = callbacks;
|
||||
clearElement(host);
|
||||
for (let i: number = 0; i < item.provider.sections.length; i++) {
|
||||
host.appendChild(this.renderSection(item.provider.sections[i]));
|
||||
}
|
||||
this.refreshDerivedElements();
|
||||
}
|
||||
|
||||
public refreshDerivedElements(): void {
|
||||
if (!this.host || !this.item) { return; }
|
||||
const previews: NodeListOf<HTMLElement> = this.host.querySelectorAll('[data-ps3-json-preview]') as NodeListOf<HTMLElement>;
|
||||
for (let i: number = 0; i < previews.length; i++) { previews[i].textContent = JSON.stringify(this.item.config, null, 2); }
|
||||
const summaries: NodeListOf<HTMLElement> = this.host.querySelectorAll('[data-ps3-summary]') as NodeListOf<HTMLElement>;
|
||||
for (let j: number = 0; j < summaries.length; j++) { this.renderSummary(summaries[j]); }
|
||||
const conditional: NodeListOf<HTMLElement> = this.host.querySelectorAll('[data-ps3-condition-path]') as NodeListOf<HTMLElement>;
|
||||
for (let c: number = 0; c < conditional.length; c++) {
|
||||
const element: HTMLElement = conditional[c];
|
||||
const path: string = element.getAttribute('data-ps3-condition-path') || '';
|
||||
const operator: string = element.getAttribute('data-ps3-condition-operator') || '';
|
||||
const expectedRaw: string = element.getAttribute('data-ps3-condition-value') || '';
|
||||
let expected: any = expectedRaw;
|
||||
try { expected = JSON.parse(expectedRaw); } catch (error) { /* String verwenden. */ }
|
||||
element.hidden = !this.matchesCondition(getPathValue(this.item.config, path), operator, expected);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSection(section: ISettingsSection): HTMLElement {
|
||||
const sectionElement: HTMLElement = section.collapsible ? createElement('details', 'ps3-section') : createElement('section', 'ps3-section');
|
||||
sectionElement.setAttribute('data-layout', section.layout || 'singleColumn');
|
||||
if (section.collapsible && !section.collapsedByDefault) { (sectionElement as any).open = true; }
|
||||
const header: HTMLElement = section.collapsible ? createElement('summary', 'ps3-sectionHeader') : createElement('div', 'ps3-sectionHeader');
|
||||
const heading: HTMLElement = createElement('h2', 'ps3-sectionTitle', section.title);
|
||||
header.appendChild(heading);
|
||||
if (section.description) { appendText(header, 'p', 'ps3-sectionDescription', section.description); }
|
||||
sectionElement.appendChild(header);
|
||||
const grid: HTMLElement = createElement('div', 'ps3-grid');
|
||||
for (let i: number = 0; i < section.elements.length; i++) { grid.appendChild(this.renderElement(section.elements[i])); }
|
||||
sectionElement.appendChild(grid);
|
||||
return sectionElement;
|
||||
}
|
||||
|
||||
private renderElement(definition: ISettingsElement): HTMLElement {
|
||||
const wrapper: HTMLElement = createElement('div', 'ps3-element ps3-span-' + (definition.columnSpan || 12));
|
||||
if (definition.visibleWhen) {
|
||||
wrapper.setAttribute('data-ps3-condition-path', definition.visibleWhen.field);
|
||||
wrapper.setAttribute('data-ps3-condition-operator', definition.visibleWhen.operator);
|
||||
wrapper.setAttribute('data-ps3-condition-value', JSON.stringify(definition.visibleWhen.value));
|
||||
}
|
||||
switch (definition.type) {
|
||||
case 'heading': return this.renderHeading(wrapper, definition);
|
||||
case 'text': appendText(wrapper, 'p', 'ps3-copy', definition.text || ''); return wrapper;
|
||||
case 'notice': return this.renderNotice(wrapper, definition);
|
||||
case 'divider': wrapper.className += ' ps3-divider'; return wrapper;
|
||||
case 'group': return this.renderGroup(wrapper, definition);
|
||||
case 'propertySummary': return this.renderSummaryElement(wrapper, definition);
|
||||
case 'jsonPreview': return this.renderPreview(wrapper, definition);
|
||||
case 'boolean': return this.renderBoolean(wrapper, definition);
|
||||
case 'choice': return this.renderChoice(wrapper, definition);
|
||||
case 'choiceCards': return this.renderChoiceCards(wrapper, definition);
|
||||
case 'number': return this.renderInput(wrapper, definition, 'number');
|
||||
case 'textField': return this.renderInput(wrapper, definition, 'text');
|
||||
case 'json': return this.renderJson(wrapper, definition);
|
||||
case 'stringList': return this.renderStringList(wrapper, definition);
|
||||
case 'termSet': return this.renderTermSet(wrapper, definition);
|
||||
case 'bindingInventory': return this.renderBindings(wrapper, definition);
|
||||
default: appendText(wrapper, 'p', 'ps3-errorText', 'Nicht unterstütztes Element: ' + definition.type); return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
private renderHeading(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const tag: 'h2' | 'h3' | 'h4' = definition.level === 2 ? 'h2' : (definition.level === 4 ? 'h4' : 'h3');
|
||||
wrapper.appendChild(createElement(tag, 'ps3-elementHeading', definition.text || definition.title || ''));
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderNotice(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
wrapper.className += ' ps3-notice ps3-notice-' + (definition.appearance || 'info');
|
||||
wrapper.setAttribute('role', definition.appearance === 'danger' ? 'alert' : 'note');
|
||||
if (definition.title) { appendText(wrapper, 'strong', 'ps3-noticeTitle', definition.title); }
|
||||
appendText(wrapper, 'p', 'ps3-noticeText', definition.text || '');
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderGroup(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
wrapper.className += ' ps3-group';
|
||||
if (definition.title) { wrapper.appendChild(createElement('h3', 'ps3-elementHeading', definition.title)); }
|
||||
const grid: HTMLElement = createElement('div', 'ps3-grid');
|
||||
const elements: ISettingsElement[] = definition.elements || [];
|
||||
for (let i: number = 0; i < elements.length; i++) { grid.appendChild(this.renderElement(elements[i])); }
|
||||
wrapper.appendChild(grid);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderInput(wrapper: HTMLElement, definition: ISettingsElement, type: string): HTMLElement {
|
||||
const input: HTMLInputElement = createElement('input', 'ps3-input') as HTMLInputElement;
|
||||
input.type = type;
|
||||
input.id = this.controlId(definition);
|
||||
input.value = String(getPathValue(this.item.config, definition.path || '') || '');
|
||||
input.readOnly = definition.readOnly === true;
|
||||
if (definition.placeholder) { input.placeholder = definition.placeholder; }
|
||||
if (definition.minimum !== undefined) { input.min = String(definition.minimum); }
|
||||
if (definition.maximum !== undefined) { input.max = String(definition.maximum); }
|
||||
if (definition.step !== undefined) { input.step = String(definition.step); }
|
||||
this.appendLabel(wrapper, definition, input.id);
|
||||
const controlRow: HTMLElement = createElement('div', 'ps3-controlRow');
|
||||
controlRow.appendChild(input);
|
||||
if (definition.suffix) { appendText(controlRow, 'span', 'ps3-suffix', definition.suffix); }
|
||||
wrapper.appendChild(controlRow);
|
||||
this.appendDescription(wrapper, definition);
|
||||
input.addEventListener('input', (): void => {
|
||||
const value: any = type === 'number' ? Number(input.value) : input.value;
|
||||
setPathValue(this.item.config, definition.path || '', value);
|
||||
this.notifyChanged();
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderBoolean(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const row: HTMLElement = createElement('label', 'ps3-toggle');
|
||||
const input: HTMLInputElement = createElement('input') as HTMLInputElement;
|
||||
input.type = 'checkbox'; input.checked = getPathValue(this.item.config, definition.path || '') === true;
|
||||
const visual: HTMLElement = createElement('span', 'ps3-toggleVisual');
|
||||
const content: HTMLElement = createElement('span', 'ps3-toggleContent');
|
||||
appendText(content, 'strong', 'ps3-label', definition.label || definition.key || '');
|
||||
if (definition.description) { appendText(content, 'span', 'ps3-description', definition.description); }
|
||||
row.appendChild(input); row.appendChild(visual); row.appendChild(content); wrapper.appendChild(row);
|
||||
input.addEventListener('change', (): void => {
|
||||
setPathValue(this.item.config, definition.path || '', input.checked);
|
||||
this.notifyChanged();
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderChoice(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const select: HTMLSelectElement = createElement('select', 'ps3-input') as HTMLSelectElement;
|
||||
select.id = this.controlId(definition);
|
||||
const value: string = String(getPathValue(this.item.config, definition.path || '') || '');
|
||||
(definition.options || []).forEach((option: ISettingsOption): void => {
|
||||
const optionElement: HTMLOptionElement = createElement('option') as HTMLOptionElement;
|
||||
optionElement.value = option.key; optionElement.textContent = option.text; optionElement.selected = option.key === value;
|
||||
select.appendChild(optionElement);
|
||||
});
|
||||
this.appendLabel(wrapper, definition, select.id); wrapper.appendChild(select); this.appendDescription(wrapper, definition);
|
||||
select.addEventListener('change', (): void => { setPathValue(this.item.config, definition.path || '', select.value); this.notifyChanged(); });
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderChoiceCards(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
this.appendLabel(wrapper, definition, '');
|
||||
const group: HTMLElement = createElement('div', 'ps3-choiceCards'); group.setAttribute('role', 'radiogroup');
|
||||
const current: string = String(getPathValue(this.item.config, definition.path || '') || '');
|
||||
(definition.options || []).forEach((option: ISettingsOption): void => {
|
||||
const card: HTMLLabelElement = createElement('label', 'ps3-choiceCard' + (option.key === current ? ' is-selected' : '')) as HTMLLabelElement;
|
||||
const input: HTMLInputElement = createElement('input') as HTMLInputElement;
|
||||
input.type = 'radio'; input.name = this.controlId(definition); input.value = option.key; input.checked = option.key === current;
|
||||
const content: HTMLElement = createElement('span', 'ps3-choiceContent');
|
||||
appendText(content, 'strong', 'ps3-choiceTitle', option.text);
|
||||
if (option.description) { appendText(content, 'span', 'ps3-description', option.description); }
|
||||
card.appendChild(input); card.appendChild(content); group.appendChild(card);
|
||||
input.addEventListener('change', (): void => {
|
||||
if (input.checked) {
|
||||
const cards: NodeListOf<HTMLElement> = group.querySelectorAll('.ps3-choiceCard') as NodeListOf<HTMLElement>;
|
||||
for (let index: number = 0; index < cards.length; index++) { cards[index].className = cards[index].className.replace(/\s+is-selected/g, ''); }
|
||||
card.className += ' is-selected';
|
||||
setPathValue(this.item.config, definition.path || '', option.key); this.notifyChanged();
|
||||
}
|
||||
});
|
||||
});
|
||||
wrapper.appendChild(group); return wrapper;
|
||||
}
|
||||
|
||||
private renderJson(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const textarea: HTMLTextAreaElement = createElement('textarea', 'ps3-textarea ps3-code') as HTMLTextAreaElement;
|
||||
textarea.id = this.controlId(definition); textarea.spellcheck = false;
|
||||
textarea.value = JSON.stringify(getPathValue(this.item.config, definition.path || ''), null, 2);
|
||||
const error: HTMLElement = createElement('div', 'ps3-fieldError'); error.hidden = true;
|
||||
this.appendLabel(wrapper, definition, textarea.id); wrapper.appendChild(textarea); wrapper.appendChild(error); this.appendDescription(wrapper, definition);
|
||||
textarea.addEventListener('input', (): void => {
|
||||
try {
|
||||
setPathValue(this.item.config, definition.path || '', JSON.parse(textarea.value));
|
||||
textarea.removeAttribute('aria-invalid'); error.hidden = true; this.notifyChanged();
|
||||
} catch (parseError) {
|
||||
textarea.setAttribute('aria-invalid', 'true'); error.textContent = 'Ungültiges JSON: ' + parseError.message; error.hidden = false;
|
||||
}
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderStringList(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const textarea: HTMLTextAreaElement = createElement('textarea', 'ps3-textarea ps3-listEditor') as HTMLTextAreaElement;
|
||||
textarea.id = this.controlId(definition); textarea.value = (getPathValue(this.item.config, definition.path || '') || []).join('\n');
|
||||
this.appendLabel(wrapper, definition, textarea.id); wrapper.appendChild(textarea); this.appendDescription(wrapper, definition);
|
||||
textarea.addEventListener('input', (): void => {
|
||||
const values: string[] = textarea.value.split(/\r?\n/).map((value: string): string => value.trim()).filter((value: string): boolean => !!value);
|
||||
setPathValue(this.item.config, definition.path || '', values); this.notifyChanged();
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderTermSet(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
this.appendLabel(wrapper, definition, '');
|
||||
const selected: HTMLElement = createElement('div', 'ps3-selectedItem');
|
||||
const text: HTMLElement = createElement('div');
|
||||
const name: string = String(getPathValue(this.item.config, definition.companionNamePath || '') || 'Kein Termset ausgewählt');
|
||||
const id: string = String(getPathValue(this.item.config, definition.path || '') || '-');
|
||||
appendText(text, 'strong', 'ps3-selectedTitle', name); appendText(text, 'span', 'ps3-mono', id);
|
||||
const button: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Auswählen') as HTMLButtonElement;
|
||||
button.type = 'button'; button.addEventListener('click', (): void => this.callbacks.selectTermSet(definition));
|
||||
selected.appendChild(text); selected.appendChild(button); wrapper.appendChild(selected); this.appendDescription(wrapper, definition);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private renderBindings(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const header: HTMLElement = createElement('div', 'ps3-bindingHeader');
|
||||
if (definition.title) { header.appendChild(createElement('h3', 'ps3-elementHeading', definition.title)); }
|
||||
const add: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Bindung hinzufügen') as HTMLButtonElement;
|
||||
add.type = 'button'; add.addEventListener('click', (): void => this.callbacks.addBinding()); header.appendChild(add); wrapper.appendChild(header);
|
||||
const bindings: any[] = this.item.context.bindings || [];
|
||||
const list: HTMLElement = createElement('div', 'ps3-bindingList');
|
||||
if (!bindings.length) { appendText(list, 'p', 'ps3-emptyInline', 'Keine Bindungen registriert.'); }
|
||||
for (let i: number = 0; i < bindings.length; i++) {
|
||||
const binding: any = bindings[i];
|
||||
const card: HTMLElement = createElement('article', 'ps3-binding');
|
||||
const bindingText: HTMLElement = createElement('div');
|
||||
appendText(bindingText, 'strong', 'ps3-bindingTitle', binding.listTitle || binding.listId || 'Liste/Bibliothek');
|
||||
appendText(bindingText, 'span', 'ps3-mono', (binding.webUrl || '') + ' · ' + (binding.fieldInternalName || binding.fieldName || ''));
|
||||
const edit: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Konfigurieren') as HTMLButtonElement;
|
||||
edit.type = 'button'; edit.addEventListener('click', (): void => this.callbacks.editBinding(binding));
|
||||
card.appendChild(bindingText); card.appendChild(edit);
|
||||
list.appendChild(card);
|
||||
}
|
||||
wrapper.appendChild(list); return wrapper;
|
||||
}
|
||||
|
||||
private renderSummaryElement(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
if (definition.title) { wrapper.appendChild(createElement('h3', 'ps3-elementHeading', definition.title)); }
|
||||
const host: HTMLElement = createElement('div', 'ps3-summary'); host.setAttribute('data-ps3-summary', 'true');
|
||||
wrapper.appendChild(host); return wrapper;
|
||||
}
|
||||
|
||||
private renderPreview(wrapper: HTMLElement, definition: ISettingsElement): HTMLElement {
|
||||
const details: any = createElement('details', 'ps3-preview');
|
||||
details.open = !definition.collapsedByDefault;
|
||||
details.appendChild(createElement('summary', 'ps3-previewTitle', definition.title || 'JSON-Vorschau'));
|
||||
const pre: HTMLElement = createElement('pre', 'ps3-code ps3-previewCode'); pre.setAttribute('data-ps3-json-preview', 'true');
|
||||
details.appendChild(pre); wrapper.appendChild(details); return wrapper;
|
||||
}
|
||||
|
||||
private renderSummary(host: HTMLElement): void {
|
||||
clearElement(host);
|
||||
const table: HTMLTableElement = createElement('table', 'ps3-summaryTable') as HTMLTableElement;
|
||||
const keys: string[] = Object.keys(this.item.config).filter((key: string): boolean => key !== 'placeholdertop' && key !== 'placeholderbottom' && key !== 'rules');
|
||||
for (let i: number = 0; i < keys.length; i++) {
|
||||
const row: HTMLTableRowElement = table.insertRow(-1);
|
||||
const keyCell: HTMLTableCellElement = row.insertCell(-1); keyCell.textContent = keys[i];
|
||||
const valueCell: HTMLTableCellElement = row.insertCell(-1);
|
||||
const value: any = this.item.config[keys[i]];
|
||||
valueCell.textContent = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
host.appendChild(table);
|
||||
}
|
||||
|
||||
private appendLabel(wrapper: HTMLElement, definition: ISettingsElement, htmlFor: string): void {
|
||||
const label: HTMLLabelElement = createElement('label', 'ps3-label', definition.label || definition.key || '') as HTMLLabelElement;
|
||||
if (htmlFor) { label.htmlFor = htmlFor; }
|
||||
if (definition.required) { label.appendChild(createElement('span', 'ps3-required', ' *')); }
|
||||
wrapper.appendChild(label);
|
||||
}
|
||||
|
||||
private appendDescription(wrapper: HTMLElement, definition: ISettingsElement): void {
|
||||
if (definition.description) { appendText(wrapper, 'p', 'ps3-description', definition.description); }
|
||||
}
|
||||
|
||||
private notifyChanged(): void {
|
||||
this.item.dirty = true; this.refreshDerivedElements(); this.callbacks.changed();
|
||||
}
|
||||
|
||||
private controlId(definition: ISettingsElement): string {
|
||||
return 'ps3-' + this.item.provider.key + '-' + String(definition.key || definition.path || 'field').replace(/[^a-z0-9_-]/gi, '-');
|
||||
}
|
||||
|
||||
private matchesCondition(actual: any, operator: string, expected: any): boolean {
|
||||
switch (operator) {
|
||||
case 'equals': return actual === expected;
|
||||
case 'notEquals': return actual !== expected;
|
||||
case 'isEmpty': return isEmptyValue(actual);
|
||||
case 'isNotEmpty': return !isEmptyValue(actual);
|
||||
case 'contains': return Array.isArray(actual) ? actual.indexOf(expected) >= 0 : String(actual || '').indexOf(String(expected)) >= 0;
|
||||
case 'greaterThan': return Number(actual) > Number(expected);
|
||||
case 'lessThan': return Number(actual) < Number(expected);
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// tslint:disable:no-any max-line-length
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { ILoadedSettingsProvider, ISettingsElement, ITermSetInfo } from '../core/ProviderContracts';
|
||||
import { setPathValue } from '../core/ObjectPath';
|
||||
import { getSettingsProviders } from '../providers/ProviderRegistry';
|
||||
import { PortalSettingsDataService } from '../services/PortalSettingsDataService';
|
||||
import { TaxonomyService } from '../services/TaxonomyService';
|
||||
import { IListOption, IFieldOption } from '../services/PortalSettingsDataService';
|
||||
import { clearElement, createElement } from './Dom';
|
||||
import { DynamicFormRenderer } from './DynamicFormRenderer';
|
||||
|
||||
export class PortalSettingsApp {
|
||||
private readonly dataService: PortalSettingsDataService;
|
||||
private readonly taxonomyService: TaxonomyService;
|
||||
private readonly formRenderer: DynamicFormRenderer = new DynamicFormRenderer();
|
||||
private host: HTMLElement;
|
||||
private contentHost: HTMLElement;
|
||||
private tabsHost: HTMLElement;
|
||||
private statusHost: HTMLElement;
|
||||
private saveButton: HTMLButtonElement;
|
||||
private items: ILoadedSettingsProvider[] = [];
|
||||
private activeKey: string = '';
|
||||
private disposed: boolean = false;
|
||||
|
||||
public constructor(private context: WebPartContext, private configuredTitle?: string) {
|
||||
this.dataService = new PortalSettingsDataService(context);
|
||||
this.taxonomyService = new TaxonomyService(context);
|
||||
}
|
||||
|
||||
public render(host: HTMLElement): void {
|
||||
this.host = host;
|
||||
this.disposed = false;
|
||||
clearElement(host);
|
||||
host.className += ' ps3-root';
|
||||
if (!this.isSiteCollectionAdmin()) {
|
||||
const denied: HTMLElement = createElement('div', 'ps3-accessDenied');
|
||||
denied.appendChild(createElement('h2', '', 'Zugriff nicht möglich'));
|
||||
denied.appendChild(createElement('p', '', 'PortalSettings kann nur von Site-Collection-Administratoren verwendet werden.'));
|
||||
host.appendChild(denied);
|
||||
return;
|
||||
}
|
||||
this.buildShell();
|
||||
this.load();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposed = true;
|
||||
if (this.host) { clearElement(this.host); }
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
private buildShell(): void {
|
||||
const shell: HTMLElement = createElement('div', 'ps3-shell');
|
||||
this.statusHost = createElement('div', 'ps3-statusHost');
|
||||
this.statusHost.setAttribute('aria-live', 'polite');
|
||||
shell.appendChild(this.statusHost);
|
||||
|
||||
const hero: HTMLElement = createElement('header', 'ps3-hero');
|
||||
hero.appendChild(createElement('span', 'ps3-eyebrow', 'PORTAL SETTINGS V3'));
|
||||
hero.appendChild(createElement('h1', 'ps3-title', this.configuredTitle || 'Zentrale Verwaltung der Portal-Erweiterungen'));
|
||||
hero.appendChild(createElement('p', 'ps3-lead', 'Installierte Solutions werden automatisch erkannt. Ihre versionierten Provider erzeugen strukturierte und validierte Einstellungsseiten.'));
|
||||
const meta: HTMLElement = createElement('div', 'ps3-heroMeta');
|
||||
meta.appendChild(this.metric('Site Collection', this.dataService.getSiteUrl()));
|
||||
meta.appendChild(this.metric('Konfigurationsmodell', 'Providerbasiert'));
|
||||
hero.appendChild(meta); shell.appendChild(hero);
|
||||
|
||||
this.tabsHost = createElement('nav', 'ps3-tabs');
|
||||
this.tabsHost.setAttribute('role', 'tablist'); this.tabsHost.setAttribute('aria-label', 'Erweiterungen');
|
||||
shell.appendChild(this.tabsHost);
|
||||
this.contentHost = createElement('main', 'ps3-content'); shell.appendChild(this.contentHost);
|
||||
this.host.appendChild(shell);
|
||||
}
|
||||
|
||||
private load(preferredKey?: string): void {
|
||||
this.showStatus('Installierte Einstellungsanbieter werden geladen …', 'info', true);
|
||||
this.dataService.load(getSettingsProviders()).then((items: ILoadedSettingsProvider[]): void => {
|
||||
if (this.disposed) { return; }
|
||||
this.items = items;
|
||||
this.activeKey = preferredKey && this.findItem(preferredKey) ? preferredKey : (items.length ? items[0].provider.key : '');
|
||||
this.renderTabs(); this.renderActive();
|
||||
this.showStatus(items.length ? items.length + ' Erweiterung(en) wurden erkannt.' : 'Keine unterstützte Erweiterung wurde aktiv registriert.', items.length ? 'success' : 'warning');
|
||||
}).catch((error: Error): void => {
|
||||
if (!this.disposed) { this.showStatus(error.message || String(error), 'danger', true); this.renderEmpty(true); }
|
||||
});
|
||||
}
|
||||
|
||||
private renderTabs(): void {
|
||||
clearElement(this.tabsHost);
|
||||
for (let i: number = 0; i < this.items.length; i++) {
|
||||
const item: ILoadedSettingsProvider = this.items[i];
|
||||
const button: HTMLButtonElement = createElement('button', 'ps3-tab', item.provider.displayName) as HTMLButtonElement;
|
||||
button.type = 'button'; button.setAttribute('role', 'tab');
|
||||
button.setAttribute('aria-selected', String(item.provider.key === this.activeKey));
|
||||
if (item.provider.key === this.activeKey) { button.className += ' is-active'; }
|
||||
if (item.dirty) { button.appendChild(createElement('span', 'ps3-dirtyDot', '●')); }
|
||||
button.addEventListener('click', (): void => {
|
||||
if (this.activeKey !== item.provider.key && this.currentItem() && this.currentItem().dirty &&
|
||||
!window.confirm('Nicht gespeicherte Änderungen verwerfen und den Tab wechseln?')) { return; }
|
||||
this.activeKey = item.provider.key; this.renderTabs(); this.renderActive();
|
||||
});
|
||||
this.tabsHost.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
private renderActive(): void {
|
||||
clearElement(this.contentHost);
|
||||
const item: ILoadedSettingsProvider = this.currentItem();
|
||||
if (!item) { this.renderEmpty(false); return; }
|
||||
const header: HTMLElement = createElement('div', 'ps3-providerHeader');
|
||||
const heading: HTMLElement = createElement('div');
|
||||
heading.appendChild(createElement('span', 'ps3-providerVersion', 'Schema ' + item.provider.schemaVersion));
|
||||
heading.appendChild(createElement('h2', 'ps3-providerTitle', item.provider.displayName));
|
||||
heading.appendChild(createElement('p', 'ps3-providerDescription', item.provider.description));
|
||||
header.appendChild(heading);
|
||||
const storage: HTMLElement = createElement('div', 'ps3-storageBadge',
|
||||
item.provider.storage.kind === 'siteUserCustomAction' ? 'Site-Collection-Action' : 'Root-Web-Standard');
|
||||
header.appendChild(storage); this.contentHost.appendChild(header);
|
||||
|
||||
const form: HTMLElement = createElement('div', 'ps3-form'); this.contentHost.appendChild(form);
|
||||
this.formRenderer.render(form, item, {
|
||||
changed: (): void => { this.updateSaveState(); this.renderTabs(); },
|
||||
selectTermSet: (element: ISettingsElement): void => this.openTermSetPicker(item, element),
|
||||
addBinding: (): void => this.openExpiryBindingDialog(item),
|
||||
editBinding: (binding: any): void => this.openExpiryBindingEditor(item, binding)
|
||||
});
|
||||
const actions: HTMLElement = createElement('div', 'ps3-actionBar');
|
||||
const reset: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Änderungen verwerfen') as HTMLButtonElement;
|
||||
reset.type = 'button'; reset.addEventListener('click', (): void => this.resetCurrent());
|
||||
this.saveButton = createElement('button', 'ps3-button ps3-button-primary', 'Konfiguration speichern') as HTMLButtonElement;
|
||||
this.saveButton.type = 'button'; this.saveButton.addEventListener('click', (): void => this.saveCurrent());
|
||||
actions.appendChild(reset); actions.appendChild(this.saveButton); this.contentHost.appendChild(actions);
|
||||
this.updateSaveState();
|
||||
}
|
||||
|
||||
private renderEmpty(failed: boolean): void {
|
||||
clearElement(this.contentHost);
|
||||
const empty: HTMLElement = createElement('section', 'ps3-empty');
|
||||
empty.appendChild(createElement('h2', '', failed ? 'PortalSettings konnte nicht geladen werden' : 'Keine aktiven Provider gefunden'));
|
||||
empty.appendChild(createElement('p', '', failed
|
||||
? 'Prüfen Sie die Statusmeldung und die REST-Berechtigungen.'
|
||||
: 'MegaMenu und CustomBranding benötigen eine zentrale Site-Collection-Action. ExpiryIndicator wird über seinen Descriptor oder vorhandene Bindungen erkannt.'));
|
||||
this.contentHost.appendChild(empty);
|
||||
}
|
||||
|
||||
private saveCurrent(): void {
|
||||
const item: ILoadedSettingsProvider = this.currentItem();
|
||||
if (!item || !item.dirty) { return; }
|
||||
const errors: string[] = item.provider.validate(item.config);
|
||||
if (errors.length) { this.showStatus(errors.join(' '), 'danger', true); return; }
|
||||
if (!window.confirm('Konfiguration für „' + item.provider.displayName + '“ speichern?')) { return; }
|
||||
this.saveButton.disabled = true; this.showStatus('Konfiguration wird gespeichert …', 'info', true);
|
||||
this.dataService.save(item).then((): void => {
|
||||
this.showStatus('Die Konfiguration wurde erfolgreich gespeichert.', 'success'); this.renderTabs(); this.renderActive();
|
||||
}).catch((error: Error): void => {
|
||||
this.showStatus(error.message || String(error), 'danger', true); this.updateSaveState();
|
||||
});
|
||||
}
|
||||
|
||||
private resetCurrent(): void {
|
||||
const item: ILoadedSettingsProvider = this.currentItem();
|
||||
if (!item || !item.dirty || !window.confirm('Alle nicht gespeicherten Änderungen verwerfen?')) { return; }
|
||||
item.config = item.provider.normalize(item.originalConfig); item.dirty = false; this.renderTabs(); this.renderActive();
|
||||
}
|
||||
|
||||
private openTermSetPicker(item: ILoadedSettingsProvider, definition: ISettingsElement): void {
|
||||
const backdrop: HTMLElement = createElement('div', 'ps3-modalBackdrop');
|
||||
const dialog: HTMLElement = createElement('section', 'ps3-modal');
|
||||
dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); dialog.setAttribute('aria-labelledby', 'ps3-termset-title');
|
||||
dialog.appendChild(createElement('h2', '', 'Termset auswählen')).id = 'ps3-termset-title';
|
||||
const search: HTMLInputElement = createElement('input', 'ps3-input') as HTMLInputElement;
|
||||
search.type = 'search'; search.placeholder = 'Gruppe, Termset oder GUID durchsuchen'; search.setAttribute('aria-label', 'Termsets durchsuchen');
|
||||
dialog.appendChild(search);
|
||||
const results: HTMLElement = createElement('div', 'ps3-pickerResults'); results.appendChild(createElement('p', 'ps3-emptyInline', 'Termsets werden geladen …'));
|
||||
dialog.appendChild(results);
|
||||
const close: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Abbrechen') as HTMLButtonElement;
|
||||
close.type = 'button'; close.addEventListener('click', (): void => { document.body.removeChild(backdrop); }); dialog.appendChild(close);
|
||||
backdrop.appendChild(dialog); document.body.appendChild(backdrop); search.focus();
|
||||
this.taxonomyService.getTermSets().then((termSets: ITermSetInfo[]): void => {
|
||||
const renderResults: () => void = (): void => {
|
||||
clearElement(results); const filter: string = search.value.trim().toLowerCase(); let count: number = 0;
|
||||
for (let i: number = 0; i < termSets.length; i++) {
|
||||
const termSet: ITermSetInfo = termSets[i];
|
||||
if (filter && (termSet.groupName + ' ' + termSet.name + ' ' + termSet.id + ' ' + termSet.description).toLowerCase().indexOf(filter) < 0) { continue; }
|
||||
const row: HTMLElement = createElement('article', 'ps3-pickerItem');
|
||||
const text: HTMLElement = createElement('div'); text.appendChild(createElement('strong', '', termSet.name));
|
||||
text.appendChild(createElement('span', 'ps3-mono', termSet.groupName + ' · ' + termSet.id));
|
||||
const select: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Auswählen') as HTMLButtonElement;
|
||||
select.type = 'button'; select.addEventListener('click', (): void => {
|
||||
setPathValue(item.config, definition.path || '', termSet.id);
|
||||
setPathValue(item.config, definition.companionNamePath || '', termSet.name);
|
||||
item.dirty = true; document.body.removeChild(backdrop); this.renderTabs(); this.renderActive();
|
||||
});
|
||||
row.appendChild(text); row.appendChild(select); results.appendChild(row); count++;
|
||||
}
|
||||
if (!count) { results.appendChild(createElement('p', 'ps3-emptyInline', 'Keine passenden Termsets gefunden.')); }
|
||||
};
|
||||
search.addEventListener('input', renderResults); renderResults();
|
||||
}).catch((error: Error): void => { clearElement(results); results.appendChild(createElement('p', 'ps3-errorText', error.message || String(error))); });
|
||||
}
|
||||
|
||||
private updateSaveState(): void {
|
||||
const item: ILoadedSettingsProvider = this.currentItem();
|
||||
if (this.saveButton) { this.saveButton.disabled = !item || !item.dirty; }
|
||||
}
|
||||
|
||||
private openExpiryBindingDialog(item: ILoadedSettingsProvider): void {
|
||||
const backdrop: HTMLElement = createElement('div', 'ps3-modalBackdrop');
|
||||
const dialog: HTMLElement = createElement('section', 'ps3-modal');
|
||||
dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true');
|
||||
dialog.appendChild(createElement('h2', '', 'ExpiryIndicator-Bindung hinzufügen'));
|
||||
dialog.appendChild(createElement('p', 'ps3-providerDescription', 'Wählen Sie ein Web, eine Liste oder Bibliothek und ein vorhandenes Datumsfeld aus. Die Solution legt keine Spalte an.'));
|
||||
const webLabel: HTMLLabelElement = createElement('label', 'ps3-label', 'Web-URL') as HTMLLabelElement;
|
||||
const webInput: HTMLInputElement = createElement('input', 'ps3-input') as HTMLInputElement;
|
||||
webInput.value = this.context.pageContext.web.absoluteUrl; webLabel.htmlFor = 'ps3-bind-web'; webInput.id = 'ps3-bind-web';
|
||||
const loadLists: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Listen laden') as HTMLButtonElement;
|
||||
loadLists.type = 'button';
|
||||
const listLabel: HTMLLabelElement = createElement('label', 'ps3-label', 'Liste oder Bibliothek') as HTMLLabelElement;
|
||||
const listSelect: HTMLSelectElement = createElement('select', 'ps3-input') as HTMLSelectElement; listSelect.id = 'ps3-bind-list'; listLabel.htmlFor = listSelect.id;
|
||||
const fieldLabel: HTMLLabelElement = createElement('label', 'ps3-label', 'Ablaufdatumsfeld') as HTMLLabelElement;
|
||||
const fieldSelect: HTMLSelectElement = createElement('select', 'ps3-input') as HTMLSelectElement; fieldSelect.id = 'ps3-bind-field'; fieldLabel.htmlFor = fieldSelect.id;
|
||||
const status: HTMLElement = createElement('div', 'ps3-dialogStatus'); status.setAttribute('aria-live', 'polite');
|
||||
const actions: HTMLElement = createElement('div', 'ps3-dialogActions');
|
||||
const cancel: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Abbrechen') as HTMLButtonElement;
|
||||
const bind: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-primary', 'Feld anbinden') as HTMLButtonElement;
|
||||
cancel.type = 'button'; bind.type = 'button'; bind.disabled = true;
|
||||
actions.appendChild(cancel); actions.appendChild(bind);
|
||||
dialog.appendChild(webLabel); const webRow: HTMLElement = createElement('div', 'ps3-controlRow'); webRow.appendChild(webInput); webRow.appendChild(loadLists); dialog.appendChild(webRow);
|
||||
dialog.appendChild(listLabel); dialog.appendChild(listSelect); dialog.appendChild(fieldLabel); dialog.appendChild(fieldSelect); dialog.appendChild(status); dialog.appendChild(actions);
|
||||
backdrop.appendChild(dialog); document.body.appendChild(backdrop);
|
||||
const closeDialog: () => void = (): void => { if (backdrop.parentNode) { backdrop.parentNode.removeChild(backdrop); } };
|
||||
cancel.addEventListener('click', closeDialog);
|
||||
const setOptions: (select: HTMLSelectElement, options: Array<{ id: string; title: string }>) => void =
|
||||
(select: HTMLSelectElement, options: Array<{ id: string; title: string }>): void => {
|
||||
clearElement(select); const empty: HTMLOptionElement = createElement('option') as HTMLOptionElement; empty.value = ''; empty.textContent = 'Bitte auswählen'; select.appendChild(empty);
|
||||
options.forEach((option: { id: string; title: string }): void => {
|
||||
const element: HTMLOptionElement = createElement('option') as HTMLOptionElement; element.value = option.id; element.textContent = option.title; select.appendChild(element);
|
||||
});
|
||||
};
|
||||
const loadWebLists: () => void = (): void => {
|
||||
status.textContent = 'Listen werden geladen …'; bind.disabled = true; setOptions(listSelect, []); setOptions(fieldSelect, []);
|
||||
this.dataService.getLists(webInput.value).then((lists: IListOption[]): void => {
|
||||
setOptions(listSelect, lists.map((list: IListOption): { id: string; title: string } => ({ id: list.id, title: list.title })));
|
||||
status.textContent = lists.length ? '' : 'Keine Listen oder Bibliotheken gefunden.';
|
||||
}).catch((error: Error): void => { status.textContent = error.message || String(error); });
|
||||
};
|
||||
loadLists.addEventListener('click', loadWebLists);
|
||||
listSelect.addEventListener('change', (): void => {
|
||||
bind.disabled = true; setOptions(fieldSelect, []); if (!listSelect.value) { return; }
|
||||
status.textContent = 'Datumsfelder werden geladen …';
|
||||
this.dataService.getDateFields(webInput.value, listSelect.value).then((fields: IFieldOption[]): void => {
|
||||
setOptions(fieldSelect, fields.map((field: IFieldOption): { id: string; title: string } => ({ id: field.id, title: field.title + ' (' + field.internalName + ')' })));
|
||||
(fieldSelect as any)._ps3Fields = fields; status.textContent = fields.length ? '' : 'Keine verwendbaren Datumsfelder gefunden.';
|
||||
}).catch((error: Error): void => { status.textContent = error.message || String(error); });
|
||||
});
|
||||
fieldSelect.addEventListener('change', (): void => { bind.disabled = !fieldSelect.value; });
|
||||
bind.addEventListener('click', (): void => {
|
||||
const lists: HTMLOptionElement = listSelect.options[listSelect.selectedIndex];
|
||||
const fields: IFieldOption[] = (fieldSelect as any)._ps3Fields || [];
|
||||
let selectedField: IFieldOption;
|
||||
for (let i: number = 0; i < fields.length; i++) { if (fields[i].id === fieldSelect.value) { selectedField = fields[i]; break; } }
|
||||
if (!selectedField) { return; }
|
||||
bind.disabled = true; status.textContent = 'Feld wird angebunden …';
|
||||
this.dataService.bindExpiryField(webInput.value, { id: listSelect.value, title: lists.text }, selectedField, item.context.bindings || [])
|
||||
.then((bindings: any[]): void => {
|
||||
item.context.bindings = bindings; closeDialog(); this.renderActive(); this.showStatus('ExpiryIndicator wurde erfolgreich an das Feld gebunden.', 'success');
|
||||
}).catch((error: Error): void => { bind.disabled = false; status.textContent = error.message || String(error); });
|
||||
});
|
||||
loadWebLists();
|
||||
}
|
||||
|
||||
private openExpiryBindingEditor(siteItem: ILoadedSettingsProvider, binding: any): void {
|
||||
const backdrop: HTMLElement = createElement('div', 'ps3-modalBackdrop');
|
||||
const dialog: HTMLElement = createElement('section', 'ps3-modal ps3-modal-large');
|
||||
dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true');
|
||||
dialog.appendChild(createElement('h2', '', 'Lokale ExpiryIndicator-Konfiguration'));
|
||||
dialog.appendChild(createElement('p', 'ps3-providerDescription', (binding.listTitle || binding.listId) + ' · ' + (binding.fieldInternalName || '')));
|
||||
const status: HTMLElement = createElement('div', 'ps3-dialogStatus'); status.textContent = 'Konfiguration wird geladen …'; status.setAttribute('aria-live', 'polite');
|
||||
const form: HTMLElement = createElement('div', 'ps3-form'); const actions: HTMLElement = createElement('div', 'ps3-dialogActions');
|
||||
dialog.appendChild(status); dialog.appendChild(form); dialog.appendChild(actions); backdrop.appendChild(dialog); document.body.appendChild(backdrop);
|
||||
const closeDialog: () => void = (): void => { if (backdrop.parentNode) { backdrop.parentNode.removeChild(backdrop); } };
|
||||
this.dataService.loadExpiryBinding(binding, siteItem.config).then((loaded: any): void => {
|
||||
status.textContent = loaded.inherited ? 'Die Bindung erbt derzeit den Site-Collection-Standard.' : 'Die Bindung verwendet eine lokale Ausnahme.';
|
||||
const provider: any = {}; Object.keys(siteItem.provider).forEach((key: string): void => { provider[key] = (siteItem.provider as any)[key]; });
|
||||
provider.sections = siteItem.provider.sections.filter(section => section.key === 'dates' || section.key === 'rules' || section.key === 'preview');
|
||||
const editItem: ILoadedSettingsProvider = {
|
||||
provider: provider, config: provider.normalize(loaded.config), originalConfig: loaded.config,
|
||||
context: {}, dirty: false
|
||||
};
|
||||
const editor: DynamicFormRenderer = new DynamicFormRenderer();
|
||||
editor.render(form, editItem, {
|
||||
changed: (): void => { saveLocal.disabled = false; },
|
||||
selectTermSet: (): void => undefined,
|
||||
addBinding: (): void => undefined,
|
||||
editBinding: (): void => undefined
|
||||
});
|
||||
const cancel: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Abbrechen') as HTMLButtonElement;
|
||||
const inherit: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Site-Standard erben') as HTMLButtonElement;
|
||||
const saveLocal: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-primary', 'Als lokale Ausnahme speichern') as HTMLButtonElement;
|
||||
cancel.type = 'button'; inherit.type = 'button'; saveLocal.type = 'button'; saveLocal.disabled = !editItem.dirty;
|
||||
cancel.addEventListener('click', closeDialog);
|
||||
inherit.addEventListener('click', (): void => {
|
||||
inherit.disabled = true; saveLocal.disabled = true; status.textContent = 'Vererbung wird aktiviert …';
|
||||
this.dataService.saveExpiryBinding(binding, loaded.field, editItem.config, true).then((): void => {
|
||||
closeDialog(); this.showStatus('Die Bindung erbt jetzt den Site-Collection-Standard.', 'success');
|
||||
}).catch((error: Error): void => { inherit.disabled = false; saveLocal.disabled = false; status.textContent = error.message || String(error); });
|
||||
});
|
||||
saveLocal.addEventListener('click', (): void => {
|
||||
const errors: string[] = provider.validate(editItem.config); if (errors.length) { status.textContent = errors.join(' '); return; }
|
||||
inherit.disabled = true; saveLocal.disabled = true; status.textContent = 'Lokale Ausnahme wird gespeichert …';
|
||||
this.dataService.saveExpiryBinding(binding, loaded.field, editItem.config, false).then((): void => {
|
||||
closeDialog(); this.showStatus('Die lokale ExpiryIndicator-Ausnahme wurde gespeichert.', 'success');
|
||||
}).catch((error: Error): void => { inherit.disabled = false; saveLocal.disabled = false; status.textContent = error.message || String(error); });
|
||||
});
|
||||
actions.appendChild(cancel); actions.appendChild(inherit); actions.appendChild(saveLocal);
|
||||
}).catch((error: Error): void => {
|
||||
status.textContent = error.message || String(error);
|
||||
const close: HTMLButtonElement = createElement('button', 'ps3-button ps3-button-secondary', 'Schließen') as HTMLButtonElement;
|
||||
close.type = 'button'; close.addEventListener('click', closeDialog); actions.appendChild(close);
|
||||
});
|
||||
}
|
||||
|
||||
private showStatus(message: string, appearance: string, persistent?: boolean): void {
|
||||
if (!this.statusHost) { return; }
|
||||
clearElement(this.statusHost);
|
||||
const status: HTMLElement = createElement('div', 'ps3-status ps3-status-' + appearance, message);
|
||||
status.setAttribute('role', appearance === 'danger' ? 'alert' : 'status'); this.statusHost.appendChild(status);
|
||||
if (!persistent) { window.setTimeout((): void => { if (!this.disposed && this.statusHost.contains(status)) { clearElement(this.statusHost); } }, 5000); }
|
||||
}
|
||||
|
||||
private metric(label: string, value: string): HTMLElement {
|
||||
const metric: HTMLElement = createElement('div', 'ps3-heroMetric'); metric.appendChild(createElement('span', '', label)); metric.appendChild(createElement('strong', '', value)); return metric;
|
||||
}
|
||||
|
||||
private currentItem(): ILoadedSettingsProvider { return this.findItem(this.activeKey); }
|
||||
private findItem(key: string): ILoadedSettingsProvider {
|
||||
for (let i: number = 0; i < this.items.length; i++) { if (this.items[i].provider.key === key) { return this.items[i]; } }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private isSiteCollectionAdmin(): boolean {
|
||||
const legacy: any = this.context.pageContext.legacyPageContext;
|
||||
return legacy && legacy.isSiteAdmin === true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
$white: "[theme: white, default: #ffffff]";
|
||||
$neutral-lighter-alt: "[theme: neutralLighterAlt, default: #f8f8f8]";
|
||||
$neutral-lighter: "[theme: neutralLighter, default: #f4f4f4]";
|
||||
$neutral-light: "[theme: neutralLight, default: #eaeaea]";
|
||||
$neutral-tertiary: "[theme: neutralTertiary, default: #a6a6a6]";
|
||||
$neutral-secondary: "[theme: neutralSecondary, default: #666666]";
|
||||
$neutral-primary: "[theme: neutralPrimary, default: #333333]";
|
||||
$theme-lighter-alt: "[theme: themeLighterAlt, default: #eff6fc]";
|
||||
$theme-lighter: "[theme: themeLighter, default: #deecf9]";
|
||||
$theme-light: "[theme: themeLight, default: #c7e0f4]";
|
||||
$theme-primary: "[theme: themePrimary, default: #0078d7]";
|
||||
$theme-dark: "[theme: themeDark, default: #005a9e]";
|
||||
$red-dark: "[theme: redDark, default: #a80000]";
|
||||
$green: "[theme: green, default: #107c10]";
|
||||
|
||||
.ps3-root {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: $neutral-lighter-alt;
|
||||
color: $neutral-primary;
|
||||
font-family: 'Segoe UI', 'Segoe UI Web (West European)', sans-serif;
|
||||
}
|
||||
|
||||
.ps3-root *, .ps3-root *::before, .ps3-root *::after { box-sizing: border-box; }
|
||||
.ps3-shell { width: 100%; max-width: 1280px; margin: 0 auto; padding: 24px; }
|
||||
.ps3-statusHost { min-height: 50px; }
|
||||
.ps3-status { margin-bottom: 14px; padding: 12px 16px; border: 1px solid $neutral-light; border-radius: 4px; background: $white; }
|
||||
.ps3-status-info { border-color: $theme-light; background: $theme-lighter-alt; color: $theme-dark; }
|
||||
.ps3-status-success { border-color: $green; color: $green; }
|
||||
.ps3-status-warning { border-color: #ca5010; background: #fff4ce; color: #8a3707; }
|
||||
.ps3-status-danger { border-color: $red-dark; background: #fde7e9; color: $red-dark; }
|
||||
|
||||
.ps3-hero {
|
||||
padding: 28px 30px;
|
||||
border-radius: 8px;
|
||||
background: $theme-primary;
|
||||
color: $white;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, .12);
|
||||
}
|
||||
.ps3-eyebrow { display: block; margin-bottom: 8px; font-size: 11px; font-weight: 700; letter-spacing: .16em; }
|
||||
.ps3-title { margin: 0; font-size: 30px; font-weight: 300; line-height: 1.2; }
|
||||
.ps3-lead { max-width: 850px; margin: 12px 0 0; font-size: 15px; line-height: 1.6; }
|
||||
.ps3-heroMeta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 20px; }
|
||||
.ps3-heroMetric { min-width: 210px; padding: 10px 14px; border: 1px solid rgba(255,255,255,.32); border-radius: 4px; background: rgba(255,255,255,.1); }
|
||||
.ps3-heroMetric span, .ps3-heroMetric strong { display: block; }
|
||||
.ps3-heroMetric span { margin-bottom: 3px; font-size: 11px; opacity: .85; text-transform: uppercase; }
|
||||
.ps3-heroMetric strong { font-size: 13px; word-break: break-word; }
|
||||
|
||||
.ps3-tabs { display: flex; gap: 4px; margin: 22px 0 0; padding: 0 4px; border-bottom: 1px solid $neutral-light; }
|
||||
.ps3-tab { position: relative; min-height: 44px; padding: 0 18px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: $neutral-secondary; cursor: pointer; font: inherit; font-weight: 600; }
|
||||
.ps3-tab:hover { background: $neutral-lighter; color: $neutral-primary; }
|
||||
.ps3-tab.is-active { border-bottom-color: $theme-primary; color: $theme-primary; }
|
||||
.ps3-tab:focus { outline: 2px solid $theme-primary; outline-offset: -2px; }
|
||||
.ps3-dirtyDot { margin-left: 8px; color: #ca5010; font-size: 9px; }
|
||||
|
||||
.ps3-content { padding-top: 18px; }
|
||||
.ps3-providerHeader { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 16px; padding: 20px 22px; border: 1px solid $neutral-light; border-radius: 6px; background: $white; }
|
||||
.ps3-providerVersion { color: $theme-primary; font-size: 11px; font-weight: 700; text-transform: uppercase; }
|
||||
.ps3-providerTitle { margin: 3px 0 5px; font-size: 24px; font-weight: 400; }
|
||||
.ps3-providerDescription { margin: 0; color: $neutral-secondary; line-height: 1.5; }
|
||||
.ps3-storageBadge { flex: 0 0 auto; padding: 6px 10px; border-radius: 12px; background: $theme-lighter-alt; color: $theme-dark; font-size: 12px; font-weight: 600; }
|
||||
|
||||
.ps3-form { display: grid; gap: 16px; }
|
||||
.ps3-section { display: block; margin: 0; padding: 0; border: 1px solid $neutral-light; border-radius: 6px; background: $white; box-shadow: 0 2px 8px rgba(0,0,0,.04); }
|
||||
.ps3-sectionHeader { display: block; padding: 18px 20px 14px; border-bottom: 1px solid $neutral-light; }
|
||||
details.ps3-section > summary { cursor: pointer; }
|
||||
details.ps3-section > summary:focus { outline: 2px solid $theme-primary; outline-offset: -2px; }
|
||||
.ps3-sectionTitle { margin: 0; font-size: 18px; font-weight: 600; }
|
||||
.ps3-sectionDescription { margin: 5px 0 0; color: $neutral-secondary; line-height: 1.5; }
|
||||
.ps3-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 18px; padding: 20px; }
|
||||
.ps3-span-1 { grid-column: span 1; } .ps3-span-2 { grid-column: span 2; } .ps3-span-3 { grid-column: span 3; }
|
||||
.ps3-span-4 { grid-column: span 4; } .ps3-span-6 { grid-column: span 6; } .ps3-span-8 { grid-column: span 8; } .ps3-span-12 { grid-column: span 12; }
|
||||
.ps3-element { min-width: 0; }
|
||||
.ps3-elementHeading { margin: 0; font-size: 15px; font-weight: 600; }
|
||||
.ps3-label { display: block; margin: 0 0 7px; font-size: 13px; font-weight: 600; }
|
||||
.ps3-required { color: $red-dark; }
|
||||
.ps3-description { display: block; margin: 6px 0 0; color: $neutral-secondary; font-size: 12px; line-height: 1.5; }
|
||||
.ps3-controlRow { display: flex; align-items: stretch; }
|
||||
.ps3-input, .ps3-textarea { width: 100%; border: 1px solid $neutral-tertiary; border-radius: 2px; background: $white; color: $neutral-primary; font: inherit; font-size: 14px; }
|
||||
.ps3-input { height: 36px; padding: 0 10px; }
|
||||
.ps3-textarea { min-height: 180px; padding: 10px; resize: vertical; line-height: 1.5; }
|
||||
.ps3-listEditor { min-height: 120px; }
|
||||
.ps3-input:focus, .ps3-textarea:focus { border-color: $theme-primary; outline: 1px solid $theme-primary; }
|
||||
.ps3-textarea[aria-invalid='true'] { border-color: $red-dark; }
|
||||
.ps3-suffix { display: flex; align-items: center; padding: 0 10px; border: 1px solid $neutral-tertiary; border-left: 0; background: $neutral-lighter; font-size: 12px; }
|
||||
.ps3-code { font-family: Consolas, 'Courier New', monospace; font-size: 12px; }
|
||||
.ps3-fieldError, .ps3-errorText { margin-top: 5px; color: $red-dark; font-size: 12px; }
|
||||
.ps3-divider { height: 1px; align-self: center; background: $neutral-light; }
|
||||
|
||||
.ps3-notice { padding: 13px 15px; border-left: 4px solid $theme-primary; background: $theme-lighter-alt; }
|
||||
.ps3-notice-warning { border-left-color: #ca5010; background: #fff4ce; }
|
||||
.ps3-notice-danger { border-left-color: $red-dark; background: #fde7e9; }
|
||||
.ps3-notice-success { border-left-color: $green; background: #dff6dd; }
|
||||
.ps3-noticeTitle { display: block; margin-bottom: 3px; }
|
||||
.ps3-noticeText { margin: 0; line-height: 1.5; }
|
||||
|
||||
.ps3-toggle { display: grid; grid-template-columns: 42px minmax(0,1fr); gap: 10px; cursor: pointer; }
|
||||
.ps3-toggle input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.ps3-toggleVisual { position: relative; width: 40px; height: 20px; margin-top: 2px; border: 1px solid $neutral-secondary; border-radius: 10px; background: $white; }
|
||||
.ps3-toggleVisual::after { content: ''; position: absolute; top: 3px; left: 3px; width: 12px; height: 12px; border-radius: 50%; background: $neutral-secondary; transition: left .12s ease; }
|
||||
.ps3-toggle input:checked + .ps3-toggleVisual { border-color: $theme-primary; background: $theme-primary; }
|
||||
.ps3-toggle input:checked + .ps3-toggleVisual::after { left: 23px; background: $white; }
|
||||
.ps3-toggle input:focus + .ps3-toggleVisual { outline: 2px solid $theme-primary; outline-offset: 2px; }
|
||||
.ps3-toggleContent .ps3-description { margin-top: 2px; }
|
||||
|
||||
.ps3-choiceCards { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
|
||||
.ps3-choiceCard { position: relative; display: block; min-height: 90px; padding: 15px; border: 1px solid $neutral-light; border-radius: 4px; cursor: pointer; }
|
||||
.ps3-choiceCard:hover { border-color: $theme-primary; background: $theme-lighter-alt; }
|
||||
.ps3-choiceCard input { position: absolute; top: 14px; right: 14px; }
|
||||
.ps3-choiceCard.is-selected { border: 2px solid $theme-primary; padding: 14px; background: $theme-lighter-alt; }
|
||||
.ps3-choiceTitle, .ps3-choiceContent { display: block; }
|
||||
|
||||
.ps3-selectedItem, .ps3-pickerItem { display: flex; justify-content: space-between; align-items: center; gap: 14px; padding: 14px; border: 1px solid $neutral-light; border-radius: 4px; background: $neutral-lighter-alt; }
|
||||
.ps3-selectedTitle, .ps3-mono { display: block; }
|
||||
.ps3-mono { margin-top: 3px; color: $neutral-secondary; font-family: Consolas, 'Courier New', monospace; font-size: 11px; word-break: break-all; }
|
||||
.ps3-bindingList, .ps3-pickerResults { display: grid; gap: 8px; }
|
||||
.ps3-bindingHeader { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.ps3-binding { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid $neutral-light; border-radius: 4px; }
|
||||
.ps3-bindingTitle { display: block; }
|
||||
.ps3-emptyInline { margin: 0; padding: 14px; color: $neutral-secondary; text-align: center; }
|
||||
|
||||
.ps3-preview { border: 1px solid $neutral-light; border-radius: 4px; }
|
||||
.ps3-previewTitle { padding: 12px 14px; cursor: pointer; font-weight: 600; }
|
||||
.ps3-previewCode { max-height: 440px; margin: 0; padding: 14px; overflow: auto; border-top: 1px solid $neutral-light; background: $neutral-lighter-alt; white-space: pre-wrap; }
|
||||
.ps3-summaryTable { width: 100%; border-collapse: collapse; }
|
||||
.ps3-summaryTable td { padding: 8px 10px; border-bottom: 1px solid $neutral-light; word-break: break-word; }
|
||||
.ps3-summaryTable td:first-child { width: 28%; color: $neutral-secondary; font-weight: 600; }
|
||||
|
||||
.ps3-actionBar { position: sticky; bottom: 12px; z-index: 3; display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; padding: 14px; border: 1px solid $neutral-light; border-radius: 6px; background: rgba(255,255,255,.96); box-shadow: 0 4px 16px rgba(0,0,0,.12); }
|
||||
.ps3-button { min-height: 36px; padding: 0 16px; border: 1px solid transparent; border-radius: 2px; cursor: pointer; font: inherit; font-size: 14px; font-weight: 600; }
|
||||
.ps3-button-primary { border-color: $theme-primary; background: $theme-primary; color: $white; }
|
||||
.ps3-button-primary:hover { border-color: $theme-dark; background: $theme-dark; }
|
||||
.ps3-button-secondary { border-color: $neutral-tertiary; background: $white; color: $neutral-primary; }
|
||||
.ps3-button-secondary:hover { border-color: $theme-primary; color: $theme-primary; }
|
||||
.ps3-button:focus { outline: 2px solid $theme-primary; outline-offset: 2px; }
|
||||
.ps3-button:disabled { cursor: default; opacity: .45; }
|
||||
|
||||
.ps3-modalBackdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 100000; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(0,0,0,.48); }
|
||||
.ps3-modal { width: 100%; max-width: 760px; max-height: calc(100vh - 48px); padding: 22px; overflow: auto; border-radius: 6px; background: $white; color: $neutral-primary; box-shadow: 0 12px 40px rgba(0,0,0,.3); }
|
||||
.ps3-modal-large { max-width: 1120px; }
|
||||
.ps3-modal h2 { margin: 0 0 14px; }
|
||||
.ps3-modal > .ps3-label { margin-top: 14px; }
|
||||
.ps3-dialogStatus { min-height: 42px; padding: 10px 0; color: $neutral-secondary; }
|
||||
.ps3-dialogActions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; }
|
||||
.ps3-pickerResults { max-height: 460px; margin: 14px 0; overflow: auto; }
|
||||
.ps3-empty, .ps3-accessDenied { padding: 28px; border: 1px solid $neutral-light; border-radius: 6px; background: $white; text-align: center; }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.ps3-shell { padding: 12px; }
|
||||
.ps3-hero { padding: 22px 18px; }
|
||||
.ps3-title { font-size: 25px; }
|
||||
.ps3-tabs { flex-direction: column; border-bottom: 0; }
|
||||
.ps3-tab { width: 100%; border-bottom: 0; border-left: 3px solid transparent; text-align: left; }
|
||||
.ps3-tab.is-active { border-left-color: $theme-primary; }
|
||||
.ps3-span-1, .ps3-span-2, .ps3-span-3, .ps3-span-4, .ps3-span-6, .ps3-span-8 { grid-column: span 12; }
|
||||
.ps3-choiceCards { grid-template-columns: 1fr; }
|
||||
.ps3-providerHeader { display: block; }
|
||||
.ps3-storageBadge { display: inline-block; margin-top: 12px; }
|
||||
.ps3-actionBar { display: grid; }
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.ps3-hero, .ps3-button-primary, .ps3-toggle input:checked + .ps3-toggleVisual { border: 1px solid CanvasText; background: Canvas; color: CanvasText; }
|
||||
.ps3-section, .ps3-providerHeader, .ps3-actionBar { border: 1px solid CanvasText; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ps3-toggleVisual::after { transition: none; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "c41d05ec-0682-4af0-b807-649806ed56b3",
|
||||
"alias": "PortalSettingsWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"preconfiguredEntries": [
|
||||
{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Portal Settings", "de-de": "Portal-Einstellungen" },
|
||||
"description": {
|
||||
"default": "Central administration for installed portal extensions.",
|
||||
"de-de": "Zentrale Verwaltung installierter Portal-Erweiterungen."
|
||||
},
|
||||
"officeFabricIconFontName": "Settings",
|
||||
"properties": {
|
||||
"title": "Zentrale Verwaltung der Portal-Erweiterungen"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'PortalSettingsWebPartStrings';
|
||||
import { PortalSettingsApp } from '../../ui/PortalSettingsApp';
|
||||
import './PortalSettings.scss';
|
||||
|
||||
export interface IPortalSettingsWebPartProperties {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export default class PortalSettingsWebPart extends BaseClientSideWebPart<IPortalSettingsWebPartProperties> {
|
||||
private app: PortalSettingsApp;
|
||||
|
||||
public render(): void {
|
||||
if (this.app) { this.app.dispose(); }
|
||||
this.app = new PortalSettingsApp(this.context, this.properties.title);
|
||||
this.app.render(this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
if (this.app) { this.app.dispose(); this.app = undefined; }
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [{
|
||||
header: { description: strings.PropertyPaneDescription },
|
||||
groups: [{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [PropertyPaneTextField('title', { label: strings.TitleFieldLabel })]
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Portal-Einstellungen",
|
||||
"BasicGroupName": "Allgemein",
|
||||
"TitleFieldLabel": "Titel"
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Portal Settings",
|
||||
"BasicGroupName": "General",
|
||||
"TitleFieldLabel": "Title"
|
||||
};
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
declare interface IPortalSettingsWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
TitleFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'PortalSettingsWebPartStrings' {
|
||||
const strings: IPortalSettingsWebPartStrings;
|
||||
export = strings;
|
||||
}
|
||||
Reference in New Issue
Block a user