domma-cms 1.10.10 → 1.10.12

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.12] - 2026-10-24
4
+
5
+ - **Email with untidy HTML looks as it should.** The reader now reads a message with the same HTML5 rules a browser uses, so badly nested tables (a `<div>` between a table and its rows - British Gas, BT, Ocean Finance) keep their coloured panels instead of spilling out of them. One broken declaration in an inline style no longer throws the rest of that style away. Background pictures count as blocked images: the "Load images" notice now offers them and loads them, where before they were always stripped. Across a 15,600-message mailbox, text the sender's own design shows went missing in 16 messages, down from 3,481 before 1.10.8.
6
+
7
+ ## [1.10.11] - 2026-10-24
8
+
9
+ - Load admin screens and plugin views only when opened, and let the first screen render while navigation data loads. Sidebar badges refresh in the background, reuse recent account-scoped results and share one authenticated request instead of a request per Tool.
10
+ - Cache content-versioned admin scripts and styles for repeat visits. Revalidate module dependencies with content ETags, rebuild entry URLs when files change, and reject superseded asset versions instead of serving different code under an immutable URL.
11
+ - Keep foreground loading indicators independent of low-priority badge polling. Combined counts preserve each route's authorization checks, staff-only visibility and partial failure handling.
12
+
3
13
  ## [1.10.10] - 2026-10-24
4
14
 
5
15
  - **Email keeps its text colours in two more cases.** A colour set on the message's `<body>` (a light-on-dark design) was lost when the reader unwrapped it, and an inline style written in capitals (`COLOR: #000000`, common in older templates) was dropped, so a stylesheet colour won instead - grey-on-grey footers and links. Found by rendering a full mailbox (16,000 messages) before and after.
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{installSmoothCarets as Q}from"./lib/smooth-caret.js";import{views as X}from"./views/index.js";import{withPluginChrome as V}from"./lib/plugin-chrome.js";import{api as r,getUser as L,isAuthenticated as m,logout as Z,sendToLogin as tt}from"./api.js";import{claimDrafts as et}from"./lib/draft-owner.js";import{isAuthScreen as C,loginHashFor as it}from"./lib/login-arrange.js";import{installHttpInterceptor as at}from"./http-interceptor.js";import{installResizableSlideovers as ot}from"./lib/slideover-resizable.js";import{installActivityThrobber as st}from"./lib/activity-throbber.js";import{installUpdateNotice as nt,renderCmsVersion as rt}from"./lib/update-notice.js";import{renderFrameworkVersions as _,wireAccountMenu as lt}from"./lib/topbar-account.js";import{refreshTopbarProject as B}from"./lib/topbar-project.js";import{isPathAllowed as ct,redirectFor as mt}from"./lib/admin-scope.js";import{deliverDesktop as pt,desktopActive as dt,notificationSettings as ut}from"./lib/desktop-notify.js";import{avatarUrl as ht,installAccountAvatar as wt}from"./lib/avatar.js";import{installCalculator as gt}from"./lib/calculator.js";import{takeOverviewPass as ft,withDashboardStrip as W}from"./lib/dashboards-strip.js";import{installHotkeys as vt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as H}from"./lib/admin-brand.js";import{installRouterCatchUp as bt}from"./lib/router-catch-up.js";import{installSidebarDrawer as St}from"./lib/sidebar-drawer.js";$(()=>{Q(document),at(),gt(),nt(),st(),ot(),St(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,c="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||mt(p,t);return i||((t.split("?")[0]||"/")==="/"&&c&&c!=="/"&&!ft()?y[c]||c:null)}const y={},N={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!m()){tt();return}const a=D(t.path);if(a){R.navigate(a);return}e()});async function f(){try{const t=await r.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,H(h),c=t.adminHome||"/",_(t.versions);const i=t.permissions||[];return w=ct(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,c="/",h||H(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(B(r),P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,c="/",w=!1,g=null)});const q=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(o=>o.refreshAdminTheme(r)),await T();const e=await f(),a=D(t.path);if(a&&R.navigate(a),j(e),ut.read().signInToasts)try{const o=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(o)?o:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}O()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const G=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function z(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!z(i))return;const a=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!a||!/\/new$/.test(i))return;let o=0;const n=60,s=1500,v=()=>{o+=n;for(const x of G){const b=document.querySelector(x);if(!b)continue;if(Array.from(b.options||[]).find(J=>J.value===a)){b.value=a,b.dispatchEvent(new Event("change",{bubbles:!0}));return}}o<s&&setTimeout(v,n)};setTimeout(v,n)});const F="cms_card_states",k=S.get(F)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(F,k))}),vt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...X},K={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(K))l[t]&&(l[t]=V(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=W(l.dashboard);const I=[...q];async function T(){if(m())try{const t=await r.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,a]of Object.entries(t.views||{}))try{const o=await import(`/plugins/${a.entry}`);l[e]=V(o[a.exportName],(t.meta||{})[e])}catch{}l.dashboard=W(l.dashboard);for(const[e,{route:a}]of Object.entries(t.takeovers||{}))a&&(y[`/${e}`]=a);for(const[e,{enabled:a,displayName:o}]of Object.entries(t.tools||{}))a===!1&&(N[`/${e}`]=o);for(const{id:e,href:a}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=a,document.head.appendChild(o)}}catch{}}function O(){const t=L();if(!t)return;if($("#topbar-user-name").length){B(r);return}const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,a=ht(t);$("#topbar-user").get(0).innerHTML=`
1
+ import{wireNotificationMenu as Q}from"./lib/topbar-notifications.js";import{installSmoothCarets as X}from"./lib/smooth-caret.js";import{viewLoaders as Z}from"./views/index.js";import{createLazyViews as tt}from"./lib/lazy-views.js";import{withPluginChrome as _}from"./lib/plugin-chrome.js";import{api as r,getUser as L,isAuthenticated as m,logout as et,sendToLogin as it}from"./api.js";import{claimDrafts as at}from"./lib/draft-owner.js";import{isAuthScreen as C,loginHashFor as ot}from"./lib/login-arrange.js";import{installHttpInterceptor as st}from"./http-interceptor.js";import{installResizableSlideovers as nt}from"./lib/slideover-resizable.js";import{installActivityThrobber as rt}from"./lib/activity-throbber.js";import{installUpdateNotice as lt,renderCmsVersion as ct}from"./lib/update-notice.js";import{renderFrameworkVersions as W,wireAccountMenu as mt}from"./lib/topbar-account.js";import{refreshTopbarProject as B}from"./lib/topbar-project.js";import{isPathAllowed as pt,redirectFor as dt}from"./lib/admin-scope.js";import{deliverDesktop as ut,desktopActive as ht,notificationSettings as wt}from"./lib/desktop-notify.js";import{avatarUrl as gt,installAccountAvatar as ft}from"./lib/avatar.js";import{installCalculator as vt}from"./lib/calculator.js";import{takeOverviewPass as bt,withDashboardStrip as H}from"./lib/dashboards-strip.js";import{installHotkeys as St}from"./lib/hotkeys-ui.js";import{applyAdminBrand as q}from"./lib/admin-brand.js";import{installRouterCatchUp as Ct}from"./lib/router-catch-up.js";import{installSidebarDrawer as Mt}from"./lib/sidebar-drawer.js";$(()=>{X(document),st(),vt(),lt(),rt(),nt(),Mt(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:a}=await import("./lib/admin-theme.js");m()?await a(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,c="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const a=y[t]||dt(p,t);return a||((t.split("?")[0]||"/")==="/"&&c&&c!=="/"&&!bt()?y[c]||c:null)}const y={},P={};R.use(async(t,a,e)=>{if(C(t.path))return e();if(!m()){it();return}const i=D(t.path);if(i){R.navigate(i);return}e()});async function f(){try{const t=await r.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,q(h),c=t.adminHome||"/",W(t.versions);const a=t.permissions||[];return w=pt(p,"/settings")&&a.some(e=>e==="settings"||e.startsWith("settings.")),g=a.some(e=>e==="notifications"||e.startsWith("notifications.")),a}catch{return p=null,c="/",h||q(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(B(r),N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:a}=await import("./lib/sidebar-renderer.js");await a({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,c="/",w=!1,g=null)});const z=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:a})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(a?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(o=>o.refreshAdminTheme(r)),await T();const e=await f(),i=D(t.path);if(i&&R.navigate(i),j(e),wt.read().signInToasts)try{const o=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(o)?o:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const l=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${l} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}V()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const G=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function J(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const a=t?String(t.path).split("?")[0]:"";if(!t||!J(a))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(a))return;let o=0;const n=60,s=1500,l=()=>{o+=n;for(const x of G){const b=document.querySelector(x);if(!b)continue;if(Array.from(b.options||[]).find(Y=>Y.value===i)){b.value=i,b.dispatchEvent(new Event("change",{bubbles:!0}));return}}o<s&&setTimeout(l,n)};setTimeout(l,n)});const F="cms_card_states",k=S.get(F)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const a=$(this).closest(".card");a.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=a.hasClass("card-collapsed")?"collapsed":"open",S.set(F,k))}),St(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const a=document.querySelector("#view-container .view-header button.btn-primary");a&&(t.preventDefault(),a.click())});const v=tt(),O=v.views,K={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,a]of Object.entries(Z))v.register(t,async()=>{let e=await a();const i=K[t];return i&&(e=_(e,{plugin:t,licence:"Built in",...i})),t==="dashboard"?H(e):e});R.use(async(t,a,e)=>{try{await v.load(t.view)}catch(i){console.error("[admin] Screen failed to load",i),O[t.view]={template:'<p role="alert">This screen could not load. Reload the page to try again.</p>'}}e()});const I=[...z];async function T(){if(m())try{const t=await r.plugins.adminConfig(),a=new Set(I.map(e=>e.path));for(const e of t.routes||[])a.has(e.path)||I.push(e);for(const[e,i]of Object.entries(t.views||{})){const o=(t.meta||{})[e];v.register(e,async()=>{const s=(await import(`/plugins/${i.entry}`))[i.exportName];if(!s)throw new Error(`Missing plugin screen: ${e}`);const l=_(s,o);return e==="dashboard"?H(l):l},JSON.stringify([i,o]))}for(const[e,{route:i}]of Object.entries(t.takeovers||{}))i&&(y[`/${e}`]=i);for(const[e,{enabled:i,displayName:o}]of Object.entries(t.tools||{}))i===!1&&(P[`/${e}`]=o);for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=i,document.head.appendChild(o)}}catch{}}function V(){const t=L();if(!t)return;if($("#topbar-user-name").length){B(r);return}const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,i=gt(t);$("#topbar-user").get(0).innerHTML=`
2
2
  <a id="topbar-project" class="topbar-action-link topbar-project" hidden>
3
3
  <span data-icon="folder"></span>
4
4
  <span id="topbar-project-name" class="topbar-project-name"></span>
@@ -32,7 +32,7 @@ import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{inst
32
32
  </button>
33
33
  <div id="topbar-account-menu" class="topbar-account-menu" hidden>
34
34
  <div class="topbar-account-identity">
35
- ${a?`<img id="topbar-avatar" class="topbar-avatar" src="${d(a)}" alt="">`:""}
35
+ ${i?`<img id="topbar-avatar" class="topbar-avatar" src="${d(i)}" alt="">`:""}
36
36
  <span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
37
37
  <span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${d(e)}</span>
38
38
  </div>
@@ -50,4 +50,4 @@ import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{inst
50
50
  </a>
51
51
  </div>
52
52
  </div>
53
- `;const o=$("#topbar-bell").get(0);o&&(o.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),Z()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),lt(t),Y({refreshBadge:U}),wt(t),_(),rt(),B(r),U(),u||(u=setInterval(U,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function U(){if(!(!m()||g===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&dt()&&pt(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=it(window.location.hash));else if(et(L()?.id),await T(),L()){const o=await f(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(o),O()}const t=R._extractParams.bind(R);R._extractParams=function(a,o){const n=o.indexOf("?");if(n!==-1&&(o=o.slice(0,n)),a.endsWith("/*")){const s=a.slice(0,-2);return o.startsWith(s+"/")?{}:null}return t(a,o)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(a){return m()&&e!==a&&(e=a,await T(),R._matchRoute(a).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(a))},bt(R),R.init({container:"#view-container",routes:I,views:l,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
53
+ `;const o=$("#topbar-bell").get(0);o&&(o.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),et()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),mt(t),Q({refreshBadge:U}),ft(t),W(),ct(),B(r),U(),u||(u=setInterval(U,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function U(){if(!(!m()||g===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const a=$("#topbar-bell-badge");t>0?a.text(t>99?"99+":String(t)).removeAttr("hidden"):a.attr("hidden","").text(""),t>0&&ht()&&ut(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=ot(window.location.hash));else{at(L()?.id);const i=L(),[o]=await Promise.all([i?f():Promise.resolve([]),T()]);if(i){const n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),j(o).catch(l=>console.error("[admin] Sidebar failed to load",l)),V()}}const t=R._extractParams.bind(R);R._extractParams=function(i,o){const n=o.indexOf("?");if(n!==-1&&(o=o.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return o.startsWith(s+"/")?{}:null}return t(i,o)};const a=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(i){return m()&&e!==i&&(e=i,await T(),R._matchRoute(i).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):a(i))},Ct(R),R.init({container:"#view-container",routes:I,views:O,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
@@ -1 +1 @@
1
- export const SHOW_AFTER_MS=200,MIN_VISIBLE_MS=350;export function isTracked(e,n){let r="";if(typeof e=="string"?r=e:e instanceof URL?r=e.href:e&&typeof e.url=="string"&&(r=e.url),!r)return!1;try{return new URL(r,n).origin===n}catch{return!1}}let i=0,o=null,a=null,u=0,t=null;function m(){return t||(t=document.createElement("span"),t.className="dm-activity",t.setAttribute("role","status"),t.setAttribute("aria-label","Loading"),t.hidden=!0,typeof E<"u"&&E.loader&&E.loader(t,{type:"spinner",size:16,color:"primary",centered:"false"}),t)}function h(e){const n=document.querySelector("#view-container .dm-plugin-banner-identity")||document.getElementById("admin-topbar");if(!n)return!1;if(e.parentNode!==n)if(n.id==="admin-topbar"){const r=n.querySelector(".topbar-brand");n.insertBefore(e,r?r.nextSibling:n.firstChild)}else n.appendChild(e);return!0}function w(){if(o=null,!i)return;const e=m();h(e)&&(e.hidden=!1,u=Date.now())}function y(){a=null,!(i||!t)&&(t.hidden=!0)}function p(){i++,clearTimeout(a),a=null,t&&!t.hidden?h(t):o||(o=setTimeout(w,200))}function b(){if(i=Math.max(0,i-1),i||(clearTimeout(o),o=null,!t||t.hidden))return;const e=350-(Date.now()-u);a=setTimeout(y,Math.max(0,e))}export function installActivityThrobber(){if(typeof window>"u"||window.fetch.__dmActivity)return;const e=window.fetch,n=window.location.origin,r=function(c,f){if(!isTracked(c,n))return e.call(this,c,f);p();let s=!1;const d=()=>{s||(s=!0,b())};try{const l=e.call(this,c,f);return l.then(d,d),l}catch(l){throw d(),l}};r.__dmActivity=!0,window.fetch=r}
1
+ export const SHOW_AFTER_MS=200,MIN_VISIBLE_MS=350;export function isTracked(e,n,{priority:i}={}){if(i==="low")return!1;let r="";if(typeof e=="string"?r=e:e instanceof URL?r=e.href:e&&typeof e.url=="string"&&(r=e.url),!r)return!1;try{return new URL(r,n).origin===n}catch{return!1}}let o=0,l=null,c=null,u=0,t=null;function m(){return t||(t=document.createElement("span"),t.className="dm-activity",t.setAttribute("role","status"),t.setAttribute("aria-label","Loading"),t.hidden=!0,typeof E<"u"&&E.loader&&E.loader(t,{type:"spinner",size:16,color:"primary",centered:"false"}),t)}function h(e){const n=document.querySelector("#view-container .dm-plugin-banner-identity")||document.getElementById("admin-topbar");if(!n)return!1;if(e.parentNode!==n)if(n.id==="admin-topbar"){const i=n.querySelector(".topbar-brand");n.insertBefore(e,i?i.nextSibling:n.firstChild)}else n.appendChild(e);return!0}function w(){if(l=null,!o)return;const e=m();h(e)&&(e.hidden=!1,u=Date.now())}function y(){c=null,!(o||!t)&&(t.hidden=!0)}function p(){o++,clearTimeout(c),c=null,t&&!t.hidden?h(t):l||(l=setTimeout(w,200))}function b(){if(o=Math.max(0,o-1),o||(clearTimeout(l),l=null,!t||t.hidden))return;const e=350-(Date.now()-u);c=setTimeout(y,Math.max(0,e))}export function installActivityThrobber(){if(typeof window>"u"||window.fetch.__dmActivity)return;const e=window.fetch,n=window.location.origin,i=function(r,f){if(!isTracked(r,n,f))return e.call(this,r,f);p();let s=!1;const d=()=>{s||(s=!0,b())};try{const a=e.call(this,r,f);return a.then(d,d),a}catch(a){throw d(),a}};i.__dmActivity=!0,window.fetch=i}
@@ -0,0 +1 @@
1
+ export function createRequestQueue(i=4){const e=[],t=new Map;let r=0;function u(){for(;r<i&&e.length;){const s=e.shift();r++,Promise.resolve().then(s.task).then(s.resolve,s.reject).finally(()=>{r--,u()})}}return(s,{key:n,priority:a=!1}={})=>{if(n&&t.has(n))return t.get(n);const o=new Promise((f,h)=>{const c={task:s,resolve:f,reject:h};a?e.unshift(c):e.push(c),u()});if(!n)return o;const l=o.finally(()=>t.delete(n));return t.set(n,l),l}}export function afterInitialView(i=globalThis.R,e=globalThis.M){return!i||i._currentRoute||!e?.subscribe?Promise.resolve():new Promise(t=>{const r=e.subscribe("router:afterChange",()=>{r(),t()})})}
@@ -0,0 +1 @@
1
+ export function createLazyViews(r={}){const t=new Map;return{views:r,register(e,n,i=n){t.get(e)?.key!==i&&(t.set(e,{loader:n,key:i,pending:null,loaded:!1}),delete r[e])},async load(e){const n=t.get(e);if(!n)throw new Error(`Unknown admin screen: ${e}`);if(n.loaded)return r[e];n.pending||(n.pending=Promise.resolve().then(n.loader).then(o=>{if(!o||!["object","function"].includes(typeof o))throw new Error(`Invalid admin screen: ${e}`);return t.get(e)===n&&(r[e]=o,n.loaded=!0),o}).finally(()=>{n.pending=null}));const i=await n.pending;return t.get(e)===n?i:this.load(e)}}}
@@ -1 +1 @@
1
- import{renderTopbarNavigation as y,updateTopbarCounts as h}from"./topbar-navigation.js";export const COUNT_CACHE_MAX_AGE=5*6e4;const f=new Map,u=new Map;let a;export function countCacheKey(o,r,t,c=[]){const n=[],s=e=>{if(e){e.url&&e.countUrl&&n.push([e.url,e.countUrl,!!e.inventory]);for(const i of[...e.items||[],...e.children||[]])s(i)}};return c.forEach(e=>s(e.item||e)),"admin.counts.v1."+JSON.stringify([o?.id||"",[...r].sort(),t,n.sort()])}export function readCountCache(o,r=Date.now()){let t=f.get(o);if(!t)try{t=JSON.parse(sessionStorage.getItem(o))}catch{}return!t||!Number.isFinite(t.at)||r<t.at||r-t.at>COUNT_CACHE_MAX_AGE||!t.counts||typeof t.counts!="object"||Array.isArray(t.counts)?null:t.counts}export function writeCountCache(o,r,t=Date.now()){const c={};for(const[s,e]of Object.entries(r))!e||!Number.isFinite(e.count)||e.count<0||(c[s]={count:e.count,label:String(e.label||""),tone:e.tone||"info",showZero:e.showZero===!0,inventory:e.inventory===!0});const n={at:t,counts:c};f.set(o,n);try{sessionStorage.setItem(o,JSON.stringify(n))}catch{}}export function loadCountSnapshot(o,r){return u.has(o)||u.set(o,Promise.resolve().then(r).then(t=>(writeCountCache(o,t),t)).finally(()=>u.delete(o))),u.get(o)}export function countSnapshot(o,r){const t=new Map,c=n=>{if(n){n.url&&n.countUrl&&t.set(n.url,!!n.inventory);for(const s of[...n.items||[],...n.children||[]])c(s)}};return r.forEach(n=>c(n.item||n)),Object.fromEntries(Object.entries(o).map(([n,s])=>[n,s?{...s,aggregate:t.has(n)&&!t.get(n)&&!s.inventory}:null]))}export async function beginSidebarCounts({user:o,permissions:r,adminScope:t,registered:c,tree:n,actions:s,fetcher:e}){const i={key:countCacheKey(o,r,t,c),registered:c};a=i;const l=readCountCache(i.key)||{};y(n,{...s,counts:countSnapshot(l,c)});try{const p=await loadCountSnapshot(i.key,e);return a===i&&h(countSnapshot(p,c),{replace:!0}),p}catch{return l}}export function publishSidebarCount(o,r){if(!a)return;h(countSnapshot({[o]:r},a.registered));const t=readCountCache(a.key)||{};r?t[o]=r:delete t[o];const c=f.get(a.key)?.at||Date.now();writeCountCache(a.key,t,c)}
1
+ import{renderTopbarNavigation as d,updateTopbarCounts as p}from"./topbar-navigation.js";import{afterInitialView as g}from"./background-requests.js";export const COUNT_CACHE_MAX_AGE=5*6e4,COUNT_REFRESH_INTERVAL=6e4;const f=new Map,l=new Map;let a;export function countCacheKey(n,r,t,c=[]){const e=[],s=o=>{if(o){o.url&&o.countUrl&&e.push([o.url,o.countUrl,!!o.inventory]);for(const i of[...o.items||[],...o.children||[]])s(i)}};return c.forEach(o=>s(o.item||o)),"admin.counts.v1."+JSON.stringify([n?.id||"",[...r].sort(),t,e.sort()])}export function readCountCache(n,r=Date.now()){let t=f.get(n);if(!t)try{t=JSON.parse(sessionStorage.getItem(n))}catch{}return!t||!Number.isFinite(t.at)||r<t.at||r-t.at>COUNT_CACHE_MAX_AGE||!t.counts||typeof t.counts!="object"||Array.isArray(t.counts)?null:(f.set(n,t),t.counts)}export function hasRecentCountCache(n,r=Date.now()){const t=readCountCache(n,r),c=f.get(n);return!!t&&r-c.at<COUNT_REFRESH_INTERVAL}export function writeCountCache(n,r,t=Date.now()){const c={};for(const[s,o]of Object.entries(r))!o||!Number.isFinite(o.count)||o.count<0||(c[s]={count:o.count,label:String(o.label||""),tone:o.tone||"info",showZero:o.showZero===!0,inventory:o.inventory===!0});const e={at:t,counts:c};f.set(n,e);try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}}export function loadCountSnapshot(n,r){return l.has(n)||l.set(n,Promise.resolve().then(r).then(t=>(writeCountCache(n,t),t)).finally(()=>l.delete(n))),l.get(n)}export function countSnapshot(n,r){const t=new Map,c=e=>{if(e){e.url&&e.countUrl&&t.set(e.url,!!e.inventory);for(const s of[...e.items||[],...e.children||[]])c(s)}};return r.forEach(e=>c(e.item||e)),Object.fromEntries(Object.entries(n).map(([e,s])=>[e,s?{...s,aggregate:t.has(e)&&!t.get(e)&&!s.inventory}:null]))}export function beginSidebarCounts({user:n,permissions:r,adminScope:t,registered:c,tree:e,actions:s,fetcher:o}){const i={key:countCacheKey(n,r,t,c),registered:c};a=i;const u=readCountCache(i.key)||{};d(e,{...s,counts:countSnapshot(u,c)});const y=(hasRecentCountCache(i.key)?Promise.resolve(u):g().then(()=>a!==i?u:loadCountSnapshot(i.key,o))).then(h=>(a===i&&p(countSnapshot(h,c),{replace:!0}),h)).catch(()=>u);return{counts:u,refresh:y,isActive:()=>a===i}}export function publishSidebarCount(n,r){if(!a)return;p(countSnapshot({[n]:r},a.registered));const t=readCountCache(a.key)||{};r?t[n]=r:delete t[n];const c=f.get(a.key)?.at||Date.now();writeCountCache(a.key,t,c)}
@@ -0,0 +1 @@
1
+ import{apiRequest as u}from"../api.js";export async function requestSidebarCounts(r,{request:o=u,legacy:n,today:e}={}){if(!r.length)return{};if(!e){const t=new Date;e=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`}try{const t=await o("/sidebar/counts",{method:"POST",priority:"low",body:JSON.stringify({urls:[...new Set(r.map(s=>s.url))],today:e})});if(!t?.counts||typeof t.counts!="object"||Array.isArray(t.counts))throw new Error("Invalid sidebar count response");return t.counts}catch(t){if([404,405].includes(t.status)&&n)return n(r);throw t}}
@@ -1,7 +1,7 @@
1
- import{api as h,getUser as F}from"../api.js";import{beginSidebarCounts as ye,publishSidebarCount as ve}from"./sidebar-count-cache.js";import{isPathAllowed as xe}from"./admin-scope.js";import{colourToCss as J,applyFloat as $e,applyHighlight as we,itemHighlightAttrs as Se}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{placeTools as ke,ensureLockedTools as Ce,managePluginsItem as qe,stripItemByUrl as Ae,insertFoldersBeforeSystem as Fe,pruneEmptyToolsFolder as Le,sidebarStateKey as Q,canAny as z,filterByPermission as Ee,MANAGE_PLUGINS_URL as ee}from"./sidebar-grouping.js";import{arrangeButtonMarkup as Be,canArrange as Me,startArrange as Te}from"./sidebar-arrange.js";import{collapseButtonMarkup as He,wireSidebarSize as je,COLLAPSED_WIDTH as Ne}from"./sidebar-size.js";import{FREQUENT_MAX as te,addFrequent as Oe,frequentFolder as Pe,moveFrequent as ne,readFrequent as G,readFrequentHidden as Ie,removeFrequent as Re,saveFrequent as _e,syncFrequent as Ue,writeFrequent as ze,writeFrequentHidden as se}from"./sidebar-frequent.js";import{hotkeyFor as oe,hotkeyTitle as We,setHotkeyTree as De}from"./hotkeys-ui.js";import{hotkeyLabel as Ge}from"./hotkeys.js";function f(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Ke=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}];async function Ve(e){if(!z(e,"projects"))return[];let t=[];try{t=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(t))return[];const s=[{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:"Site 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 t.map(o=>{const d="#/projects/"+encodeURIComponent(o.slug),i=o.counts||{},l=[{text:"Overview",url:d,icon:o.icon||"folder"}];for(const a of s){const r=Number(i[a.key])||0;r<=0||l.push({text:`${a.text} (${r})`,url:d+a.path,icon:a.icon})}l.push({text:"Settings",url:d+"/settings",icon:"settings"});const u=o.enabled===!1;return{text:o.name||o.slug,key:`project.${o.slug}`,icon:o.icon||"folder",items:l,...u&&{colour:"neutral",badge:{text:"off"}}}})}function Xe(e,t){for(const s of e)if(!s.hidden&&(s.key==="projects"||!s.key&&(s.text||"").toLowerCase()==="projects")){s.items=Array.isArray(s.items)?s.items.slice():[],s.items.push(...t);return}}function ie(e,t){const s=re.includes(t.tone)?t.tone:"info",o=t.label||String(t.count);t.details?I.set(e,t.details):I.delete(e);const n=!t.inventory&&L.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${s}" data-count-for="${f(e)}"${n} data-badge-label="${f(o)}" aria-label="${f(o)}">${f(String(t.count))}</span>`}function ae(e){for(const t of e.querySelectorAll("details")){const s=t.querySelector(":scope > summary");if(!s)continue;let o=0,n=!1;const d=[];for(const r of t.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const m=Number(r.textContent)||0;m&&(o+=m,n||(n=r.classList.contains("sidebar-badge--danger")),d.push({text:r.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:r.dataset.badgeLabel,tone:r.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const i=`folder:${t.dataset.stateKey||s.textContent.trim()}`;let l=s.querySelector(":scope > .sidebar-badge--sum");if(!o){l?.remove(),I.delete(i);continue}l||(l=document.createElement("span"),s.querySelector(".sidebar-text")?.after(l));const u=s.querySelector(".sidebar-text")?.textContent.trim()||"This folder",a=d.map(r=>`${r.text}: ${r.meta}`).join(" \xB7 ");l.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,l.dataset.countFor=i,l.dataset.badgeLabel=a,l.setAttribute("aria-label",a),l.textContent=String(o),I.set(i,{title:u,items:d})}}const I=new Map;function Ze(e,t){if(!e.url||!t)return"";const s=t[e.url];return!s||!(s.count>0)&&!s.showZero?"":ie(e.url,s)}const re=["info","attention","danger","live"],Ye={"#/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 K(e,{fallbackLabel:t="",tone:s="info"}={}){const o=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(o)||o<0)return null;const n=typeof e?.label=="string"&&e.label||(t?`${o} - ${t}`:String(o)),d={count:Math.round(o),label:n,tone:re.includes(e?.tone)?e.tone:s};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 le(e,t,s=null,{inFrequent:o=!1}={}){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="theme-switcher")return"";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--${f(b)}" aria-hidden="true"></div>`}const n=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",d=e.icon?`<span data-icon="${f(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${f(n)}</span>`,i=f(e.text||""),l=e.url?f(e.url):"",u=Array.isArray(e.items)&&e.items.length>0,a=e.colour?J(e.colour):"",r=Se(e),m=[r.hover&&` data-menu-item-hover="${f(r.hover)}"`,r.active&&` data-menu-item-active="${f(r.active)}"`].filter(Boolean).join(""),y=[a&&`color:${f(a)}`,r.accent&&`--dm-menu-highlight:${f(r.accent)}`].filter(Boolean).join(";"),v=y?` style="${y}"`:"",w=e.url?We(e.url,e.text||"",{inFrequent:o}):"",k=w?` title="${f(w)}"`:"";let q="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?J(e.badge.variant):"";q=`<span class="dm-menu-badge"${b?` style="background:${f(b)};color:var(--dm-text-inverse,#fff)"`:""}>${f(String(e.badge.text))}</span>`}if(e.frequent&&e.empty){const b=Q(e,s),A=S.get(b)!==!1;return`<details data-state-key="${f(b)}"${A?" open":""} data-frequent data-frequent-empty>
2
- <summary${v}>${d} <span class="sidebar-text">${i}</span></summary>
1
+ import{api as g,getUser as k,isAuthenticated as Se}from"../api.js";import{createRequestQueue as ke}from"./background-requests.js";import{requestSidebarCounts as qe}from"./sidebar-counts.js";import{beginSidebarCounts as Ce,publishSidebarCount as Ae}from"./sidebar-count-cache.js";import{isPathAllowed as Fe}from"./admin-scope.js";import{colourToCss as te,applyFloat as Le,applyHighlight as Ee,itemHighlightAttrs as je}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{placeTools as Be,ensureLockedTools as Oe,managePluginsItem as Me,stripItemByUrl as Te,insertFoldersBeforeSystem as He,pruneEmptyToolsFolder as Ne,sidebarStateKey as ne,canAny as W,filterByPermission as Re,MANAGE_PLUGINS_URL as se}from"./sidebar-grouping.js";import{arrangeButtonMarkup as Ie,canArrange as Pe,startArrange as _e}from"./sidebar-arrange.js";import{collapseButtonMarkup as Ue,wireSidebarSize as ze,COLLAPSED_WIDTH as We}from"./sidebar-size.js";import{FREQUENT_MAX as oe,addFrequent as De,frequentFolder as Ge,moveFrequent as ie,readFrequent as K,readFrequentHidden as Ke,removeFrequent as Ve,saveFrequent as Xe,syncFrequent as Ze,writeFrequent as Je,writeFrequentHidden as ae}from"./sidebar-frequent.js";import{hotkeyFor as re,hotkeyTitle as Ye,setHotkeyTree as Qe}from"./hotkeys-ui.js";import{hotkeyLabel as et}from"./hotkeys.js";function m(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const tt=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}];async function nt(e){if(!W(e,"projects"))return[];let n=[];try{n=await g.projects.listWithCounts()}catch{return[]}if(!Array.isArray(n))return[];const t=[{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:"Site 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(o=>{const l="#/projects/"+encodeURIComponent(o.slug),u=o.counts||{},i=[{text:"Overview",url:l,icon:o.icon||"folder"}];for(const d of t){const r=Number(u[d.key])||0;r<=0||i.push({text:`${d.text} (${r})`,url:l+d.path,icon:d.icon})}i.push({text:"Settings",url:l+"/settings",icon:"settings"});const a=o.enabled===!1;return{text:o.name||o.slug,key:`project.${o.slug}`,icon:o.icon||"folder",items:i,...a&&{colour:"neutral",badge:{text:"off"}}}})}function st(e,n){for(const t of e)if(!t.hidden&&(t.key==="projects"||!t.key&&(t.text||"").toLowerCase()==="projects")){t.items=Array.isArray(t.items)?t.items.slice():[],t.items.push(...n);return}}function le(e,n){const t=de.includes(n.tone)?n.tone:"info",o=n.label||String(n.count);n.details?R.set(e,n.details):R.delete(e);const s=!n.inventory&&L.some(l=>l.url===e&&!l.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${t}" data-count-for="${m(e)}"${s} data-badge-label="${m(o)}" aria-label="${m(o)}">${m(String(n.count))}</span>`}function ce(e){for(const n of e.querySelectorAll("details")){const t=n.querySelector(":scope > summary");if(!t)continue;let o=0,s=!1;const l=[];for(const r of n.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const f=Number(r.textContent)||0;f&&(o+=f,s||(s=r.classList.contains("sidebar-badge--danger")),l.push({text:r.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:r.dataset.badgeLabel,tone:r.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const u=`folder:${n.dataset.stateKey||t.textContent.trim()}`;let i=t.querySelector(":scope > .sidebar-badge--sum");if(!o){i?.remove(),R.delete(u);continue}i||(i=document.createElement("span"),t.querySelector(".sidebar-text")?.after(i));const a=t.querySelector(".sidebar-text")?.textContent.trim()||"This folder",d=l.map(r=>`${r.text}: ${r.meta}`).join(" \xB7 ");i.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${s?"danger":"info"}`,i.dataset.countFor=u,i.dataset.badgeLabel=d,i.setAttribute("aria-label",d),i.textContent=String(o),R.set(u,{title:a,items:l})}}const R=new Map;function ot(e,n){if(!e.url||!n)return"";const t=n[e.url];return!t||!(t.count>0)&&!t.showZero?"":le(e.url,t)}const de=["info","attention","danger","live"],it={"#/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 D(e,{fallbackLabel:n="",tone:t="info"}={}){const o=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(o)||o<0)return null;const s=typeof e?.label=="string"&&e.label||(n?`${o} - ${n}`:String(o)),l={count:Math.round(o),label:s,tone:de.includes(e?.tone)?e.tone:t};return e?.details&&typeof e.details=="object"&&(l.details=e.details),e?.inventory===!0&&(l.inventory=!0),e?.showZero===!0&&(l.showZero=!0),l}function ue(e,n,t=null,{inFrequent:o=!1}={}){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="theme-switcher")return"";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--${m(b)}" aria-hidden="true"></div>`}const s=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",l=e.icon?`<span data-icon="${m(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${m(s)}</span>`,u=m(e.text||""),i=e.url?m(e.url):"",a=Array.isArray(e.items)&&e.items.length>0,d=e.colour?te(e.colour):"",r=je(e),f=[r.hover&&` data-menu-item-hover="${m(r.hover)}"`,r.active&&` data-menu-item-active="${m(r.active)}"`].filter(Boolean).join(""),h=[d&&`color:${m(d)}`,r.accent&&`--dm-menu-highlight:${m(r.accent)}`].filter(Boolean).join(";"),v=h?` style="${h}"`:"",w=e.url?Ye(e.url,e.text||"",{inFrequent:o}):"",q=w?` title="${m(w)}"`:"";let F="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?te(e.badge.variant):"";F=`<span class="dm-menu-badge"${b?` style="background:${m(b)};color:var(--dm-text-inverse,#fff)"`:""}>${m(String(e.badge.text))}</span>`}if(e.frequent&&e.empty){const b=ne(e,t),A=S.get(b)!==!1;return`<details data-state-key="${m(b)}"${A?" open":""} data-frequent data-frequent-empty>
2
+ <summary${v}>${l} <span class="sidebar-text">${u}</span></summary>
3
3
  <div class="sidebar-children"><p class="sidebar-frequent-empty">Right-click any Tool and choose <strong>Add to Frequent</strong> to keep it here.</p></div>
4
- </details>`}if(u){const b=Q(e,s),A=S.get(b)!==!1,B=e.items.map(N=>le(N,t,b,{inFrequent:!!e.frequent})).join(""),j=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${f(b)}"${A?" open":""}${j}>
5
- <summary${v}${k}>${d} <span class="sidebar-text">${i}</span>${q}</summary>
4
+ </details>`}if(a){const b=ne(e,t),A=S.get(b)!==!1,B=e.items.map(I=>ue(I,n,b,{inFrequent:!!e.frequent})).join(""),j=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${m(b)}"${A?" open":""}${j}>
5
+ <summary${v}${q}>${l} <span class="sidebar-text">${u}</span>${F}</summary>
6
6
  <div class="sidebar-children">${B}</div>
7
- </details>`}return`<a href="${l}" class="sidebar-link" data-url="${l}"${v}${m}${k}>${d} <span class="sidebar-text">${i}</span>${Ze(e,t)}${q}</a>`}async function Je(e=[],t=[],s=null){const o=g=>g.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0);L=tt(e,s);const n=new Set(L.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"},i=(g,x)=>z(t,g)&&!n.has(d[g])?x():Promise.resolve(0),[l,u,a,r,m,y,v,w,k,q,b,A,B,j]=await Promise.all([i("pages",()=>o(h.pages.list())),i("media",()=>o(h.media.list())),i("menus",()=>o(h.menus.list())),i("context-menus",()=>o(h.contextMenus.list())),i("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),i("collections",()=>o(h.collections.list())),i("forms",()=>o(h.forms.list())),i("views",()=>o(h.views.list())),i("actions",()=>o(h.actions.list())),i("blocks",()=>o(h.blocks.list())),i("components",()=>o(h.components.list())),i("users",()=>o(h.users.list())),i("plugins",()=>o(h.plugins.list())),i("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),N={};await Promise.all(L.map(async g=>{const x=await ce(g);x&&(N[g.url]=x)}));const U={"#/pages":l,"#/media":u,"#/menus":a,"#/context-menus":r,"#/theme":m,"#/collections":y,"#/forms":v,"#/views":w,"#/actions":k,"#/blocks":q,"#/components":b,"#/users":A,"#/plugins":B,"#/system/notifications":j},R={};for(const[g,x]of Object.entries(U)){if(n.has(g))continue;const[W,D]=Ye[g]??["item","items"],T=K(x);T&&(R[g]={...T,label:`${T.count} ${T.count===1?W:D}`})}return{...R,...N}}let L=[];const Qe="dm:sidebar-count",et=6e4;function tt(e=[],t=null){const s=[],o=n=>{if(n&&!(t&&typeof n.url=="string"&&n.url.startsWith("#/")&&!xe(t,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&s.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||[]])o(d)}};for(const n of e??[])o(n?.item??n);return s}async function ce(e){try{const t=new Date,s=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`,o=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${s}`;return K(await H.get(o),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function de(e,t,s){ve(t,s);const o=[...e.querySelectorAll("a.sidebar-link")].filter(n=>n.dataset.url===t);if(o.length){for(const n of o){const d=n.querySelector(".sidebar-badge[data-count-for]");if(!s||!(s.count>0)&&!s.showZero)d?.remove();else{const i=document.createRange();i.selectNodeContents(n);const l=i.createContextualFragment(ie(t,s)).firstElementChild;d?d.replaceWith(l):n.querySelector(".sidebar-text")?.after(l)}}V.set(t,s),ae(e)}}const V=new Map;async function X(e,t=null){const s=t?L.filter(o=>o.url===t):L;await Promise.all(s.map(async o=>{const n=await ce(o),d=V.get(o.url);JSON.stringify(d??null)!==JSON.stringify(n)&&de(e,o.url,n)}))}function nt(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(Qe,t=>{const s=t.detail||{};if(s.url)if(s.count!==void 0){const o=L.find(n=>n.url===s.url);de(e,s.url,K(s,{fallbackLabel:o?.countLabel||o?.text||""}))}else X(e,s.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&X(e)},et),document.addEventListener("visibilitychange",()=>{document.hidden||X(e)}),st(e))}function ue(e,t,s){const o=(i,l,u)=>{const a=document.createElement(i);return l&&(a.className=l),u!=null&&(a.textContent=String(u)),a};if(!s){e.textContent=t;return}e.appendChild(o("strong","dm-badge-pop-title",s.title||t)),(s.rows?.length||!s.items?.length)&&e.appendChild(o("p","dm-badge-pop-sub",t));const n=Array.isArray(s.rows)?s.rows.slice(0,6):[];if(n.length){const i=o("dl","dm-badge-pop-rows");for(const l of n)i.appendChild(o("dt",null,l.label)),i.appendChild(o("dd",l.tone==="danger"?"is-danger":null,l.value));e.appendChild(i)}const d=Array.isArray(s.items)?s.items.slice(0,6):[];if(d.length){const i=o("ul","dm-badge-pop-items");for(const l of d){const u=o("li",l.tone==="danger"?"is-danger":null);u.appendChild(o("span","dm-badge-pop-text",l.text)),l.meta&&u.appendChild(o("span","dm-badge-pop-meta",l.meta)),i.appendChild(u)}e.appendChild(i)}else s.empty&&e.appendChild(o("p","dm-badge-pop-empty",s.empty))}function st(e){let t=null,s=null;const o=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(s),t?.remove(),t=null},d=(a,r)=>{n(),s=setTimeout(()=>{t=document.createElement("div"),t.className="dm-help-pop dm-badge-pop",t.setAttribute("role","tooltip"),r(t),document.body.appendChild(t);const m=a.getBoundingClientRect(),y=t.getBoundingClientRect(),v=Math.min(m.right+10,innerWidth-y.width-8);t.style.left=`${Math.max(8,v)}px`,t.style.top=`${Math.max(8,Math.min(m.top+m.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},i=a=>{const r=a?.dataset.badgeLabel;if(!r)return n();d(a,m=>ue(m,r,I.get(a.dataset.countFor)))},l=a=>{const r=a.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",m=a.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(a,y=>{const v=m&&I.get(m.dataset.countFor);if(v)return ue(y,m.dataset.badgeLabel,{...v,title:v.title||r});if(!m){y.textContent=r;return}const w=document.createElement("strong");w.className="dm-badge-pop-title",w.textContent=r;const k=document.createElement("p");k.className="dm-badge-pop-sub",k.textContent=m.dataset.badgeLabel,y.append(w,k)})},u=a=>{const r=a.closest?.("a.sidebar-link, summary");return r&&e.contains(r)?r:null};e.addEventListener("mouseover",a=>{if(o()){const m=u(a.target);m&&!m.contains(a.relatedTarget)&&l(m);return}const r=a.target.closest?.(".sidebar-badge[data-badge-label]");r&&i(r)}),e.addEventListener("mouseout",a=>{if(o()){const r=u(a.target);r&&!r.contains(a.relatedTarget)&&n();return}a.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",a=>{const r=a.target.closest?.("a, summary");if(o()&&r&&e.contains(r)&&r.querySelector(":scope > .sidebar-text"))return l(r);const m=r?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");m?i(m):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function ot(e=[],t=null){if(t?.title)return t;if(!z(e,"settings"))return{title:"Domma CMS"};try{const s=await h.settings.get();return{...s?.adminBrand,title:s?.adminBrand?.title||s?.title||"Domma CMS"}}catch{return{title:"Domma CMS"}}}export async function renderAdminSidebar({mount:e,permissions:t,adminScope:s=null,brand:o=null}){const n=$(e).get(0);if(!n)return;const d=Ue(h,F()).catch(()=>{});let i,l=null,u=null,a=null,r=null,m=null;try{const c=await h.get("/sidebar/menu");i=Array.isArray(c?.items)?c.items:null,l=c?.variant||null,u=c?.position||null,a=c?.style||null,r=c?.float||null,m=c?.highlight||null}catch{i=null}(!i||!i.some(c=>c&&(!c.hidden||c.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=Ke.slice());let y=[];try{y=await H.get("/api/sidebar/registered-items")||[]}catch{}i=Ae(i,ee),i=ke(i,y.filter(c=>(c?.item??c)?.url!==ee)),i=Ce(i),i=Fe(i,[qe()]);const v=await Ve(t);v.length&&Xe(i,v),i=Ee(i,t,s),i=Le(i),await d;const w=Pe(i,G(F()),{hidden:Ie(F())});w&&(i=[w,...i]),De(i);const k=y.filter(c=>z(t,c?.item?.permission)),[q,b]=await Promise.all([ye({user:F(),permissions:t,adminScope:s,registered:k,tree:i,actions:{linkItems:pe,folderItems:he},fetcher:()=>Je(k,t,s)}),ot(t,o)]),A=l?` dm-admin-sidebar--${f(l)}`:"";function B(c,C="px"){if(c==null)return c;const p=String(c).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${C}`:p}let j="";if(a){const c=[];a.fontFamily&&c.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&c.push(`font-size: ${B(a.fontSize)}`),a.fontWeight&&c.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&c.push(`letter-spacing: ${B(a.letterSpacing,"em")}`);const C=[];if(c.length&&C.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 { ${c.join("; ")} }`),a.iconSize){const p=B(a.iconSize);C.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}C.length&&(j=`<style data-admin-sidebar-style>${C.join(" ")}</style>`)}const N=Me(t)?Be():"",U=String(b.logo||"").trim(),R=/^(?:\/(?!\/)|https?:\/\/)/i.test(U)?U:"",x=`<div class="dm-admin-sidebar-header">${R?`<img src="${f(R)}" alt="" style="width:28px;height:28px;object-fit:contain;flex-shrink:0;">`:`<span data-icon="${f(b.icon||"layout")}"></span>`}<span class="dm-admin-sidebar-title">${f(b.title)}</span>${N}${He()}</div>`,W=i.some(c=>c&&c.type==="spacer"&&c.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",D=`${j}<nav class="dm-admin-sidebar${A}${W}">${x}${i.map(c=>le(c,q)).join("")}</nav>`,T=document.createRange();T.selectNodeContents(n);const ge=T.createContextualFragment(D);n.replaceChildren(ge),Domma.icons.scan(n),n.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>Te(n));for(const c of L)V.set(c.url,q[c.url]??null);ae(n),nt(n),n.removeAttribute("data-menu-hover"),n.removeAttribute("data-menu-active"),n.style.removeProperty("--dm-menu-highlight"),we(n,{highlight:m});const O=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(c=>{n.style[c]=""}),n.classList.remove("dm-menu-floating"),O&&(O.style.paddingLeft="",O.style.paddingRight=""),n.__dmGutter=()=>{if(u!=="floating"||!O)return;const c=r&&r.anchor||"TL",C=Number.isFinite(Number(r?.offsetX))?Number(r.offsetX):16,p=n.classList.contains("is-collapsed")?Ne:parseInt(n.style.width,10)||n.getBoundingClientRect().width||260,P=Math.round(p+C*2)+"px";c==="R"||c==="TR"||c==="BR"?O.style.paddingRight=P:O.style.paddingLeft=P},u==="floating"&&$e(n,{position:"floating",float:r||{}}),n.querySelectorAll("details").forEach(c=>{c.addEventListener("toggle",function(){const C=this.getAttribute("data-state-key");C&&S.set(C,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const P=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=P+"px",p.addEventListener("transitionend",function be(){p.style.maxHeight="none",p.removeEventListener("transitionend",be)})})}else{const P=p.scrollHeight;p.style.maxHeight=P+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),je(n),at(n),n.dataset.routeWired||(n.dataset.routeWired="1",M.subscribe("router:afterChange",()=>n.__dmSetActive?.()));function Y(){const c=/^#\/(dashboards|my-dashboard)\b/.test(location.hash)?"#/":location.hash||"#/";$(n).find(".sidebar-link").removeClass("active"),$(n).find(`.sidebar-link[data-url="${c}"]`).addClass("active")}Y(),n.__dmSetActive=Y}function me(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function Z(e){return`${location.origin}${location.pathname}${e}`}function _(e,t){const s=F(),o=e(G(s));if(!ze(s,o)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}o.length&&se(s,!1),fe(s),t&&E.toast(t,{type:"success",duration:2500}),me()}function fe(e){_e(h,e).catch(()=>E.toast("Frequent is saved in this browser only - the server could not keep it.",{type:"warning"}))}function it(e){if(!se(F(),e)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}fe(F()),E.toast(e?"Frequent hidden. Add a Tool to it from its right-click menu to bring it back.":"Frequent shown.",{type:"success",duration:3500}),me()}function pe(e){const t=e.dataset.url;if(!t)return[];const s=e.querySelector(".sidebar-text")?.textContent.trim()||t,o=G(F()),n=o.includes(t),d=!!e.closest("[data-frequent]"),i=o.indexOf(t),l=!n&&o.length>=te;return[{header:s},{label:"Open",icon:"arrow-right",...oe(t)&&{shortcut:Ge(oe(t))},action:()=>{location.hash=t}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(Z(t),"_blank","noopener")},{type:"divider"},n?{label:"Remove from Frequent",icon:"star",action:()=>_(u=>Re(u,t),`${s} removed from Frequent.`)}:{label:l?`Frequent is full (${te})`:"Add to Frequent",icon:"star",disabled:l,action:()=>_(u=>Oe(u,t),`${s} added to Frequent.`)},...d?[{label:"Move up",icon:"arrow-up",disabled:i<=0,action:()=>_(u=>ne(u,t,-1))},{label:"Move down",icon:"arrow-down",disabled:i<0||i>=o.length-1,action:()=>_(u=>ne(u,t,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(Z(t)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(Z(t),{type:"info",duration:8e3})}}}]}function he(e,t,s={}){const o=e.closest("details, [data-frequent]"),n=s.isOpen||(()=>!!o?.open),d=s.setOpen||(u=>{o&&(o.open=u)}),i=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",l=s.setAll||(u=>t.querySelectorAll("details").forEach(a=>{a.open!==u&&(a.open=u)}));return[{header:i},{label:n()?"Close":"Open",icon:n()?"chevron-up":"chevron-down",action:()=>d(!n())},...s.singleBranch?[]:[{label:"Open every folder",icon:"chevrons-down",action:()=>l(!0)}],{label:s.singleBranch?"Close all menus":"Close every folder",icon:"chevrons-up",action:()=>l(!1)},...o?.hasAttribute("data-frequent-empty")?[{type:"divider"},{label:"Hide Frequent until I add a Tool",icon:"eye-off",action:()=>it(!0)}]:o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>_(()=>[],"Frequent cleared.")}]:[]]}function at(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:t=>t.matches("summary")?he(t,e):pe(t)}))}
7
+ </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${v}${f}${q}>${l} <span class="sidebar-text">${u}</span>${ot(e,n)}${F}</a>`}async function at(e=[],n=[],t=null){const o=y=>y.then(C=>Array.isArray(C)?C.length:Array.isArray(C?.entries)?C.entries.length:0).catch(()=>0),s=fe(e,t),l=new Set(s.map(y=>y.url)),u={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"},i=(y,C)=>W(n,y)&&!l.has(u[y])?C():Promise.resolve(0),[a,d,r,f,h,v,w,q,F,b,A,B,j,I]=await Promise.all([i("pages",()=>o(g.pages.list())),i("media",()=>o(g.media.list())),i("menus",()=>o(g.menus.list())),i("context-menus",()=>o(g.contextMenus.list())),i("theme",()=>g.theme.get().then(y=>(y?.overrides||[]).length).catch(()=>0)),i("collections",()=>o(g.collections.list())),i("forms",()=>o(g.forms.list())),i("views",()=>o(g.views.list())),i("actions",()=>o(g.actions.list())),i("blocks",()=>o(g.blocks.list())),i("components",()=>o(g.components.list())),i("users",()=>o(g.users.list())),i("plugins",()=>o(g.plugins.list())),i("notifications",()=>g.system?.notifications?.unreadCount?.().then(y=>y?.count??0).catch(()=>0)??Promise.resolve(0))]),P=await pe(s),U={"#/pages":a,"#/media":d,"#/menus":r,"#/context-menus":f,"#/theme":h,"#/collections":v,"#/forms":w,"#/views":q,"#/actions":F,"#/blocks":b,"#/components":A,"#/users":B,"#/plugins":j,"#/system/notifications":I},z={};for(const[y,C]of Object.entries(U)){if(l.has(y))continue;const[Y,G]=it[y]??["item","items"],O=D(C);O&&(z[y]={...O,label:`${O.count} ${O.count===1?Y:G}`})}return{...z,...P}}let L=[];const rt="dm:sidebar-count",lt=6e4,ct=ke(4);function fe(e=[],n=null){const t=[],o=s=>{if(s&&!(n&&typeof s.url=="string"&&s.url.startsWith("#/")&&!Fe(n,s.url.slice(1)))){s.url&&typeof s.countUrl=="string"&&s.countUrl&&t.push({url:s.url,countUrl:s.countUrl,text:s.text||"",countLabel:s.countLabel||"",inventory:s.inventory===!0});for(const l of[...s.children||[],...s.items||[]])o(l)}};for(const s of e??[])o(s?.item??s);return t}async function me(e,{priority:n=!1}={}){try{const t=new Date,o=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`,s=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${o}`,l=k()?.id,u=await ct(()=>!Se()||k()?.id!==l?null:H.get(s,{priority:"low"}),{key:JSON.stringify([l,s]),priority:n});return D(u,{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,n,t){Ae(n,t);const o=[...e.querySelectorAll("a.sidebar-link")].filter(s=>s.dataset.url===n);if(o.length){for(const s of o){const l=s.querySelector(".sidebar-badge[data-count-for]");if(!t||!(t.count>0)&&!t.showZero)l?.remove();else{const u=document.createRange();u.selectNodeContents(s);const i=u.createContextualFragment(le(n,t)).firstElementChild;l?l.replaceWith(i):s.querySelector(".sidebar-text")?.after(i)}}X.set(n,t),ce(e)}}const X=new Map;async function pe(e){const n=await qe(e,{legacy:async t=>{const o=await Promise.all(t.map(async s=>[s.url,await me(s)]));return Object.fromEntries(o)}});return Object.fromEntries(e.map(t=>[t.url,n[t.url]==null?null:D(n[t.url],{fallbackLabel:t.countLabel||t.text})]))}async function Z(e,n=null){const t=k()?.id,o=L;try{const s=n?{[n]:await me(L.find(l=>l.url===n),{priority:!0})}:await pe(L);if(k()?.id!==t||L!==o)return;for(const[l,u]of Object.entries(s)){const i=X.get(l);JSON.stringify(i??null)!==JSON.stringify(u)&&V(e,l,u)}}catch{}}function dt(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(rt,n=>{const t=n.detail||{};if(t.url)if(t.count!==void 0){const o=L.find(s=>s.url===t.url);V(e,t.url,D(t,{fallbackLabel:o?.countLabel||o?.text||""}))}else Z(e,t.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&Z(e)},lt),document.addEventListener("visibilitychange",()=>{document.hidden||Z(e)}),ut(e))}function he(e,n,t){const o=(u,i,a)=>{const d=document.createElement(u);return i&&(d.className=i),a!=null&&(d.textContent=String(a)),d};if(!t){e.textContent=n;return}e.appendChild(o("strong","dm-badge-pop-title",t.title||n)),(t.rows?.length||!t.items?.length)&&e.appendChild(o("p","dm-badge-pop-sub",n));const s=Array.isArray(t.rows)?t.rows.slice(0,6):[];if(s.length){const u=o("dl","dm-badge-pop-rows");for(const i of s)u.appendChild(o("dt",null,i.label)),u.appendChild(o("dd",i.tone==="danger"?"is-danger":null,i.value));e.appendChild(u)}const l=Array.isArray(t.items)?t.items.slice(0,6):[];if(l.length){const u=o("ul","dm-badge-pop-items");for(const i of l){const a=o("li",i.tone==="danger"?"is-danger":null);a.appendChild(o("span","dm-badge-pop-text",i.text)),i.meta&&a.appendChild(o("span","dm-badge-pop-meta",i.meta)),u.appendChild(a)}e.appendChild(u)}else t.empty&&e.appendChild(o("p","dm-badge-pop-empty",t.empty))}function ut(e){let n=null,t=null;const o=()=>e.classList.contains("is-collapsed"),s=()=>{clearTimeout(t),n?.remove(),n=null},l=(d,r)=>{s(),t=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),r(n),document.body.appendChild(n);const f=d.getBoundingClientRect(),h=n.getBoundingClientRect(),v=Math.min(f.right+10,innerWidth-h.width-8);n.style.left=`${Math.max(8,v)}px`,n.style.top=`${Math.max(8,Math.min(f.top+f.height/2-h.height/2,innerHeight-h.height-8))}px`},150)},u=d=>{const r=d?.dataset.badgeLabel;if(!r)return s();l(d,f=>he(f,r,R.get(d.dataset.countFor)))},i=d=>{const r=d.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",f=d.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l(d,h=>{const v=f&&R.get(f.dataset.countFor);if(v)return he(h,f.dataset.badgeLabel,{...v,title:v.title||r});if(!f){h.textContent=r;return}const w=document.createElement("strong");w.className="dm-badge-pop-title",w.textContent=r;const q=document.createElement("p");q.className="dm-badge-pop-sub",q.textContent=f.dataset.badgeLabel,h.append(w,q)})},a=d=>{const r=d.closest?.("a.sidebar-link, summary");return r&&e.contains(r)?r:null};e.addEventListener("mouseover",d=>{if(o()){const f=a(d.target);f&&!f.contains(d.relatedTarget)&&i(f);return}const r=d.target.closest?.(".sidebar-badge[data-badge-label]");r&&u(r)}),e.addEventListener("mouseout",d=>{if(o()){const r=a(d.target);r&&!r.contains(d.relatedTarget)&&s();return}d.target.closest?.(".sidebar-badge[data-badge-label]")&&s()}),e.addEventListener("focusin",d=>{const r=d.target.closest?.("a, summary");if(o()&&r&&e.contains(r)&&r.querySelector(":scope > .sidebar-text"))return i(r);const f=r?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");f?u(f):s()}),e.addEventListener("focusout",s),e.addEventListener("click",s),window.addEventListener("scroll",s,!0),window.addEventListener("hashchange",s)}async function ft(e=[],n=null){if(n?.title)return n;if(!W(e,"settings"))return{title:"Domma CMS"};try{const t=await g.settings.get();return{...t?.adminBrand,title:t?.adminBrand?.title||t?.title||"Domma CMS"}}catch{return{title:"Domma CMS"}}}export async function renderAdminSidebar({mount:e,permissions:n,adminScope:t=null,brand:o=null}){const s=$(e).get(0);if(!s)return;const l=Ze(g,k()).catch(()=>{}),u=Promise.resolve().then(()=>H.get("/api/sidebar/registered-items")).catch(()=>[]),i=nt(n);let a,d=null,r=null,f=null,h=null,v=null;try{const c=await g.get("/sidebar/menu");a=Array.isArray(c?.items)?c.items:null,d=c?.variant||null,r=c?.position||null,f=c?.style||null,h=c?.float||null,v=c?.highlight||null}catch{a=null}(!a||!a.some(c=>c&&(!c.hidden||c.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),a=tt.slice());let w=[];try{w=await u||[]}catch{}a=Te(a,se),a=Be(a,w.filter(c=>(c?.item??c)?.url!==se)),a=Oe(a),a=He(a,[Me()]);const q=await i;q.length&&st(a,q),a=Re(a,n,t),a=Ne(a),await l;const F=Ge(a,K(k()),{hidden:Ke(k())});F&&(a=[F,...a]),Qe(a);const b=w.filter(c=>W(n,c?.item?.permission));L=fe(b,t);const A=Ce({user:k(),permissions:n,adminScope:t,registered:b,tree:a,actions:{linkItems:ye,folderItems:ve},fetcher:()=>at(b,n,t)}),B=A.counts,j=await ft(n,o),I=d?` dm-admin-sidebar--${m(d)}`:"";function P(c,x="px"){if(c==null)return c;const p=String(c).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${x}`:p}let U="";if(f){const c=[];f.fontFamily&&c.push(`font-family: ${f.fontFamily}, sans-serif`),f.fontSize&&c.push(`font-size: ${P(f.fontSize)}`),f.fontWeight&&c.push(`font-weight: ${f.fontWeight}`),f.letterSpacing&&c.push(`letter-spacing: ${P(f.letterSpacing,"em")}`);const x=[];if(c.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 { ${c.join("; ")} }`),f.iconSize){const p=P(f.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}x.length&&(U=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const z=Pe(n)?Ie():"",y=String(j.logo||"").trim(),C=/^(?:\/(?!\/)|https?:\/\/)/i.test(y)?y:"",G=`<div class="dm-admin-sidebar-header">${C?`<img src="${m(C)}" alt="" style="width:28px;height:28px;object-fit:contain;flex-shrink:0;">`:`<span data-icon="${m(j.icon||"layout")}"></span>`}<span class="dm-admin-sidebar-title">${m(j.title)}</span>${z}${Ue()}</div>`,O=a.some(c=>c&&c.type==="spacer"&&c.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",xe=`${U}<nav class="dm-admin-sidebar${I}${O}">${G}${a.map(c=>ue(c,B)).join("")}</nav>`,Q=document.createRange();Q.selectNodeContents(s);const $e=Q.createContextualFragment(xe);s.replaceChildren($e),Domma.icons.scan(s),s.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>_e(s));for(const c of L)X.set(c.url,B[c.url]??null);ce(s),dt(s),A.refresh.then(c=>{if(!(!A.isActive()||!s.isConnected))for(const[x,p]of Object.entries(c))V(s,x,p)}),s.removeAttribute("data-menu-hover"),s.removeAttribute("data-menu-active"),s.style.removeProperty("--dm-menu-highlight"),Ee(s,{highlight:v});const T=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(c=>{s.style[c]=""}),s.classList.remove("dm-menu-floating"),T&&(T.style.paddingLeft="",T.style.paddingRight=""),s.__dmGutter=()=>{if(r!=="floating"||!T)return;const c=h&&h.anchor||"TL",x=Number.isFinite(Number(h?.offsetX))?Number(h.offsetX):16,p=s.classList.contains("is-collapsed")?We:parseInt(s.style.width,10)||s.getBoundingClientRect().width||260,N=Math.round(p+x*2)+"px";c==="R"||c==="TR"||c==="BR"?T.style.paddingRight=N:T.style.paddingLeft=N},r==="floating"&&Le(s,{position:"floating",float:h||{}}),s.querySelectorAll("details").forEach(c=>{c.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const N=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=N+"px",p.addEventListener("transitionend",function we(){p.style.maxHeight="none",p.removeEventListener("transitionend",we)})})}else{const N=p.scrollHeight;p.style.maxHeight=N+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),ze(s),pt(s),s.dataset.routeWired||(s.dataset.routeWired="1",M.subscribe("router:afterChange",()=>s.__dmSetActive?.()));function ee(){const c=/^#\/(dashboards|my-dashboard)\b/.test(location.hash)?"#/":location.hash||"#/";$(s).find(".sidebar-link").removeClass("active"),$(s).find(`.sidebar-link[data-url="${c}"]`).addClass("active")}ee(),s.__dmSetActive=ee}function ge(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function J(e){return`${location.origin}${location.pathname}${e}`}function _(e,n){const t=k(),o=e(K(t));if(!Je(t,o)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}o.length&&ae(t,!1),be(t),n&&E.toast(n,{type:"success",duration:2500}),ge()}function be(e){Xe(g,e).catch(()=>E.toast("Frequent is saved in this browser only - the server could not keep it.",{type:"warning"}))}function mt(e){if(!ae(k(),e)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}be(k()),E.toast(e?"Frequent hidden. Add a Tool to it from its right-click menu to bring it back.":"Frequent shown.",{type:"success",duration:3500}),ge()}function ye(e){const n=e.dataset.url;if(!n)return[];const t=e.querySelector(".sidebar-text")?.textContent.trim()||n,o=K(k()),s=o.includes(n),l=!!e.closest("[data-frequent]"),u=o.indexOf(n),i=!s&&o.length>=oe;return[{header:t},{label:"Open",icon:"arrow-right",...re(n)&&{shortcut:et(re(n))},action:()=>{location.hash=n}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(J(n),"_blank","noopener")},{type:"divider"},s?{label:"Remove from Frequent",icon:"star",action:()=>_(a=>Ve(a,n),`${t} removed from Frequent.`)}:{label:i?`Frequent is full (${oe})`:"Add to Frequent",icon:"star",disabled:i,action:()=>_(a=>De(a,n),`${t} added to Frequent.`)},...l?[{label:"Move up",icon:"arrow-up",disabled:u<=0,action:()=>_(a=>ie(a,n,-1))},{label:"Move down",icon:"arrow-down",disabled:u<0||u>=o.length-1,action:()=>_(a=>ie(a,n,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(J(n)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(J(n),{type:"info",duration:8e3})}}}]}function ve(e,n,t={}){const o=e.closest("details, [data-frequent]"),s=t.isOpen||(()=>!!o?.open),l=t.setOpen||(a=>{o&&(o.open=a)}),u=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",i=t.setAll||(a=>n.querySelectorAll("details").forEach(d=>{d.open!==a&&(d.open=a)}));return[{header:u},{label:s()?"Close":"Open",icon:s()?"chevron-up":"chevron-down",action:()=>l(!s())},...t.singleBranch?[]:[{label:"Open every folder",icon:"chevrons-down",action:()=>i(!0)}],{label:t.singleBranch?"Close all menus":"Close every folder",icon:"chevrons-up",action:()=>i(!1)},...o?.hasAttribute("data-frequent-empty")?[{type:"divider"},{label:"Hide Frequent until I add a Tool",icon:"eye-off",action:()=>mt(!0)}]:o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>_(()=>[],"Frequent cleared.")}]:[]]}function pt(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")?ve(n,e):ye(n)}))}
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{dashboardsView as i}from"./dashboards.js";import{pagesView as r}from"./pages.js";import{pageEditorView as e}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as m}from"./theme.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as s}from"./layouts.js";import{mediaView as n}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 d}from"./documentation.js";import{tutorialsView as l}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 h}from"./api-reference.js";import{collectionsView as v}from"./collections.js";import{collectionEditorView as b}from"./collection-editor.js";import{collectionEntriesView as y}from"./collection-entries.js";import{formsView as j}from"./forms.js";import{formEditorView as L}from"./form-editor.js";import{formSubmissionsView as x}from"./form-submissions.js";import{viewsListView as M}from"./views-list.js";import{viewEditorView as k}from"./view-editor.js";import{viewPreviewView as D}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{componentsView as A}from"./components.js";import{myProfileView as C}from"./my-profile.js";import{rolesView as I}from"./roles.js";import{roleEditorView as N}from"./role-editor.js";import{effectsView as T}from"./effects.js";import{searchView as U}from"./search.js";import{contentApiView as q}from"./content-api.js";import{notesView as z}from"./notes.js";import{todoView as B}from"./todo.js";import{calculatorView as F}from"../lib/calculator.js";import{analyticsView as H}from"./analytics.js";import{seoView as J}from"./seo.js";import{wordpressImportView as K}from"./wordpress-import.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 eo}from"./api-endpoints.js";import{apiEndpointEditorView as to}from"./api-endpoint-editor.js";const mo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:$,projectDetail:oo,projectSettings:io,apiTokens:ro,apiEndpoints:eo,apiEndpointEditor:to,dashboard:o,dashboards:i,pages:r,pageEditor:e,settings:t,theme:m,layouts:s,media:n,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:h,collections:v,collectionEditor:b,collectionEntries:y,forms:j,formEditor:L,formSubmissions:x,viewsList:M,viewEditor:k,viewPreview:D,actionsList:G,actionEditor:P,proDocs:R,blocks:S,components:A,myProfile:C,roles:I,roleEditor:N,effects:T,search:U,contentApi:q,notes:z,todo:B,calculator:F,analytics:H,seo:J,wordpressImport:K,contacts:O,callNew:Q,notifications:p,menus:W,contextMenus:X,contextMenuEditor:Y,menuEditor:Z,menuLocations:_,menusRedirect:mo,pluginGuides:E,pluginGuide:g,...u};
1
+ const t={templateUrl:"",async onMount(){location.hash="#/menus"}};export const viewLoaders={projects:()=>import("./projects.js").then(e=>e.projectsView),projectDetail:()=>import("./project-detail.js").then(e=>e.projectDetailView),projectSettings:()=>import("./project-settings.js").then(e=>e.projectSettingsView),apiTokens:()=>import("./api-tokens.js").then(e=>e.apiTokensView),apiEndpoints:()=>import("./api-endpoints.js").then(e=>e.apiEndpointsView),apiEndpointEditor:()=>import("./api-endpoint-editor.js").then(e=>e.apiEndpointEditorView),dashboard:()=>import("./dashboard.js").then(e=>e.dashboardView),dashboards:()=>import("./dashboards.js").then(e=>e.dashboardsView),pages:()=>import("./pages.js").then(e=>e.pagesView),pageEditor:()=>import("./page-editor.js").then(e=>e.pageEditorView),settings:()=>import("./settings.js").then(e=>e.settingsView),theme:()=>import("./theme.js").then(e=>e.themeView),layouts:()=>import("./layouts.js").then(e=>e.layoutsView),media:()=>import("./media.js").then(e=>e.mediaView),login:()=>import("./login.js").then(e=>e.loginView),users:()=>import("./users.js").then(e=>e.usersView),userEditor:()=>import("./user-editor.js").then(e=>e.userEditorView),plugins:()=>import("./plugins.js").then(e=>e.pluginsView),pluginCode:()=>import("./plugin-code.js").then(e=>e.pluginCodeView),documentation:()=>import("./documentation.js").then(e=>e.documentationView),tutorials:()=>import("./tutorials.js").then(e=>e.tutorialsView),apiReference:()=>import("./api-reference.js").then(e=>e.apiReferenceView),collections:()=>import("./collections.js").then(e=>e.collectionsView),collectionEditor:()=>import("./collection-editor.js").then(e=>e.collectionEditorView),collectionEntries:()=>import("./collection-entries.js").then(e=>e.collectionEntriesView),forms:()=>import("./forms.js").then(e=>e.formsView),formEditor:()=>import("./form-editor.js").then(e=>e.formEditorView),formSubmissions:()=>import("./form-submissions.js").then(e=>e.formSubmissionsView),viewsList:()=>import("./views-list.js").then(e=>e.viewsListView),viewEditor:()=>import("./view-editor.js").then(e=>e.viewEditorView),viewPreview:()=>import("./view-preview.js").then(e=>e.viewPreviewView),actionsList:()=>import("./actions-list.js").then(e=>e.actionsListView),actionEditor:()=>import("./action-editor.js").then(e=>e.actionEditorView),proDocs:()=>import("./pro-docs.js").then(e=>e.proDocsView),blocks:()=>import("./blocks.js").then(e=>e.blocksView),components:()=>import("./components.js").then(e=>e.componentsView),myProfile:()=>import("./my-profile.js").then(e=>e.myProfileView),roles:()=>import("./roles.js").then(e=>e.rolesView),roleEditor:()=>import("./role-editor.js").then(e=>e.roleEditorView),effects:()=>import("./effects.js").then(e=>e.effectsView),search:()=>import("./search.js").then(e=>e.searchView),contentApi:()=>import("./content-api.js").then(e=>e.contentApiView),notes:()=>import("./notes.js").then(e=>e.notesView),todo:()=>import("./todo.js").then(e=>e.todoView),calculator:()=>import("../lib/calculator.js").then(e=>e.calculatorView),analytics:()=>import("./analytics.js").then(e=>e.analyticsView),seo:()=>import("./seo.js").then(e=>e.seoView),wordpressImport:()=>import("./wordpress-import.js").then(e=>e.wordpressImportView),contacts:()=>import("./contacts.js").then(e=>e.contactsView),callNew:()=>import("./call-new.js").then(e=>e.callNewView),notifications:()=>import("./notifications.js").then(e=>e.notificationsView),menus:()=>import("./menus.js").then(e=>e.menusView),contextMenus:()=>import("./context-menus.js").then(e=>e.contextMenusView),contextMenuEditor:()=>import("./context-menu-editor.js").then(e=>e.contextMenuEditorView),menuEditor:()=>import("./menu-editor.js").then(e=>e.menuEditorView),menuLocations:()=>import("./menu-locations.js").then(e=>e.menuLocationsView),menusRedirect:async()=>t,pluginGuides:()=>import("./plugin-guides.js").then(e=>e.pluginGuidesView),pluginGuide:()=>import("./plugin-guides.js").then(e=>e.pluginGuideView)};for(const e of["tutorialCrud","tutorialPlugin","tutorialEditor","tutorialForms","componentsReference","componentsHowto","componentsWalkthrough","componentsRules","usagePages","usageMedia","usageNavigation","usageDconfig","usageShortcodes","usageSiteSettings","usagePlugins","usageUsersRoles","usageViews","usageActions","usageCtaShortcode","usageEditions","usageCollections","usageBlocks","usageTools","usageDashboard","usageSearchEngines","apiRefAuthentication","apiRefPages","apiRefSettings","apiRefLayouts","apiRefNavigation","apiRefMedia","apiRefUsers","apiRefPlugins","apiRefCollections","apiRefViews","apiRefActions","apiRefForms","apiRefScaffold","apiRefExternal","apiRefContent","apiRefBuilder"])viewLoaders[e]=()=>import("./doc-pages.js").then(i=>i.docViews[e]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.10",
3
+ "version": "1.10.12",
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",
@@ -95,6 +95,8 @@
95
95
  "mailparser": "^3.12.2",
96
96
  "marked": "^15.0.0",
97
97
  "nodemailer": "10.2.0",
98
+ "parse5": "^8.0.1",
99
+ "parse5-htmlparser2-tree-adapter": "^8.0.1",
98
100
  "postcss": "^8.5.31",
99
101
  "sanitize-html": "^2.18.1",
100
102
  "sharp": "^0.35.5",
@@ -29,6 +29,8 @@ import {createHash} from 'node:crypto';
29
29
  import {selectAll} from 'css-select';
30
30
  import {render as renderDom} from 'dom-serializer';
31
31
  import {DomUtils, parseDocument} from 'htmlparser2';
32
+ import {parse as parseHtml5} from 'parse5';
33
+ import {adapter as domAdapter} from 'parse5-htmlparser2-tree-adapter';
32
34
  import postcss from 'postcss';
33
35
  import sanitizeHtml from 'sanitize-html';
34
36
 
@@ -65,20 +67,79 @@ function specificity(selector) {
65
67
  }
66
68
 
67
69
  /**
68
- * Drop the parts of a `background` shorthand that load something, keeping its
69
- * colour. `background:#d97757 url(x.png) no-repeat` becomes `#d97757 no-repeat`.
70
+ * An inline style reduced to well-formed `name:value` declarations, names in
71
+ * lower case. Splits on `;` outside quotes and brackets (a data: URI has one
72
+ * inside), and drops any piece with no name or no colon.
73
+ *
74
+ * `background` and `background-image` lose whatever would load, unless remote
75
+ * images are allowed, when a plain http(s) `url()` stays - so a design drawn
76
+ * on a background picture comes back with "Load images" like any other.
77
+ *
78
+ * @param {string} style
79
+ * @param {{allowRemoteImages: boolean, blocked: {count: number}}} options
80
+ * @returns {string}
81
+ */
82
+ function tidyStyle(style, options) {
83
+ const pieces = [];
84
+ let current = '', quote = '', depth = 0;
85
+ for (const ch of style) {
86
+ if (quote) {
87
+ if (ch === quote) quote = '';
88
+ } else if (ch === '"' || ch === "'") {
89
+ quote = ch;
90
+ } else if (ch === '(') {
91
+ depth += 1;
92
+ } else if (ch === ')') {
93
+ depth = Math.max(0, depth - 1);
94
+ } else if (ch === ';' && depth === 0) {
95
+ pieces.push(current);
96
+ current = '';
97
+ continue;
98
+ }
99
+ current += ch;
100
+ }
101
+ pieces.push(current);
102
+ return pieces
103
+ .map((piece) => piece.match(/^\s*([a-zA-Z-][\w-]*)\s*:([\s\S]*)$/))
104
+ .filter((match) => match && match[2].trim())
105
+ .map((match) => {
106
+ const prop = match[1].toLowerCase();
107
+ let value = match[2].trim();
108
+ if (prop === 'background' || prop === 'background-image') {
109
+ value = backgroundValue(value, options);
110
+ }
111
+ return value ? `${prop}:${value}` : '';
112
+ })
113
+ .filter(Boolean)
114
+ .join(';');
115
+ }
116
+
117
+ /** A `url()` the reader may load once remote images are allowed: plain http(s), nothing nested. */
118
+ const REMOTE_URL = /^url\(\s*(['"]?)https?:\/\/[^'"()\s\\]+\1\s*\)$/i;
119
+
120
+ /**
121
+ * A background value with the parts that load something cut out, keeping its
122
+ * colour: `#d97757 url(x.png) no-repeat` becomes `#d97757 no-repeat`. With
123
+ * remote images allowed, a plain http(s) `url()` is kept instead. Each one cut
124
+ * counts as a blocked image, so the "Load images" notice offers it.
70
125
  *
71
126
  * @param {string} value
127
+ * @param {{allowRemoteImages: boolean, blocked: {count: number}}} options
72
128
  * @returns {string}
73
129
  */
74
- function stripBackgroundLoads(value) {
130
+ function backgroundValue(value, options) {
75
131
  if (value.includes('\\')) return '';
76
- return value.replace(/\b(url|image|image-set|element|cross-fade|expression)\s*\((?:[^()]|\([^()]*\))*\)/gi, '').trim();
132
+ return value.replace(/\b(url|image|image-set|element|cross-fade|expression)\s*\((?:[^()]|\([^()]*\))*\)/gi, (loader) => {
133
+ if (options.allowRemoteImages && REMOTE_URL.test(loader)) return loader;
134
+ if (/^url/i.test(loader)) options.blocked.count += 1;
135
+ return '';
136
+ }).replace(/\s{2,}/g, ' ').trim();
77
137
  }
78
138
 
79
139
  /**
80
- * Move a message's `<style>` rules onto the elements they match, and tidy the
81
- * inline styles the sanitiser is about to judge.
140
+ * Repair a message's markup the way a browser would, move its `<style>` rules
141
+ * onto the elements they match, and tidy the inline styles the sanitiser is
142
+ * about to judge.
82
143
  *
83
144
  * Mail clients have done this for years because so much mail needs it: a
84
145
  * button is often `<a class="button">` with its colours in the head, plus a
@@ -92,14 +153,23 @@ function stripBackgroundLoads(value) {
92
153
  * sanitiser's allowlist afterwards.
93
154
  *
94
155
  * @param {string} html
95
- * @returns {string}
156
+ * @param {{allowRemoteImages: boolean}} options
157
+ * @returns {{html: string, blockedBackgrounds: number}}
96
158
  */
97
- function inlineStyles(html) {
159
+ function inlineStyles(html, {allowRemoteImages}) {
160
+ const blocked = {count: 0};
98
161
  let dom;
99
162
  try {
100
- dom = parseDocument(html);
163
+ // The browser's own HTML5 parser, not htmlparser2's. A lot of mail is
164
+ // malformed - a <div> between a <table> and its rows - and browsers
165
+ // repair it by moving the stray element out. htmlparser2 keeps it
166
+ // where it was, the sanitiser then drops the table tags around it,
167
+ // and the coloured cells a design is built from end up outside any
168
+ // table. Repaired here, the sanitiser reads the same tree a browser
169
+ // would have drawn.
170
+ dom = parseHtml5(html, {treeAdapter: domAdapter});
101
171
  } catch {
102
- return html;
172
+ return {html, blockedBackgrounds: 0};
103
173
  }
104
174
 
105
175
  const pending = new Map();
@@ -176,32 +246,24 @@ function inlineStyles(html) {
176
246
  DomUtils.appendChild(body, div);
177
247
  }
178
248
 
179
- // CSS property names are case-insensitive, the sanitiser's allowlist is
180
- // not: `COLOR: #000000` (Outlook-era templates) was dropped, and a sheet
181
- // rule we had just inlined then won instead of it.
182
- const shouted = DomUtils.findAll((node) => /(^|;)\s*[a-z-]*[A-Z][\w-]*\s*:/.test(node.attribs?.style ?? ''), dom.children);
183
- for (const element of shouted) {
184
- element.attribs.style = element.attribs.style.replace(
185
- /(^|;)(\s*)([\w-]+)(\s*:)/g,
186
- (match, lead, space, prop, colon) => `${lead}${space}${prop.toLowerCase()}${colon}`
187
- );
249
+ // Read every inline style the way a browser does before the sanitiser
250
+ // judges it. Property names are case-insensitive but its allowlist is not
251
+ // (`COLOR: #000000` was dropped, and an inlined sheet colour won), and one
252
+ // broken declaration (`color:#fff; centre; ...`) made it throw the whole
253
+ // attribute away, where a browser skips just that one.
254
+ // The legacy `background="picture.jpg"` attribute becomes the style it
255
+ // means, so it is judged - and offered by "Load images" - the same way.
256
+ for (const element of DomUtils.findAll((node) => Boolean(node.attribs?.background), dom.children)) {
257
+ const src = element.attribs.background.trim();
258
+ delete element.attribs.background;
259
+ if (!src) continue;
260
+ element.attribs.style = [`background-image:url("${src.replace(/"/g, '')}")`, element.attribs.style ?? ''].filter(Boolean).join(';');
188
261
  }
189
-
190
- // A background shorthand loses only what would load; its colour stays.
191
- const backgrounds = DomUtils.findAll((node) => /background\s*:/i.test(node.attribs?.style ?? ''), dom.children);
192
- for (const element of backgrounds) {
193
- element.attribs.style = element.attribs.style.replace(
194
- /(^|;)(\s*background\s*:)([^;]*)/gi,
195
- (match, lead, prop, value) => {
196
- const kept = stripBackgroundLoads(value);
197
- return kept ? `${lead}${prop}${kept}` : lead;
198
- }
199
- );
262
+ for (const element of DomUtils.findAll((node) => Boolean(node.attribs?.style), dom.children)) {
263
+ element.attribs.style = tidyStyle(element.attribs.style, {allowRemoteImages, blocked});
200
264
  }
201
265
 
202
- // Untouched mail goes to the sanitiser exactly as it arrived.
203
- if (!pending.size && !bodyStyle && !shouted.length && !backgrounds.length) return html;
204
- return renderDom(dom);
266
+ return {html: renderDom(dom), blockedBackgrounds: blocked.count};
205
267
  }
206
268
 
207
269
  /**
@@ -234,9 +296,16 @@ function inlineImageMap(attachments, maxBytes) {
234
296
  * @returns {{html: string, blockedImages: number}}
235
297
  */
236
298
  function sanitiseBody(html, options) {
237
- let blockedImages = 0;
238
-
239
- const clean = sanitizeHtml(inlineStyles(html), {
299
+ const {html: repaired, blockedBackgrounds} = inlineStyles(html, {allowRemoteImages: options.allowRemoteImages});
300
+ let blockedImages = blockedBackgrounds;
301
+ // inlineStyles() has already cut out anything that loads, bar the plain
302
+ // http(s) url() it keeps when remote images are allowed; this refuses
303
+ // whatever got past it.
304
+ const background = options.allowRemoteImages
305
+ ? /^(?![\s\S]*(\\|\b(image|image-set|element|cross-fade|expression)\s*\(|\burl\s*\(\s*(?!['"]?https?:\/\/)))[\s\S]*$/i
306
+ : /^(?![\s\S]*(\\|\b(url|image|image-set|element|cross-fade|expression)\s*\())[\s\S]*$/i;
307
+
308
+ const clean = sanitizeHtml(repaired, {
240
309
  allowedTags: ALLOWED_TAGS,
241
310
  allowedAttributes: {
242
311
  // target/rel are added by transformTags below, and attribute
@@ -262,9 +331,8 @@ function sanitiseBody(html, options) {
262
331
  'color': [/^.*$/],
263
332
  'background-color': [/^.*$/],
264
333
  // The shorthand is how a lot of mail colours its buttons.
265
- // inlineStyles() has already cut out anything that loads; this
266
- // refuses whatever got past it.
267
- 'background': [/^(?![\s\S]*(\\|\b(url|image|image-set|element|cross-fade|expression)\s*\())[\s\S]*$/i],
334
+ 'background': [background],
335
+ ...(options.allowRemoteImages ? {'background-image': [background]} : {}),
268
336
  'border-radius': [/^.*$/],
269
337
  'border-top': [/^.*$/],
270
338
  'border-right': [/^.*$/],
@@ -0,0 +1,66 @@
1
+ import {authenticate as defaultAuthenticate, checkVisibility} from '../../middleware/auth.js';
2
+ import {getRegisteredSidebarItems} from '../../services/hooks.js';
3
+
4
+ /** Only registered count routes can be requested, always with the caller's credentials. */
5
+ export async function sidebarCountRoutes(fastify, opts = {}) {
6
+ const authenticate = opts.authenticate || defaultAuthenticate;
7
+ const getItems = opts.getItems || getRegisteredSidebarItems;
8
+ const isItemAllowed = opts.isItemAllowed || (() => true);
9
+ const timeoutMs = opts.countTimeoutMs || 5000;
10
+ fastify.post('/sidebar/counts', {
11
+ preHandler: [authenticate], bodyLimit: 32 * 1024,
12
+ schema: {body: {type: 'object', additionalProperties: false, properties: {
13
+ urls: {type: 'array', maxItems: 256, uniqueItems: true, items: {type: 'string', maxLength: 512}},
14
+ today: {type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$'}
15
+ }}}
16
+ }, async (request, reply) => {
17
+ reply.header('Cache-Control', 'no-store');
18
+ const wanted = request.body?.urls ? new Set(request.body.urls) : null;
19
+ const today = request.body?.today || new Date().toISOString().slice(0, 10);
20
+ const items = new Map();
21
+ const visit = item => {
22
+ if (!item || item.hidden || !checkVisibility(request.user, item.visibility) || !isItemAllowed(item, request.user)) return;
23
+ if (typeof item.url === 'string' && typeof item.countUrl === 'string' && (!wanted || wanted.has(item.url))) {
24
+ try {
25
+ const url = new URL(item.countUrl, 'http://localhost');
26
+ const decoded = decodeURIComponent(url.pathname);
27
+ if (item.countUrl.startsWith('/api/') && url.origin === 'http://localhost'
28
+ && !decoded.includes('\\') && !decoded.includes('\0') && !decoded.includes('..')
29
+ && !decoded.startsWith('/api/sidebar/') && !decoded.startsWith('/api/v1/')
30
+ && !decoded.startsWith('/api/x/') && !decoded.startsWith('/api/content/')) {
31
+ url.searchParams.set('today', today);
32
+ items.set(item.url, url.pathname + url.search);
33
+ }
34
+ } catch { /* malformed plugin count route */ }
35
+ }
36
+ for (const child of [...(item.items || []), ...(item.children || [])]) visit(child);
37
+ };
38
+ getItems().forEach(entry => visit(entry.item || entry));
39
+ const targets = [...new Set(items.values())];
40
+ const answers = new Map();
41
+ let cursor = 0;
42
+ await Promise.all(Array.from({length: Math.min(4, targets.length)}, async () => {
43
+ while (cursor < targets.length) {
44
+ const url = targets[cursor++];
45
+ let timer;
46
+ try {
47
+ const response = await Promise.race([
48
+ fastify.inject({method: 'GET', url, remoteAddress: request.ip,
49
+ headers: request.headers.authorization ? {authorization: request.headers.authorization} : {}}),
50
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), timeoutMs); })
51
+ ]);
52
+ if (!response || response.statusCode !== 200) { answers.set(url, null); continue; }
53
+ const answer = response.json();
54
+ const value = typeof answer === 'object' && answer !== null ? answer.count : answer;
55
+ const count = Number(value);
56
+ if (value == null || !Number.isFinite(count) || count < 0) { answers.set(url, null); continue; }
57
+ if (typeof answer !== 'object') answers.set(url, count);
58
+ else answers.set(url, Object.fromEntries(['count', 'label', 'tone', 'inventory', 'showZero', 'details']
59
+ .filter(key => answer[key] !== undefined).map(key => [key, answer[key]])));
60
+ } catch { answers.set(url, null); }
61
+ finally { clearTimeout(timer); }
62
+ }
63
+ }));
64
+ return {counts: Object.fromEntries([...items].map(([url, target]) => [url, answers.get(target) ?? null]))};
65
+ });
66
+ }
@@ -23,12 +23,15 @@ import {filterItemsForUser, getMenu} from '../../services/menus.js';
23
23
  import {isDashboardPro} from '../../services/widgets.js';
24
24
  import {boardName, readPrefs, readSite} from '../../services/dashboardSettings.js';
25
25
  import {frequentProblems, readFrequent, writeFrequent} from '../../services/sidebarFrequent.js';
26
+ import {sidebarCountRoutes} from './sidebar-counts.js';
26
27
 
27
28
  export const ADMIN_SIDEBAR_SLUG = 'admin-sidebar';
28
29
 
29
30
  export async function sidebarRoutes(fastify, opts = {}) {
30
31
  const authenticate = opts.authenticate || defaultAuthenticate;
31
32
  const signedIn = {preHandler: [authenticate]};
33
+ await sidebarCountRoutes(fastify, {...opts, authenticate,
34
+ isItemAllowed: (item, user) => !item.staffOnly || isDcmStaff(user)});
32
35
 
33
36
  /** Dashboard Pro: the Dashboard entry (`#/`) is Dashboards, wearing the name this person (or the site) gave it. */
34
37
  const dashboardsLabel = async (request) => (isDashboardPro()
package/server/server.js CHANGED
@@ -33,6 +33,7 @@ import * as cache from './services/cache/index.js';
33
33
  import {parseTrustProxy} from './services/network.js';
34
34
  import {requestSerializer} from './services/request-log.js';
35
35
  import {rateLimitOptions} from './services/rate-limit.js';
36
+ import {registerAdminAssets} from './services/adminAssets.js';
36
37
  import {connectSources, frameSources, imageSources} from './services/embedCsp.js';
37
38
  import {cspSources as gaSources} from './services/googleAnalytics.js';
38
39
 
@@ -164,6 +165,7 @@ if (publicCorsPrefixes.length) {
164
165
  await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
165
166
  await app.register(rateLimit, rateLimitOptions());
166
167
 
168
+ registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
167
169
  // Text assets (scripts, stylesheets, SVG, JSON) from memory, compressed once
168
170
  // per encoding - services/assetCache.js. Its onSend hook must be added BEFORE
169
171
  // compress registers its own, so compress finds content-encoding set and
@@ -206,19 +208,14 @@ await app.register(staticPlugin, {
206
208
  decorateReply: false
207
209
  });
208
210
 
209
- // Serve admin panel assets - no-store on JS/CSS so ESM module URLs are never
210
- // cached. `no-cache` revalidates but browsers may keep the parsed ESM module
211
- // keyed by URL across reloads, which means a hard refresh can still load the
212
- // stale code path. `no-store` skips the cache entirely. Other admin assets
213
- // (images, html) keep no-cache. Local-only impact - admin is not public.
211
+ // Versioned entry assets are immutable; dependencies revalidate with ETags.
214
212
  await app.register(staticPlugin, {
215
213
  root: path.join(ROOT, 'admin'),
216
214
  prefix: '/admin/',
217
215
  decorateReply: false,
218
216
  cacheControl: false,
219
- setHeaders: (reply, filePath) => {
220
- const noStore = /\.(?:js|mjs|css)$/i.test(filePath);
221
- reply.header('Cache-Control', noStore ? 'no-store' : 'no-cache');
217
+ setHeaders: (reply) => {
218
+ reply.header('Cache-Control', 'private, no-cache');
222
219
  }
223
220
  });
224
221
 
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {createHash} from 'node:crypto';
4
+
5
+ const TYPES = {'.js': 'application/javascript', '.mjs': 'application/javascript', '.css': 'text/css'};
6
+ const IMMUTABLE = 'private, max-age=31536000, immutable';
7
+
8
+ /** Fingerprint entry assets; unversioned module dependencies still revalidate. */
9
+ export function registerAdminAssets(app, {root, dommaDist, maxBytes = 16 * 1024 * 1024}) {
10
+ const entries = new Map();
11
+ const inflight = new Map();
12
+ let bytes = 0;
13
+ function resolve(url) {
14
+ let name;
15
+ try { name = decodeURIComponent(new URL(url, 'http://localhost').pathname); } catch { return null; }
16
+ if (name.includes('\\') || name.includes('\0') || name.split('/').some(p => p === '..')) return null;
17
+ if (!TYPES[path.extname(name)]) return null;
18
+ let base, relative;
19
+ if (name.startsWith('/dist/domma/')) { base = dommaDist; relative = name.slice(12); }
20
+ else if (name.startsWith('/admin/')) { base = path.join(root, 'admin'); relative = name.slice(7); }
21
+ else if (name.startsWith('/public/')) { base = path.join(root, 'public'); relative = name.slice(8); }
22
+ else if (/^\/plugins\/[^/]+\/(admin|public)\//.test(name)) { base = path.join(root, 'plugins'); relative = name.slice(9); }
23
+ else return null;
24
+ const file = path.resolve(base, relative);
25
+ return file.startsWith(path.resolve(base) + path.sep) ? file : null;
26
+ }
27
+ async function load(file) {
28
+ const stat = await fs.stat(file);
29
+ if (!stat.isFile()) throw new Error('Not an asset');
30
+ const signature = `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;
31
+ const previous = entries.get(file);
32
+ if (previous?.signature === signature) return previous;
33
+ const body = await fs.readFile(file);
34
+ const entry = {body, signature, hash: createHash('sha256').update(body).digest('hex').slice(0, 20)};
35
+ const stored = entries.get(file);
36
+ if (stored) { entries.delete(file); bytes -= stored.body.length; }
37
+ if (body.length <= maxBytes) {
38
+ while (bytes + body.length > maxBytes && entries.size) {
39
+ const [key, value] = entries.entries().next().value;
40
+ entries.delete(key); bytes -= value.body.length;
41
+ }
42
+ entries.set(file, entry); bytes += body.length;
43
+ }
44
+ return entry;
45
+ }
46
+ function read(file) {
47
+ if (!inflight.has(file)) inflight.set(file, load(file).finally(() => inflight.delete(file)));
48
+ return inflight.get(file);
49
+ }
50
+ async function version(url) {
51
+ if (!url.startsWith('/') || url.startsWith('//')) return url;
52
+ const file = resolve(url);
53
+ if (!file) return url;
54
+ try {
55
+ const entry = await read(file);
56
+ const parsed = new URL(url, 'http://localhost');
57
+ parsed.searchParams.set('_av', entry.hash);
58
+ return parsed.pathname + parsed.search + parsed.hash;
59
+ } catch { return url; }
60
+ }
61
+ async function index() {
62
+ let html = await fs.readFile(path.join(root, 'admin', 'index.html'), 'utf8');
63
+ const matches = [...html.matchAll(/(<(?:script|link)\b[^>]*?\b(?:src|href)=)(["'])([^"']+)\2/gi)];
64
+ const replacements = await Promise.all(matches.map(async match => {
65
+ const url = match[3].replaceAll('&amp;', '&');
66
+ return match[1] + match[2] + (await version(url)).replaceAll('&', '&amp;') + match[2];
67
+ }));
68
+ for (let i = matches.length - 1; i >= 0; i--) {
69
+ const match = matches[i];
70
+ html = html.slice(0, match.index) + replacements[i] + html.slice(match.index + match[0].length);
71
+ }
72
+ return html;
73
+ }
74
+ // HTML must be rebuilt even when only a referenced script or stylesheet changed.
75
+ for (const url of ['/admin/', '/admin/index.html']) {
76
+ app.get(url, async (_request, reply) => reply.header('Cache-Control', 'no-store')
77
+ .type('text/html; charset=utf-8').send(await index()));
78
+ }
79
+ app.addHook('onRequest', async (request, reply) => {
80
+ if (!['GET', 'HEAD'].includes(request.method)) return;
81
+ let url;
82
+ try { url = new URL(request.raw.url, 'http://localhost'); } catch { return; }
83
+ const versioned = url.searchParams.has('_av');
84
+ const adminCode = url.pathname.startsWith('/admin/') || /^\/plugins\/[^/]+\/admin\//.test(url.pathname);
85
+ if (!versioned && !adminCode) return;
86
+ const file = resolve(request.raw.url);
87
+ if (!file) return;
88
+ try {
89
+ const entry = await read(file);
90
+ if (!versioned) {
91
+ const etag = `"${entry.hash}"`;
92
+ reply.header('Cache-Control', 'private, no-cache').header('ETag', etag);
93
+ const matches = String(request.headers['if-none-match'] || '').split(',').map(value => value.trim().replace(/^W\//, ''));
94
+ if (matches.includes(etag) || matches.includes('*')) return reply.code(304).send();
95
+ return reply.type(TYPES[path.extname(file)] + '; charset=utf-8').send(entry.body);
96
+ }
97
+ if (url.searchParams.get('_av') === entry.hash) {
98
+ // Serve the exact bytes that were hashed, including during an atomic file replacement.
99
+ return reply.header('Cache-Control', IMMUTABLE).type(TYPES[path.extname(file)] + '; charset=utf-8').send(entry.body);
100
+ }
101
+ } catch { /* missing or replaced asset */ }
102
+ return reply.code(404).header('Cache-Control', 'no-store').send({error: 'Asset version unavailable. Reload the admin.'});
103
+ });
104
+ app.addHook('onSend', async (request, reply, payload) => {
105
+ if (request.raw.url.split('?')[0] !== '/api/plugins/admin-config' || reply.statusCode !== 200 || typeof payload !== 'string') return payload;
106
+ const config = JSON.parse(payload);
107
+ await Promise.all(Object.values(config.views || {}).map(async view => {
108
+ if (typeof view.entry === 'string') view.entry = (await version('/plugins/' + view.entry)).slice('/plugins/'.length);
109
+ }));
110
+ await Promise.all((config.css || []).map(async css => {
111
+ if (typeof css.href === 'string') css.href = await version(css.href);
112
+ }));
113
+ reply.removeHeader('content-length');
114
+ return JSON.stringify(config);
115
+ });
116
+ return {version};
117
+ }