domma-cms 1.10.10 → 1.10.11
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 +6 -0
- package/admin/js/app.js +3 -3
- package/admin/js/lib/activity-throbber.js +1 -1
- package/admin/js/lib/background-requests.js +1 -0
- package/admin/js/lib/lazy-views.js +1 -0
- package/admin/js/lib/sidebar-count-cache.js +1 -1
- package/admin/js/lib/sidebar-counts.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +5 -5
- package/admin/js/views/index.js +1 -1
- package/package.json +1 -1
- package/server/routes/api/sidebar-counts.js +66 -0
- package/server/routes/api/sidebar.js +3 -0
- package/server/server.js +5 -8
- package/server/services/adminAssets.js +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.11] - 2026-10-24
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
3
9
|
## [1.10.10] - 2026-10-24
|
|
4
10
|
|
|
5
11
|
- **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
|
-
${
|
|
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(),
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}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
|
|
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
|
|
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
|
|
2
|
-
<summary${v}>${
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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(
|
|
5
|
-
<summary${v}${
|
|
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)}))}
|
package/admin/js/views/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
@@ -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
|
-
//
|
|
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
|
|
220
|
-
|
|
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('&', '&');
|
|
66
|
+
return match[1] + match[2] + (await version(url)).replaceAll('&', '&') + 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
|
+
}
|