domma-cms 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{api as h}from"../api.js";import{isPathAllowed as J}from"./admin-scope.js";import{colourToCss as U,applyFloat as Y,applyHighlight as Q,itemHighlightAttrs as ee}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{placeTools as te,ensureLockedTools as ne,managePluginsItem as se,stripItemByUrl as ie,insertFoldersBeforeSystem as ae,pruneEmptyToolsFolder as oe,sidebarStateKey as re,canAny as B,filterByPermission as le,MANAGE_PLUGINS_URL as z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as ce,canArrange as de,startArrange as ue}from"./sidebar-arrange.js";import{collapseButtonMarkup as me,wireSidebarSize as fe,COLLAPSED_WIDTH as ge}from"./sidebar-size.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const pe=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function he(e){if(!B(e,"projects"))return[];let s=[];try{s=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const i=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(t=>{const d="#/projects/"+encodeURIComponent(t.slug),c=t.counts||{},o=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const r of i){const l=Number(c[r.key])||0;l<=0||o.push({text:`${r.text} (${l})`,url:d+r.path,icon:r.icon})}o.push({text:"Settings",url:d+"/settings",icon:"settings"});const m=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:o,...m&&{colour:"neutral",badge:{text:"off"}}}})}function be(e,s){for(const i of e)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...s);return}}function O(e,s){const i=D.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?E.set(e,s.details):E.delete(e);const n=!s.inventory&&A.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${p(e)}"${n} data-badge-label="${p(t)}" aria-label="${p(t)}">${p(String(s.count))}</span>`}function W(e){for(const s of e.querySelectorAll("details")){const i=s.querySelector(":scope > summary");if(!i)continue;let t=0,n=!1;const d=[];for(const l of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const u=Number(l.textContent)||0;u&&(t+=u,n||(n=l.classList.contains("sidebar-badge--danger")),d.push({text:l.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:l.dataset.badgeLabel,tone:l.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const c=`folder:${s.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!t){o?.remove(),E.delete(c);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const m=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",r=d.map(l=>`${l.text}: ${l.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,o.dataset.countFor=c,o.dataset.badgeLabel=r,o.setAttribute("aria-label",r),o.textContent=String(t),E.set(c,{title:m,items:d})}}const E=new Map;function ye(e,s){if(!e.url||!s)return"";const i=s[e.url];return!i||!(i.count>0)&&!i.showZero?"":O(e.url,i)}const D=["info","attention","danger","live"],ve={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function _(e,{fallbackLabel:s="",tone:i="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const n=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),d={count:Math.round(t),label:n,tone:D.includes(e?.tone)?e.tone:i};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function G(e,s,i=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",n=e.icon?`<span data-icon="${p(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${p(t)}</span>`,d=p(e.text||""),c=e.url?p(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,m=e.colour?U(e.colour):"",r=ee(e),l=[r.hover&&` data-menu-item-hover="${p(r.hover)}"`,r.active&&` data-menu-item-active="${p(r.active)}"`].filter(Boolean).join(""),u=[m&&`color:${p(m)}`,r.accent&&`--dm-menu-highlight:${p(r.accent)}`].filter(Boolean).join(";"),y=u?` style="${u}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?U(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${p(b)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(e.badge.text))}</span>`}if(o){const b=re(e,i),C=S.get(b)!==!1,L=e.items.map(j=>G(j,s,b)).join("");return`<details data-state-key="${p(b)}"${C?" open":""}>
2
- <summary${y}>${n} <span class="sidebar-text">${d}</span>${v}</summary>
3
- <div class="sidebar-children">${L}</div>
4
- </details>`}return`<a href="${c}" class="sidebar-link" data-url="${c}"${y}${l}>${n} <span class="sidebar-text">${d}</span>${ye(e,s)}${v}</a>`}async function xe(e=[],s=[],i=null){const t=g=>g.then(a=>Array.isArray(a)?a.length:Array.isArray(a?.entries)?a.entries.length:0).catch(()=>0);A=Ce(e,i);const n=new Set(A.map(g=>g.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},c=(g,a)=>B(s,g)&&!n.has(d[g])?a():Promise.resolve(0),[o,m,r,l,u,y,v,b,C,L,j,F,N,P]=await Promise.all([c("pages",()=>t(h.pages.list())),c("media",()=>t(h.media.list())),c("menus",()=>t(h.menus.list())),c("context-menus",()=>t(h.contextMenus.list())),c("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),c("collections",()=>t(h.collections.list())),c("forms",()=>t(h.forms.list())),c("views",()=>t(h.views.list())),c("actions",()=>t(h.actions.list())),c("blocks",()=>t(h.blocks.list())),c("components",()=>t(h.components.list())),c("users",()=>t(h.users.list())),c("plugins",()=>t(h.plugins.list())),c("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),q={};await Promise.all(A.map(async g=>{const a=await K(g);a&&(q[g.url]=a)}));const T={"#/pages":o,"#/media":m,"#/menus":r,"#/context-menus":l,"#/theme":u,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":L,"#/components":j,"#/users":F,"#/plugins":N,"#/system/notifications":P},k={};for(const[g,a]of Object.entries(T)){if(n.has(g))continue;const[x,f]=ve[g]??["item","items"],w=_(a);w&&(k[g]={...w,label:`${w.count} ${w.count===1?x:f}`})}return{...k,...q}}let A=[];const $e="dm:sidebar-count",Se=6e4;function Ce(e=[],s=null){const i=[],t=n=>{if(n&&!(s&&typeof n.url=="string"&&n.url.startsWith("#/")&&!J(s,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&i.push({url:n.url,countUrl:n.countUrl,text:n.text||"",countLabel:n.countLabel||"",inventory:n.inventory===!0});for(const d of[...n.children||[],...n.items||[]])t(d)}};for(const n of e??[])t(n?.item??n);return i}async function K(e){try{const s=new Date,i=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${i}`;return _(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,s,i){const t=[...e.querySelectorAll("a.sidebar-link")].find(d=>d.dataset.url===s);if(!t)return;const n=t.querySelector(".sidebar-badge[data-count-for]");if(!i||!(i.count>0)&&!i.showZero)n?.remove();else{const d=document.createRange();d.selectNodeContents(t);const c=d.createContextualFragment(O(s,i)).firstElementChild;n?n.replaceWith(c):t.querySelector(".sidebar-text")?.after(c)}R.set(s,i),W(e)}const R=new Map;async function I(e,s=null){const i=s?A.filter(t=>t.url===s):A;await Promise.all(i.map(async t=>{const n=await K(t),d=R.get(t.url);JSON.stringify(d??null)!==JSON.stringify(n)&&V(e,t.url,n)}))}function we(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener($e,s=>{const i=s.detail||{};if(i.url)if(i.count!==void 0){const t=A.find(n=>n.url===i.url);V(e,i.url,_(i,{fallbackLabel:t?.countLabel||t?.text||""}))}else I(e,i.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&I(e)},Se),document.addEventListener("visibilitychange",()=>{document.hidden||I(e)}),ke(e))}function Z(e,s,i){const t=(c,o,m)=>{const r=document.createElement(c);return o&&(r.className=o),m!=null&&(r.textContent=String(m)),r};if(!i){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",i.title||s)),(i.rows?.length||!i.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const n=Array.isArray(i.rows)?i.rows.slice(0,6):[];if(n.length){const c=t("dl","dm-badge-pop-rows");for(const o of n)c.appendChild(t("dt",null,o.label)),c.appendChild(t("dd",o.tone==="danger"?"is-danger":null,o.value));e.appendChild(c)}const d=Array.isArray(i.items)?i.items.slice(0,6):[];if(d.length){const c=t("ul","dm-badge-pop-items");for(const o of d){const m=t("li",o.tone==="danger"?"is-danger":null);m.appendChild(t("span","dm-badge-pop-text",o.text)),o.meta&&m.appendChild(t("span","dm-badge-pop-meta",o.meta)),c.appendChild(m)}e.appendChild(c)}else i.empty&&e.appendChild(t("p","dm-badge-pop-empty",i.empty))}function ke(e){let s=null,i=null;const t=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(i),s?.remove(),s=null},d=(r,l)=>{n(),i=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),l(s),document.body.appendChild(s);const u=r.getBoundingClientRect(),y=s.getBoundingClientRect(),v=Math.min(u.right+10,innerWidth-y.width-8);s.style.left=`${Math.max(8,v)}px`,s.style.top=`${Math.max(8,Math.min(u.top+u.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},c=r=>{const l=r?.dataset.badgeLabel;if(!l)return n();d(r,u=>Z(u,l,E.get(r.dataset.countFor)))},o=r=>{const l=r.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",u=r.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(r,y=>{const v=u&&E.get(u.dataset.countFor);if(v)return Z(y,u.dataset.badgeLabel,{...v,title:v.title||l});if(!u){y.textContent=l;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=l;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=u.dataset.badgeLabel,y.append(b,C)})},m=r=>{const l=r.closest?.("a.sidebar-link, summary");return l&&e.contains(l)?l:null};e.addEventListener("mouseover",r=>{if(t()){const u=m(r.target);u&&!u.contains(r.relatedTarget)&&o(u);return}const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&c(l)}),e.addEventListener("mouseout",r=>{if(t()){const l=m(r.target);l&&!l.contains(r.relatedTarget)&&n();return}r.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary");if(t()&&l&&e.contains(l)&&l.querySelector(":scope > .sidebar-text"))return o(l);const u=l?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");u?c(u):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function Ae(e=[]){if(!B(e,"settings"))return"Admin";try{const s=await h.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s,adminScope:i=null}){const t=$(e).get(0);if(!t)return;let n,d=null,c=null,o=null,m=null,r=null;try{const a=await h.get("/sidebar/menu");n=Array.isArray(a?.items)?a.items:null,d=a?.variant||null,c=a?.position||null,o=a?.style||null,m=a?.float||null,r=a?.highlight||null}catch{n=null}(!n||!n.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=pe.slice());let l=[];try{l=await H.get("/api/sidebar/registered-items")||[]}catch{}n=ie(n,z),n=te(n,l.filter(a=>(a?.item??a)?.url!==z)),n=ne(n),n=ae(n,[se()]);const u=await he(s);u.length&&be(n,u),n=le(n,s,i),n=oe(n);const[y,v]=await Promise.all([xe(l.filter(a=>B(s,a?.item?.permission)),s,i).catch(()=>({})),Ae(s)]),b=d?` dm-admin-sidebar--${p(d)}`:"";function C(a,x="px"){if(a==null)return a;const f=String(a).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${x}`:f}let L="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${C(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${C(o.letterSpacing,"em")}`);const x=[];if(a.length&&x.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const f=C(o.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}x.length&&(L=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const j=de(s)?ce():"",F=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${p(v)}</span>${j}${me()}</div>`,N=n.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",P=`${L}<nav class="dm-admin-sidebar${b}${N}">${F}${n.map(a=>G(a,y)).join("")}</nav>`,q=document.createRange();q.selectNodeContents(t);const T=q.createContextualFragment(P);t.replaceChildren(T),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ue(t));for(const a of A)R.set(a.url,y[a.url]??null);W(t),we(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),Q(t,{highlight:r});const k=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{t.style[a]=""}),t.classList.remove("dm-menu-floating"),k&&(k.style.paddingLeft="",k.style.paddingRight=""),t.__dmGutter=()=>{if(c!=="floating"||!k)return;const a=m&&m.anchor||"TL",x=Number.isFinite(Number(m?.offsetX))?Number(m.offsetX):16,f=t.classList.contains("is-collapsed")?ge:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,w=Math.round(f+x*2)+"px";a==="R"||a==="TR"||a==="BR"?k.style.paddingRight=w:k.style.paddingLeft=w},c==="floating"&&Y(t,{position:"floating",float:m||{}}),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const w=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=w+"px",f.addEventListener("transitionend",function X(){f.style.maxHeight="none",f.removeEventListener("transitionend",X)})})}else{const w=f.scrollHeight;f.style.maxHeight=w+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})}),fe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function g(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}g(),t.__dmSetActive=g}
1
+ import{api as h,getUser as O}from"../api.js";import{isPathAllowed as ie}from"./admin-scope.js";import{colourToCss as K,applyFloat as ae,applyHighlight as re,itemHighlightAttrs as le}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{placeTools as ce,ensureLockedTools as de,managePluginsItem as ue,stripItemByUrl as me,insertFoldersBeforeSystem as fe,pruneEmptyToolsFolder as pe,sidebarStateKey as ge,canAny as _,filterByPermission as he,MANAGE_PLUGINS_URL as V}from"./sidebar-grouping.js";import{arrangeButtonMarkup as be,canArrange as ye,startArrange as ve}from"./sidebar-arrange.js";import{collapseButtonMarkup as xe,wireSidebarSize as $e,COLLAPSED_WIDTH as we}from"./sidebar-size.js";import{FREQUENT_MAX as X,addFrequent as Se,frequentFolder as Ce,moveFrequent as Z,readFrequent as U,removeFrequent as ke,writeFrequent as Ae}from"./sidebar-frequent.js";function g(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Le=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function qe(e){if(!_(e,"projects"))return[];let n=[];try{n=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(n))return[];const o=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return n.map(t=>{const d="#/projects/"+encodeURIComponent(t.slug),a=t.counts||{},i=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const l of o){const c=Number(a[l.key])||0;c<=0||i.push({text:`${l.text} (${c})`,url:d+l.path,icon:l.icon})}i.push({text:"Settings",url:d+"/settings",icon:"settings"});const u=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:i,...u&&{colour:"neutral",badge:{text:"off"}}}})}function Fe(e,n){for(const o of e)if(!o.hidden&&(o.key==="projects"||!o.key&&(o.text||"").toLowerCase()==="projects")){o.items=Array.isArray(o.items)?o.items.slice():[],o.items.push(...n);return}}function Y(e,n){const o=Q.includes(n.tone)?n.tone:"info",t=n.label||String(n.count);n.details?F.set(e,n.details):F.delete(e);const s=!n.inventory&&k.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${o}" data-count-for="${g(e)}"${s} data-badge-label="${g(t)}" aria-label="${g(t)}">${g(String(n.count))}</span>`}function J(e){for(const n of e.querySelectorAll("details")){const o=n.querySelector(":scope > summary");if(!o)continue;let t=0,s=!1;const d=[];for(const c of n.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const m=Number(c.textContent)||0;m&&(t+=m,s||(s=c.classList.contains("sidebar-badge--danger")),d.push({text:c.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:c.dataset.badgeLabel,tone:c.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const a=`folder:${n.dataset.stateKey||o.textContent.trim()}`;let i=o.querySelector(":scope > .sidebar-badge--sum");if(!t){i?.remove(),F.delete(a);continue}i||(i=document.createElement("span"),o.querySelector(".sidebar-text")?.after(i));const u=o.querySelector(".sidebar-text")?.textContent.trim()||"This folder",l=d.map(c=>`${c.text}: ${c.meta}`).join(" \xB7 ");i.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${s?"danger":"info"}`,i.dataset.countFor=a,i.dataset.badgeLabel=l,i.setAttribute("aria-label",l),i.textContent=String(t),F.set(a,{title:u,items:d})}}const F=new Map;function Ee(e,n){if(!e.url||!n)return"";const o=n[e.url];return!o||!(o.count>0)&&!o.showZero?"":Y(e.url,o)}const Q=["info","attention","danger","live"],Me={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function z(e,{fallbackLabel:n="",tone:o="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const s=typeof e?.label=="string"&&e.label||(n?`${t} - ${n}`:String(t)),d={count:Math.round(t),label:s,tone:Q.includes(e?.tone)?e.tone:o};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function ee(e,n,o=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${g(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",s=e.icon?`<span data-icon="${g(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${g(t)}</span>`,d=g(e.text||""),a=e.url?g(e.url):"",i=Array.isArray(e.items)&&e.items.length>0,u=e.colour?K(e.colour):"",l=le(e),c=[l.hover&&` data-menu-item-hover="${g(l.hover)}"`,l.active&&` data-menu-item-active="${g(l.active)}"`].filter(Boolean).join(""),m=[u&&`color:${g(u)}`,l.accent&&`--dm-menu-highlight:${g(l.accent)}`].filter(Boolean).join(";"),y=m?` style="${m}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?K(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${g(b)};color:var(--dm-text-inverse,#fff)"`:""}>${g(String(e.badge.text))}</span>`}if(i){const b=ge(e,o),C=S.get(b)!==!1,A=e.items.map(T=>ee(T,n,b)).join(""),L=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${g(b)}"${C?" open":""}${L}>
2
+ <summary${y}>${s} <span class="sidebar-text">${d}</span>${v}</summary>
3
+ <div class="sidebar-children">${A}</div>
4
+ </details>`}return`<a href="${a}" class="sidebar-link" data-url="${a}"${y}${c}>${s} <span class="sidebar-text">${d}</span>${Ee(e,n)}${v}</a>`}async function Te(e=[],n=[],o=null){const t=f=>f.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0);k=Be(e,o);const s=new Set(k.map(f=>f.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},a=(f,x)=>_(n,f)&&!s.has(d[f])?x():Promise.resolve(0),[i,u,l,c,m,y,v,b,C,A,L,T,R,I]=await Promise.all([a("pages",()=>t(h.pages.list())),a("media",()=>t(h.media.list())),a("menus",()=>t(h.menus.list())),a("context-menus",()=>t(h.contextMenus.list())),a("theme",()=>h.theme.get().then(f=>(f?.overrides||[]).length).catch(()=>0)),a("collections",()=>t(h.collections.list())),a("forms",()=>t(h.forms.list())),a("views",()=>t(h.views.list())),a("actions",()=>t(h.actions.list())),a("blocks",()=>t(h.blocks.list())),a("components",()=>t(h.components.list())),a("users",()=>t(h.users.list())),a("plugins",()=>t(h.plugins.list())),a("notifications",()=>h.system?.notifications?.unreadCount?.().then(f=>f?.count??0).catch(()=>0)??Promise.resolve(0))]),j={};await Promise.all(k.map(async f=>{const x=await te(f);x&&(j[f.url]=x)}));const B={"#/pages":i,"#/media":u,"#/menus":l,"#/context-menus":c,"#/theme":m,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":A,"#/components":L,"#/users":T,"#/plugins":R,"#/system/notifications":I},P={};for(const[f,x]of Object.entries(B)){if(s.has(f))continue;const[r,w]=Me[f]??["item","items"],p=z(x);p&&(P[f]={...p,label:`${p.count} ${p.count===1?r:w}`})}return{...P,...j}}let k=[];const Ne="dm:sidebar-count",je=6e4;function Be(e=[],n=null){const o=[],t=s=>{if(s&&!(n&&typeof s.url=="string"&&s.url.startsWith("#/")&&!ie(n,s.url.slice(1)))){s.url&&typeof s.countUrl=="string"&&s.countUrl&&o.push({url:s.url,countUrl:s.countUrl,text:s.text||"",countLabel:s.countLabel||"",inventory:s.inventory===!0});for(const d of[...s.children||[],...s.items||[]])t(d)}};for(const s of e??[])t(s?.item??s);return o}async function te(e){try{const n=new Date,o=`${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,"0")}-${String(n.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${o}`;return z(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function ne(e,n,o){const t=[...e.querySelectorAll("a.sidebar-link")].filter(s=>s.dataset.url===n);if(t.length){for(const s of t){const d=s.querySelector(".sidebar-badge[data-count-for]");if(!o||!(o.count>0)&&!o.showZero)d?.remove();else{const a=document.createRange();a.selectNodeContents(s);const i=a.createContextualFragment(Y(n,o)).firstElementChild;d?d.replaceWith(i):s.querySelector(".sidebar-text")?.after(i)}}W.set(n,o),J(e)}}const W=new Map;async function D(e,n=null){const o=n?k.filter(t=>t.url===n):k;await Promise.all(o.map(async t=>{const s=await te(t),d=W.get(t.url);JSON.stringify(d??null)!==JSON.stringify(s)&&ne(e,t.url,s)}))}function Pe(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(Ne,n=>{const o=n.detail||{};if(o.url)if(o.count!==void 0){const t=k.find(s=>s.url===o.url);ne(e,o.url,z(o,{fallbackLabel:t?.countLabel||t?.text||""}))}else D(e,o.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&D(e)},je),document.addEventListener("visibilitychange",()=>{document.hidden||D(e)}),_e(e))}function se(e,n,o){const t=(a,i,u)=>{const l=document.createElement(a);return i&&(l.className=i),u!=null&&(l.textContent=String(u)),l};if(!o){e.textContent=n;return}e.appendChild(t("strong","dm-badge-pop-title",o.title||n)),(o.rows?.length||!o.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",n));const s=Array.isArray(o.rows)?o.rows.slice(0,6):[];if(s.length){const a=t("dl","dm-badge-pop-rows");for(const i of s)a.appendChild(t("dt",null,i.label)),a.appendChild(t("dd",i.tone==="danger"?"is-danger":null,i.value));e.appendChild(a)}const d=Array.isArray(o.items)?o.items.slice(0,6):[];if(d.length){const a=t("ul","dm-badge-pop-items");for(const i of d){const u=t("li",i.tone==="danger"?"is-danger":null);u.appendChild(t("span","dm-badge-pop-text",i.text)),i.meta&&u.appendChild(t("span","dm-badge-pop-meta",i.meta)),a.appendChild(u)}e.appendChild(a)}else o.empty&&e.appendChild(t("p","dm-badge-pop-empty",o.empty))}function _e(e){let n=null,o=null;const t=()=>e.classList.contains("is-collapsed"),s=()=>{clearTimeout(o),n?.remove(),n=null},d=(l,c)=>{s(),o=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),c(n),document.body.appendChild(n);const m=l.getBoundingClientRect(),y=n.getBoundingClientRect(),v=Math.min(m.right+10,innerWidth-y.width-8);n.style.left=`${Math.max(8,v)}px`,n.style.top=`${Math.max(8,Math.min(m.top+m.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},a=l=>{const c=l?.dataset.badgeLabel;if(!c)return s();d(l,m=>se(m,c,F.get(l.dataset.countFor)))},i=l=>{const c=l.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",m=l.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(l,y=>{const v=m&&F.get(m.dataset.countFor);if(v)return se(y,m.dataset.badgeLabel,{...v,title:v.title||c});if(!m){y.textContent=c;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=c;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=m.dataset.badgeLabel,y.append(b,C)})},u=l=>{const c=l.closest?.("a.sidebar-link, summary");return c&&e.contains(c)?c:null};e.addEventListener("mouseover",l=>{if(t()){const m=u(l.target);m&&!m.contains(l.relatedTarget)&&i(m);return}const c=l.target.closest?.(".sidebar-badge[data-badge-label]");c&&a(c)}),e.addEventListener("mouseout",l=>{if(t()){const c=u(l.target);c&&!c.contains(l.relatedTarget)&&s();return}l.target.closest?.(".sidebar-badge[data-badge-label]")&&s()}),e.addEventListener("focusin",l=>{const c=l.target.closest?.("a, summary");if(t()&&c&&e.contains(c)&&c.querySelector(":scope > .sidebar-text"))return i(c);const m=c?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");m?a(m):s()}),e.addEventListener("focusout",s),e.addEventListener("click",s),window.addEventListener("scroll",s,!0),window.addEventListener("hashchange",s)}async function He(e=[]){if(!_(e,"settings"))return"Admin";try{const n=await h.settings.get();return n?.adminBrand?.title||n?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:n,adminScope:o=null}){const t=$(e).get(0);if(!t)return;let s,d=null,a=null,i=null,u=null,l=null;try{const r=await h.get("/sidebar/menu");s=Array.isArray(r?.items)?r.items:null,d=r?.variant||null,a=r?.position||null,i=r?.style||null,u=r?.float||null,l=r?.highlight||null}catch{s=null}(!s||!s.some(r=>r&&(!r.hidden||r.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),s=Le.slice());let c=[];try{c=await H.get("/api/sidebar/registered-items")||[]}catch{}s=me(s,V),s=ce(s,c.filter(r=>(r?.item??r)?.url!==V)),s=de(s),s=fe(s,[ue()]);const m=await qe(n);m.length&&Fe(s,m),s=he(s,n,o),s=pe(s);const y=Ce(s,U(O()));y&&(s=[y,...s]);const[v,b]=await Promise.all([Te(c.filter(r=>_(n,r?.item?.permission)),n,o).catch(()=>({})),He(n)]),C=d?` dm-admin-sidebar--${g(d)}`:"";function A(r,w="px"){if(r==null)return r;const p=String(r).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${w}`:p}let L="";if(i){const r=[];i.fontFamily&&r.push(`font-family: ${i.fontFamily}, sans-serif`),i.fontSize&&r.push(`font-size: ${A(i.fontSize)}`),i.fontWeight&&r.push(`font-weight: ${i.fontWeight}`),i.letterSpacing&&r.push(`letter-spacing: ${A(i.letterSpacing,"em")}`);const w=[];if(r.length&&w.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${r.join("; ")} }`),i.iconSize){const p=A(i.iconSize);w.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}w.length&&(L=`<style data-admin-sidebar-style>${w.join(" ")}</style>`)}const T=ye(n)?be():"",R=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${g(b)}</span>${T}${xe()}</div>`,I=s.some(r=>r&&r.type==="spacer"&&r.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",j=`${L}<nav class="dm-admin-sidebar${C}${I}">${R}${s.map(r=>ee(r,v)).join("")}</nav>`,B=document.createRange();B.selectNodeContents(t);const P=B.createContextualFragment(j);t.replaceChildren(P),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ve(t));for(const r of k)W.set(r.url,v[r.url]??null);J(t),Pe(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),re(t,{highlight:l});const f=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(r=>{t.style[r]=""}),t.classList.remove("dm-menu-floating"),f&&(f.style.paddingLeft="",f.style.paddingRight=""),t.__dmGutter=()=>{if(a!=="floating"||!f)return;const r=u&&u.anchor||"TL",w=Number.isFinite(Number(u?.offsetX))?Number(u.offsetX):16,p=t.classList.contains("is-collapsed")?we:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,q=Math.round(p+w*2)+"px";r==="R"||r==="TR"||r==="BR"?f.style.paddingRight=q:f.style.paddingLeft=q},a==="floating"&&ae(t,{position:"floating",float:u||{}}),t.querySelectorAll("details").forEach(r=>{r.addEventListener("toggle",function(){const w=this.getAttribute("data-state-key");w&&S.set(w,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const q=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=q+"px",p.addEventListener("transitionend",function oe(){p.style.maxHeight="none",p.removeEventListener("transitionend",oe)})})}else{const q=p.scrollHeight;p.style.maxHeight=q+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),$e(t),Ue(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function x(){const r=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${r}"]`).addClass("active")}x(),t.__dmSetActive=x}function Re(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function G(e){return`${location.origin}${location.pathname}${e}`}function N(e,n){const o=O();if(!Ae(o,e(U(o)))){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}n&&E.toast(n,{type:"success",duration:2500}),Re()}function Ie(e){const n=e.dataset.url;if(!n)return[];const o=e.querySelector(".sidebar-text")?.textContent.trim()||n,t=U(O()),s=t.includes(n),d=!!e.closest("details[data-frequent]"),a=t.indexOf(n),i=!s&&t.length>=X;return[{header:o},{label:"Open",icon:"arrow-right",action:()=>{location.hash=n}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(G(n),"_blank","noopener")},{type:"divider"},s?{label:"Remove from Frequent",icon:"star",action:()=>N(u=>ke(u,n),`${o} removed from Frequent.`)}:{label:i?`Frequent is full (${X})`:"Add to Frequent",icon:"star",disabled:i,action:()=>N(u=>Se(u,n),`${o} added to Frequent.`)},...d?[{label:"Move up",icon:"arrow-up",disabled:a<=0,action:()=>N(u=>Z(u,n,-1))},{label:"Move down",icon:"arrow-down",disabled:a<0||a>=t.length-1,action:()=>N(u=>Z(u,n,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(G(n)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(G(n),{type:"info",duration:8e3})}}}]}function Oe(e,n){const o=e.closest("details"),t=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",s=d=>n.querySelectorAll("details").forEach(a=>{a.open!==d&&(a.open=d)});return[{header:t},{label:o?.open?"Close":"Open",icon:o?.open?"chevron-up":"chevron-down",action:()=>{o&&(o.open=!o.open)}},{label:"Open every folder",icon:"chevrons-down",action:()=>s(!0)},{label:"Close every folder",icon:"chevrons-up",action:()=>s(!1)},...o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>N(()=>[],"Frequent cleared.")}]:[]]}function Ue(e){e.dataset.ctxWired||typeof E>"u"||!E.contextMenu||(e.dataset.ctxWired="1",E.contextMenu(e,{match:"a.sidebar-link, details > summary",inherit:!1,density:"compact",items:n=>n.matches("summary")?Oe(n,e):Ie(n)}))}
@@ -0,0 +1 @@
1
+ const h=["ok","warn","danger","info","attention","live"],w=t=>Number.isFinite(Number(t))?Number(t).toLocaleString("en-GB"):String(t??"");function i(t,e,r){const n=document.createElement(t);return e&&(n.className=e),r!=null&&r!==""&&(n.textContent=String(r)),n}function l(t){const e=t==="warn"?"attention":t;return h.includes(e)?` is-${e}`:""}function u(t,e){const r=i("div","dbp-rows");if(!t.length)return r.append(i("p","dbp-empty",e||"Nothing to show.")),r;for(const n of t){const a=i(n.href?"a":"div",`dbp-row${l(n.tone)}`);n.href&&/^#\//.test(n.href)&&(a.href=n.href);const o=i("span","dbp-row-main");o.append(i("span","dbp-row-text",n.text)),n.meta&&o.append(i("small","dbp-row-meta",n.meta)),a.append(o),n.value!==void 0&&n.value!==""&&a.append(i("span","dbp-row-value",n.value)),r.append(a)}return r}function g(t,e){const r=i("div",`dbp-figure${l(e.tone)}`);r.append(i("span","dbp-figure-value",w(e.count??0))),e.label&&r.append(i("span","dbp-figure-label",e.label)),t.append(r);const n=e.details||{};if(Array.isArray(n.rows)&&n.rows.length){const a=i("dl","dbp-facts");for(const o of n.rows.slice(0,4))a.append(i("dt","",o.label),i("dd",l(o.tone).trim(),o.value));t.append(a)}Array.isArray(n.items)&&n.items.length&&t.append(u(n.items.slice(0,5),""))}function v(t,e){const r=i("div",`dbp-figure${l(e.tone)}`);r.append(i("span","dbp-figure-value",e.value??"-")),e.label&&r.append(i("span","dbp-figure-label",e.label)),t.append(r),e.sub&&t.append(i("p","dbp-sub",e.sub))}function C(t,e){t.append(u(Array.isArray(e.items)?e.items.slice(0,12):[],e.empty))}let d=null;const y="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js";export function loadChart(){return window.Chart?Promise.resolve(!0):(d||(d=new Promise(t=>{const e=document.createElement("script");e.src=y,e.async=!0,e.onload=()=>t(!0),e.onerror=()=>{d=null,t(!1)},document.head.appendChild(e)})),d)}async function A(t,e){const r=Array.isArray(e.labels)?e.labels:[],n=Array.isArray(e.values)?e.values.map(p=>Number(p)||0):[];if(!n.some(p=>p>0)){t.append(i("p","dbp-empty","Nothing recorded yet."));return}const a=i("div","dbp-chart"),o=document.createElement("canvas");if(a.append(o),t.append(a),!await loadChart()||!o.isConnected){a.replaceWith(i("p","dbp-empty","The chart could not load."));return}const f=getComputedStyle(t),s=(p,b)=>f.getPropertyValue(p).trim()||b,c=s("--dm-primary","#5b8cff"),m=new window.Chart(o,{type:"line",data:{labels:r,datasets:[{label:e.label||"",data:n,borderColor:c,backgroundColor:/^#[0-9a-f]{6}$/i.test(c)?`${c}26`:"transparent",fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:s("--dm-text-muted","#94a3b8")},grid:{color:s("--dm-border","rgba(127,127,127,.2)")}},y:{beginAtZero:!0,ticks:{precision:0,color:s("--dm-text-muted","#94a3b8")},grid:{color:s("--dm-border","rgba(127,127,127,.2)")}}}}});t.__dbpChart=m}const N={count:g,stat:v,list:C,chart:A};export async function drawWidget(t,e,r){destroyWidget(t),t.replaceChildren();const n=N[e];if(!n||!r||typeof r!="object"){t.append(i("p","dbp-empty","This widget has nothing to show."));return}await n(t,r)}export function drawError(t,e){destroyWidget(t),t.replaceChildren(i("p","dbp-empty is-error",e||"Could not load."))}export function destroyWidget(t){try{t.__dbpChart?.destroy()}catch{}t.__dbpChart=null}
@@ -0,0 +1,82 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="book"></span> Dashboard</h1>
3
+ <a href="#/documentation" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All usage topics</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body">
9
+
10
+ <p>The <strong>Dashboard</strong> is the admin's home: what the site is doing and what needs you, at a glance -
11
+ page views today, visitors now, pages and drafts, form submissions, site health, the traffic chart and a
12
+ "Needs attention" list gathered from every screen's sidebar badge. Every site has it, and it looks the same
13
+ for everyone.</p>
14
+
15
+ <h3>Dashboard Pro: a Dashboard you build yourself</h3>
16
+ <p>With the <strong>Dashboard Pro</strong> Tool (from the Marketplace), the Dashboard becomes a board of
17
+ <strong>widgets</strong> that each person arranges for themselves. Where another Tool owns the Dashboard - the
18
+ fleet manager's, say - the board is at <strong>My Dashboard</strong> in the sidebar instead. Switch Dashboard
19
+ Pro off and the Dashboard goes back to the fixed one at once; everyone's boards are kept for when it comes back.</p>
20
+
21
+ <h3>Adding widgets</h3>
22
+ <p>Choose <strong>Add widget</strong> in the banner, or right-click anywhere on the board. The gallery lists every
23
+ widget from the Tools you may open, grouped by Tool, with a search box. There are three kinds of source:</p>
24
+ <ul>
25
+ <li><strong>Built in</strong>: Traffic (the last 30 days), Visitors now, Needs attention, Recent activity,
26
+ Site health and Top pages. The administrator-only ones (traffic, activity, health) are offered to
27
+ administrators only.</li>
28
+ <li><strong>Every Tool with a count in the sidebar</strong>: unread email, bookings, support tickets, form
29
+ submissions, pages, media and the rest - the same number as its badge, with the badge's details beneath.</li>
30
+ <li><strong>Widgets a Tool adds of its own</strong> - anything richer than a count.</li>
31
+ </ul>
32
+ <p>You only ever see widgets for Tools you are allowed to open.</p>
33
+
34
+ <h3>Arranging the board</h3>
35
+ <ul>
36
+ <li><strong>Move</strong>: drag a card by its grip (the three lines at its left), or use Move earlier / Move
37
+ later in its menu. With a card focused, Alt+arrow keys move it and Delete removes it.</li>
38
+ <li><strong>Size</strong>: Small, Medium, Large or Full width, from the card's ⋮ menu or by right-clicking it.</li>
39
+ <li><strong>Fold</strong>: the chevron folds a card to its title bar; the double chevron in the banner folds or
40
+ opens them all. A folded card does not refresh until you open it again.</li>
41
+ <li><strong>Remove</strong>: from the card's menu. It stays in the gallery to add again.</li>
42
+ </ul>
43
+ <p>Cards pack upwards, so a short one never leaves a gap that a later card could fill. Each card refreshes on
44
+ its own, and at once when its Tool's count changes.</p>
45
+
46
+ <h3>Several boards</h3>
47
+ <p>Keep up to twelve boards - Sales, Content, Support - as tabs above the Dashboard. <strong>+</strong> makes a
48
+ new one and opens the gallery for it. Right-click a tab (or double-click it to rename) to rename, duplicate,
49
+ move or delete it. Each board has its own widgets and arrangement, and the board you were last on is the one
50
+ that opens.</p>
51
+
52
+ <h3>Settings (the cog)</h3>
53
+ <p>The cog in the banner holds your own settings:</p>
54
+ <ul>
55
+ <li><strong>Name</strong> - what your Dashboard is called, in its banner, the browser tab and the sidebar.</li>
56
+ <li><strong>Greeting</strong> - good morning, your name and today's date.</li>
57
+ <li><strong>Columns</strong> - 3, 4 or 6 on a wide screen. A narrow window always uses fewer.</li>
58
+ <li><strong>Cards</strong> - normal or compact.</li>
59
+ <li><strong>Updates</strong> - Live, Slower (three times as long between refreshes), or Paused (only when you
60
+ press Refresh).</li>
61
+ <li><strong>Open the admin on this board</strong> - signing in, or pressing Dashboard, brings you here.</li>
62
+ </ul>
63
+
64
+ <h3>For administrators</h3>
65
+ <ul>
66
+ <li><strong>The default</strong>: <em>Make this the default for everyone</em> in the banner makes the board you
67
+ are on the starting point for everyone who has not arranged their own. <em>Reset</em> takes anyone back to it.</li>
68
+ <li><strong>Board name</strong> (in the cog, under For everyone) - what the Dashboard is called for everyone who
69
+ has not named their own.</li>
70
+ <li><strong>Lock the layout</strong> - everyone but administrators sees the site's default board and can fold
71
+ and refresh widgets but not add, move, resize or remove them. Turning the lock off gives people their own
72
+ boards back.</li>
73
+ </ul>
74
+
75
+ <h3>For Tool authors</h3>
76
+ <p>A Tool with a sidebar count already has a widget. For anything more, a Tool lists widgets in its
77
+ <code>plugin.json</code> under <code>admin.widgets</code> - a kind (count, stat, list or chart) and a data
78
+ route, no code. See <strong>Dashboard widgets</strong> in the plugin guide.</p>
79
+
80
+ </div>
81
+ </div>
82
+ </div>
@@ -62,6 +62,10 @@
62
62
  <div class="card-body"><h3 style="margin-top:0;"><span data-icon="list"></span> Built-in Tools</h3>
63
63
  <p class="text-muted" style="margin-bottom:0;">Contacts, Notes, Todo, Analytics, SEO, notifications and search.</p></div>
64
64
  </a>
65
+ <a href="#/docs/usage/dashboard" class="card" style="text-decoration:none;display:block;">
66
+ <div class="card-body"><h3 style="margin-top:0;"><span data-icon="home"></span> Dashboard</h3>
67
+ <p class="text-muted" style="margin-bottom:0;">The admin's home, and Dashboard Pro: widgets, boards and settings.</p></div>
68
+ </a>
65
69
  <a href="#/docs/usage/plugins" class="card" style="text-decoration:none;display:block;">
66
70
  <div class="card-body"><h3 style="margin-top:0;"><span data-icon="package"></span> Plugins</h3>
67
71
  <p class="text-muted" style="margin-bottom:0;">The Marketplace: install, update, restarts, switches and dependencies.</p></div>
@@ -0,0 +1,3 @@
1
+ <!-- My Dashboard - the Dashboard Pro board at its own address (admin/js/views/my-dashboard.js), for a site
2
+ whose `#/` belongs to another Tool (the fleet manager's Dashboard, a chosen admin home). -->
3
+ <div class="dbx"></div>
@@ -0,0 +1,63 @@
1
+ import{apiRequest as m,getUser as ke}from"../api.js";import{addBannerAction as A}from"../lib/plugin-chrome.js";import{helpPopovers as ne}from"../lib/help-popover.js";import{openMenuAt as ie}from"../lib/tool-kit.js";import{SIZE_LABELS as Se,SIZES as Ee,addWidget as $e,anyOpen as R,boardCards as Ce,dropWidget as Le,galleryGroups as Ae,removeWidget as de,resizeWidget as De,sameLayout as re,setAllCollapsed as Me,setCollapsed as Te,shiftWidget as z,withToday as xe}from"../lib/dashboard-board-arrange.js";import{destroyWidget as qe,drawError as Ne,drawWidget as Oe}from"../lib/widget-render.js";const Re=400,ze=h=>String(h||"").replace(/^\/api(?=\/)/,""),r=h=>String(h??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");export async function mountBoard(h,f,{isAdmin:Y=!1}={}){const G=h.get(0),c=G.querySelector(".dbx")||G;c.classList.add("dbp"),c.replaceChildren(),c.insertAdjacentHTML("beforeend",`
2
+ <p class="dbp-greeting" hidden></p>
3
+ <div class="dbp-head">
4
+ <p class="dbp-hint">Your own Dashboard.
5
+ <span data-help-title="Dashboard Pro" data-help="Add widgets from the Tools you use - Add widget in the banner, or right-click the board. Each card keeps itself up to date. Keep several boards as tabs: + makes one, right-click a tab to rename, duplicate, move or delete it. Your boards are yours - other people arrange their own. Reset goes back to the site's default; an administrator can make a board the default for everyone who has not arranged their own."></span></p>
6
+ <span class="dbp-source" aria-live="polite"></span>
7
+ </div>
8
+ <div class="dbp-tabs" role="tablist" aria-label="Your boards" hidden></div>
9
+ <div class="dbp-board" role="list" aria-label="Dashboard widgets"></div>
10
+ <div class="dbp-empty-board" hidden>
11
+ <p>Nothing on your Dashboard yet.</p>
12
+ <button type="button" class="btn btn-primary dbp-add-first"><span data-icon="plus" data-icon-size="14"></span> Add a widget</button>
13
+ </div>`);const b=c.querySelector(".dbp-board"),le=c.querySelector(".dbp-source"),U=c.querySelector(".dbp-greeting"),ce=c.querySelector(".dbp-hint"),w=c.querySelector(".dbp-tabs"),H=()=>c.isConnected;let y=[],i={version:1,widgets:[]},o={version:2,active:"main",boards:[{id:"main",name:"Main",widgets:[]}]},q="",D="built-in",d={mine:{name:"",greeting:!0,columns:4,density:"normal",updates:"live",home:!1},site:{name:"My Dashboard",locked:!1},name:"My Dashboard",isAdmin:Y,locked:!1};const M=new Map;f.add(()=>{for(const e of M.values())clearInterval(e);M.clear()});async function pe(){const[e,t,a]=await Promise.all([m("/widgets",{method:"GET"}).catch(()=>[]),m("/dashboard/boards",{method:"GET"}).catch(()=>null),m("/dashboard/settings",{method:"GET"}).catch(()=>null)]);a&&(d={...d,...a}),y=Array.isArray(e)?e:[],t?.boards?.length&&(o={version:2,active:t.active,boards:t.boards}),i={version:1,widgets:T().widgets},q=JSON.stringify(o),D=t?.source||"built-in"}const T=()=>o.boards.find(e=>e.id===o.active)||o.boards[0],N=()=>{T().widgets=i.widgets};let P=null;function S(){clearTimeout(P),!d.locked&&(P=setTimeout(async()=>{N();const e=JSON.stringify(o);if(e!==q)try{const t=await m("/dashboard/boards",{method:"PUT",body:e});q=e,D=t?.source||"mine",B()}catch(t){E.toast(`Your layout was not saved: ${t.message||t}`,{type:"error"})}},Re))}f.add(()=>clearTimeout(P));function B(){le.textContent={mine:"Your layout",site:"The site's default layout","built-in":"The starting layout",locked:"Set by your administrator"}[D]||""}function g(e){re(e,i)||(i=e,k(),S())}function F(e,t){i=Te(i,e,t);const a=b.querySelector(`.dbp-card[data-id="${CSS.escape(e)}"]`);a&&j(a,t),S()}function j(e,t){const a=e.classList.contains("is-collapsed");e.classList.toggle("is-collapsed",t);const s=e.querySelector(".dbp-body");s&&(s.hidden=t);const n=e.querySelector(".dbp-fold"),l=e.getAttribute("aria-label")||"";n&&(n.setAttribute("aria-expanded",t?"false":"true"),n.title=t?"Open":"Fold",n.setAttribute("aria-label",`${t?"Open":"Fold"} ${l}`)),a&&!t&&v(e)}function J(e){i=Me(i,e),b.querySelectorAll(".dbp-card").forEach(t=>j(t,e)),S()}const be=4,ue=16;function K(e){const t=e.getBoundingClientRect().height;t&&(e.style.gridRowEnd=`span ${Math.max(1,Math.ceil((t+ue)/be))}`)}const W=typeof ResizeObserver=="function"?new ResizeObserver(e=>{for(const t of e)K(t.target)}):null;f.add(()=>W?.disconnect());async function v(e){const t=y.find(s=>s.id===e.dataset.id),a=e.querySelector(".dbp-body");if(!(!t||!a)){e.classList.add("is-loading");try{const s=t.kind==="count"?xe(t.dataUrl):t.dataUrl,n=await m(ze(s),{method:"GET"});if(!H()||!e.isConnected)return;await Oe(a,t.kind,n)}catch(s){e.isConnected&&Ne(a,s.status===403?"You cannot see this any more.":"Could not load.")}finally{e.classList.remove("is-loading")}}}function he(e){const t=e.href?`<a class="dbp-title" href="${r(e.href)}" title="Open ${r(e.title)}">${r(e.title)}</a>`:`<span class="dbp-title">${r(e.title)}</span>`;return`<section class="dbp-card dbp-size-${r(e.size)}${e.collapsed?" is-collapsed":""}" data-id="${r(e.id)}" role="listitem" tabindex="0" aria-label="${r(e.title)}">
14
+ <header class="dbp-card-head">
15
+ <span class="dbp-grip" title="Drag to move" aria-hidden="true"><span data-icon="menu" data-icon-size="14"></span></span>
16
+ <span class="dbp-icon" aria-hidden="true"><span data-icon="${r(e.icon||"grid")}" data-icon-size="16"></span></span>
17
+ ${t}
18
+ <small class="dbp-from">${e.source&&e.source!=="Built in"?r(e.source):""}</small>
19
+ <button type="button" class="dbp-fold" aria-expanded="${e.collapsed?"false":"true"}" aria-label="${e.collapsed?"Open":"Fold"} ${r(e.title)}" title="${e.collapsed?"Open":"Fold"}"><span data-icon="chevron-down" data-icon-size="14"></span></button>
20
+ <button type="button" class="dbp-kebab" aria-label="Actions for ${r(e.title)}" title="Actions">\u22EE</button>
21
+ </header>
22
+ <div class="dbp-body"${e.collapsed?" hidden":""}><div class="dbp-skeleton"></div></div>
23
+ </section>`}function $(){if(w.hidden=d.locked===!0,d.locked){w.replaceChildren();return}w.innerHTML=o.boards.map(e=>`<button type="button" class="dbp-tab${e.id===o.active?" is-active":""}" role="tab"
24
+ aria-selected="${e.id===o.active}" data-board="${r(e.id)}" title="${r(e.name)} - right-click to rename, duplicate or delete">${r(e.name)}</button>`).join("")+(o.boards.length<12?'<button type="button" class="dbp-tab dbp-tab-new" data-new-board title="New board" aria-label="New board"><span data-icon="plus" data-icon-size="14"></span></button>':""),I.scan(w)}function _(e){e===o.active||!o.boards.some(t=>t.id===e)||(N(),o.active=e,i={version:1,widgets:T().widgets},$(),k(),S())}const V=()=>`b-${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`;async function X(e,t=""){const a=await E.prompt(e,{inputValue:r(t),inputPlaceholder:"Sales, Content, Support\u2026"});return a==null?null:String(a).replace(/\s+/g," ").trim().slice(0,40)||null}async function Z(){if(o.boards.length>=12)return;const e=await X("Name the new board");if(!e)return;N();const t={id:V(),name:e,widgets:[]};o.boards.push(t),o.active=t.id,i={version:1,widgets:[]},$(),k(),S(),x()}async function C(e,t){const a=o.boards.findIndex(n=>n.id===e);if(a<0)return;N();const s=o.boards[a];if(t==="rename"){const n=await X("Rename the board",s.name);if(!n)return;s.name=n}else if(t==="duplicate"){if(o.boards.length>=12)return;const n={id:V(),name:`${s.name} copy`.slice(0,40),widgets:s.widgets.map(l=>({...l}))};o.boards.splice(a+1,0,n),o.active=n.id}else if(t==="delete"){if(o.boards.length<2||!await E.confirm(`Delete the board "${r(s.name)}"? Its widgets go with it (they stay available to add again).`))return;o.boards.splice(a,1),o.active===e&&(o.active=o.boards[Math.max(0,a-1)].id)}else t==="left"&&a>0?[o.boards[a-1],o.boards[a]]=[o.boards[a],o.boards[a-1]]:t==="right"&&a<o.boards.length-1&&([o.boards[a+1],o.boards[a]]=[o.boards[a],o.boards[a+1]]);i={version:1,widgets:T().widgets},$(),k(),S()}w.addEventListener("click",e=>{if(e.target.closest("[data-new-board]")){Z();return}const t=e.target.closest("[data-board]");t&&_(t.dataset.board)}),w.addEventListener("dblclick",e=>{const t=e.target.closest("[data-board]");t&&C(t.dataset.board,"rename")});const fe=E.contextMenu(w,{cursor:"glyph",match:"[data-board]",inherit:!1,density:"compact",items:e=>{const t=e.dataset.board,a=o.boards.findIndex(s=>s.id===t);return[{header:o.boards[a]?.name||"Board"},{label:"Open",icon:"arrow-right",disabled:t===o.active,action:()=>_(t)},{label:"Rename\u2026",icon:"edit",action:()=>C(t,"rename")},{label:"Duplicate",icon:"copy",disabled:o.boards.length>=12,action:()=>C(t,"duplicate")},{type:"divider"},{label:"Move left",icon:"arrow-left",disabled:a<=0,action:()=>C(t,"left")},{label:"Move right",icon:"arrow-right",disabled:a>=o.boards.length-1,action:()=>C(t,"right")},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,disabled:o.boards.length<2,action:()=>C(t,"delete")}]}});f.add(()=>fe?.destroy?.());function k(){for(const t of M.values())clearInterval(t);M.clear(),b.querySelectorAll(".dbp-body").forEach(qe);const e=Ce(i,y);b.innerHTML=e.map(he).join(""),c.querySelector(".dbp-empty-board").hidden=e.length>0,W?.disconnect();for(const t of b.querySelectorAll(".dbp-card"))K(t),W?.observe(t);I.scan(b);for(const t of b.querySelectorAll(".dbp-card")){t.classList.contains("is-collapsed")||v(t);const a=y.find(n=>n.id===t.dataset.id);if(d.mine.updates==="paused")continue;const s=(a?.refresh||60)*1e3*(d.mine.updates==="slower"?3:1);M.set(t.dataset.id,setInterval(()=>{!document.hidden&&t.isConnected&&!t.classList.contains("is-collapsed")&&v(t)},s))}B()}const Q=e=>{const t=e.detail?.url;if(t)for(const a of b.querySelectorAll(".dbp-card"))y.find(n=>n.id===a.dataset.id)?.href===t&&v(a)};window.addEventListener("dm:sidebar-count",Q),f.add(()=>window.removeEventListener("dm:sidebar-count",Q));function me(e){const t=e.dataset.id,a=y.find(p=>p.id===t),s=i.widgets.find(p=>p.id===t);if(!a||!s)return[];const n=i.widgets.findIndex(p=>p.id===t),l=[{header:a.title},...a.href?[{label:`Open ${a.title}`,icon:"arrow-right",action:()=>{location.hash=a.href}}]:[],{label:s.collapsed?"Open":"Fold",icon:s.collapsed?"chevron-down":"chevron-up",action:()=>F(t,!s.collapsed)},{label:"Refresh",icon:"refresh-cw",disabled:s.collapsed===!0,action:()=>v(e)}];return d.locked?l:[{header:a.title},{label:"Add widget\u2026",icon:"plus",action:()=>x()},{type:"divider"},...a.href?[{label:`Open ${a.title}`,icon:"arrow-right",action:()=>{location.hash=a.href}}]:[],{label:s.collapsed?"Open":"Fold",icon:s.collapsed?"chevron-down":"chevron-up",action:()=>F(t,!s.collapsed)},{label:"Refresh",icon:"refresh-cw",disabled:s.collapsed===!0,action:()=>v(e)},{type:"divider"},...Ee.map(p=>({label:Se[p],icon:s.size===p?"check":"maximize-2",disabled:s.size===p,action:()=>g(De(i,t,p))})),{type:"divider"},{label:"Move earlier",icon:"arrow-left",disabled:n<=0,action:()=>g(z(i,t,-1))},{label:"Move later",icon:"arrow-right",disabled:n>=i.widgets.length-1,action:()=>g(z(i,t,1))},{type:"divider"},{label:"Remove from the Dashboard",icon:"x",action:()=>g(de(i,t))}]}const ge=E.contextMenu(b,{cursor:"glyph",match:".dbp-card",inherit:!1,density:"compact",items:e=>me(e)});f.add(()=>ge?.destroy?.());const ee=e=>{const t=e.target.closest(".dbp-kebab");if(t){ie(t);return}const a=e.target.closest(".dbp-fold");if(a){const s=a.closest(".dbp-card");s&&F(s.dataset.id,!s.classList.contains("is-collapsed"))}},te=e=>{const t=e.target.closest(".dbp-card");if(t){if(e.key==="F10"&&e.shiftKey){e.preventDefault(),ie(t);return}e.target===t&&(e.altKey&&(e.key==="ArrowLeft"||e.key==="ArrowUp")?(e.preventDefault(),g(z(i,t.dataset.id,-1)),ae(t.dataset.id)):e.altKey&&(e.key==="ArrowRight"||e.key==="ArrowDown")?(e.preventDefault(),g(z(i,t.dataset.id,1)),ae(t.dataset.id)):e.key==="Delete"&&(e.preventDefault(),g(de(i,t.dataset.id))))}},ae=e=>requestAnimationFrame(()=>b.querySelector(`.dbp-card[data-id="${CSS.escape(e)}"]`)?.focus());if(b.addEventListener("click",ee),b.addEventListener("keydown",te),f.add(()=>{b.removeEventListener("click",ee),b.removeEventListener("keydown",te)}),typeof E.sortable=="function"){const e=E.sortable(b,{items:".dbp-card",handle:".dbp-grip",live:!1,axis:"x",onDrop({key:t,targetKey:a,zone:s}){if(d.locked||s!=="before"&&s!=="after")return!1;const n=Le(i,t,a,s);return re(n,i)?!1:(g(n),!0)}});f.add(()=>e?.destroy?.())}function x(){const e=E.slideover({title:"Add a widget",size:"md",position:"right"}),t=document.createElement("div");t.className="dbp-gallery",t.innerHTML=`<input type="search" class="form-input dbp-gallery-search" placeholder="Search widgets" aria-label="Search widgets">
25
+ <p class="dbp-gallery-hint">Widgets from the Tools you can open. Each one is added at its usual size - resize it from its \u22EE menu.</p>
26
+ <div class="dbp-gallery-list"></div>`,e.element.appendChild(t);const a=t.querySelector(".dbp-gallery-list"),s=t.querySelector(".dbp-gallery-search");function n(){const l=Ae(y,i,s.value);a.innerHTML=l.length?l.map(p=>`
27
+ <h4 class="dbp-gallery-group">${r(p.source)}</h4>
28
+ ${p.widgets.map(u=>`
29
+ <div class="dbp-gallery-item">
30
+ <span class="dbp-icon"><span data-icon="${r(u.icon||"grid")}" data-icon-size="16"></span></span>
31
+ <span class="dbp-gallery-main"><strong>${r(u.title)}</strong><small>${r(u.description||"")}</small></span>
32
+ ${u.added?'<span class="dbp-gallery-added">On the board</span>':`<button type="button" class="btn btn-sm btn-primary" data-add="${r(u.id)}">Add</button>`}
33
+ </div>`).join("")}`).join(""):'<p class="dbp-empty">No widgets match.</p>',I.scan(a)}a.addEventListener("click",l=>{const p=l.target.closest("[data-add]")?.dataset.add,u=p&&y.find(O=>O.id===p);u&&(g($e(i,u)),E.toast(`${u.title} added.`,{type:"success",duration:2e3}),n())}),s.addEventListener("input",n),e.open(),n(),setTimeout(()=>s.focus(),50)}c.querySelector(".dbp-add-first").addEventListener("click",x);function ye(e){const t=G.querySelector(".dm-plugin-banner-name");if(!t)return;const a=t.querySelector(".dm-plugin-banner-sep");if(a){for(;a.nextSibling;)a.nextSibling.remove();t.appendChild(document.createTextNode(e))}else t.textContent=e}function ve(){const e=new Date().getHours(),t=e<12?"Good morning":e<18?"Good afternoon":"Good evening",a=String(ke()?.name||"").trim().split(/\s+/)[0],s=new Date().toLocaleDateString("en-GB",{weekday:"long",day:"numeric",month:"long"});return`${t}${a?`, ${a}`:""}. ${s}.`}function se(){const e=d.mine;ye(d.name||"My Dashboard"),document.title=`${d.name||"My Dashboard"} - Domma CMS`,U.hidden=!e.greeting,U.textContent=e.greeting?ve():"",c.classList.remove("dbp-cols-3","dbp-cols-4","dbp-cols-6"),c.classList.add(`dbp-cols-${e.columns||4}`),c.classList.toggle("is-compact",e.density==="compact"),c.classList.toggle("is-locked",d.locked===!0),ce.firstChild.textContent=d.locked?"Your administrator has set this layout. You can fold and refresh widgets; the cog has your own settings. ":"Your own Dashboard. Right-click the board to add widgets; drag a card by its grip; the chevron folds it; a card's \u22EE resizes or removes it. Tabs hold your boards. ",d.locked&&(D="locked"),B()}const L=(e,t,a)=>`<option value="${r(e)}"${String(a)===String(e)?" selected":""}>${r(t)}</option>`;function oe(){const e=d.mine,t=E.slideover({title:"Dashboard settings",size:"md",position:"right"}),a=document.createElement("form");a.className="dbp-settings",a.noValidate=!0,a.innerHTML=`
34
+ <h4 class="dbp-settings-section">Yours</h4>
35
+ <label class="dbp-field"><span class="form-label">Name
36
+ <span data-help="What your board is called - in its banner and in the sidebar. Leave it empty to use the site's name for it."></span></span>
37
+ <input class="form-input" name="name" maxlength="40" placeholder="${r(d.site.name||"My Dashboard")}" value="${r(e.name)}" autocomplete="off"></label>
38
+ <label class="dbp-field dbp-switch"><input type="checkbox" class="form-switch" name="greeting"${e.greeting?" checked":""}>
39
+ <span>Greeting <span data-help="A line at the top: good morning, your name and today's date."></span></span></label>
40
+ <div class="dbp-field-row">
41
+ <label class="dbp-field"><span class="form-label">Columns
42
+ <span data-help="How many columns the board has on a wide screen. More columns fit more small widgets side by side; a narrow window always uses fewer."></span></span>
43
+ <select class="form-select" name="columns">${[3,4,6].map(n=>L(n,`${n} columns`,e.columns)).join("")}</select></label>
44
+ <label class="dbp-field"><span class="form-label">Cards
45
+ <span data-help="Compact cards use less padding and smaller figures, so more fits on the screen."></span></span>
46
+ <select class="form-select" name="density">${L("normal","Normal",e.density)}${L("compact","Compact",e.density)}</select></label>
47
+ </div>
48
+ <label class="dbp-field"><span class="form-label">Updates
49
+ <span data-help="Live: each widget refreshes on its own interval. Slower: three times as long between refreshes - kinder to a slow connection. Paused: only when you press Refresh. A Tool saying its count changed still updates its widget."></span></span>
50
+ <select class="form-select" name="updates">${L("live","Live",e.updates)}${L("slower","Slower",e.updates)}${L("paused","Paused - refresh by hand",e.updates)}</select></label>
51
+ <label class="dbp-field dbp-switch"><input type="checkbox" class="form-switch" name="home"${e.home?" checked":""}>
52
+ <span>Open the admin on this board <span data-help="Signing in, or pressing Dashboard, brings you here instead of the usual Dashboard. Takes effect the next time the admin loads."></span></span></label>
53
+ ${d.isAdmin?`
54
+ <h4 class="dbp-settings-section">For everyone</h4>
55
+ <label class="dbp-field"><span class="form-label">Board name
56
+ <span data-help="What the board is called for everyone who has not named their own."></span></span>
57
+ <input class="form-input" name="siteName" maxlength="40" placeholder="My Dashboard" value="${r(d.site.name)}" autocomplete="off"></label>
58
+ <label class="dbp-field dbp-switch"><input type="checkbox" class="form-switch" name="locked"${d.site.locked?" checked":""}>
59
+ <span>Lock the layout <span data-help="Everyone but administrators sees the site's default layout and cannot add, move, resize or remove widgets - they can still fold and refresh them. Set the default with Make this the default for everyone. Turning the lock off gives people their own layouts back."></span></span></label>`:""}
60
+ <div class="dbp-settings-actions">
61
+ <button type="button" class="btn btn-ghost" data-act="cancel">Cancel</button>
62
+ <button type="submit" class="btn btn-primary"><span data-icon="check" data-icon-size="14"></span> Save</button>
63
+ </div>`,t.element.appendChild(a);const s=ne(a,{position:"left"});I.scan(a),a.querySelector('[data-act="cancel"]').addEventListener("click",()=>t.close()),a.addEventListener("submit",async n=>{n.preventDefault();const l=a.elements,p={name:l.name.value.trim(),greeting:l.greeting.checked,columns:Number(l.columns.value),density:l.density.value,updates:l.updates.value,home:l.home.checked};try{const u=await m("/dashboard/settings",{method:"PUT",body:JSON.stringify(p)});if(d={...d,mine:u.mine,site:u.site,name:u.name},d.isAdmin&&l.siteName){const O=await m("/dashboard/settings/site",{method:"PUT",body:JSON.stringify({name:l.siteName.value.trim(),locked:l.locked.checked})});d.site=O.site,d.name=u.mine.name||O.site.name}se(),$(),k();try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}E.toast("Dashboard settings saved.",{type:"success"}),s?.(),t.close()}catch(u){E.toast(`Not saved: ${u.message||u}`,{type:"error"})}}),t.open()}if(await pe(),!H())return;d.locked||A(h,{id:"dashboard-add-widget",icon:"plus",title:"Add widget",onClick:x}),A(h,{id:"dashboard-fold-all",icon:"chevrons-up",title:"Fold or open every widget",onClick:()=>J(R(i))}),A(h,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh every widget",onClick:()=>b.querySelectorAll(".dbp-card").forEach(v)}),d.locked||A(h,{id:"dashboard-reset",icon:"rotate-ccw",title:"Reset to the site's default layout",onClick:async()=>{if(await E.confirm("Go back to the site's default layout? All your boards are replaced by the one default board."))try{await m("/dashboard/layout",{method:"DELETE"});const e=await m("/dashboard/boards",{method:"GET"});o={version:2,active:e.active,boards:e.boards},i={version:1,widgets:T().widgets},q=JSON.stringify(o),D=e?.source||"built-in",$(),k(),E.toast("Back to the default layout.",{type:"success"})}catch(e){E.toast(`Could not reset: ${e.message||e}`,{type:"error"})}}}),Y&&A(h,{id:"dashboard-make-default",icon:"users",title:"Make this the default for everyone",onClick:async()=>{if(await E.confirm("Make this layout the default? Everyone who has not arranged their own Dashboard sees it. (Widgets they may not use are left out for them.)"))try{await m("/dashboard/layout/default",{method:"PUT",body:JSON.stringify(i)}),E.toast("This is now the default layout.",{type:"success"})}catch(e){E.toast(`Could not set the default: ${e.message||e}`,{type:"error"})}}}),A(h,{id:"dashboard-settings",icon:"settings",title:"Dashboard settings",onClick:oe});const we=E.contextMenu(c,{cursor:!1,inherit:!1,density:"compact",items:()=>[{header:d.name||"My Dashboard"},...d.locked?[]:[{label:"Add widget\u2026",icon:"plus",action:()=>x()},{label:"New board\u2026",icon:"layout",disabled:o.boards.length>=12,action:()=>Z()},{type:"divider"}],{label:R(i)?"Fold every widget":"Open every widget",icon:R(i)?"chevrons-up":"chevrons-down",action:()=>J(R(i))},{label:"Refresh every widget",icon:"refresh-cw",action:()=>b.querySelectorAll(".dbp-card").forEach(v)},{type:"divider"},{label:"Dashboard settings\u2026",icon:"settings",action:()=>oe()}]});f.add(()=>we?.destroy?.()),f.add(ne(c,{position:"bottom"})),I.scan(c),se(),$(),k()}
@@ -1,7 +1,7 @@
1
- import{api as L,apiRequest as m}from"../api.js";import{addBannerAction as ne}from"../lib/plugin-chrome.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{openMenuAt as re,viewLifetime as Ee}from"../lib/tool-kit.js";import{SHAPES as ie,showLoading as j}from"../lib/loading.js";import{BADGE_SOURCES as Se,activityRows as Ae,attentionFrom as Pe,canAny as _,chartSeries as Re,isSitePath as O,kpisFrom as Te}from"../lib/dashboard-arrange.js";const le=/^(#\/?)?(\?.*)?$/,Le=3e4,je="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js",_e={type:"custom",template:'<div class="skeleton skeleton-rect" style="flex:1;aspect-ratio:auto;height:240px"></div>'},Oe=`
1
+ import{api as j,apiRequest as f}from"../api.js";import{addBannerAction as ie}from"../lib/plugin-chrome.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{openMenuAt as le,viewLifetime as Se}from"../lib/tool-kit.js";import{SHAPES as ce,showLoading as _}from"../lib/loading.js";import{BADGE_SOURCES as Ae,activityRows as Pe,attentionFrom as Re,canAny as B,chartSeries as Te,isSitePath as O,kpisFrom as Le}from"../lib/dashboard-arrange.js";const q=/^(#\/?)?(\?.*)?$/,je=3e4,_e="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js",Be={type:"custom",template:'<div class="skeleton skeleton-rect" style="flex:1;aspect-ratio:auto;height:240px"></div>'},Oe=`
2
2
  <div class="dbx-row" data-id="{{id}}" data-href="{{href}}" data-url="{{url}}" tabindex="0" data-bind-class="rowCls">
3
3
  <span class="dbx-main"><strong>{{text}}</strong><small data-bind-hidden="!meta">{{meta}}</small></span>
4
4
  <span class="dbx-value" data-bind-hidden="!value">{{value}}</span>
5
5
  <span class="dbx-chip" data-bind-hidden="!chip" data-bind-class="chipCls">{{chip}}</span>
6
6
  <button type="button" class="dbx-kebab" aria-label="Actions" title="Actions" data-bind-hidden="!menu">\u22EE</button>
7
- </div>`;let b=null;function Be(){return window.Chart?Promise.resolve(!0):(b||(b=new Promise(i=>{const o=document.createElement("script");o.src=je,o.async=!0,o.onload=()=>i(!0),o.onerror=()=>{b=null,i(!1)},document.head.appendChild(o)})),b)}const c=i=>Number(i||0).toLocaleString("en-GB"),qe=i=>`${Number(i)>=0?"+":""}${Number(i).toFixed(0)}%`,v=(i,o,d,l={})=>({id:`${i}${o}`,text:d,meta:"",href:"",url:d,value:"",chip:"",chipCls:"",rowCls:"",menu:!0,...l}),u=Ee(le);export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(i){u.reset();const o=i.get(0).querySelector(".dbx")||i.get(0),d=()=>le.test(location.hash)&&o.isConnected,l=M.observable(null),g=M.observable({}),y=M.observable(!1),w=M.observable(""),x=M.observable(null),k=M.observable(Date.now()),p=M.observable(null),C=M.observable(!!window.Chart);let f=[],B=new Set;const ce=M.computed(()=>Te(l.value,g.value)),de=M.computed(()=>{const e=x.value;if(!e)return"";const a=Math.floor((k.value-e)/6e4);return a<1?"Updated just now":`Updated ${a} min ago`}),q=M.observable([]),F=M.observable([]),N=M.observable([]),D=M.observable([]),U=M.observable([]),H=M.observable([]),G=M.observable([]),K=M.observable([]);u.add(M.effect(()=>{const e=l.value,a=t=>t==="danger"?"is-danger":"is-warn";q.value=Pe(g.value,e?.health?.checks||[]).map(t=>({id:t.id,text:`${t.title}: ${t.text}`,meta:t.meta,href:t.href,url:"",value:"",chip:t.tone==="danger"?"Urgent":"Check",chipCls:a(t.tone),rowCls:a(t.tone)+(t.href?"":" is-static"),menu:!!t.href})),F.value=Ae(e?.activity||[],k.value).map(t=>({id:t.id,text:t.text,meta:t.meta,href:t.href,url:t.url,value:"",chip:"",chipCls:"",rowCls:"",menu:!0})),N.value=(e?.topPages||[]).map((t,r)=>v("t",r,t.url,{value:c(t.hits),chip:t.deltaPct==null?"":qe(t.deltaPct),chipCls:Number(t.deltaPct)>=0?"is-ok":"is-danger"})),D.value=(e?.spikes||[]).map((t,r)=>v("s",r,t.url,{value:c(t.hits),chip:`\xD7${Number(t.ratio)||0}`,chipCls:"is-warn"}));const s=e?.journeys;U.value=(s?.entry||[]).map((t,r)=>v("e",r,t.url,{value:c(t.count)})),H.value=(s?.exit||[]).map((t,r)=>v("x",r,t.url,{value:c(t.count)})),G.value=(s?.paths||[]).map((t,r)=>({id:`p${r}`,text:`${t.from} \u2192 ${t.to}`,meta:"",href:"",url:"",value:c(t.count),chip:"",chipCls:"",rowCls:"is-static",menu:!1}));const n={ok:["OK","is-ok"],warn:["Look","is-warn"],fail:["Failing","is-danger"]};K.value=(e?.health?.checks||[]).map((t,r)=>({id:`c${r}`,text:String(t.label||t.id),meta:Array.isArray(t.detail)?t.detail.slice(0,5).join(", "):"",href:"",url:"",value:String(t.value??""),chip:(n[t.level]||n.warn)[0],chipCls:(n[t.level]||n.warn)[1],rowCls:"is-static",menu:!1}))}));const ue=M.computed(()=>!!l.value?.journeys?.totalSessions),he=M.computed(()=>{const e=l.value?.journeys;return e?.totalSessions?`Bounce rate ${(Number(e.bounceRate)*100).toFixed(1)}%`:""}),$=M.computed(()=>Re(l.value?.traffic?.series)),S=M.computed(()=>C.value&&!$.value.empty),pe=M.computed(()=>y.value?l.value?C.value?"No page views recorded yet.":"The chart could not load (Chart.js).":"Traffic needs an administrator.":"Loading\u2026"),me=M.computed(()=>!!p.value?.enabled),fe=M.computed(()=>{const e=p.value||{},a=(e.hits||0)+(e.misses||0),s=n=>{if(!n)return"never";const t=Math.max(0,Math.floor((Date.now()-new Date(n).getTime())/1e3));return t<60?`${t}s ago`:t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} h ago`:`${Math.floor(t/86400)} days ago`};return{hitRate:e.hitRate==null?"-":`${(e.hitRate*100).toFixed(1)}%`,hitsMisses:a?`${c(e.hits)} hits \xB7 ${c(e.misses)} misses`:"no visits yet",size:e.maxItems?`${c(e.size)} / ${c(e.maxItems)}`:c(e.size),driver:e.driver?`driver: ${e.driver}`:"",writes:c(e.writes),invalidations:`${c(e.invalidations)} invalidations`,cleared:s(e.lastClearedAt)}});let A=0;async function V(){const e=Se.filter(s=>_(f,s.permission)),a=await Promise.all(e.map(s=>m(s.countUrl,{method:"GET"}).catch(()=>null)));d()&&(g.value=Object.fromEntries(e.map((s,n)=>[s.key,a[n]]).filter(([,s])=>s)))}async function z({first:e=!1}={}){const a=++A,s=e?[j(o.querySelectorAll(".dbx-kpi-value"),ie.figure),j(o.querySelectorAll(".dbx-loading"),{...ie.rows,count:3}),j(o.querySelector(".dbx-chart-note"),_e)]:[],n=()=>Promise.all(s.map(t=>t.done()));try{f=(await m("/auth/permissions",{method:"GET"}).catch(()=>({})))?.permissions||[];const[r,Me,oe]=await Promise.all([L.dashboard.summary().catch(T=>({__error:T})),m("/cache/status",{method:"GET"}).catch(()=>null),_(f,"pages")?L.pages.list().catch(()=>[]):Promise.resolve([]),V()]);if(await n(),a!==A||!d())return;B=new Set((Array.isArray(oe)?oe:[]).map(T=>T.urlPath).filter(Boolean)),r?.__error?(l.value=null,w.value=/403|forbidden/i.test(String(r.__error?.message))?"":`The dashboard could not load: ${r.__error.message}`):(l.value=r,w.value=r?.warnings?.length?`Some parts could not load: ${r.warnings.join(" \xB7 ")}`:""),p.value=Me,x.value=Date.now()}finally{await n(),a===A&&(y.value=!0)}}let be=0;async function ve(){if(!(!d()||!l.peek())){k.value=Date.now();try{const e=await L.dashboard.summaryLite();if(!d())return;const a={...l.peek()};e.traffic&&(a.traffic={...a.traffic,...e.traffic}),e.realtime&&(a.realtime=e.realtime),e.spikes&&(a.spikes=e.spikes),e.health&&(a.health={...a.health,status:e.health.status}),l.value=a,x.value=Date.now()}catch{}++be%2===0&&V(),m("/cache/status",{method:"GET"}).then(e=>{d()&&(p.value=e)}).catch(()=>{})}}const ge=setInterval(ve,Le);u.add(()=>clearInterval(ge));let h=null;const P=o.querySelector(".dbx-canvas"),ye=()=>{const e=getComputedStyle(o),a=(s,n)=>e.getPropertyValue(s).trim()||n;return{line:a("--dm-primary","#5b8cff"),text:a("--dm-text-muted","#94a3b8"),grid:a("--dm-border","rgba(127,127,127,.2)")}},we=e=>/^#[0-9a-f]{6}$/i.test(e)?`${e}26`:"transparent";function J(){if(!S.peek()||!P?.isConnected||!P.offsetParent)return;const{labels:e,values:a}=$.peek(),{line:s,text:n,grid:t}=ye();if(h){h.data.labels=e,h.data.datasets[0].data=a,h.update("none");return}h=new window.Chart(P,{type:"line",data:{labels:e,datasets:[{data:a,borderColor:s,backgroundColor:we(s),fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:n},grid:{color:t}},y:{beginAtZero:!0,ticks:{precision:0,color:n},grid:{color:t}}}}})}u.add(M.effect(()=>{$.value,S.value,requestAnimationFrame(J)})),u.add(()=>{try{h?.destroy()}catch{}h=null});const W=()=>[...document.body.classList].filter(e=>/^dm-(theme|custom)-/.test(e)).sort().join(" ");let Z=W();const Q=new MutationObserver(()=>{const e=W();if(e!==Z){Z=e;try{h?.destroy()}catch{}h=null,requestAnimationFrame(J)}});Q.observe(document.body,{attributes:!0,attributeFilter:["class"]}),u.add(()=>Q.disconnect());const R=e=>new URL(e,location.origin).href,X=()=>_(f,"pages"),Y=e=>O(e)&&B.has(e.split(/[?#]/)[0].replace(/\/$/,"")||"/");async function ee(e,a){try{await navigator.clipboard.writeText(e),E.toast(`${a} copied.`,{type:"success"})}catch{E.toast("Could not copy - select it and copy it by hand.",{type:"error"})}}function te(e){const{href:a,url:s}=e.dataset;if(a){location.hash=a;return}if(s&&X()&&Y(s)){location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`;return}s&&O(s)&&window.open(R(s),"_blank","noopener")}function xe(e){const{href:a,url:s}=e.dataset,n=[{header:(e.querySelector(".dbx-main strong")?.textContent||"").slice(0,60)}];return a&&n.push({label:"Open",icon:"arrow-right",action:()=>{location.hash=a}}),s&&O(s)?(n.push({label:"Open on the site",icon:"external-link",action:()=>window.open(R(s),"_blank","noopener")}),n.push({label:"Copy address",icon:"copy",action:()=>ee(R(s),"Address")}),X()&&n.push({label:"Edit the page",icon:"edit",disabled:!Y(s),action:()=>{location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`}})):s&&n.push({label:"Copy address",icon:"copy",action:()=>ee(s,"Address")}),n.length>1?n:[]}for(const e of[".dbx-attention",".dbx-activity",".dbx-top-pages",".dbx-spikes",".dbx-entry",".dbx-exit",".dbx-paths",".dbx-checks"]){const a=o.querySelector(e);a&&(a.innerHTML=Oe)}const ke=M.applyBindings({summary:l,loaded:y,warning:w,asOf:de,kpi:ce,attention:q,activity:F,topPages:N,spikes:D,entry:U,exit:H,paths:G,checks:K,hasJourneys:ue,bounce:he,hasChart:S,chartNote:pe,cache:p,cacheOn:me,cacheView:fe},o,{methods:{async toggleCache(e){const a=!!e?.target?.checked;try{p.value=await m("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})}),E.toast(`The cache is ${a?"on":"off"}.`,{type:"success"})}catch(s){e?.target&&(e.target.checked=!a),E.toast(s.message||"Could not change the cache.",{type:"error"})}},async clearCache(){if(await E.confirm("Clear the whole response cache? Pages render afresh until it fills again."))try{p.value=await m("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}}}});u.add(()=>ke.dispose()),u.add($e(o,{position:"bottom"}));const Ce=E.contextMenu(o,{cursor:"glyph",match:".dbx-row",inherit:!1,density:"compact",items:e=>xe(e)});u.add(()=>Ce?.destroy?.());const ae=e=>{const a=e.target.closest(".dbx-row");if(!a||!o.contains(a))return;const s=e.target.closest(".dbx-kebab");if(s){re(s);return}te(a)},se=e=>{const a=e.target.closest(".dbx-row");!a||!o.contains(a)||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),re(a)):e.key==="Enter"&&e.target===a&&te(a))};o.addEventListener("click",ae),o.addEventListener("keydown",se),u.add(()=>{o.removeEventListener("click",ae),o.removeEventListener("keydown",se)}),ne(i,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>z()}),ne(i,{id:"dashboard-new-page",icon:"plus",title:"New page",onClick:()=>{location.hash="#/pages/new"}}),I.scan(o),Be().then(e=>{d()&&(C.value=e)}),await z({first:!0}),d()&&I.scan(o)}};
7
+ </div>`;let v=null;function qe(){return window.Chart?Promise.resolve(!0):(v||(v=new Promise(i=>{const u=document.createElement("script");u.src=_e,u.async=!0,u.onload=()=>i(!0),u.onerror=()=>{v=null,i(!1)},document.head.appendChild(u)})),v)}const c=i=>Number(i||0).toLocaleString("en-GB"),Fe=i=>`${Number(i)>=0?"+":""}${Number(i).toFixed(0)}%`,g=(i,u,n,h={})=>({id:`${i}${u}`,text:n,meta:"",href:"",url:n,value:"",chip:"",chipCls:"",rowCls:"",menu:!0,...h}),d=Se(q);export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(i){d.reset();const u=await f("/dashboard/pro",{method:"GET"}).catch(()=>null);if(!q.test(location.hash))return;if(u?.enabled){const{mountBoard:e}=await import("./dashboard-board.js");await e(i,d,{isAdmin:u.isAdmin===!0});return}const n=i.get(0).querySelector(".dbx")||i.get(0),h=()=>q.test(location.hash)&&n.isConnected,l=M.observable(null),y=M.observable({}),w=M.observable(!1),x=M.observable(""),k=M.observable(null),C=M.observable(Date.now()),m=M.observable(null),$=M.observable(!!window.Chart);let b=[],F=new Set;const de=M.computed(()=>Le(l.value,y.value)),ue=M.computed(()=>{const e=k.value;if(!e)return"";const a=Math.floor((C.value-e)/6e4);return a<1?"Updated just now":`Updated ${a} min ago`}),N=M.observable([]),D=M.observable([]),U=M.observable([]),G=M.observable([]),H=M.observable([]),K=M.observable([]),V=M.observable([]),z=M.observable([]);d.add(M.effect(()=>{const e=l.value,a=t=>t==="danger"?"is-danger":"is-warn";N.value=Re(y.value,e?.health?.checks||[]).map(t=>({id:t.id,text:`${t.title}: ${t.text}`,meta:t.meta,href:t.href,url:"",value:"",chip:t.tone==="danger"?"Urgent":"Check",chipCls:a(t.tone),rowCls:a(t.tone)+(t.href?"":" is-static"),menu:!!t.href})),D.value=Pe(e?.activity||[],C.value).map(t=>({id:t.id,text:t.text,meta:t.meta,href:t.href,url:t.url,value:"",chip:"",chipCls:"",rowCls:"",menu:!0})),U.value=(e?.topPages||[]).map((t,r)=>g("t",r,t.url,{value:c(t.hits),chip:t.deltaPct==null?"":Fe(t.deltaPct),chipCls:Number(t.deltaPct)>=0?"is-ok":"is-danger"})),G.value=(e?.spikes||[]).map((t,r)=>g("s",r,t.url,{value:c(t.hits),chip:`\xD7${Number(t.ratio)||0}`,chipCls:"is-warn"}));const s=e?.journeys;H.value=(s?.entry||[]).map((t,r)=>g("e",r,t.url,{value:c(t.count)})),K.value=(s?.exit||[]).map((t,r)=>g("x",r,t.url,{value:c(t.count)})),V.value=(s?.paths||[]).map((t,r)=>({id:`p${r}`,text:`${t.from} \u2192 ${t.to}`,meta:"",href:"",url:"",value:c(t.count),chip:"",chipCls:"",rowCls:"is-static",menu:!1}));const o={ok:["OK","is-ok"],warn:["Look","is-warn"],fail:["Failing","is-danger"]};z.value=(e?.health?.checks||[]).map((t,r)=>({id:`c${r}`,text:String(t.label||t.id),meta:Array.isArray(t.detail)?t.detail.slice(0,5).join(", "):"",href:"",url:"",value:String(t.value??""),chip:(o[t.level]||o.warn)[0],chipCls:(o[t.level]||o.warn)[1],rowCls:"is-static",menu:!1}))}));const he=M.computed(()=>!!l.value?.journeys?.totalSessions),pe=M.computed(()=>{const e=l.value?.journeys;return e?.totalSessions?`Bounce rate ${(Number(e.bounceRate)*100).toFixed(1)}%`:""}),S=M.computed(()=>Te(l.value?.traffic?.series)),A=M.computed(()=>$.value&&!S.value.empty),me=M.computed(()=>w.value?l.value?$.value?"No page views recorded yet.":"The chart could not load (Chart.js).":"Traffic needs an administrator.":"Loading\u2026"),fe=M.computed(()=>!!m.value?.enabled),be=M.computed(()=>{const e=m.value||{},a=(e.hits||0)+(e.misses||0),s=o=>{if(!o)return"never";const t=Math.max(0,Math.floor((Date.now()-new Date(o).getTime())/1e3));return t<60?`${t}s ago`:t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} h ago`:`${Math.floor(t/86400)} days ago`};return{hitRate:e.hitRate==null?"-":`${(e.hitRate*100).toFixed(1)}%`,hitsMisses:a?`${c(e.hits)} hits \xB7 ${c(e.misses)} misses`:"no visits yet",size:e.maxItems?`${c(e.size)} / ${c(e.maxItems)}`:c(e.size),driver:e.driver?`driver: ${e.driver}`:"",writes:c(e.writes),invalidations:`${c(e.invalidations)} invalidations`,cleared:s(e.lastClearedAt)}});let P=0;async function J(){const e=Ae.filter(s=>B(b,s.permission)),a=await Promise.all(e.map(s=>f(s.countUrl,{method:"GET"}).catch(()=>null)));h()&&(y.value=Object.fromEntries(e.map((s,o)=>[s.key,a[o]]).filter(([,s])=>s)))}async function W({first:e=!1}={}){const a=++P,s=e?[_(n.querySelectorAll(".dbx-kpi-value"),ce.figure),_(n.querySelectorAll(".dbx-loading"),{...ce.rows,count:3}),_(n.querySelector(".dbx-chart-note"),Be)]:[],o=()=>Promise.all(s.map(t=>t.done()));try{b=(await f("/auth/permissions",{method:"GET"}).catch(()=>({})))?.permissions||[];const[r,Ee,re]=await Promise.all([j.dashboard.summary().catch(L=>({__error:L})),f("/cache/status",{method:"GET"}).catch(()=>null),B(b,"pages")?j.pages.list().catch(()=>[]):Promise.resolve([]),J()]);if(await o(),a!==P||!h())return;F=new Set((Array.isArray(re)?re:[]).map(L=>L.urlPath).filter(Boolean)),r?.__error?(l.value=null,x.value=/403|forbidden/i.test(String(r.__error?.message))?"":`The dashboard could not load: ${r.__error.message}`):(l.value=r,x.value=r?.warnings?.length?`Some parts could not load: ${r.warnings.join(" \xB7 ")}`:""),m.value=Ee,k.value=Date.now()}finally{await o(),a===P&&(w.value=!0)}}let ve=0;async function ge(){if(!(!h()||!l.peek())){C.value=Date.now();try{const e=await j.dashboard.summaryLite();if(!h())return;const a={...l.peek()};e.traffic&&(a.traffic={...a.traffic,...e.traffic}),e.realtime&&(a.realtime=e.realtime),e.spikes&&(a.spikes=e.spikes),e.health&&(a.health={...a.health,status:e.health.status}),l.value=a,k.value=Date.now()}catch{}++ve%2===0&&J(),f("/cache/status",{method:"GET"}).then(e=>{h()&&(m.value=e)}).catch(()=>{})}}const ye=setInterval(ge,je);d.add(()=>clearInterval(ye));let p=null;const R=n.querySelector(".dbx-canvas"),we=()=>{const e=getComputedStyle(n),a=(s,o)=>e.getPropertyValue(s).trim()||o;return{line:a("--dm-primary","#5b8cff"),text:a("--dm-text-muted","#94a3b8"),grid:a("--dm-border","rgba(127,127,127,.2)")}},xe=e=>/^#[0-9a-f]{6}$/i.test(e)?`${e}26`:"transparent";function Z(){if(!A.peek()||!R?.isConnected||!R.offsetParent)return;const{labels:e,values:a}=S.peek(),{line:s,text:o,grid:t}=we();if(p){p.data.labels=e,p.data.datasets[0].data=a,p.update("none");return}p=new window.Chart(R,{type:"line",data:{labels:e,datasets:[{data:a,borderColor:s,backgroundColor:xe(s),fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:o},grid:{color:t}},y:{beginAtZero:!0,ticks:{precision:0,color:o},grid:{color:t}}}}})}d.add(M.effect(()=>{S.value,A.value,requestAnimationFrame(Z)})),d.add(()=>{try{p?.destroy()}catch{}p=null});const Q=()=>[...document.body.classList].filter(e=>/^dm-(theme|custom)-/.test(e)).sort().join(" ");let X=Q();const Y=new MutationObserver(()=>{const e=Q();if(e!==X){X=e;try{p?.destroy()}catch{}p=null,requestAnimationFrame(Z)}});Y.observe(document.body,{attributes:!0,attributeFilter:["class"]}),d.add(()=>Y.disconnect());const T=e=>new URL(e,location.origin).href,ee=()=>B(b,"pages"),te=e=>O(e)&&F.has(e.split(/[?#]/)[0].replace(/\/$/,"")||"/");async function ae(e,a){try{await navigator.clipboard.writeText(e),E.toast(`${a} copied.`,{type:"success"})}catch{E.toast("Could not copy - select it and copy it by hand.",{type:"error"})}}function se(e){const{href:a,url:s}=e.dataset;if(a){location.hash=a;return}if(s&&ee()&&te(s)){location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`;return}s&&O(s)&&window.open(T(s),"_blank","noopener")}function ke(e){const{href:a,url:s}=e.dataset,o=[{header:(e.querySelector(".dbx-main strong")?.textContent||"").slice(0,60)}];return a&&o.push({label:"Open",icon:"arrow-right",action:()=>{location.hash=a}}),s&&O(s)?(o.push({label:"Open on the site",icon:"external-link",action:()=>window.open(T(s),"_blank","noopener")}),o.push({label:"Copy address",icon:"copy",action:()=>ae(T(s),"Address")}),ee()&&o.push({label:"Edit the page",icon:"edit",disabled:!te(s),action:()=>{location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`}})):s&&o.push({label:"Copy address",icon:"copy",action:()=>ae(s,"Address")}),o.length>1?o:[]}for(const e of[".dbx-attention",".dbx-activity",".dbx-top-pages",".dbx-spikes",".dbx-entry",".dbx-exit",".dbx-paths",".dbx-checks"]){const a=n.querySelector(e);a&&(a.innerHTML=Oe)}const Ce=M.applyBindings({summary:l,loaded:w,warning:x,asOf:ue,kpi:de,attention:N,activity:D,topPages:U,spikes:G,entry:H,exit:K,paths:V,checks:z,hasJourneys:he,bounce:pe,hasChart:A,chartNote:me,cache:m,cacheOn:fe,cacheView:be},n,{methods:{async toggleCache(e){const a=!!e?.target?.checked;try{m.value=await f("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})}),E.toast(`The cache is ${a?"on":"off"}.`,{type:"success"})}catch(s){e?.target&&(e.target.checked=!a),E.toast(s.message||"Could not change the cache.",{type:"error"})}},async clearCache(){if(await E.confirm("Clear the whole response cache? Pages render afresh until it fills again."))try{m.value=await f("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}}}});d.add(()=>Ce.dispose()),d.add($e(n,{position:"bottom"}));const Me=E.contextMenu(n,{cursor:"glyph",match:".dbx-row",inherit:!1,density:"compact",items:e=>ke(e)});d.add(()=>Me?.destroy?.());const oe=e=>{const a=e.target.closest(".dbx-row");if(!a||!n.contains(a))return;const s=e.target.closest(".dbx-kebab");if(s){le(s);return}se(a)},ne=e=>{const a=e.target.closest(".dbx-row");!a||!n.contains(a)||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),le(a)):e.key==="Enter"&&e.target===a&&se(a))};n.addEventListener("click",oe),n.addEventListener("keydown",ne),d.add(()=>{n.removeEventListener("click",oe),n.removeEventListener("keydown",ne)}),ie(i,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>W()}),ie(i,{id:"dashboard-new-page",icon:"plus",title:"New page",onClick:()=>{location.hash="#/pages/new"}}),I.scan(n),qe().then(e=>{h()&&($.value=e)}),await W({first:!0}),h()&&I.scan(n)}};
@@ -1 +1 @@
1
- import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,n){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof n=="function"&&n(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const n=o.find("#tutorial-scaffolder-mount").get(0);n&&i(n,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const c=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","tools"],a=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],s=o=>o.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join("");for(const o of c)docViews["usage"+s(o)]=t(`usage-${o}`);for(const o of a)docViews["apiRef"+s(o)]=t(`api-${o}`);
1
+ import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,n){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof n=="function"&&n(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const n=o.find("#tutorial-scaffolder-mount").get(0);n&&i(n,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","tools","dashboard"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],s=o=>o.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join("");for(const o of a)docViews["usage"+s(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+s(o)]=t(`api-${o}`);
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as n}from"./media.js";import{loginView as s}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{pluginCodeView as V}from"./plugin-code.js";import{documentationView as a}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as d}from"./doc-pages.js";import{pluginGuidesView as u,pluginGuideView as E}from"./plugin-guides.js";import{apiReferenceView as g}from"./api-reference.js";import{collectionsView as v}from"./collections.js";import{collectionEditorView as b}from"./collection-editor.js";import{collectionEntriesView as h}from"./collection-entries.js";import{formsView as y}from"./forms.js";import{formEditorView as j}from"./form-editor.js";import{formSubmissionsView as k}from"./form-submissions.js";import{viewsListView as L}from"./views-list.js";import{viewEditorView as x}from"./view-editor.js";import{viewPreviewView as M}from"./view-preview.js";import{actionsListView as D}from"./actions-list.js";import{actionEditorView as G}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import{componentsView as A}from"./components.js";import{componentEditorView as C}from"./component-editor.js";import{myProfileView as N}from"./my-profile.js";import{rolesView as T}from"./roles.js";import{roleEditorView as U}from"./role-editor.js";import{effectsView as q}from"./effects.js";import{searchView as z}from"./search.js";import{contentApiView as B}from"./content-api.js";import{notesView as F}from"./notes.js";import{todoView as H}from"./todo.js";import{calculatorView as I}from"../lib/calculator.js";import{analyticsView as J}from"./analytics.js";import{seoView as K}from"./seo.js";import{contactsView as O}from"./contacts.js";import{callNewView as Q}from"./call-new.js";import{menusView as W}from"./menus.js";import{contextMenusView as X}from"./context-menus.js";import{contextMenuEditorView as Y}from"./context-menu-editor.js";import{menuEditorView as Z}from"./menu-editor.js";import{menuLocationsView as _}from"./menu-locations.js";import{projectsView as $}from"./projects.js";import{projectDetailView as oo}from"./project-detail.js";import{projectSettingsView as io}from"./project-settings.js";import{apiTokensView as ro}from"./api-tokens.js";import{apiEndpointsView as to}from"./api-endpoints.js";import{apiEndpointEditorView as eo}from"./api-endpoint-editor.js";const mo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:$,projectDetail:oo,projectSettings:io,apiTokens:ro,apiEndpoints:to,apiEndpointEditor:eo,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,layouts:p,media:n,login:s,users:f,userEditor:w,plugins:c,pluginCode:V,documentation:a,tutorials:l,apiReference:g,collections:v,collectionEditor:b,collectionEntries:h,forms:y,formEditor:j,formSubmissions:k,viewsList:L,viewEditor:x,viewPreview:M,actionsList:D,actionEditor:G,proDocs:P,blocks:R,blockEditor:S,components:A,componentEditor:C,myProfile:N,roles:T,roleEditor:U,effects:q,search:z,contentApi:B,notes:F,todo:H,calculator:I,analytics:J,seo:K,contacts:O,callNew:Q,notifications:m,menus:W,contextMenus:X,contextMenuEditor:Y,menuEditor:Z,menuLocations:_,menusRedirect:mo,pluginGuides:u,pluginGuide:E,...d};
1
+ import{dashboardView as o}from"./dashboard.js";import{myDashboardView as i}from"./my-dashboard.js";import{pagesView as r}from"./pages.js";import{pageEditorView as t}from"./page-editor.js";import{settingsView as e}from"./settings.js";import{themeView as m}from"./theme.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as l}from"./documentation.js";import{tutorialsView as d}from"./tutorials.js";import{docViews as u}from"./doc-pages.js";import{pluginGuidesView as E,pluginGuideView as g}from"./plugin-guides.js";import{apiReferenceView as b}from"./api-reference.js";import{collectionsView as h}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as y}from"./collection-entries.js";import{formsView as j}from"./forms.js";import{formEditorView as k}from"./form-editor.js";import{formSubmissionsView as D}from"./form-submissions.js";import{viewsListView as L}from"./views-list.js";import{viewEditorView as x}from"./view-editor.js";import{viewPreviewView as M}from"./view-preview.js";import{actionsListView as G}from"./actions-list.js";import{actionEditorView as P}from"./action-editor.js";import{proDocsView as R}from"./pro-docs.js";import{blocksView as S}from"./blocks.js";import{blockEditorView as A}from"./block-editor.js";import{componentsView as C}from"./components.js";import{componentEditorView as N}from"./component-editor.js";import{myProfileView as T}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{searchView as B}from"./search.js";import{contentApiView as F}from"./content-api.js";import{notesView as H}from"./notes.js";import{todoView as I}from"./todo.js";import{calculatorView as J}from"../lib/calculator.js";import{analyticsView as K}from"./analytics.js";import{seoView as O}from"./seo.js";import{contactsView as Q}from"./contacts.js";import{callNewView as W}from"./call-new.js";import{menusView as X}from"./menus.js";import{contextMenusView as Y}from"./context-menus.js";import{contextMenuEditorView as Z}from"./context-menu-editor.js";import{menuEditorView as _}from"./menu-editor.js";import{menuLocationsView as $}from"./menu-locations.js";import{projectsView as oo}from"./projects.js";import{projectDetailView as io}from"./project-detail.js";import{projectSettingsView as ro}from"./project-settings.js";import{apiTokensView as to}from"./api-tokens.js";import{apiEndpointsView as eo}from"./api-endpoints.js";import{apiEndpointEditorView as mo}from"./api-endpoint-editor.js";const po={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:oo,projectDetail:io,projectSettings:ro,apiTokens:to,apiEndpoints:eo,apiEndpointEditor:mo,dashboard:o,myDashboard:i,pages:r,pageEditor:t,settings:e,theme:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:l,tutorials:d,apiReference:b,collections:h,collectionEditor:v,collectionEntries:y,forms:j,formEditor:k,formSubmissions:D,viewsList:L,viewEditor:x,viewPreview:M,actionsList:G,actionEditor:P,proDocs:R,blocks:S,blockEditor:A,components:C,componentEditor:N,myProfile:T,roles:U,roleEditor:q,effects:z,search:B,contentApi:F,notes:H,todo:I,calculator:J,analytics:K,seo:O,contacts:Q,callNew:W,notifications:p,menus:X,contextMenus:Y,contextMenuEditor:Z,menuEditor:_,menuLocations:$,menusRedirect:po,pluginGuides:E,pluginGuide:g,...u};
@@ -0,0 +1 @@
1
+ import{apiRequest as r}from"../api.js";import{viewLifetime as m}from"../lib/tool-kit.js";const o=/^#\/my-dashboard\/?(\?.*)?$/,a=m(o);export const myDashboardView={templateUrl:"/admin/js/templates/my-dashboard.html",async onMount(t){a.reset();const e=await r("/dashboard/pro",{method:"GET"}).catch(()=>null);if(!o.test(location.hash))return;const i=t.get(0).querySelector(".dbx")||t.get(0);if(!e?.enabled){i.innerHTML='<p class="dbp-empty">My Dashboard comes with Dashboard Pro, which is not switched on for this site. Find it in the Marketplace.</p>';return}const{mountBoard:s}=await import("./dashboard-board.js");await s(t,a,{isAdmin:e.isAdmin===!0})}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -48,6 +48,8 @@ import {isSmtpConfigured} from '../../services/email.js';
48
48
  import {loginFailed, loginRateLimited} from '../../services/notification-sources.js';
49
49
  import {afterIndexOf, checkPassword, runLoginChecks, verifyChallenge} from '../../services/authChecks.js';
50
50
  import {createSession, isLegacyTokenRevoked, listSessions, revokeLegacyToken, revokeSession, revokeUserSessions, useSession} from '../../services/sessions.js';
51
+ import {isDashboardPro} from '../../services/widgets.js';
52
+ import {readPrefs} from '../../services/dashboardSettings.js';
51
53
 
52
54
  const { accessTokenExpiry, refreshTokenExpiry } = config.auth;
53
55
 
@@ -240,7 +242,7 @@ export async function authRoutes(fastify) {
240
242
  const adminScope = getAdminScope(roles);
241
243
  // adminHome: the screen `#/` opens for this user - see services/adminHome.js.
242
244
  // The menu is read only when a site home has to be checked against it.
243
- const adminHome = await resolveAdminHome({
245
+ let adminHome = await resolveAdminHome({
244
246
  scope: adminScope,
245
247
  permissions,
246
248
  siteHome: getConfig('site')?.adminHome,
@@ -248,6 +250,8 @@ export async function authRoutes(fastify) {
248
250
  menuItems: async () => (await getMenu('admin-sidebar').catch(() => null))?.items || [],
249
251
  canSee: (visibility) => checkVisibility(request.user, visibility)
250
252
  });
253
+ // Dashboard Pro's "Open the admin on my board" - for a user the admin does not confine.
254
+ if (!adminScope && isDashboardPro() && (await readPrefs(request.user)).home) adminHome = '/my-dashboard';
251
255
  return {permissions, adminScope, adminHome};
252
256
  });
253
257