domma-cms 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -245,6 +245,13 @@ id="dm-theme-overrides">` is injected **before** `content/custom.css` so hand-wr
245
245
  both render paths in `renderer.js` go through `buildThemeView()` - wiring one and not the other is the
246
246
  standing trap in that file. Token values that could close their own declaration are refused, not escaped.
247
247
 
248
+ **Custom themes** (a plugin's - the Theme Roller) come from `hooks.registerThemes(provider)` and join the
249
+ built-in ones everywhere except the public switcher. Domma's engine strips every `dm-theme-*` class it did
250
+ not add, so a custom theme is `dm-theme-<base> dm-custom-<id>` plus one generated rule of the changed
251
+ tokens (`buildCustomThemeStyleTag`, both render paths); `resolveThemeClasses()` is the one place that
252
+ decides, and config/theme.json's `customBases` keeps a page on the base if the plugin goes away. The
253
+ admin applies its own through `GET /api/theme/admin` + `admin/js/lib/admin-theme.js`.
254
+
248
255
  ## Multi-menu system, overlays & admin sidebar
249
256
 
250
257
  Menus live in `config/menus/<slug>.json`, mapped to slots by `config/menu-locations.json` (built-in
package/admin/js/api.js CHANGED
@@ -1 +1 @@
1
- const m="/api";function d(){return S.get("auth_token")}function p(){return S.get("auth_refresh_token")}function y(e){S.set("auth_token",e)}function u(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function f(){const e=p();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw u(),R.navigate("/login"),new Error("Token refresh failed");const{token:n}=await o.json();return y(n),n}async function t(e,o={}){let n=d();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:r(n)});if(s.status===401&&p())try{n=await f(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),a=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(a.validation=i.validation),a.status=s.status,a}return s.status===204?null:s.json()}async function g(e,o){const n=d(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>g("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=d(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const T=await s.json().catch(()=>({}));throw new Error(T.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",a=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=a||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=d(),a=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await a.json().catch(()=>({}));if(a.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!a.ok)throw new Error(c.error||`Install failed (${a.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const a=await n.json().catch(()=>({}));throw new Error(a.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const a=await n.json().catch(()=>({}));throw new Error(a.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!d()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&y(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}export function logout(){const e=p();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),u(),R.navigate("/login")}export{t as apiRequest,f as refreshAccessToken,d as getToken,u as clearAuth};
1
+ const m="/api";function a(){return S.get("auth_token")}function p(){return S.get("auth_refresh_token")}function y(e){S.set("auth_token",e)}function u(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function f(){const e=p();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw u(),R.navigate("/login"),new Error("Token refresh failed");const{token:n}=await o.json();return y(n),n}async function t(e,o={}){let n=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:r(n)});if(s.status===401&&p())try{n=await f(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),d=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(d.validation=i.validation),d.status=s.status,d}return s.status===204?null:s.json()}async function g(e,o){const n=a(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:()=>t("/theme/admin",{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>g("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const T=await s.json().catch(()=>({}));throw new Error(T.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!d.ok)throw new Error(c.error||`Install failed (${d.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&y(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}export function logout(){const e=p();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),u(),R.navigate("/login")}export{t as apiRequest,f as refreshAccessToken,a as getToken,u as clearAuth};
package/admin/js/app.js CHANGED
@@ -1,6 +1,6 @@
1
- import{views as B}from"./views/index.js";import{withPluginChrome as I}from"./lib/plugin-chrome.js";import{api as p,getUser as U,isAuthenticated as d,logout as O}from"./api.js";import{installHttpInterceptor as V}from"./http-interceptor.js";import{installResizableSlideovers as F}from"./lib/slideover-resizable.js";import{installActivityThrobber as H}from"./lib/activity-throbber.js";import{isPathAllowed as q,redirectFor as K}from"./lib/admin-scope.js";$(()=>{V(),H(),F(),(async()=>{try{const t=d()?await p.theme.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();let l=null,r="/",u=!1;function v(t){const e=f[t]||K(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?f[r]||r:null)}const f={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=v(t.path);if(i){R.navigate(i);return}o()});async function C(){try{const t=await p.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=q(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),e}catch{return l=null,r="/",u=!1,[]}}document.addEventListener("dm:admin-home-changed",()=>{C()});async function y(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(m&&(clearInterval(m),m=null),l=null,r="/",u=!1)});const T=[{path:"/",view:"dashboard",title:"Dashboard - 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/plugin",view:"tutorialPlugin",title:"Writing a Plugin - 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/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:"/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:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",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:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - 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:"projectEditor",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projectEditor",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:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await A();const o=await C(),i=v(t.path);i&&R.navigate(i),y(o);try{const a=await p.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const w=h(s.data?.title||""),P=h((s.data?.body||"").slice(0,120));E.toast(`${w} - ${P}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}k()}}),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 x=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function L(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 e=t?String(t.path).split("?")[0]:"";if(!t||!L(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,w=()=>{a+=n;for(const P of x){const g=document.querySelector(P);if(!g)continue;if(Array.from(g.options||[]).find(_=>_.value===i)){g.value=i,g.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(w,n)};setTimeout(w,n)});const j="cms_card_states",D=S.get(j)||{};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&&D[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 e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(D[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(j,D))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const c={...B},W={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"}};for(const[t,e]of Object.entries(W))c[t]&&(c[t]=I(c[t],{plugin:t,licence:"Built in",...e}));const b=[...T];async function A(){if(d())try{const t=await p.plugins.adminConfig();t.routes?.length&&b.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);c[e]=I(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(f[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function k(){const t=U();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
2
- <span id="topbar-user-name" class="topbar-user-name">${h(t.name)}</span>
3
- <span class="topbar-role-badge topbar-role-badge--${h(t.role)}">${o}</span>
1
+ import{views as V}from"./views/index.js";import{withPluginChrome as T}from"./lib/plugin-chrome.js";import{api as c,getUser as L,isAuthenticated as d,logout as F}from"./api.js";import{installHttpInterceptor as H}from"./http-interceptor.js";import{installResizableSlideovers as q}from"./lib/slideover-resizable.js";import{installActivityThrobber as K}from"./lib/activity-throbber.js";import{isPathAllowed as z,redirectFor as J}from"./lib/admin-scope.js";$(()=>{H(),K(),q(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");d()?await e(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let l=null,r="/",u=!1;function v(t){const e=C[t]||J(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?C[r]||r:null)}const C={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=v(t.path);if(i){R.navigate(i);return}o()});async function h(){try{const t=await c.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=z(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),e}catch{return l=null,r="/",u=!1,[]}}document.addEventListener("dm:admin-home-changed",()=>{h()});let D=null,b=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(D){b=!0;return}D=(async()=>{do{b=!1;try{await P(await h())}catch{}}while(b)})().finally(()=>{D=null})});async function P(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(p&&(clearInterval(p),p=null),l=null,r="/",u=!1)});const x=[{path:"/",view:"dashboard",title:"Dashboard - 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/plugin",view:"tutorialPlugin",title:"Writing a Plugin - 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/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:"/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:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",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:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - 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:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await N();const o=await h(),i=v(t.path);i&&R.navigate(i),P(o);try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const g=w(s.data?.title||""),A=w((s.data?.body||"").slice(0,120));E.toast(`${g} - ${A}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}I()}}),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 W=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function _(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 e=t?String(t.path).split("?")[0]:"";if(!t||!_(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,g=()=>{a+=n;for(const A of W){const f=document.querySelector(A);if(!f)continue;if(Array.from(f.options||[]).find(O=>O.value===i)){f.value=i,f.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(g,n)};setTimeout(g,n)});const k="cms_card_states",y=S.get(k)||{};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&&y[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 e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(y[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(k,y))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...V},B={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"}};for(const[t,e]of Object.entries(B))m[t]&&(m[t]=T(m[t],{plugin:t,licence:"Built in",...e}));const j=[...x];async function N(){if(d())try{const t=await c.plugins.adminConfig();t.routes?.length&&j.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);m[e]=T(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(C[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function I(){const t=L();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
2
+ <span id="topbar-user-name" class="topbar-user-name">${w(t.name)}</span>
3
+ <span class="topbar-role-badge topbar-role-badge--${w(t.role)}">${o}</span>
4
4
  `),$("#topbar-actions").html(`
5
5
  <a href="#/system/notifications" id="topbar-bell" class="topbar-action-link" data-tooltip="Notifications" data-tooltip-placement="bottom" hidden>
6
6
  <span data-icon="bell"></span>
@@ -18,4 +18,4 @@ import{views as B}from"./views/index.js";import{withPluginChrome as I}from"./lib
18
18
  <span data-icon="log-out"></span>
19
19
  <span>Sign out</span>
20
20
  </a>
21
- `);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),O()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),N(),m||(m=setInterval(N,6e4))}function h(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let m=null;async function N(){if(d())try{const{count:t}=await p.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text("")}catch{}}(async()=>{if(!d()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await A(),U()){const a=await C(),n=(window.location.hash||"#/").slice(1)||"/",s=v(n);s&&(window.location.hash=`#${s}`),await y(a),k()}R.init({container:"#view-container",routes:b,views:c,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(i,a)};const e=(window.location.hash||"#/").slice(1)||"/";b.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
21
+ `);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),F()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),U(),p||(p=setInterval(U,6e4))}function w(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let p=null;async function U(){if(d())try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text("")}catch{}}(async()=>{if(!d()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await N(),L()){const a=await h(),n=(window.location.hash||"#/").slice(1)||"/",s=v(n);s&&(window.location.hash=`#${s}`),await P(a),I()}R.init({container:"#view-container",routes:j,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(i,a)};const e=(window.location.hash||"#/").slice(1)||"/";j.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
@@ -0,0 +1 @@
1
+ const a="dm-custom-themes";export function applyAdminTheme(e,{init:s=!1}={}){const o=e?.dommaTheme||"charcoal-dark";s?Domma.theme.init({theme:o,persist:!0}):Domma.theme.set(o);let t=document.getElementById(a);e?.css?(t||(t=document.createElement("style"),t.id=a,document.head.appendChild(t)),t.textContent=e.css):t&&t.remove();const m=document.body;for(const c of[...m.classList])c.startsWith("dm-custom-")&&m.classList.remove(c);e?.customClass&&m.classList.add(e.customClass)}export async function refreshAdminTheme(e,s={}){try{applyAdminTheme(await e.theme.admin(),s)}catch{applyAdminTheme({dommaTheme:"charcoal-dark"},s)}}
@@ -0,0 +1 @@
1
+ export const SLUG_RE=/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/,CORE="core",FILTERS=["all","enabled","disabled"],RESERVED=["new","edit"],TYPE_LABELS={pages:["page","pages"],collections:["collection","collections"],forms:["form","forms"],actions:["action","actions"],menus:["menu","menus"],blocks:["block","blocks"],components:["component","components"],views:["view","views"],roles:["role","roles"],users:["user","users"],apis:["API","APIs"]};const m=(t,[r,n])=>`${t} ${t===1?r:n}`;export function slugify(t){return String(t??"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64).replace(/-+$/,"")}export function artefactSummary(t){const r=Object.entries(t||{}).filter(([,e])=>Number(e)>0).sort((e,s)=>s[1]-e[1]||Object.keys(TYPE_LABELS).indexOf(e[0])-Object.keys(TYPE_LABELS).indexOf(s[0])),n=r.reduce((e,[,s])=>e+Number(s),0);if(!n)return{total:0,text:"No artefacts yet"};const i=r.slice(0,3).map(([e,s])=>m(s,TYPE_LABELS[e]||[e,e])),o=r.length>3?", \u2026":"";return{total:n,text:`${m(n,["artefact","artefacts"])} - ${i.join(", ")}${o}`}}export function arrangeProjects(t,{filter:r="all",query:n=""}={}){const i=String(n).trim().toLowerCase();return[...t||[]].filter(o=>r==="enabled"?o.enabled!==!1:r==="disabled"?o.enabled===!1:!0).filter(o=>!i||[o.slug,o.name,o.description,o.rootUrl].some(e=>String(e||"").toLowerCase().includes(i))).sort((o,e)=>(o.sortOrder??0)-(e.sortOrder??0)||String(o.name||o.slug).localeCompare(String(e.name||e.slug)))}export function projectRow(t){const{total:r,text:n}=artefactSummary(t.counts),i=t.enabled===!1;return{slug:t.slug,name:t.name||t.slug,icon:t.icon||"folder",meta:[t.slug,t.rootUrl||"no root URL",t.counts?n:""].filter(Boolean).join(" \xB7 "),description:t.description||"",total:r,off:i,system:t.protected===!0,rowCls:i?"is-off":""}}export function checkProject(t,{isNew:r,isProtected:n=!1,slug:i="",taken:o=[]}={}){const e=t||{},s={},l=r?String(e.slug??"").trim():i;r&&(SLUG_RE.test(l)?RESERVED.includes(l)?s.slug=`"${l}" is reserved - the admin uses it in its addresses.`:o.includes(l)&&(s.slug=`"${l}" is already a project.`):s.slug="Lowercase letters, numbers and hyphens; no hyphen at either end.");const a=String(e.name??"").trim();a||(s.name="A project needs a name.");const c=String(e.rootUrl??"").trim();!n&&c&&(c.startsWith("/")?c==="/"&&l!==CORE&&(s.rootUrl='"/" belongs to the core project.'):s.rootUrl="Starts with / - for example /members.");const u=Number.parseInt(e.sortOrder,10),d={...r&&{slug:l},name:a,description:String(e.description??"").trim(),icon:String(e.icon??"").trim()||"folder",sortOrder:Number.isFinite(u)?u:0,...!n&&(c||!r)&&{rootUrl:c},...l!==CORE&&{enabled:e.enabled!==!1}};return{errors:s,payload:d}}
@@ -1,4 +1,4 @@
1
- import{api as f}from"../api.js";import{isPathAllowed as ee}from"./admin-scope.js";import{colourToCss as X,applyFloat as te,applyHighlight as ne,itemHighlightAttrs as se}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as ie,ensureLockedTools as oe,managePluginsItem as ae,stripItemByUrl as re,insertFoldersBeforeSystem as le,pruneEmptyToolsFolder as ce,MANAGE_PLUGINS_URL as de}from"./sidebar-grouping.js";function p(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function q(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function K(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!q(s,i.permission))continue;const r=typeof i.url=="string"?i.url:"";if(n&&r.startsWith("#/")&&!ee(n,r.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,o=l?K(i.items,s,n):[];l&&!o.length&&!r||e.push({...i,items:o})}return e}async function me(t){if(!q(t,"projects"))return[];let s=[];try{s=await f.projects.list()}catch{return[]}const n={};await Promise.all(s.map(async i=>{try{const r=await f.projects.artefacts(i.slug);n[i.slug]=r}catch{n[i.slug]={}}}));const e=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(i=>{const l="#/projects/"+encodeURIComponent(i.slug),o=n[i.slug]||{},c=[{text:"Overview",url:l,icon:i.icon||"folder"}];for(const d of e){const x=o[d.key],h=Array.isArray(x)?x.length:0;h<=0||c.push({text:`${d.text} (${h})`,url:l+d.path,icon:d.icon})}c.push({text:"Settings",url:l+"/settings",icon:"settings"});const m=i.enabled===!1;return{text:i.name||i.slug,icon:i.icon||"folder",items:c,...m&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function V(t,s){const n=J.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?F.set(t,s.details):F.delete(t);const i=A.some(r=>r.url===t)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${p(t)}"${i} data-badge-label="${p(e)}" aria-label="${p(e)}">${p(String(s.count))}</span>`}function G(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const r=[];for(const d of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const x=Number(d.textContent)||0;x&&(e+=x,i||(i=d.classList.contains("sidebar-badge--danger")),r.push({text:d.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:d.dataset.badgeLabel,tone:d.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let o=n.querySelector(":scope > .sidebar-badge--sum");if(!e){o?.remove(),F.delete(l);continue}o||(o=document.createElement("span"),n.querySelector(".sidebar-text")?.after(o));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",m=r.map(d=>`${d.text}: ${d.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,o.dataset.countFor=l,o.dataset.badgeLabel=m,o.setAttribute("aria-label",m),o.textContent=String(e),F.set(l,{title:c,items:r})}}const F=new Map;function ge(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":V(t.url,n)}const J=["info","attention","danger","live"],pe={"#/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 I(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),r={count:Math.round(e),label:i,tone:J.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(r.details=t.details),r}function he(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Y(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const h=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(h)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${p(t.icon)}"></span>`:"",e=p(t.text||""),i=t.url?p(t.url):"",r=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",o=se(t),c=[o.hover&&` data-menu-item-hover="${p(o.hover)}"`,o.active&&` data-menu-item-active="${p(o.active)}"`].filter(Boolean).join(""),m=[l&&`color:${p(l)}`,o.accent&&`--dm-menu-highlight:${p(o.accent)}`].filter(Boolean).join(";"),d=m?` style="${m}"`:"";let x="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const h=t.badge.variant?X(t.badge.variant):"";x=`<span class="dm-menu-badge"${h?` style="background:${p(h)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(t.badge.text))}</span>`}if(r){const h=he(t),L=S.get(h)!==!1,E=t.items.map(w=>Y(w,s)).join("");return`<details data-state-key="${p(h)}"${L?" open":""}>
2
- <summary${d}>${n} <span class="sidebar-text">${e}</span>${x}</summary>
1
+ import{api as p}from"../api.js";import{isPathAllowed as ee}from"./admin-scope.js";import{colourToCss as X,applyFloat as te,applyHighlight as ne,itemHighlightAttrs as se}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as ie,ensureLockedTools as oe,managePluginsItem as ae,stripItemByUrl as re,insertFoldersBeforeSystem as le,pruneEmptyToolsFolder as ce,MANAGE_PLUGINS_URL as de}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function _(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function K(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!_(s,i.permission))continue;const r=typeof i.url=="string"?i.url:"";if(n&&r.startsWith("#/")&&!ee(n,r.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,o=l?K(i.items,s,n):[];l&&!o.length&&!r||e.push({...i,items:o})}return e}async function me(t){if(!_(t,"projects"))return[];let s=[];try{s=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(e=>{const r="#/projects/"+encodeURIComponent(e.slug),l=e.counts||{},o=[{text:"Overview",url:r,icon:e.icon||"folder"}];for(const d of n){const u=Number(l[d.key])||0;u<=0||o.push({text:`${d.text} (${u})`,url:r+d.path,icon:d.icon})}o.push({text:"Settings",url:r+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:o,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function V(t,s){const n=J.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?P.set(t,s.details):P.delete(t);const i=A.some(r=>r.url===t)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${h(t)}"${i} data-badge-label="${h(e)}" aria-label="${h(e)}">${h(String(s.count))}</span>`}function G(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const r=[];for(const u of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const v=Number(u.textContent)||0;v&&(e+=v,i||(i=u.classList.contains("sidebar-badge--danger")),r.push({text:u.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:u.dataset.badgeLabel,tone:u.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let o=n.querySelector(":scope > .sidebar-badge--sum");if(!e){o?.remove(),P.delete(l);continue}o||(o=document.createElement("span"),n.querySelector(".sidebar-text")?.after(o));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",d=r.map(u=>`${u.text}: ${u.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,o.dataset.countFor=l,o.dataset.badgeLabel=d,o.setAttribute("aria-label",d),o.textContent=String(e),P.set(l,{title:c,items:r})}}const P=new Map;function pe(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":V(t.url,n)}const J=["info","attention","danger","live"],he={"#/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 T(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),r={count:Math.round(e),label:i,tone:J.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(r.details=t.details),r}function ge(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Y(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(y)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${h(t.icon)}"></span>`:"",e=h(t.text||""),i=t.url?h(t.url):"",r=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",o=se(t),c=[o.hover&&` data-menu-item-hover="${h(o.hover)}"`,o.active&&` data-menu-item-active="${h(o.active)}"`].filter(Boolean).join(""),d=[l&&`color:${h(l)}`,o.accent&&`--dm-menu-highlight:${h(o.accent)}`].filter(Boolean).join(";"),u=d?` style="${d}"`:"";let v="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const y=t.badge.variant?X(t.badge.variant):"";v=`<span class="dm-menu-badge"${y?` style="background:${h(y)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(t.badge.text))}</span>`}if(r){const y=ge(t),L=S.get(y)!==!1,E=t.items.map(w=>Y(w,s)).join("");return`<details data-state-key="${h(y)}"${L?" open":""}>
2
+ <summary${u}>${n} <span class="sidebar-text">${e}</span>${v}</summary>
3
3
  <div class="sidebar-children">${E}</div>
4
- </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${d}${c}>${n} <span class="sidebar-text">${e}</span>${ge(t,s)}${x}</a>`}async function be(t=[],s=[]){const n=b=>b.then(g=>Array.isArray(g)?g.length:Array.isArray(g?.entries)?g.entries.length:0).catch(()=>0),e=(b,g)=>q(s,b)?g():Promise.resolve(0),[i,r,l,o,c,m,d,x,h,L,E,w,N,T]=await Promise.all([e("pages",()=>n(f.pages.list())),e("media",()=>n(f.media.list())),e("menus",()=>n(f.menus.list())),e("context-menus",()=>n(f.contextMenus.list())),e("theme",()=>f.theme.get().then(b=>(b?.overrides||[]).length).catch(()=>0)),e("collections",()=>n(f.collections.list())),e("forms",()=>n(f.forms.list())),e("views",()=>n(f.views.list())),e("actions",()=>n(f.actions.list())),e("blocks",()=>n(f.blocks.list())),e("components",()=>n(f.components.list())),e("users",()=>n(f.users.list())),e("plugins",()=>n(f.plugins.list())),e("notifications",()=>f.system?.notifications?.unreadCount?.().then(b=>b?.count??0).catch(()=>0)??Promise.resolve(0))]);A=ve(t);const B={};await Promise.all(A.map(async b=>{const g=await Q(b);g&&(B[b.url]=g)}));const z={"#/pages":i,"#/media":r,"#/menus":l,"#/context-menus":o,"#/theme":c,"#/collections":m,"#/forms":d,"#/views":x,"#/actions":h,"#/blocks":L,"#/components":E,"#/users":w,"#/plugins":N,"#/system/notifications":T},j={};for(const[b,g]of Object.entries(z)){const[C,k]=pe[b]??["item","items"],v=I(g);v&&(j[b]={...v,label:`${v.count} ${v.count===1?C:k}`})}return{...j,...B}}let A=[];const ye="dm:sidebar-count",xe=6e4;function ve(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||""});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Q(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return I(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function Z(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(r=>r.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const r=document.createRange();r.selectNodeContents(e);const l=r.createContextualFragment(V(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}U.set(s,n),G(t)}const U=new Map;async function O(t,s=null){const n=s?A.filter(e=>e.url===s):A;await Promise.all(n.map(async e=>{const i=await Q(e),r=U.get(e.url);JSON.stringify(r??null)!==JSON.stringify(i)&&Z(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=A.find(i=>i.url===n.url);Z(t,n.url,I(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else O(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&O(t)},xe),document.addEventListener("visibilitychange",()=>{document.hidden||O(t)}),Se(t))}function Ce(t,s,n){const e=(l,o,c)=>{const m=document.createElement(l);return o&&(m.className=o),c!=null&&(m.textContent=String(c)),m};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const o of i)l.appendChild(e("dt",null,o.label)),l.appendChild(e("dd",o.tone==="danger"?"is-danger":null,o.value));t.appendChild(l)}const r=Array.isArray(n.items)?n.items.slice(0,6):[];if(r.length){const l=e("ul","dm-badge-pop-items");for(const o of r){const c=e("li",o.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",o.text)),o.meta&&c.appendChild(e("span","dm-badge-pop-meta",o.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=r=>{e();const l=r?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,F.get(r.dataset.countFor)),document.body.appendChild(s);const o=r.getBoundingClientRect(),c=s.getBoundingClientRect(),m=Math.min(o.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,m)}px`,s.style.top=`${Math.max(8,Math.min(o.top+o.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",r=>{const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",r=>{r.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(){try{const t=await f.settings.get();return t?.adminBrand?.title||t?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,r=null,l=null,o=null,c=null,m=null;try{const a=await f.get("/sidebar/menu");i=Array.isArray(a?.items)?a.items:null,r=a?.variant||null,l=a?.position||null,o=a?.style||null,c=a?.float||null,m=a?.highlight||null}catch{i=null}(!i||!i.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let d=[];try{d=await H.get("/api/sidebar/registered-items")||[]}catch{}i=re(i,de),i=ie(i,d),i=oe(i),i=le(i,[ae()]);const x=await me(s);x.length&&fe(i,x),i=K(i,s,n),i=ce(i);const[h,L]=await Promise.all([be(d.filter(a=>q(s,a?.item?.permission)),s).catch(()=>({})),we()]),E=r?` dm-admin-sidebar--${p(r)}`:"";function w(a,y="px"){if(a==null)return a;const u=String(a).trim();return/^\d+(\.\d+)?$/.test(u)?`${u}${y}`:u}let N="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${w(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${w(o.letterSpacing,"em")}`);const y=[];if(a.length&&y.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const u=w(o.iconSize);y.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${u} !important; height: ${u} !important; }`)}y.length&&(N=`<style data-admin-sidebar-style>${y.join(" ")}</style>`)}const T=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${p(L)}</div>`,B=i.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",z=`${N}<nav class="dm-admin-sidebar${E}${B}">${T}${i.map(a=>Y(a,h)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const b=j.createContextualFragment(z);e.replaceChildren(b),Domma.icons.scan(e);for(const a of A)U.set(a.url,h[a.url]??null);G(e),$e(e);const g=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(g)&&g>=180&&g<=480&&(e.style.width=g+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),ne(e,{highlight:m});const C=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{e.style[a]=""}),e.classList.remove("dm-menu-floating"),C&&(C.style.paddingLeft="",C.style.paddingRight=""),l==="floating"){te(e,{position:"floating",float:c||{}});const y=c&&c.anchor||"TL",u=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,P=e.getBoundingClientRect().width||g||260;if(C){const R=Math.round(P+u*2)+"px";y==="R"||y==="TR"||y==="BR"?C.style.paddingRight=R:C.style.paddingLeft=R}}const k=document.createElement("div");k.className="dm-admin-sidebar-handle",k.setAttribute("aria-label","Resize sidebar"),e.appendChild(k),e.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const y=this.getAttribute("data-state-key");y&&S.set(y,this.open);const u=this.querySelector(":scope > .sidebar-children");if(u)if(this.open){const P=u.scrollHeight;u.style.maxHeight="0",requestAnimationFrame(()=>{u.style.maxHeight=P+"px",u.addEventListener("transitionend",function R(){u.style.maxHeight="none",u.removeEventListener("transitionend",R)})})}else{const P=u.scrollHeight;u.style.maxHeight=P+"px",requestAnimationFrame(()=>{u.style.maxHeight="0"})}})});let v=!1,_=0,W=0;k.addEventListener("mousedown",a=>{v=!0,_=a.clientX,W=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",a.preventDefault()}),document.addEventListener("mousemove",a=>{if(!v)return;const y=Math.max(180,Math.min(480,W+(a.clientX-_)));e.style.width=y+"px"}),document.addEventListener("mouseup",()=>{v&&(v=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))});function D(){const a=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}D(),M.subscribe("router:afterChange",D)}
4
+ </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${u}${c}>${n} <span class="sidebar-text">${e}</span>${pe(t,s)}${v}</a>`}async function be(t=[],s=[]){const n=g=>g.then(f=>Array.isArray(f)?f.length:Array.isArray(f?.entries)?f.entries.length:0).catch(()=>0),e=(g,f)=>_(s,g)?f():Promise.resolve(0),[i,r,l,o,c,d,u,v,y,L,E,w,F,B]=await Promise.all([e("pages",()=>n(p.pages.list())),e("media",()=>n(p.media.list())),e("menus",()=>n(p.menus.list())),e("context-menus",()=>n(p.contextMenus.list())),e("theme",()=>p.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),e("collections",()=>n(p.collections.list())),e("forms",()=>n(p.forms.list())),e("views",()=>n(p.views.list())),e("actions",()=>n(p.actions.list())),e("blocks",()=>n(p.blocks.list())),e("components",()=>n(p.components.list())),e("users",()=>n(p.users.list())),e("plugins",()=>n(p.plugins.list())),e("notifications",()=>p.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]);A=xe(t);const R={};await Promise.all(A.map(async g=>{const f=await Q(g);f&&(R[g.url]=f)}));const q={"#/pages":i,"#/media":r,"#/menus":l,"#/context-menus":o,"#/theme":c,"#/collections":d,"#/forms":u,"#/views":v,"#/actions":y,"#/blocks":L,"#/components":E,"#/users":w,"#/plugins":F,"#/system/notifications":B},j={};for(const[g,f]of Object.entries(q)){const[C,k]=he[g]??["item","items"],x=T(f);x&&(j[g]={...x,label:`${x.count} ${x.count===1?C:k}`})}return{...j,...R}}let A=[];const ye="dm:sidebar-count",ve=6e4;function xe(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||""});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Q(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return T(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function Z(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(r=>r.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const r=document.createRange();r.selectNodeContents(e);const l=r.createContextualFragment(V(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}I.set(s,n),G(t)}const I=new Map;async function U(t,s=null){const n=s?A.filter(e=>e.url===s):A;await Promise.all(n.map(async e=>{const i=await Q(e),r=I.get(e.url);JSON.stringify(r??null)!==JSON.stringify(i)&&Z(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=A.find(i=>i.url===n.url);Z(t,n.url,T(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else U(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&U(t)},ve),document.addEventListener("visibilitychange",()=>{document.hidden||U(t)}),Se(t))}function Ce(t,s,n){const e=(l,o,c)=>{const d=document.createElement(l);return o&&(d.className=o),c!=null&&(d.textContent=String(c)),d};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const o of i)l.appendChild(e("dt",null,o.label)),l.appendChild(e("dd",o.tone==="danger"?"is-danger":null,o.value));t.appendChild(l)}const r=Array.isArray(n.items)?n.items.slice(0,6):[];if(r.length){const l=e("ul","dm-badge-pop-items");for(const o of r){const c=e("li",o.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",o.text)),o.meta&&c.appendChild(e("span","dm-badge-pop-meta",o.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=r=>{e();const l=r?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,P.get(r.dataset.countFor)),document.body.appendChild(s);const o=r.getBoundingClientRect(),c=s.getBoundingClientRect(),d=Math.min(o.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,d)}px`,s.style.top=`${Math.max(8,Math.min(o.top+o.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",r=>{const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",r=>{r.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(){try{const t=await p.settings.get();return t?.adminBrand?.title||t?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,r=null,l=null,o=null,c=null,d=null;try{const a=await p.get("/sidebar/menu");i=Array.isArray(a?.items)?a.items:null,r=a?.variant||null,l=a?.position||null,o=a?.style||null,c=a?.float||null,d=a?.highlight||null}catch{i=null}(!i||!i.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let u=[];try{u=await H.get("/api/sidebar/registered-items")||[]}catch{}i=re(i,de),i=ie(i,u),i=oe(i),i=le(i,[ae()]);const v=await me(s);v.length&&fe(i,v),i=K(i,s,n),i=ce(i);const[y,L]=await Promise.all([be(u.filter(a=>_(s,a?.item?.permission)),s).catch(()=>({})),we()]),E=r?` dm-admin-sidebar--${h(r)}`:"";function w(a,b="px"){if(a==null)return a;const m=String(a).trim();return/^\d+(\.\d+)?$/.test(m)?`${m}${b}`:m}let F="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${w(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${w(o.letterSpacing,"em")}`);const b=[];if(a.length&&b.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const m=w(o.iconSize);b.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${m} !important; height: ${m} !important; }`)}b.length&&(F=`<style data-admin-sidebar-style>${b.join(" ")}</style>`)}const B=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(L)}</div>`,R=i.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",q=`${F}<nav class="dm-admin-sidebar${E}${R}">${B}${i.map(a=>Y(a,y)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const g=j.createContextualFragment(q);e.replaceChildren(g),Domma.icons.scan(e);for(const a of A)I.set(a.url,y[a.url]??null);G(e),$e(e);const f=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(f)&&f>=180&&f<=480&&(e.style.width=f+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),ne(e,{highlight:d});const C=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{e.style[a]=""}),e.classList.remove("dm-menu-floating"),C&&(C.style.paddingLeft="",C.style.paddingRight=""),l==="floating"){te(e,{position:"floating",float:c||{}});const b=c&&c.anchor||"TL",m=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,N=e.getBoundingClientRect().width||f||260;if(C){const z=Math.round(N+m*2)+"px";b==="R"||b==="TR"||b==="BR"?C.style.paddingRight=z:C.style.paddingLeft=z}}const k=document.createElement("div");k.className="dm-admin-sidebar-handle",k.setAttribute("aria-label","Resize sidebar"),e.appendChild(k),e.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const b=this.getAttribute("data-state-key");b&&S.set(b,this.open);const m=this.querySelector(":scope > .sidebar-children");if(m)if(this.open){const N=m.scrollHeight;m.style.maxHeight="0",requestAnimationFrame(()=>{m.style.maxHeight=N+"px",m.addEventListener("transitionend",function z(){m.style.maxHeight="none",m.removeEventListener("transitionend",z)})})}else{const N=m.scrollHeight;m.style.maxHeight=N+"px",requestAnimationFrame(()=>{m.style.maxHeight="0"})}})});let x=!1,W=0,O=0;k.addEventListener("mousedown",a=>{x=!0,W=a.clientX,O=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",a.preventDefault()}),e.dataset.resizeWired||(e.dataset.resizeWired="1",document.addEventListener("mousemove",a=>e.__dmResize?.move(a)),document.addEventListener("mouseup",()=>e.__dmResize?.up()),M.subscribe("router:afterChange",()=>e.__dmSetActive?.())),e.__dmResize={move(a){if(!x)return;const b=Math.max(180,Math.min(480,O+(a.clientX-W)));e.style.width=b+"px"},up(){x&&(x=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))}};function D(){const a=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}D(),e.__dmSetActive=D}
@@ -1,13 +1,72 @@
1
- <div class="page-header">
2
- <h2><span data-icon="folder"></span> Projects</h2>
3
- <div class="header-actions">
4
- <a class="btn btn-primary" href="#/projects/new"><span data-icon="plus"></span> New project</a>
5
- </div>
6
- </div>
7
- <p class="text-muted mb-4">Group related artefacts under a project for navigation and per-user access scoping.</p>
1
+ <!-- Manage projects. Bound with M.applyBindings - see admin/views/projects.js, which also inserts the row
2
+ template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading, and New project is a banner action. -->
4
+ <div class="pjx">
5
+ <p class="pjx-note">Group related pages, collections, forms and the rest under a project - for the sidebar, and to
6
+ limit what a user can see.
7
+ <span data-help-title="Projects" data-help="Everything not filed under a project belongs to Core, which cannot be removed. A project's root URL files pages under it automatically (a page at /members/join belongs to the project whose root URL is /members). A user given projects sees only those, plus Core. Disabling a project takes its public pages and external API offline; editing in the admin carries on."></span></p>
8
8
 
9
- <div class="card">
10
- <div class="card-body">
11
- <div id="projects-table"></div>
9
+ <div class="pjx-card">
10
+ <div class="pjx-bar">
11
+ <div class="pjx-seg" role="group" aria-label="Show">
12
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
13
+ <button type="button" data-on-click="showEnabled" data-bind-class="filter.value === 'enabled' && 'is-on'">Enabled <b data-bind-text="counts.value.enabled"></b></button>
14
+ <button type="button" data-on-click="showDisabled" data-bind-class="filter.value === 'disabled' && 'is-on'">Disabled <b data-bind-text="counts.value.disabled"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input pjx-search" placeholder="Search projects…" aria-label="Search projects" data-model="query.value">
17
+ <span data-help-title="The list" data-help="Click a project to open its overview. Right-click it (or ⋮, or Shift+F10) to edit it, open its settings, enable or disable it, untag everything filed under it, or delete it. A project can only be deleted once nothing is filed under it; built-in projects cannot be deleted."></span>
18
+ </div>
19
+ <p class="pjx-note" data-bind-hidden="!loading.value">Loading…</p>
20
+ <p class="pjx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
21
+ <p class="pjx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
22
+ <div class="pjx-list" data-each="rows.value key=slug"></div>
12
23
  </div>
13
24
  </div>
25
+
26
+ <style>
27
+ .pjx { display: flex; flex-direction: column; gap: .75rem; }
28
+ .pjx [hidden], .pjx-form [hidden] { display: none !important; }
29
+ .pjx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
30
+ .pjx-note.is-bad { color: var(--dm-danger); }
31
+ .pjx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
32
+ .pjx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
33
+ .pjx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
34
+ .pjx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
35
+ .pjx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
36
+ .pjx-seg button + button { border-left: 1px solid var(--dm-border); }
37
+ .pjx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
38
+ .pjx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
39
+ .pjx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
40
+ .pjx-list { display: flex; flex-direction: column; }
41
+ .pjx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
42
+ .pjx-row:last-child { border-bottom: 0; }
43
+ .pjx-row:hover, .pjx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
44
+ .pjx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
45
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
46
+ .pjx-row.is-off .pjx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
47
+ .pjx-row.is-off .pjx-main strong { color: var(--dm-text-muted); }
48
+ .pjx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
49
+ .pjx-main strong, .pjx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ .pjx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
51
+ .pjx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; }
52
+ .pjx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
53
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
54
+ .pjx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
55
+ .pjx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
56
+ .pjx-count { min-width: 2.5rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
57
+ .pjx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
58
+ .pjx-kebab:hover { color: var(--dm-primary); }
59
+ /* The editor slideover */
60
+ .pjx-form { display: flex; flex-direction: column; gap: .85rem; }
61
+ .pjx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .85rem; }
62
+ .pjx-wide { grid-column: 1 / -1; }
63
+ .pjx-form .form-label { display: flex; align-items: center; gap: .3rem; }
64
+ .pjx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
65
+ .pjx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
66
+ .pjx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
67
+ .pjx-setting .form-label { margin: 0; }
68
+ .pjx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
69
+ .pjx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
70
+ .pjx-spacer { flex: 1; }
71
+ @media (max-width: 640px) { .pjx-grid { grid-template-columns: 1fr; } .pjx-count { display: none; } }
72
+ </style>
@@ -44,9 +44,6 @@
44
44
  <input id="theme-auto-enabled" type="checkbox" class="form-check">
45
45
  Switch automatically between a day and a night theme
46
46
  </label>
47
- <p id="theme-auto-roller-hint" class="form-hint text-muted"
48
- style="display:none;margin-top:.35rem;font-size:.8rem;">
49
- Custom themes (Theme Roller) do not support auto-switching.</p>
50
47
  </div>
51
48
  </div>
52
49
  <div id="theme-auto-fields" class="row mt-3" style="display:none;">
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{navigationView as m}from"./navigation.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as d}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as E}from"./doc-pages.js";import{apiReferenceView as u}from"./api-reference.js";import{collectionsView as g}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as b}from"./collection-entries.js";import{formsView as h}from"./forms.js";import{formEditorView as j}from"./form-editor.js";import{formSubmissionsView as y}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as x}from"./view-preview.js";import{actionsListView as M}from"./actions-list.js";import{actionEditorView as D}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as C}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as U}from"./my-profile.js";import{rolesView as q}from"./roles.js";import{roleEditorView as z}from"./role-editor.js";import{effectsView as A}from"./effects.js";import{searchView as B}from"./search.js";import{notesView as F}from"./notes.js";import{todoView as G}from"./todo.js";import{analyticsView as H}from"./analytics.js";import{contactsView as I}from"./contacts.js";import{menusView as J}from"./menus.js";import{contextMenusView as K}from"./context-menus.js";import{contextMenuEditorView as N}from"./context-menu-editor.js";import{menuEditorView as O}from"./menu-editor.js";import{menuLocationsView as Q}from"./menu-locations.js";import{projectsView as W}from"./projects.js";import{projectEditorView as X}from"./project-editor.js";import{projectDetailView as Y}from"./project-detail.js";import{projectSettingsView as Z}from"./project-settings.js";import{apiTokensView as _}from"./api-tokens.js";import{apiEndpointsView as $}from"./api-endpoints.js";import{apiEndpointEditorView as oo}from"./api-endpoint-editor.js";const io={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:W,projectEditor:X,projectDetail:Y,projectSettings:Z,apiTokens:_,apiEndpoints:$,apiEndpointEditor:oo,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,navigation:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:u,collections:g,collectionEditor:v,collectionEntries:b,forms:h,formEditor:j,formSubmissions:y,viewsList:k,viewEditor:L,viewPreview:x,actionsList:M,actionEditor:D,proDocs:P,blocks:R,blockEditor:S,components:C,componentEditor:T,myProfile:U,roles:q,roleEditor:z,effects:A,search:B,notes:F,todo:G,analytics:H,contacts:I,notifications:p,menus:J,contextMenus:K,contextMenuEditor:N,menuEditor:O,menuLocations:Q,menusRedirect:io,...E};
1
+ import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{navigationView as m}from"./navigation.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as d}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as u}from"./doc-pages.js";import{apiReferenceView as E}from"./api-reference.js";import{collectionsView as g}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as b}from"./collection-entries.js";import{formsView as h}from"./forms.js";import{formEditorView as y}from"./form-editor.js";import{formSubmissionsView as j}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as x}from"./view-preview.js";import{actionsListView as M}from"./actions-list.js";import{actionEditorView as D}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as C}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as U}from"./my-profile.js";import{rolesView as q}from"./roles.js";import{roleEditorView as z}from"./role-editor.js";import{effectsView as A}from"./effects.js";import{searchView as B}from"./search.js";import{notesView as F}from"./notes.js";import{todoView as G}from"./todo.js";import{analyticsView as H}from"./analytics.js";import{contactsView as I}from"./contacts.js";import{menusView as J}from"./menus.js";import{contextMenusView as K}from"./context-menus.js";import{contextMenuEditorView as N}from"./context-menu-editor.js";import{menuEditorView as O}from"./menu-editor.js";import{menuLocationsView as Q}from"./menu-locations.js";import{projectsView as W}from"./projects.js";import{projectDetailView as X}from"./project-detail.js";import{projectSettingsView as Y}from"./project-settings.js";import{apiTokensView as Z}from"./api-tokens.js";import{apiEndpointsView as _}from"./api-endpoints.js";import{apiEndpointEditorView as $}from"./api-endpoint-editor.js";const oo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:W,projectDetail:X,projectSettings:Y,apiTokens:Z,apiEndpoints:_,apiEndpointEditor:$,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,navigation:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:E,collections:g,collectionEditor:v,collectionEntries:b,forms:h,formEditor:y,formSubmissions:j,viewsList:k,viewEditor:L,viewPreview:x,actionsList:M,actionEditor:D,proDocs:P,blocks:R,blockEditor:S,components:C,componentEditor:T,myProfile:U,roles:q,roleEditor:z,effects:A,search:B,notes:F,todo:G,analytics:H,contacts:I,notifications:p,menus:J,contextMenus:K,contextMenuEditor:N,menuEditor:O,menuLocations:Q,menusRedirect:oo,...u};
@@ -1,3 +1,58 @@
1
- import{api as l}from"../api.js";import{keepTableIconsAlive as m}from"../lib/table-icons.js";function s(n){return String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const projectsView={templateUrl:"/admin/js/templates/projects.html",async onMount(n){const d=E.loader(n.get(0),{type:"dots"});let i,r;try{i=await l.projects.list().catch(()=>[]),r=await Promise.all(i.map(t=>l.projects.artefacts(t.slug).then(e=>Object.values(e).reduce((a,c)=>a+c.length,0)).catch(()=>0)))}finally{d.destroy()}const b=i.map((t,e)=>({...t,artefactCount:r[e]})),o=t=>t.enabled===!1;T.create("#projects-table",{pageSize:25,data:b,emptyMessage:'No projects yet. Click "New project" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:(t,e)=>o(e)?`<span class="text-muted">${s(t)}</span>`:s(t)},{key:"name",title:"Name",sortable:!0,render:(t,e)=>{const a=o(e)?`<span class="text-muted">${s(t)}</span>`:s(t),c=e.protected?' <span class="badge badge-secondary" data-tooltip="Built-in project - cannot be deleted">System</span>':"",p=o(e)?' <span class="badge badge-warning" data-tooltip="Disabled - public pages and external API are off">Disabled</span>':"";return a+c+p}},{key:"icon",title:"Icon",render:(t,e)=>t?`<span class="${o(e)?"text-muted":""}" data-icon="${s(t)}"></span>`:""},{key:"rootUrl",title:"Root URL",render:t=>t?`<code>${s(t)}</code>`:'<span class="text-muted">-</span>'},{key:"artefactCount",title:"Artefacts",render:t=>`<span class="badge badge-info">${t??0}</span>`},{key:"_actions",title:"Actions",render:(t,e)=>{const a=encodeURIComponent(e.slug),c=o(e)?`<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="eye" data-icon-size="14"></span></span>
2
- <span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="settings" data-icon-size="14"></span></span>`:`<a class="btn btn-sm btn-ghost" href="#/projects/${a}" data-tooltip="Overview"><span data-icon="eye" data-icon-size="14"></span></a>
3
- <a class="btn btn-sm btn-ghost" href="#/projects/${a}/settings" data-tooltip="Settings"><span data-icon="settings" data-icon-size="14"></span></a>`,g=e.slug==="core"?"":o(e)?`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="1" data-tooltip="Enable"><span class="text-success" data-icon="play" data-icon-size="14"></span></button>`:`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="0" data-tooltip="Disable"><span class="text-warning" data-icon="pause" data-icon-size="14"></span></button>`,u=e.protected?"":`<button class="btn btn-sm btn-ghost btn-delete-project" data-slug="${s(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash" data-icon-size="14"></span></button>`;return`<span style="display:inline-flex;gap:0.25rem;">${c}${g}${u}</span>`}}]}),Domma.icons.scan(n.get(0)),m(n.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),n.off("click",".btn-toggle-project").on("click",".btn-toggle-project",async function(){const t=$(this).data("slug"),e=String($(this).data("enable"))==="1";if(!(!e&&!await E.confirm(`Disable project "${t}"? Its public pages will 404 and its external API will stop responding. Admin editing is unaffected.`)))try{await l.projects.update(t,{enabled:e}),E.toast(e?"Project enabled.":"Project disabled.",{type:"success"}),location.reload()}catch(a){E.toast(`Failed: ${a.message||a}`,{type:"error"})}}),n.off("click",".btn-delete-project").on("click",".btn-delete-project",async function(){const t=$(this).data("slug");if(await E.confirm(`Delete project "${t}"? This cannot be undone.`))try{await l.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
1
+ import{api as y}from"../api.js";import{addBannerAction as Ce}from"../lib/plugin-chrome.js";import{helpPopovers as ce}from"../lib/help-popover.js";import{onceClosed as Le,openMenuAt as de,sidebarCount as Oe,viewLifetime as $e}from"../lib/tool-kit.js";import{makeIconInput as Te}from"../lib/shortcode-modal.js";import{CORE as ue,FILTERS as Ie,arrangeProjects as Ue,checkProject as Ae,projectRow as De,slugify as Re}from"../lib/projects-arrange.js";const pe=/^#\/projects(\/new|\/edit\/[^/?#]+)?\/?(\?.*)?$/,Ne="#/projects",T="projects.draft",_="projects.filter",Pe=`
2
+ <div class="pjx-row" data-slug="{{slug}}" tabindex="0" data-bind-class="rowCls">
3
+ <span class="pjx-icon" aria-hidden="true"><span data-icon="{{icon}}" data-icon-size="16"></span></span>
4
+ <span class="pjx-main"><strong>{{name}}</strong><small>{{meta}}</small></span>
5
+ <span class="pjx-chips">
6
+ <span class="pjx-chip" data-bind-hidden="!system" title="Built in - cannot be deleted">Built in</span>
7
+ <span class="pjx-chip is-warn" data-bind-hidden="!off" title="Public pages and external API are off">Disabled</span>
8
+ </span>
9
+ <span class="pjx-count" title="Artefacts filed under it">{{total}}</span>
10
+ <button type="button" class="pjx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
11
+ </div>`,C=m=>String(m??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function qe(m){const c=document.createElement("template");return c.innerHTML=m.trim(),c.content.firstElementChild}const l=$e(pe);function U(){Oe(Ne);try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}export const projectsView={templateUrl:"/admin/js/templates/projects.html",async onMount(m){l.reset();const c=m.get(0).querySelector(".pjx")||m.get(0),L=()=>pe.test(location.hash)&&c.isConnected,f=M.observable([]),z=M.observable(!0),A=M.observable("");let H="all";try{H=Ie.includes(S.get(_))?S.get(_):"all"}catch{}const g=M.observable(H),Y=M.observable("");l.add(M.effect(()=>{const e=g.value;try{S.set(_,e)}catch{}}));const D=M.observable([]);l.add(M.effect(()=>{D.value=Ue(f.value,{filter:g.value,query:Y.value}).map(De)}));const me=M.computed(()=>{const e=f.value,t=e.filter(a=>a.enabled===!1).length;return{all:e.length,enabled:e.length-t,disabled:t}}),fe=M.computed(()=>f.value.length?"No projects match.":"No projects yet - New project in the banner makes one."),j=e=>f.peek().find(t=>t.slug===e);async function w(){try{const e=await y.projects.listWithCounts();if(!L())return;f.value=Array.isArray(e)?e:[],A.value=""}catch(e){A.value=`Could not load the projects: ${e.message||e}`}finally{z.value=!1}}async function V(e,t){if(!(!t&&!await E.confirm(`Disable "${C(e.name||e.slug)}"? Its public pages will 404 and its external API will stop answering. Editing it in the admin carries on.`)))try{await y.projects.update(e.slug,{enabled:t}),E.toast(`"${e.name||e.slug}" ${t?"enabled":"disabled"}.`,{type:"success"}),U(),await w()}catch(a){E.toast(`Not changed: ${a.message||a}`,{type:"error"})}}async function be(e){const t=Object.values(e.counts||{}).reduce((a,s)=>a+s,0);if(await E.confirm(`Untag ${t?`all ${t} artefacts`:"everything"} filed under "${C(e.name||e.slug)}"? They stay where they are and belong to Core from then on. The project itself is kept.`))try{const a=await y.projects.untagAll(e.slug),s=Object.values(a?.untagged||{}).reduce((k,v)=>k+(Number(v)||0),0);E.toast(`Untagged ${s} artefact${s===1?"":"s"}.`,{type:"success"}),U(),await w()}catch(a){E.toast(`Untag failed: ${a.message||a}`,{type:"error"})}}async function ge(e){if(await E.confirm(`Delete the project "${C(e.name||e.slug)}"? This cannot be undone.`))try{await y.projects.remove(e.slug),E.toast(`Deleted "${e.name||e.slug}".`,{type:"success"}),U(),await w()}catch(t){E.toast(`Not deleted: ${t.message||t} - untag them first (\u22EE \u2192 Untag all).`,{type:"error",duration:7e3})}}const W=e=>{location.hash=`#/projects/${encodeURIComponent(e)}`};let b=null;const R=new Set;let J=!1;function x(e=null,t=null){const a=e?j(e):null;if(e&&!a){E.toast(`There is no project "${e}".`,{type:"warning"});return}const s=!a,k=a?.slug===ue,v=a?.protected===!0,O={slug:a?.slug??"",name:a?.name??"",description:a?.description??"",icon:a?.icon??"folder",rootUrl:a?.rootUrl??"",sortOrder:String(a?.sortOrder??0),enabled:a?a.enabled!==!1:!0,...t||{}},i=Object.fromEntries(Object.entries(O).map(([n,p])=>[n,M.observable(p)]));let ee=!!t?.slug||!s;const te=n=>JSON.stringify(Object.fromEntries(Object.entries(n).map(([p,F])=>[p,typeof F=="boolean"?F:String(F??"")]))),ae=()=>Object.fromEntries(Object.keys(i).map(n=>[n,i[n].peek()])),je=te(O),ne=()=>te(ae())!==je||!!t,$=M.observable(!1),P=M.computed(()=>Ae(Object.fromEntries(Object.keys(i).map(n=>[n,i[n].value])),{isNew:s,isProtected:v,slug:a?.slug,taken:f.value.map(n=>n.slug)})),q=n=>M.computed(()=>P.value.errors[n]||""),se={...i,saving:$,isNew:s,notNew:!s,isProtected:v,canToggle:!k,slugErr:q("slug"),nameErr:q("name"),rootErr:q("rootUrl"),cannot:M.computed(()=>$.value||Object.keys(P.value.errors).length>0),saveText:s?"Create project":"Save"},we=s?M.effect(()=>{const n=i.name.value;ee||(i.slug.value=Re(n))}):()=>{},o=E.slideover({title:s?"New project":`Edit ${C(a.name||a.slug)}`,size:"md",position:"right"}),u=qe(`
12
+ <div class="pjx-form">
13
+ <div class="pjx-grid">
14
+ <div>
15
+ <label class="form-label" for="pjx-f-name">Name <span class="text-danger">*</span></label>
16
+ <input id="pjx-f-name" type="text" class="form-input" maxlength="80" placeholder="Members area" data-model="name.value">
17
+ <small class="pjx-err" data-bind-hidden="!nameErr.value" data-bind-text="nameErr.value"></small>
18
+ </div>
19
+ <div>
20
+ <label class="form-label" for="pjx-f-slug">Slug
21
+ <span data-help="The project's id - in its admin addresses and in every artefact filed under it. Made from the name as you type; it cannot be changed once the project exists."></span></label>
22
+ <input id="pjx-f-slug" type="text" class="form-input font-mono" maxlength="64" data-model="slug.value" data-bind-disabled="notNew">
23
+ <small class="pjx-err" data-bind-hidden="!slugErr.value" data-bind-text="slugErr.value"></small>
24
+ </div>
25
+ <div class="pjx-wide">
26
+ <label class="form-label" for="pjx-f-desc">Description</label>
27
+ <textarea id="pjx-f-desc" class="form-input" rows="2" maxlength="300" data-model="description.value"></textarea>
28
+ </div>
29
+ <div>
30
+ <label class="form-label" for="pjx-f-icon">Icon</label>
31
+ <div class="pjx-icon-mount"></div>
32
+ </div>
33
+ <div>
34
+ <label class="form-label" for="pjx-f-root">Root URL
35
+ <span data-help-title="Root URL" data-help="Pages under this address are filed under the project without being tagged one by one - with /members, a page at /members/join belongs here. Leave it empty to file pages by hand. The core project owns /."></span></label>
36
+ <input id="pjx-f-root" type="text" class="form-input font-mono" placeholder="/members" data-model="rootUrl.value" data-bind-disabled="isProtected">
37
+ <small class="pjx-err" data-bind-hidden="!rootErr.value" data-bind-text="rootErr.value"></small>
38
+ <small class="pjx-hint" data-bind-hidden="!isProtected">Locked - a built-in project.</small>
39
+ </div>
40
+ <div>
41
+ <label class="form-label" for="pjx-f-sort">Sort order
42
+ <span data-help="Lower numbers come first, here and in the sidebar."></span></label>
43
+ <input id="pjx-f-sort" type="number" class="form-input" data-model="sortOrder.value">
44
+ </div>
45
+ <div class="pjx-setting" data-bind-hidden="!canToggle">
46
+ <label class="form-label" for="pjx-f-enabled">Enabled
47
+ <span data-help="Off is a hard switch: the project's public pages 404 and its external API stops answering. Editing it in the admin carries on."></span></label>
48
+ <input id="pjx-f-enabled" type="checkbox" class="form-switch" data-model="enabled.value">
49
+ </div>
50
+ </div>
51
+ <div class="pjx-actions">
52
+ <small class="pjx-hint">Ctrl+Enter saves.</small>
53
+ <span class="pjx-spacer"></span>
54
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
55
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
56
+ <span data-icon="check" data-icon-size="13"></span> <span data-bind-text="saveText"></span></button>
57
+ </div>
58
+ </div>`),h=Te("folder, users, box\u2026",i.icon.peek());h.input.id="pjx-f-icon",h.input.classList.add("form-input");const oe=()=>{i.icon.value=h.input.value};h.input.addEventListener("input",oe),u.querySelector(".pjx-icon-mount").appendChild(h.el);async function le(){if(se.cannot.peek())return;$.value=!0;const{payload:n}=P.peek();try{s?await y.projects.create(n):await y.projects.update(a.slug,n),E.toast(s?`Created "${n.name}".`:"Saved.",{type:"success"}),B=!0,o.close(),U(),L()&&await w()}catch(p){E.toast(`Not saved: ${p.message||p}`,{type:"error"})}finally{$.value=!1}}o.element.appendChild(u);const xe=ce(u),Ee=M.applyBindings(se,u,{methods:{save:le,cancel:()=>o.close()}}),re=u.querySelector("#pjx-f-slug"),ke=()=>{ee=!!re.value};re.addEventListener("input",ke);const Se=n=>{(n.ctrlKey||n.metaKey)&&n.key==="Enter"&&(n.preventDefault(),le())};u.addEventListener("keydown",Se);const ie={slug:a?.slug??null,dirty:ne,values:ae};b=ie,R.add(o),Le(o,()=>{we(),Ee.dispose(),xe(),h.input.removeEventListener("input",oe),R.delete(o),b===ie&&(b=null),!J&&/^#\/projects\/(new|edit\/)/.test(location.hash)&&(history.replaceState(null,"","#/projects"),document.title="Projects - Domma CMS")});const Me=o.close.bind(o);let B=!1,K=!1;o.close=async n=>{if(!B&&n!==!0&&ne()){if(K)return;K=!0;const p=await E.confirm("Discard the changes you have not saved?");if(K=!1,!p)return}return B=!0,Me()},I.scan(u),o.open(),u.querySelector("#pjx-f-name")?.focus()}function ve(e){const t=j(e);if(!t)return[];const a=t.enabled===!1,s=t.slug===ue,k=Object.values(t.counts||{}).reduce((v,O)=>v+O,0);return[{header:t.name||t.slug},{label:"Open overview",icon:"eye",disabled:a,action:()=>W(t.slug)},{label:"Edit\u2026",icon:"edit",action:()=>x(t.slug)},{label:"Settings page",icon:"settings",disabled:a,action:()=>{location.hash=`#/projects/${encodeURIComponent(t.slug)}/settings`}},...s?[]:[{type:"divider"},a?{label:"Enable",icon:"play",action:()=>V(t,!0)}:{label:"Disable\u2026",icon:"pause",action:()=>V(t,!1)}],...t.protected?[]:[{type:"divider"},{label:"Untag all\u2026",icon:"tag",disabled:!k,action:()=>be(t)},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>ge(t)}]]}const d=c.querySelector(".pjx-list");d.innerHTML=Pe;const he=M.applyBindings({projects:f,loading:z,loadError:A,filter:g,query:Y,rows:D,counts:me,emptyText:fe},c,{methods:{showAll:()=>{g.value="all"},showEnabled:()=>{g.value="enabled"},showDisabled:()=>{g.value="disabled"}}});l.add(()=>he.dispose()),l.add(ce(c,{position:"bottom"})),l.add(M.effect(()=>{D.value,requestAnimationFrame(()=>{d.isConnected&&I.scan(d)})}));const ye=E.contextMenu(d,{cursor:"glyph",match:".pjx-row",inherit:!1,density:"compact",items:e=>ve(e.dataset.slug)});l.add(()=>ye?.destroy?.());const G=e=>{const t=j(e);t&&(t.enabled===!1?x(e):W(e))},Q=e=>{const t=e.target.closest(".pjx-row");if(!t)return;const a=e.target.closest(".pjx-kebab");if(a){de(a);return}G(t.dataset.slug)},X=e=>{const t=e.target.closest(".pjx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),de(t)):e.key==="Enter"&&e.target===t&&G(t.dataset.slug))};d.addEventListener("click",Q),d.addEventListener("keydown",X),l.add(()=>{d.removeEventListener("click",Q),d.removeEventListener("keydown",X)}),Ce(m,{id:"projects-new",icon:"plus",title:"New project",onClick:()=>x()});const Z=e=>{b?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Z),l.add(()=>window.removeEventListener("beforeunload",Z)),l.add(()=>{if(b?.dirty())try{S.set(T,{slug:b.slug,values:b.values(),at:new Date().toISOString()})}catch{}J=!0,[...R].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(c),await w(),!L())return;const N=location.hash.match(/^#\/projects\/(new|edit\/([^/?#]+))/);let r=null;try{r=S.get(T)}catch{}if(r?.values){try{S.remove?S.remove(T):S.set(T,null)}catch{}const e=r.slug?`your changes to "${C(j(r.slug)?.name||r.slug)}"`:"the new project you started";if(await E.confirm(`You left ${e} unsaved. Pick up where you left off?`)&&L()){x(r.slug&&j(r.slug)?r.slug:null,r.values);return}}N&&x(N[2]?decodeURIComponent(N[2]):null)}};