domma-cms 1.10.10 → 1.10.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -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 +3 -1
- package/plugins/_lib/mail/render.js +107 -39
- 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,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.12] - 2026-10-24
|
|
4
|
+
|
|
5
|
+
- **Email with untidy HTML looks as it should.** The reader now reads a message with the same HTML5 rules a browser uses, so badly nested tables (a `<div>` between a table and its rows - British Gas, BT, Ocean Finance) keep their coloured panels instead of spilling out of them. One broken declaration in an inline style no longer throws the rest of that style away. Background pictures count as blocked images: the "Load images" notice now offers them and loads them, where before they were always stripped. Across a 15,600-message mailbox, text the sender's own design shows went missing in 16 messages, down from 3,481 before 1.10.8.
|
|
6
|
+
|
|
7
|
+
## [1.10.11] - 2026-10-24
|
|
8
|
+
|
|
9
|
+
- Load admin screens and plugin views only when opened, and let the first screen render while navigation data loads. Sidebar badges refresh in the background, reuse recent account-scoped results and share one authenticated request instead of a request per Tool.
|
|
10
|
+
- Cache content-versioned admin scripts and styles for repeat visits. Revalidate module dependencies with content ETags, rebuild entry URLs when files change, and reject superseded asset versions instead of serving different code under an immutable URL.
|
|
11
|
+
- Keep foreground loading indicators independent of low-priority badge polling. Combined counts preserve each route's authorization checks, staff-only visibility and partial failure handling.
|
|
12
|
+
|
|
3
13
|
## [1.10.10] - 2026-10-24
|
|
4
14
|
|
|
5
15
|
- **Email keeps its text colours in two more cases.** A colour set on the message's `<body>` (a light-on-dark design) was lost when the reader unwrapped it, and an inline style written in capitals (`COLOR: #000000`, common in older templates) was dropped, so a stylesheet colour won instead - grey-on-grey footers and links. Found by rendering a full mailbox (16,000 messages) before and after.
|
package/admin/js/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{installSmoothCarets as Q}from"./lib/smooth-caret.js";import{views as X}from"./views/index.js";import{withPluginChrome as V}from"./lib/plugin-chrome.js";import{api as r,getUser as L,isAuthenticated as m,logout as Z,sendToLogin as tt}from"./api.js";import{claimDrafts as et}from"./lib/draft-owner.js";import{isAuthScreen as C,loginHashFor as it}from"./lib/login-arrange.js";import{installHttpInterceptor as at}from"./http-interceptor.js";import{installResizableSlideovers as ot}from"./lib/slideover-resizable.js";import{installActivityThrobber as st}from"./lib/activity-throbber.js";import{installUpdateNotice as nt,renderCmsVersion as rt}from"./lib/update-notice.js";import{renderFrameworkVersions as _,wireAccountMenu as lt}from"./lib/topbar-account.js";import{refreshTopbarProject as B}from"./lib/topbar-project.js";import{isPathAllowed as ct,redirectFor as mt}from"./lib/admin-scope.js";import{deliverDesktop as pt,desktopActive as dt,notificationSettings as ut}from"./lib/desktop-notify.js";import{avatarUrl as ht,installAccountAvatar as wt}from"./lib/avatar.js";import{installCalculator as gt}from"./lib/calculator.js";import{takeOverviewPass as ft,withDashboardStrip as W}from"./lib/dashboards-strip.js";import{installHotkeys as vt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as H}from"./lib/admin-brand.js";import{installRouterCatchUp as bt}from"./lib/router-catch-up.js";import{installSidebarDrawer as St}from"./lib/sidebar-drawer.js";$(()=>{Q(document),at(),gt(),nt(),st(),ot(),St(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,c="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||mt(p,t);return i||((t.split("?")[0]||"/")==="/"&&c&&c!=="/"&&!ft()?y[c]||c:null)}const y={},N={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!m()){tt();return}const a=D(t.path);if(a){R.navigate(a);return}e()});async function f(){try{const t=await r.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,H(h),c=t.adminHome||"/",_(t.versions);const i=t.permissions||[];return w=ct(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,c="/",h||H(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(B(r),P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,c="/",w=!1,g=null)});const q=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(o=>o.refreshAdminTheme(r)),await T();const e=await f(),a=D(t.path);if(a&&R.navigate(a),j(e),ut.read().signInToasts)try{const o=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(o)?o:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}O()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const G=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function z(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!z(i))return;const a=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!a||!/\/new$/.test(i))return;let o=0;const n=60,s=1500,v=()=>{o+=n;for(const x of G){const b=document.querySelector(x);if(!b)continue;if(Array.from(b.options||[]).find(J=>J.value===a)){b.value=a,b.dispatchEvent(new Event("change",{bubbles:!0}));return}}o<s&&setTimeout(v,n)};setTimeout(v,n)});const F="cms_card_states",k=S.get(F)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(F,k))}),vt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...X},K={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(K))l[t]&&(l[t]=V(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=W(l.dashboard);const I=[...q];async function T(){if(m())try{const t=await r.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,a]of Object.entries(t.views||{}))try{const o=await import(`/plugins/${a.entry}`);l[e]=V(o[a.exportName],(t.meta||{})[e])}catch{}l.dashboard=W(l.dashboard);for(const[e,{route:a}]of Object.entries(t.takeovers||{}))a&&(y[`/${e}`]=a);for(const[e,{enabled:a,displayName:o}]of Object.entries(t.tools||{}))a===!1&&(N[`/${e}`]=o);for(const{id:e,href:a}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=a,document.head.appendChild(o)}}catch{}}function O(){const t=L();if(!t)return;if($("#topbar-user-name").length){B(r);return}const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,a=ht(t);$("#topbar-user").get(0).innerHTML=`
|
|
1
|
+
import{wireNotificationMenu as Q}from"./lib/topbar-notifications.js";import{installSmoothCarets as X}from"./lib/smooth-caret.js";import{viewLoaders as Z}from"./views/index.js";import{createLazyViews as tt}from"./lib/lazy-views.js";import{withPluginChrome as _}from"./lib/plugin-chrome.js";import{api as r,getUser as L,isAuthenticated as m,logout as et,sendToLogin as it}from"./api.js";import{claimDrafts as at}from"./lib/draft-owner.js";import{isAuthScreen as C,loginHashFor as ot}from"./lib/login-arrange.js";import{installHttpInterceptor as st}from"./http-interceptor.js";import{installResizableSlideovers as nt}from"./lib/slideover-resizable.js";import{installActivityThrobber as rt}from"./lib/activity-throbber.js";import{installUpdateNotice as lt,renderCmsVersion as ct}from"./lib/update-notice.js";import{renderFrameworkVersions as W,wireAccountMenu as mt}from"./lib/topbar-account.js";import{refreshTopbarProject as B}from"./lib/topbar-project.js";import{isPathAllowed as pt,redirectFor as dt}from"./lib/admin-scope.js";import{deliverDesktop as ut,desktopActive as ht,notificationSettings as wt}from"./lib/desktop-notify.js";import{avatarUrl as gt,installAccountAvatar as ft}from"./lib/avatar.js";import{installCalculator as vt}from"./lib/calculator.js";import{takeOverviewPass as bt,withDashboardStrip as H}from"./lib/dashboards-strip.js";import{installHotkeys as St}from"./lib/hotkeys-ui.js";import{applyAdminBrand as q}from"./lib/admin-brand.js";import{installRouterCatchUp as Ct}from"./lib/router-catch-up.js";import{installSidebarDrawer as Mt}from"./lib/sidebar-drawer.js";$(()=>{X(document),st(),vt(),lt(),rt(),nt(),Mt(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:a}=await import("./lib/admin-theme.js");m()?await a(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,c="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const a=y[t]||dt(p,t);return a||((t.split("?")[0]||"/")==="/"&&c&&c!=="/"&&!bt()?y[c]||c:null)}const y={},P={};R.use(async(t,a,e)=>{if(C(t.path))return e();if(!m()){it();return}const i=D(t.path);if(i){R.navigate(i);return}e()});async function f(){try{const t=await r.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,q(h),c=t.adminHome||"/",W(t.versions);const a=t.permissions||[];return w=pt(p,"/settings")&&a.some(e=>e==="settings"||e.startsWith("settings.")),g=a.some(e=>e==="notifications"||e.startsWith("notifications.")),a}catch{return p=null,c="/",h||q(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(B(r),N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:a}=await import("./lib/sidebar-renderer.js");await a({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,c="/",w=!1,g=null)});const z=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:a})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(a?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(o=>o.refreshAdminTheme(r)),await T();const e=await f(),i=D(t.path);if(i&&R.navigate(i),j(e),wt.read().signInToasts)try{const o=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(o)?o:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const l=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${l} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}V()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const G=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function J(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const a=t?String(t.path).split("?")[0]:"";if(!t||!J(a))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(a))return;let o=0;const n=60,s=1500,l=()=>{o+=n;for(const x of G){const b=document.querySelector(x);if(!b)continue;if(Array.from(b.options||[]).find(Y=>Y.value===i)){b.value=i,b.dispatchEvent(new Event("change",{bubbles:!0}));return}}o<s&&setTimeout(l,n)};setTimeout(l,n)});const F="cms_card_states",k=S.get(F)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const a=$(this).closest(".card");a.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=a.hasClass("card-collapsed")?"collapsed":"open",S.set(F,k))}),St(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const a=document.querySelector("#view-container .view-header button.btn-primary");a&&(t.preventDefault(),a.click())});const v=tt(),O=v.views,K={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,a]of Object.entries(Z))v.register(t,async()=>{let e=await a();const i=K[t];return i&&(e=_(e,{plugin:t,licence:"Built in",...i})),t==="dashboard"?H(e):e});R.use(async(t,a,e)=>{try{await v.load(t.view)}catch(i){console.error("[admin] Screen failed to load",i),O[t.view]={template:'<p role="alert">This screen could not load. Reload the page to try again.</p>'}}e()});const I=[...z];async function T(){if(m())try{const t=await r.plugins.adminConfig(),a=new Set(I.map(e=>e.path));for(const e of t.routes||[])a.has(e.path)||I.push(e);for(const[e,i]of Object.entries(t.views||{})){const o=(t.meta||{})[e];v.register(e,async()=>{const s=(await import(`/plugins/${i.entry}`))[i.exportName];if(!s)throw new Error(`Missing plugin screen: ${e}`);const l=_(s,o);return e==="dashboard"?H(l):l},JSON.stringify([i,o]))}for(const[e,{route:i}]of Object.entries(t.takeovers||{}))i&&(y[`/${e}`]=i);for(const[e,{enabled:i,displayName:o}]of Object.entries(t.tools||{}))i===!1&&(P[`/${e}`]=o);for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=i,document.head.appendChild(o)}}catch{}}function V(){const t=L();if(!t)return;if($("#topbar-user-name").length){B(r);return}const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,i=gt(t);$("#topbar-user").get(0).innerHTML=`
|
|
2
2
|
<a id="topbar-project" class="topbar-action-link topbar-project" hidden>
|
|
3
3
|
<span data-icon="folder"></span>
|
|
4
4
|
<span id="topbar-project-name" class="topbar-project-name"></span>
|
|
@@ -32,7 +32,7 @@ import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{inst
|
|
|
32
32
|
</button>
|
|
33
33
|
<div id="topbar-account-menu" class="topbar-account-menu" hidden>
|
|
34
34
|
<div class="topbar-account-identity">
|
|
35
|
-
${
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domma-cms",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.12",
|
|
4
4
|
"description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/server.js",
|
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"mailparser": "^3.12.2",
|
|
96
96
|
"marked": "^15.0.0",
|
|
97
97
|
"nodemailer": "10.2.0",
|
|
98
|
+
"parse5": "^8.0.1",
|
|
99
|
+
"parse5-htmlparser2-tree-adapter": "^8.0.1",
|
|
98
100
|
"postcss": "^8.5.31",
|
|
99
101
|
"sanitize-html": "^2.18.1",
|
|
100
102
|
"sharp": "^0.35.5",
|
|
@@ -29,6 +29,8 @@ import {createHash} from 'node:crypto';
|
|
|
29
29
|
import {selectAll} from 'css-select';
|
|
30
30
|
import {render as renderDom} from 'dom-serializer';
|
|
31
31
|
import {DomUtils, parseDocument} from 'htmlparser2';
|
|
32
|
+
import {parse as parseHtml5} from 'parse5';
|
|
33
|
+
import {adapter as domAdapter} from 'parse5-htmlparser2-tree-adapter';
|
|
32
34
|
import postcss from 'postcss';
|
|
33
35
|
import sanitizeHtml from 'sanitize-html';
|
|
34
36
|
|
|
@@ -65,20 +67,79 @@ function specificity(selector) {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
+
* An inline style reduced to well-formed `name:value` declarations, names in
|
|
71
|
+
* lower case. Splits on `;` outside quotes and brackets (a data: URI has one
|
|
72
|
+
* inside), and drops any piece with no name or no colon.
|
|
73
|
+
*
|
|
74
|
+
* `background` and `background-image` lose whatever would load, unless remote
|
|
75
|
+
* images are allowed, when a plain http(s) `url()` stays - so a design drawn
|
|
76
|
+
* on a background picture comes back with "Load images" like any other.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} style
|
|
79
|
+
* @param {{allowRemoteImages: boolean, blocked: {count: number}}} options
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
function tidyStyle(style, options) {
|
|
83
|
+
const pieces = [];
|
|
84
|
+
let current = '', quote = '', depth = 0;
|
|
85
|
+
for (const ch of style) {
|
|
86
|
+
if (quote) {
|
|
87
|
+
if (ch === quote) quote = '';
|
|
88
|
+
} else if (ch === '"' || ch === "'") {
|
|
89
|
+
quote = ch;
|
|
90
|
+
} else if (ch === '(') {
|
|
91
|
+
depth += 1;
|
|
92
|
+
} else if (ch === ')') {
|
|
93
|
+
depth = Math.max(0, depth - 1);
|
|
94
|
+
} else if (ch === ';' && depth === 0) {
|
|
95
|
+
pieces.push(current);
|
|
96
|
+
current = '';
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
current += ch;
|
|
100
|
+
}
|
|
101
|
+
pieces.push(current);
|
|
102
|
+
return pieces
|
|
103
|
+
.map((piece) => piece.match(/^\s*([a-zA-Z-][\w-]*)\s*:([\s\S]*)$/))
|
|
104
|
+
.filter((match) => match && match[2].trim())
|
|
105
|
+
.map((match) => {
|
|
106
|
+
const prop = match[1].toLowerCase();
|
|
107
|
+
let value = match[2].trim();
|
|
108
|
+
if (prop === 'background' || prop === 'background-image') {
|
|
109
|
+
value = backgroundValue(value, options);
|
|
110
|
+
}
|
|
111
|
+
return value ? `${prop}:${value}` : '';
|
|
112
|
+
})
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join(';');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A `url()` the reader may load once remote images are allowed: plain http(s), nothing nested. */
|
|
118
|
+
const REMOTE_URL = /^url\(\s*(['"]?)https?:\/\/[^'"()\s\\]+\1\s*\)$/i;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A background value with the parts that load something cut out, keeping its
|
|
122
|
+
* colour: `#d97757 url(x.png) no-repeat` becomes `#d97757 no-repeat`. With
|
|
123
|
+
* remote images allowed, a plain http(s) `url()` is kept instead. Each one cut
|
|
124
|
+
* counts as a blocked image, so the "Load images" notice offers it.
|
|
70
125
|
*
|
|
71
126
|
* @param {string} value
|
|
127
|
+
* @param {{allowRemoteImages: boolean, blocked: {count: number}}} options
|
|
72
128
|
* @returns {string}
|
|
73
129
|
*/
|
|
74
|
-
function
|
|
130
|
+
function backgroundValue(value, options) {
|
|
75
131
|
if (value.includes('\\')) return '';
|
|
76
|
-
return value.replace(/\b(url|image|image-set|element|cross-fade|expression)\s*\((?:[^()]|\([^()]*\))*\)/gi,
|
|
132
|
+
return value.replace(/\b(url|image|image-set|element|cross-fade|expression)\s*\((?:[^()]|\([^()]*\))*\)/gi, (loader) => {
|
|
133
|
+
if (options.allowRemoteImages && REMOTE_URL.test(loader)) return loader;
|
|
134
|
+
if (/^url/i.test(loader)) options.blocked.count += 1;
|
|
135
|
+
return '';
|
|
136
|
+
}).replace(/\s{2,}/g, ' ').trim();
|
|
77
137
|
}
|
|
78
138
|
|
|
79
139
|
/**
|
|
80
|
-
*
|
|
81
|
-
* inline styles the sanitiser is
|
|
140
|
+
* Repair a message's markup the way a browser would, move its `<style>` rules
|
|
141
|
+
* onto the elements they match, and tidy the inline styles the sanitiser is
|
|
142
|
+
* about to judge.
|
|
82
143
|
*
|
|
83
144
|
* Mail clients have done this for years because so much mail needs it: a
|
|
84
145
|
* button is often `<a class="button">` with its colours in the head, plus a
|
|
@@ -92,14 +153,23 @@ function stripBackgroundLoads(value) {
|
|
|
92
153
|
* sanitiser's allowlist afterwards.
|
|
93
154
|
*
|
|
94
155
|
* @param {string} html
|
|
95
|
-
* @
|
|
156
|
+
* @param {{allowRemoteImages: boolean}} options
|
|
157
|
+
* @returns {{html: string, blockedBackgrounds: number}}
|
|
96
158
|
*/
|
|
97
|
-
function inlineStyles(html) {
|
|
159
|
+
function inlineStyles(html, {allowRemoteImages}) {
|
|
160
|
+
const blocked = {count: 0};
|
|
98
161
|
let dom;
|
|
99
162
|
try {
|
|
100
|
-
|
|
163
|
+
// The browser's own HTML5 parser, not htmlparser2's. A lot of mail is
|
|
164
|
+
// malformed - a <div> between a <table> and its rows - and browsers
|
|
165
|
+
// repair it by moving the stray element out. htmlparser2 keeps it
|
|
166
|
+
// where it was, the sanitiser then drops the table tags around it,
|
|
167
|
+
// and the coloured cells a design is built from end up outside any
|
|
168
|
+
// table. Repaired here, the sanitiser reads the same tree a browser
|
|
169
|
+
// would have drawn.
|
|
170
|
+
dom = parseHtml5(html, {treeAdapter: domAdapter});
|
|
101
171
|
} catch {
|
|
102
|
-
return html;
|
|
172
|
+
return {html, blockedBackgrounds: 0};
|
|
103
173
|
}
|
|
104
174
|
|
|
105
175
|
const pending = new Map();
|
|
@@ -176,32 +246,24 @@ function inlineStyles(html) {
|
|
|
176
246
|
DomUtils.appendChild(body, div);
|
|
177
247
|
}
|
|
178
248
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
);
|
|
249
|
+
// Read every inline style the way a browser does before the sanitiser
|
|
250
|
+
// judges it. Property names are case-insensitive but its allowlist is not
|
|
251
|
+
// (`COLOR: #000000` was dropped, and an inlined sheet colour won), and one
|
|
252
|
+
// broken declaration (`color:#fff; centre; ...`) made it throw the whole
|
|
253
|
+
// attribute away, where a browser skips just that one.
|
|
254
|
+
// The legacy `background="picture.jpg"` attribute becomes the style it
|
|
255
|
+
// means, so it is judged - and offered by "Load images" - the same way.
|
|
256
|
+
for (const element of DomUtils.findAll((node) => Boolean(node.attribs?.background), dom.children)) {
|
|
257
|
+
const src = element.attribs.background.trim();
|
|
258
|
+
delete element.attribs.background;
|
|
259
|
+
if (!src) continue;
|
|
260
|
+
element.attribs.style = [`background-image:url("${src.replace(/"/g, '')}")`, element.attribs.style ?? ''].filter(Boolean).join(';');
|
|
188
261
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const backgrounds = DomUtils.findAll((node) => /background\s*:/i.test(node.attribs?.style ?? ''), dom.children);
|
|
192
|
-
for (const element of backgrounds) {
|
|
193
|
-
element.attribs.style = element.attribs.style.replace(
|
|
194
|
-
/(^|;)(\s*background\s*:)([^;]*)/gi,
|
|
195
|
-
(match, lead, prop, value) => {
|
|
196
|
-
const kept = stripBackgroundLoads(value);
|
|
197
|
-
return kept ? `${lead}${prop}${kept}` : lead;
|
|
198
|
-
}
|
|
199
|
-
);
|
|
262
|
+
for (const element of DomUtils.findAll((node) => Boolean(node.attribs?.style), dom.children)) {
|
|
263
|
+
element.attribs.style = tidyStyle(element.attribs.style, {allowRemoteImages, blocked});
|
|
200
264
|
}
|
|
201
265
|
|
|
202
|
-
|
|
203
|
-
if (!pending.size && !bodyStyle && !shouted.length && !backgrounds.length) return html;
|
|
204
|
-
return renderDom(dom);
|
|
266
|
+
return {html: renderDom(dom), blockedBackgrounds: blocked.count};
|
|
205
267
|
}
|
|
206
268
|
|
|
207
269
|
/**
|
|
@@ -234,9 +296,16 @@ function inlineImageMap(attachments, maxBytes) {
|
|
|
234
296
|
* @returns {{html: string, blockedImages: number}}
|
|
235
297
|
*/
|
|
236
298
|
function sanitiseBody(html, options) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
299
|
+
const {html: repaired, blockedBackgrounds} = inlineStyles(html, {allowRemoteImages: options.allowRemoteImages});
|
|
300
|
+
let blockedImages = blockedBackgrounds;
|
|
301
|
+
// inlineStyles() has already cut out anything that loads, bar the plain
|
|
302
|
+
// http(s) url() it keeps when remote images are allowed; this refuses
|
|
303
|
+
// whatever got past it.
|
|
304
|
+
const background = options.allowRemoteImages
|
|
305
|
+
? /^(?![\s\S]*(\\|\b(image|image-set|element|cross-fade|expression)\s*\(|\burl\s*\(\s*(?!['"]?https?:\/\/)))[\s\S]*$/i
|
|
306
|
+
: /^(?![\s\S]*(\\|\b(url|image|image-set|element|cross-fade|expression)\s*\())[\s\S]*$/i;
|
|
307
|
+
|
|
308
|
+
const clean = sanitizeHtml(repaired, {
|
|
240
309
|
allowedTags: ALLOWED_TAGS,
|
|
241
310
|
allowedAttributes: {
|
|
242
311
|
// target/rel are added by transformTags below, and attribute
|
|
@@ -262,9 +331,8 @@ function sanitiseBody(html, options) {
|
|
|
262
331
|
'color': [/^.*$/],
|
|
263
332
|
'background-color': [/^.*$/],
|
|
264
333
|
// The shorthand is how a lot of mail colours its buttons.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
'background': [/^(?![\s\S]*(\\|\b(url|image|image-set|element|cross-fade|expression)\s*\())[\s\S]*$/i],
|
|
334
|
+
'background': [background],
|
|
335
|
+
...(options.allowRemoteImages ? {'background-image': [background]} : {}),
|
|
268
336
|
'border-radius': [/^.*$/],
|
|
269
337
|
'border-top': [/^.*$/],
|
|
270
338
|
'border-right': [/^.*$/],
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {authenticate as defaultAuthenticate, checkVisibility} from '../../middleware/auth.js';
|
|
2
|
+
import {getRegisteredSidebarItems} from '../../services/hooks.js';
|
|
3
|
+
|
|
4
|
+
/** Only registered count routes can be requested, always with the caller's credentials. */
|
|
5
|
+
export async function sidebarCountRoutes(fastify, opts = {}) {
|
|
6
|
+
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
7
|
+
const getItems = opts.getItems || getRegisteredSidebarItems;
|
|
8
|
+
const isItemAllowed = opts.isItemAllowed || (() => true);
|
|
9
|
+
const timeoutMs = opts.countTimeoutMs || 5000;
|
|
10
|
+
fastify.post('/sidebar/counts', {
|
|
11
|
+
preHandler: [authenticate], bodyLimit: 32 * 1024,
|
|
12
|
+
schema: {body: {type: 'object', additionalProperties: false, properties: {
|
|
13
|
+
urls: {type: 'array', maxItems: 256, uniqueItems: true, items: {type: 'string', maxLength: 512}},
|
|
14
|
+
today: {type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$'}
|
|
15
|
+
}}}
|
|
16
|
+
}, async (request, reply) => {
|
|
17
|
+
reply.header('Cache-Control', 'no-store');
|
|
18
|
+
const wanted = request.body?.urls ? new Set(request.body.urls) : null;
|
|
19
|
+
const today = request.body?.today || new Date().toISOString().slice(0, 10);
|
|
20
|
+
const items = new Map();
|
|
21
|
+
const visit = item => {
|
|
22
|
+
if (!item || item.hidden || !checkVisibility(request.user, item.visibility) || !isItemAllowed(item, request.user)) return;
|
|
23
|
+
if (typeof item.url === 'string' && typeof item.countUrl === 'string' && (!wanted || wanted.has(item.url))) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(item.countUrl, 'http://localhost');
|
|
26
|
+
const decoded = decodeURIComponent(url.pathname);
|
|
27
|
+
if (item.countUrl.startsWith('/api/') && url.origin === 'http://localhost'
|
|
28
|
+
&& !decoded.includes('\\') && !decoded.includes('\0') && !decoded.includes('..')
|
|
29
|
+
&& !decoded.startsWith('/api/sidebar/') && !decoded.startsWith('/api/v1/')
|
|
30
|
+
&& !decoded.startsWith('/api/x/') && !decoded.startsWith('/api/content/')) {
|
|
31
|
+
url.searchParams.set('today', today);
|
|
32
|
+
items.set(item.url, url.pathname + url.search);
|
|
33
|
+
}
|
|
34
|
+
} catch { /* malformed plugin count route */ }
|
|
35
|
+
}
|
|
36
|
+
for (const child of [...(item.items || []), ...(item.children || [])]) visit(child);
|
|
37
|
+
};
|
|
38
|
+
getItems().forEach(entry => visit(entry.item || entry));
|
|
39
|
+
const targets = [...new Set(items.values())];
|
|
40
|
+
const answers = new Map();
|
|
41
|
+
let cursor = 0;
|
|
42
|
+
await Promise.all(Array.from({length: Math.min(4, targets.length)}, async () => {
|
|
43
|
+
while (cursor < targets.length) {
|
|
44
|
+
const url = targets[cursor++];
|
|
45
|
+
let timer;
|
|
46
|
+
try {
|
|
47
|
+
const response = await Promise.race([
|
|
48
|
+
fastify.inject({method: 'GET', url, remoteAddress: request.ip,
|
|
49
|
+
headers: request.headers.authorization ? {authorization: request.headers.authorization} : {}}),
|
|
50
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(null), timeoutMs); })
|
|
51
|
+
]);
|
|
52
|
+
if (!response || response.statusCode !== 200) { answers.set(url, null); continue; }
|
|
53
|
+
const answer = response.json();
|
|
54
|
+
const value = typeof answer === 'object' && answer !== null ? answer.count : answer;
|
|
55
|
+
const count = Number(value);
|
|
56
|
+
if (value == null || !Number.isFinite(count) || count < 0) { answers.set(url, null); continue; }
|
|
57
|
+
if (typeof answer !== 'object') answers.set(url, count);
|
|
58
|
+
else answers.set(url, Object.fromEntries(['count', 'label', 'tone', 'inventory', 'showZero', 'details']
|
|
59
|
+
.filter(key => answer[key] !== undefined).map(key => [key, answer[key]])));
|
|
60
|
+
} catch { answers.set(url, null); }
|
|
61
|
+
finally { clearTimeout(timer); }
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
return {counts: Object.fromEntries([...items].map(([url, target]) => [url, answers.get(target) ?? null]))};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -23,12 +23,15 @@ import {filterItemsForUser, getMenu} from '../../services/menus.js';
|
|
|
23
23
|
import {isDashboardPro} from '../../services/widgets.js';
|
|
24
24
|
import {boardName, readPrefs, readSite} from '../../services/dashboardSettings.js';
|
|
25
25
|
import {frequentProblems, readFrequent, writeFrequent} from '../../services/sidebarFrequent.js';
|
|
26
|
+
import {sidebarCountRoutes} from './sidebar-counts.js';
|
|
26
27
|
|
|
27
28
|
export const ADMIN_SIDEBAR_SLUG = 'admin-sidebar';
|
|
28
29
|
|
|
29
30
|
export async function sidebarRoutes(fastify, opts = {}) {
|
|
30
31
|
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
31
32
|
const signedIn = {preHandler: [authenticate]};
|
|
33
|
+
await sidebarCountRoutes(fastify, {...opts, authenticate,
|
|
34
|
+
isItemAllowed: (item, user) => !item.staffOnly || isDcmStaff(user)});
|
|
32
35
|
|
|
33
36
|
/** Dashboard Pro: the Dashboard entry (`#/`) is Dashboards, wearing the name this person (or the site) gave it. */
|
|
34
37
|
const dashboardsLabel = async (request) => (isDashboardPro()
|
package/server/server.js
CHANGED
|
@@ -33,6 +33,7 @@ import * as cache from './services/cache/index.js';
|
|
|
33
33
|
import {parseTrustProxy} from './services/network.js';
|
|
34
34
|
import {requestSerializer} from './services/request-log.js';
|
|
35
35
|
import {rateLimitOptions} from './services/rate-limit.js';
|
|
36
|
+
import {registerAdminAssets} from './services/adminAssets.js';
|
|
36
37
|
import {connectSources, frameSources, imageSources} from './services/embedCsp.js';
|
|
37
38
|
import {cspSources as gaSources} from './services/googleAnalytics.js';
|
|
38
39
|
|
|
@@ -164,6 +165,7 @@ if (publicCorsPrefixes.length) {
|
|
|
164
165
|
await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
|
|
165
166
|
await app.register(rateLimit, rateLimitOptions());
|
|
166
167
|
|
|
168
|
+
registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
|
|
167
169
|
// Text assets (scripts, stylesheets, SVG, JSON) from memory, compressed once
|
|
168
170
|
// per encoding - services/assetCache.js. Its onSend hook must be added BEFORE
|
|
169
171
|
// compress registers its own, so compress finds content-encoding set and
|
|
@@ -206,19 +208,14 @@ await app.register(staticPlugin, {
|
|
|
206
208
|
decorateReply: false
|
|
207
209
|
});
|
|
208
210
|
|
|
209
|
-
//
|
|
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
|
+
}
|