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 +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/projects-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/templates/projects.html +69 -10
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/projects.js +58 -3
- package/package.json +1 -1
- package/server/routes/api/projects.js +16 -2
- package/server/server.js +4 -0
- package/server/services/projects.js +86 -26
- package/admin/js/templates/project-editor.html +0 -45
- package/admin/js/views/project-editor.js +0 -1
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">${
|
|
3
|
-
<span class="topbar-role-badge topbar-role-badge--${
|
|
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(),
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}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
|
|
2
|
-
<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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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}"${
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
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
|
-
|
|
11
|
-
|
|
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>
|
package/admin/js/views/index.js
CHANGED
|
@@ -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
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");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,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
|
-
|
|
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
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
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:
|
|
406
|
-
* `meta.project` tag
|
|
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
|
-
* @
|
|
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
|
|
413
|
-
const
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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"})}})}};
|