domma-cms 0.73.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/admin/js/api.js CHANGED
@@ -1 +1 @@
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"}),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};
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 c,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()=>{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=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 c.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")&&(p&&(clearInterval(p),p=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(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await A();const o=await C(),i=v(t.path);i&&R.navigate(i),y(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 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 m={...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))m[t]&&(m[t]=I(m[t],{plugin:t,licence:"Built in",...e}));const b=[...T];async function A(){if(d())try{const t=await c.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}`);m[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(),p||(p=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 p=null;async function N(){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 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: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)||"/";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
+ 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>
@@ -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)}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Projects API
3
3
  *
4
- * GET /api/projects - list visible projects
4
+ * GET /api/projects - list visible projects (?counts=1 adds per-type artefact counts)
5
+ * GET /api/projects/_count - the sidebar badge (the underscore can never be a slug)
5
6
  * GET /api/projects/:slug - full project
6
7
  * POST /api/projects - create
7
8
  * PUT /api/projects/:slug - edit (slug immutable)
@@ -19,9 +20,12 @@ import {
19
20
  CORE_PROJECT_SLUG,
20
21
  createProject,
21
22
  deleteProject,
23
+ emptyGroups,
24
+ getArtefactsByProject,
22
25
  getArtefactsForProject,
23
26
  getProject,
24
27
  listProjectsForUser,
28
+ projectsBadge,
25
29
  untagAllForProject,
26
30
  updateProject
27
31
  } from '../../services/projects.js';
@@ -44,9 +48,19 @@ export async function projectsRoutes(fastify, opts = {}) {
44
48
  const canDelete = {preHandler: [authenticate, requirePermission('projects', 'delete')]};
45
49
 
46
50
  fastify.get('/projects', canRead, async (request) => {
47
- return listProjectsForUser(request.user);
51
+ const list = await listProjectsForUser(request.user);
52
+ if (request.query?.counts !== '1') return list;
53
+ // One request for the Manage projects screen and the sidebar, and one pass over the artefacts.
54
+ const groups = await getArtefactsByProject().catch(() => ({}));
55
+ return list.map((p) => ({
56
+ ...p,
57
+ counts: Object.fromEntries(Object.entries(groups[p.slug] || emptyGroups()).map(([k, v]) => [k, v.length]))
58
+ }));
48
59
  });
49
60
 
61
+ // Registered before /projects/:slug; `_` is outside the slug alphabet, so no project can shadow it.
62
+ fastify.get('/projects/_count', canRead, async (request) => projectsBadge(await listProjectsForUser(request.user)));
63
+
50
64
  fastify.get('/projects/:slug', canRead, async (request, reply) => {
51
65
  const project = await getProject(request.params.slug);
52
66
  if (!project) return reply.status(404).send({error: 'Project not found'});
package/server/server.js CHANGED
@@ -525,6 +525,10 @@ registerSidebarItem({item: {source: 'Built in', text: 'Notes', url: '#/notes', i
525
525
  registerSidebarItem({item: {source: 'Built in', text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'}});
526
526
  registerSidebarItem({item: {source: 'Built in', text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'}});
527
527
  registerSidebarItem({item: {source: 'Built in', text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'}});
528
+ // Manage projects lives in the menu (the Projects folder, sidebar-migration.js);
529
+ // registering it here only adds its badge - the renderer merges a registered
530
+ // item into the menu node with the same url rather than adding a second one.
531
+ registerSidebarItem({folder: 'projects', item: {source: 'Built in', text: 'Manage projects', url: '#/projects', icon: 'folder', permission: 'projects', countUrl: '/api/projects/_count'}});
528
532
 
529
533
  await registerPlugins(app);
530
534
 
@@ -108,8 +108,16 @@ export async function getProject(slug) {
108
108
  * @returns {Promise<object>} The persisted project record.
109
109
  * @throws {Error} If validation fails or the slug already exists.
110
110
  */
111
+ /**
112
+ * Slugs a NEW project cannot take: `#/projects/new` and `#/projects/edit/…`
113
+ * are admin addresses, so a project called either could never be opened.
114
+ * (Checked on create only - an existing record keeps working.)
115
+ */
116
+ export const RESERVED_PROJECT_SLUGS = ['new', 'edit'];
117
+
111
118
  export async function createProject(input) {
112
119
  const errors = validateProject(input);
120
+ if (RESERVED_PROJECT_SLUGS.includes(input?.slug)) errors.push(`"${input.slug}" is reserved - the admin uses it in its own addresses`);
113
121
  if (errors.length) throw new Error('Validation failed: ' + errors.join('; '));
114
122
  if (await getProject(input.slug)) {
115
123
  throw new Error(`Project "${input.slug}" already exists`);
@@ -397,23 +405,28 @@ export async function getProjectForPage(urlPath, explicitProject) {
397
405
  return best ?? coreFallback;
398
406
  }
399
407
 
408
+ /** The per-type buckets every project's artefacts are grouped into. */
409
+ export function emptyGroups() {
410
+ return {
411
+ pages: [], collections: [], forms: [], actions: [],
412
+ menus: [], blocks: [], components: [], views: [], roles: [], users: [], apis: []
413
+ };
414
+ }
415
+
400
416
  /**
401
- * Enumerate every artefact belonging to the given project. Returns a flat
402
- * grouping by type - each value is an array of artefact summary objects.
403
- * Used by the project detail page (counts + per-type tabs).
417
+ * Every artefact, grouped by the project it resolves to - ONE pass over each
418
+ * artefact type, however many projects there are. The Manage projects list
419
+ * (GET /api/projects?counts=1) needs every project's counts, and asking
420
+ * getArtefactsForProject once per project re-read every artefact N times.
404
421
  *
405
- * Membership is by resolution, not tag equality: artefacts without a
406
- * `meta.project` tag belong to the core project, so
407
- * `getArtefactsForProject('core')` enumerates everything unclaimed.
422
+ * Membership is by resolution, not tag equality: an artefact without a
423
+ * `meta.project` tag lands under core.
408
424
  *
409
- * @param {string} projectSlug
410
- * @returns {Promise<{pages:object[], collections:object[], forms:object[], actions:object[], menus:object[], blocks:object[], components:object[], views:object[], roles:object[], users:object[], apis:object[]}>}
425
+ * @returns {Promise<Record<string, ReturnType<typeof emptyGroups>>>} keyed by project slug; a project with nothing is absent
411
426
  */
412
- export async function getArtefactsForProject(projectSlug) {
413
- const out = {
414
- pages: [], collections: [], forms: [], actions: [],
415
- menus: [], blocks: [], components: [], views: [], roles: [], users: [], apis: []
416
- };
427
+ export async function getArtefactsByProject() {
428
+ const groups = {};
429
+ const out = (slug) => (groups[slug] ??= emptyGroups());
417
430
 
418
431
  // The three loops below re-read each artefact by slug; guard PER ITEM so a
419
432
  // single unreadable artefact (e.g. a form whose internal slug doesn't match
@@ -423,7 +436,7 @@ export async function getArtefactsForProject(projectSlug) {
423
436
  for (const m of await listMenus()) {
424
437
  try {
425
438
  const full = await getMenu(m.slug);
426
- if (resolveArtefactProject(full) === projectSlug) out.menus.push(m);
439
+ out(resolveArtefactProject(full)).menus.push(m);
427
440
  } catch { /* skip broken menu */ }
428
441
  }
429
442
  } catch { /* service unavailable */ }
@@ -433,7 +446,7 @@ export async function getArtefactsForProject(projectSlug) {
433
446
  for (const f of await listForms()) {
434
447
  try {
435
448
  const full = await readForm(f.slug);
436
- if (resolveArtefactProject(full) === projectSlug) out.forms.push(f);
449
+ out(resolveArtefactProject(full)).forms.push(f);
437
450
  } catch { /* skip broken form */ }
438
451
  }
439
452
  } catch { /* skip */ }
@@ -443,7 +456,7 @@ export async function getArtefactsForProject(projectSlug) {
443
456
  for (const c of await listCollections()) {
444
457
  try {
445
458
  const full = await getCollection(c.slug);
446
- if (resolveArtefactProject(full) === projectSlug) out.collections.push(c);
459
+ out(resolveArtefactProject(full)).collections.push(c);
447
460
  } catch { /* skip broken collection */ }
448
461
  }
449
462
  } catch { /* skip */ }
@@ -451,7 +464,7 @@ export async function getArtefactsForProject(projectSlug) {
451
464
  try {
452
465
  const {listActions} = await import('./actions.js');
453
466
  for (const a of await listActions()) {
454
- if (resolveArtefactProject(a) === projectSlug) out.actions.push(a);
467
+ out(resolveArtefactProject(a)).actions.push(a);
455
468
  }
456
469
  } catch { /* skip */ }
457
470
 
@@ -460,7 +473,7 @@ export async function getArtefactsForProject(projectSlug) {
460
473
  // per-block read is needed. (Blocks are keyed by `name`, not `slug`.)
461
474
  const {listBlocks} = await import('./blocks.js');
462
475
  for (const b of await listBlocks()) {
463
- if (resolveArtefactProject(b) === projectSlug) out.blocks.push(b);
476
+ out(resolveArtefactProject(b)).blocks.push(b);
464
477
  }
465
478
  } catch { /* skip */ }
466
479
 
@@ -469,28 +482,28 @@ export async function getArtefactsForProject(projectSlug) {
469
482
  // per-component read is needed. (Components are keyed by `name`.)
470
483
  const {listComponents} = await import('./components.js');
471
484
  for (const c of await listComponents()) {
472
- if (resolveArtefactProject(c) === projectSlug) out.components.push(c);
485
+ out(resolveArtefactProject(c)).components.push(c);
473
486
  }
474
487
  } catch { /* skip */ }
475
488
 
476
489
  try {
477
490
  const {listViews} = await import('./views.js');
478
491
  for (const v of await listViews()) {
479
- if (resolveArtefactProject(v) === projectSlug) out.views.push(v);
492
+ out(resolveArtefactProject(v)).views.push(v);
480
493
  }
481
494
  } catch { /* skip */ }
482
495
 
483
496
  try {
484
497
  const {getRoleMap} = await import('./roles.js');
485
498
  for (const role of getRoleMap().values()) {
486
- if (resolveArtefactProject(role) === projectSlug) out.roles.push(role);
499
+ out(resolveArtefactProject(role)).roles.push(role);
487
500
  }
488
501
  } catch { /* skip */ }
489
502
 
490
503
  try {
491
504
  const {listUsers} = await import('./users.js');
492
505
  for (const u of await listUsers()) {
493
- if (resolveArtefactProject(u) === projectSlug) out.users.push(u);
506
+ out(resolveArtefactProject(u)).users.push(u);
494
507
  }
495
508
  } catch { /* skip */ }
496
509
 
@@ -500,7 +513,7 @@ export async function getArtefactsForProject(projectSlug) {
500
513
  const url = p.url || p.urlPath || '/';
501
514
  const explicit = (p.project === undefined) ? undefined : p.project;
502
515
  const pageProject = await getProjectForPage(url, explicit);
503
- if (pageProject === projectSlug) out.pages.push(p);
516
+ out(pageProject).pages.push(p);
504
517
  }
505
518
  } catch { /* skip */ }
506
519
 
@@ -509,13 +522,27 @@ export async function getArtefactsForProject(projectSlug) {
509
522
  // field - no untagged-→core fallback applies, so match directly).
510
523
  const {entries} = await listEntries('api-endpoints', {limit: 0});
511
524
  for (const e of entries) {
512
- if (e.data?.project === projectSlug) {
513
- out.apis.push({id: e.id, name: e.data.name, path: e.data.path, collection: e.data.collection});
525
+ if (e.data?.project) {
526
+ out(e.data.project).apis.push({id: e.id, name: e.data.name, path: e.data.path, collection: e.data.collection});
514
527
  }
515
528
  }
516
529
  } catch { /* skip */ }
517
530
 
518
- return out;
531
+ return groups;
532
+ }
533
+
534
+ /**
535
+ * Enumerate every artefact belonging to the given project. Returns a flat
536
+ * grouping by type - each value is an array of artefact summary objects.
537
+ * Used by the project detail page (counts + per-type tabs).
538
+ *
539
+ * `getArtefactsForProject('core')` enumerates everything unclaimed.
540
+ *
541
+ * @param {string} projectSlug
542
+ * @returns {Promise<ReturnType<typeof emptyGroups>>}
543
+ */
544
+ export async function getArtefactsForProject(projectSlug) {
545
+ return (await getArtefactsByProject())[projectSlug] ?? emptyGroups();
519
546
  }
520
547
 
521
548
  /**
@@ -644,3 +671,36 @@ export async function untagAllForProject(projectSlug) {
644
671
 
645
672
  return counts;
646
673
  }
674
+
675
+ /**
676
+ * The sidebar badge on Manage projects (its countUrl, GET /api/projects/_count):
677
+ * how many projects this user sees - a count to glance at, not a warning -
678
+ * with the disabled ones called out. Plain text throughout: the
679
+ * popover sets it with textContent.
680
+ *
681
+ * @param {object[]} projects - as listProjectsForUser returns them
682
+ * @returns {{count: number, label: string, tone: string, details: object}}
683
+ */
684
+ export function projectsBadge(projects = []) {
685
+ const list = [...projects].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)
686
+ || String(a.name || a.slug).localeCompare(String(b.name || b.slug)));
687
+ const off = list.filter(p => p.enabled === false);
688
+ const n = list.length;
689
+ return {
690
+ count: n,
691
+ label: `${n} project${n === 1 ? '' : 's'}${off.length ? `, ${off.length} disabled` : ''}`,
692
+ tone: 'info',
693
+ details: {
694
+ title: 'Projects',
695
+ rows: [
696
+ {label: 'Enabled', value: String(n - off.length)},
697
+ {label: 'Disabled', value: String(off.length)}
698
+ ],
699
+ items: list.slice(0, 8).map(p => ({
700
+ text: String(p.name || p.slug),
701
+ meta: [p.slug, p.rootUrl, p.enabled === false ? 'disabled' : '', p.protected ? 'built in' : ''].filter(Boolean).join(' · ')
702
+ })),
703
+ empty: 'No projects yet.'
704
+ }
705
+ };
706
+ }
@@ -1,45 +0,0 @@
1
- <div class="page-header">
2
- <h2><span data-icon="folder"></span> <span id="pe-title">Edit project</span></h2>
3
- <div class="header-actions">
4
- <a class="btn btn-secondary" href="#/projects"><span data-icon="arrow-left"></span> Back</a>
5
- <button class="btn btn-primary" id="pe-save"><span data-icon="check"></span> Save</button>
6
- </div>
7
- </div>
8
-
9
- <div class="card">
10
- <div class="card-header">
11
- <span data-icon="info"></span> Project details
12
- </div>
13
- <div class="card-body">
14
- <div class="grid grid-cols-2 dm-pe-grid">
15
- <div class="form-group">
16
- <label for="pe-slug">Slug <span class="required">*</span></label>
17
- <input type="text" id="pe-slug" name="slug" class="form-input" required>
18
- <small class="form-hint">Lowercase alphanumeric + hyphen; immutable after create</small>
19
- </div>
20
- <div class="form-group">
21
- <label for="pe-name">Name <span class="required">*</span></label>
22
- <input type="text" id="pe-name" name="name" class="form-input" required>
23
- </div>
24
- <div class="form-group col-span-2">
25
- <label for="pe-description">Description</label>
26
- <textarea id="pe-description" name="description" class="form-input" rows="2"></textarea>
27
- </div>
28
- <div class="form-group">
29
- <label for="pe-icon">Icon</label>
30
- <div id="pe-icon-mount"></div>
31
- <small class="form-hint">Click <code>⊞</code> to browse Domma icons; defaults to <code>folder</code></small>
32
- </div>
33
- <div class="form-group">
34
- <label for="pe-rootUrl">Root URL</label>
35
- <input type="text" id="pe-rootUrl" name="rootUrl" class="form-input" placeholder="/members">
36
- <small class="form-hint">URL prefix for auto-inheritance (must start with /)</small>
37
- </div>
38
- <div class="form-group">
39
- <label for="pe-sortOrder">Sort order</label>
40
- <input type="number" id="pe-sortOrder" name="sortOrder" class="form-input" value="0">
41
- <small class="form-hint">Lower = higher in the sidebar</small>
42
- </div>
43
- </div>
44
- </div>
45
- </div>
@@ -1 +0,0 @@
1
- import{api as d}from"../api.js";import{makeIconInput as h}from"../lib/shortcode-modal.js";export const projectEditorView={templateUrl:"/admin/js/templates/project-editor.html",async onMount(t){const p=location.hash.match(/^#\/projects\/edit\/(.+)$/),i=p?decodeURIComponent(p[1]):null,s=!i;let e={slug:"",name:"",description:"",icon:"folder",rootUrl:"",sortOrder:0};if(!s){const o=E.loader(t.get(0),{type:"dots"});try{e=await d.projects.get(i)}catch(r){E.toast(`Failed to load project: ${r.message||r}`,{type:"error"}),location.hash="#/projects";return}finally{o.destroy()}}t.find("#pe-title").text(s?"New project":`Edit "${e.slug}"`);const l=t.find("#pe-slug"),n=t.find("#pe-name"),m=t.find("#pe-description"),u=t.find("#pe-rootUrl"),f=t.find("#pe-sortOrder");l.val(e.slug||""),n.val(e.name||""),m.val(e.description||""),u.val(e.rootUrl||""),f.val(e.sortOrder??0),s||l.prop("disabled",!0);const g=t.find("#pe-icon-mount").get(0),a=h("e.g. folder, users, box",e.icon||"folder");a.input.id="pe-icon",a.input.classList.add("form-input"),g.appendChild(a.el),Domma.icons.scan(t.get(0)),t.find("#pe-save").on("click",async()=>{const o={slug:l.val().trim(),name:n.val().trim(),description:m.val().trim(),icon:a.input.value.trim()||"folder",sortOrder:Number.parseInt(f.val(),10)||0},r=u.val().trim();r&&(o.rootUrl=r);try{if(s)await d.projects.create(o);else{const{slug:c,...v}=o;await d.projects.update(i,v)}E.toast("Saved.",{type:"success"}),location.hash="#/projects"}catch(c){E.toast(`Save failed: ${c.message||c}`,{type:"error"})}})}};