domma-cms 0.36.16 → 0.36.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +1 -1
- package/admin/js/lib/simple-editor.js +51 -5
- package/admin/js/lib/syntax-highlight.js +2 -0
- package/admin/js/templates/plugin-code.html +82 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/plugin-code.js +4 -0
- package/admin/js/views/plugins.js +13 -12
- package/package.json +1 -1
- package/server/routes/api/plugins.js +51 -3
- package/server/services/permissionRegistry.js +2 -1
- package/server/services/pluginFiles.js +216 -0
package/CLAUDE.md
CHANGED
|
@@ -169,6 +169,7 @@ Add custom public-site JS to `public/js/site.js` or new files loaded from `publi
|
|
|
169
169
|
| `images.js` | Image processing / thumbnail generation |
|
|
170
170
|
| `plugins.js` | Plugin discovery, validation, registration, injection snippets |
|
|
171
171
|
| `pluginScaffold.js` | Scaffold new plugins from `plugins/_template` (`POST /api/plugins/scaffold`, pre-enabled, restart to activate) |
|
|
172
|
+
| `pluginFiles.js` | In-admin plugin code editor I/O — list/read/write/delete files confined to `plugins/<name>/`, gated by `plugins.develop` |
|
|
172
173
|
| `cache/index.js` | Pluggable response cache (Memory/None/Redis drivers) with tag-based invalidation. See `docs/cache.md`. |
|
|
173
174
|
|
|
174
175
|
## Storage Adapters
|
package/admin/js/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const a="/api";function d(){return S.get("auth_token")}function c(){return S.get("auth_refresh_token")}function l(e){S.set("auth_token",e)}function h(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function u(){const e=c();if(!e)throw new Error("No refresh token");const o=await fetch(`${a}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw h(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return l(s),s}async function t(e,o={}){let s=d();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${a}${e}`,{...o,headers:r(s)});if(n.status===401&&c())try{s=await u(),n=await fetch(`${a}${e}`,{...o,headers:r(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function y(e,o){const s=d(),r=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${a}${e}`,{method:"POST",headers:r,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.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})}),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})})},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"})},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=>y("/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)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o)=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o})}),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 s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{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,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),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 s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{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 s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),s=await fetch(`${a}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const m=await s.json().catch(()=>({}));throw new Error(m.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${a}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Block already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),s=await fetch(`${a}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const m=await s.json().catch(()=>({}));throw new Error(m.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${a}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Component already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!d()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&l(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=c();e&&fetch(`${a}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),h(),R.navigate("/login")}export{t as apiRequest,u as refreshAccessToken,d as getToken,h as clearAuth};
|
|
1
|
+
const a="/api";function d(){return S.get("auth_token")}function c(){return S.get("auth_refresh_token")}function l(e){S.set("auth_token",e)}function h(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function u(){const e=c();if(!e)throw new Error("No refresh token");const o=await fetch(`${a}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw h(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return l(s),s}async function t(e,o={}){let s=d();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${a}${e}`,{...o,headers:r(s)});if(n.status===401&&c())try{s=await u(),n=await fetch(`${a}${e}`,{...o,headers:r(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function p(e,o){const s=d(),r=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${a}${e}`,{method:"POST",headers:r,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.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})}),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})})},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"})},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=>p("/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,s)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:s})}),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"})},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o)=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o})}),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 s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{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,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),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 s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{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 s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),s=await fetch(`${a}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const m=await s.json().catch(()=>({}));throw new Error(m.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${a}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Block already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=d(),s=await fetch(`${a}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const m=await s.json().catch(()=>({}));throw new Error(m.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${a}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...d()?{Authorization:`Bearer ${d()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Component already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!d()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&l(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=c();e&&fetch(`${a}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),h(),R.navigate("/login")}export{t as apiRequest,u as refreshAccessToken,d as getToken,h as clearAuth};
|
package/admin/js/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{views as W}from"./views/index.js";import{api as l,getUser as c,isAuthenticated as m,logout as _}from"./api.js";import{installHttpInterceptor as B}from"./http-interceptor.js";$(()=>{B(),(async()=>{try{const t=m()?await l.settings.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();const A=["jb-company","jb-agent","jb-candidate"],k=["/job-board","/my-profile"];function h(t){return t&&A.includes(t.role)}function w(t){return k.some(e=>t===e||t.startsWith(e+"/"))}R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!m()){R.navigate("/login");return}if(h(c())&&!w(t.path)){R.navigate("/job-board");return}o()});async function f(){try{return(await l.get("/auth/permissions")).permissions||[]}catch{return[]}}async function C(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&n&&(clearInterval(n),n=null)});const I=[{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:"/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:"/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:"/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:"/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(h(c())&&!w(t.path)){R.navigate("/job-board");return}if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await P();const o=await f();C(o);try{const i=await l.system.notifications.list().catch(()=>[]),s=(Array.isArray(i)?i:[]).filter(a=>a.unread&&["warning","critical"].includes(a.data?.severity)).slice(0,3);for(const a of s){const r=p(a.data?.title||""),d=p((a.data?.body||"").slice(0,120));E.toast(`${r} \u2014 ${d}`,{type:a.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}j()}}),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 U=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function N(t){return/^\/(pages|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||!N(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let s=0;const a=60,r=1500,d=()=>{s+=a;for(const L of U){const u=document.querySelector(L);if(!u)continue;if(Array.from(u.options||[]).find(T=>T.value===i)){u.value=i,u.dispatchEvent(new Event("change",{bubbles:!0}));return}}s<r&&setTimeout(d,a)};setTimeout(d,a)});const D="cms_card_states",g=S.get(D)||{};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&&g[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&&(g[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(D,g))}),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 b={...W},v=[...I];async function P(){if(m())try{const t=await l.plugins.adminConfig();t.routes?.length&&v.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);b[e]=i[o.exportName]}catch{}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 j(){const t=c();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
|
|
1
|
+
import{views as W}from"./views/index.js";import{api as l,getUser as c,isAuthenticated as m,logout as _}from"./api.js";import{installHttpInterceptor as B}from"./http-interceptor.js";$(()=>{B(),(async()=>{try{const t=m()?await l.settings.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();const A=["jb-company","jb-agent","jb-candidate"],k=["/job-board","/my-profile"];function h(t){return t&&A.includes(t.role)}function w(t){return k.some(e=>t===e||t.startsWith(e+"/"))}R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!m()){R.navigate("/login");return}if(h(c())&&!w(t.path)){R.navigate("/job-board");return}o()});async function f(){try{return(await l.get("/auth/permissions")).permissions||[]}catch{return[]}}async function C(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&n&&(clearInterval(n),n=null)});const I=[{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:"/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:"/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:"/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(h(c())&&!w(t.path)){R.navigate("/job-board");return}if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await P();const o=await f();C(o);try{const i=await l.system.notifications.list().catch(()=>[]),s=(Array.isArray(i)?i:[]).filter(a=>a.unread&&["warning","critical"].includes(a.data?.severity)).slice(0,3);for(const a of s){const r=p(a.data?.title||""),d=p((a.data?.body||"").slice(0,120));E.toast(`${r} \u2014 ${d}`,{type:a.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}j()}}),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 U=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function N(t){return/^\/(pages|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||!N(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let s=0;const a=60,r=1500,d=()=>{s+=a;for(const L of U){const u=document.querySelector(L);if(!u)continue;if(Array.from(u.options||[]).find(T=>T.value===i)){u.value=i,u.dispatchEvent(new Event("change",{bubbles:!0}));return}}s<r&&setTimeout(d,a)};setTimeout(d,a)});const D="cms_card_states",g=S.get(D)||{};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&&g[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&&(g[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(D,g))}),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 b={...W},v=[...I];async function P(){if(m())try{const t=await l.plugins.adminConfig();t.routes?.length&&v.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);b[e]=i[o.exportName]}catch{}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 j(){const t=c();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
2
|
<span id="topbar-user-name" class="topbar-user-name">${p(t.name)}</span>
|
|
3
3
|
<span class="topbar-role-badge topbar-role-badge--${p(t.role)}">${o}</span>
|
|
4
4
|
`),$("#topbar-actions").html(`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
import{highlight as S}from"./syntax-highlight.js";function T(){if(document.getElementById("dm-simple-editor-style"))return;const r=document.createElement("style");r.id="dm-simple-editor-style",r.textContent=`
|
|
2
2
|
.dm-edit-wrap {
|
|
3
3
|
display: flex;
|
|
4
4
|
flex-direction: row;
|
|
@@ -43,7 +43,53 @@ function w(){if(document.getElementById("dm-simple-editor-style"))return;const r
|
|
|
43
43
|
overflow: auto;
|
|
44
44
|
box-sizing: border-box;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
/* \u2500\u2500 Highlight overlay (language mode only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
47
|
+
.dm-edit-host {
|
|
48
|
+
position: relative;
|
|
49
|
+
flex: 1;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
display: flex;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
54
|
+
.dm-edit-host .dm-edit-area {
|
|
55
|
+
position: relative;
|
|
56
|
+
z-index: 1;
|
|
57
|
+
width: 100%;
|
|
58
|
+
color: transparent;
|
|
59
|
+
caret-color: var(--dm-text, #ddd);
|
|
60
|
+
}
|
|
61
|
+
.dm-edit-host .dm-edit-area::selection { background: rgba(102, 148, 255, .35); }
|
|
62
|
+
.dm-edit-highlight {
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
z-index: 0;
|
|
66
|
+
margin: 0;
|
|
67
|
+
padding: .75rem;
|
|
68
|
+
font-family: var(--dm-font-mono, 'Fira Code', ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
69
|
+
font-size: .85rem;
|
|
70
|
+
line-height: 1.55;
|
|
71
|
+
tab-size: 2;
|
|
72
|
+
-moz-tab-size: 2;
|
|
73
|
+
white-space: pre;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
color: var(--dm-text, #ddd);
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
}
|
|
79
|
+
/* Token palette \u2014 single set chosen to stay legible on light and
|
|
80
|
+
dark themes alike. */
|
|
81
|
+
.dm-tok-comment { color: #8b949e; font-style: italic; }
|
|
82
|
+
.dm-tok-string { color: #4f9e4f; }
|
|
83
|
+
.dm-tok-keyword { color: #b559c8; }
|
|
84
|
+
.dm-tok-literal { color: #2f9bb3; }
|
|
85
|
+
.dm-tok-number { color: #c98a2c; }
|
|
86
|
+
.dm-tok-func { color: #4078f2; }
|
|
87
|
+
.dm-tok-key { color: #b8762e; }
|
|
88
|
+
.dm-tok-tag { color: #cc5c66; }
|
|
89
|
+
`,document.head.appendChild(r)}export function createSimpleEditor(r,w={}){if(!r)throw new Error("createSimpleEditor: mountEl is required.");const{initialValue:y="",onChange:v=null,language:x=null,autoIndent:C=!1,onCursor:k=null}=w;for(T();r.firstChild;)r.removeChild(r.firstChild);const a=document.createElement("div"),s=document.createElement("div"),e=document.createElement("textarea");a.className="dm-edit-wrap",s.className="dm-edit-gutter",e.className="dm-edit-area",e.spellcheck=!1,e.setAttribute("autocomplete","off"),e.setAttribute("autocorrect","off"),e.setAttribute("autocapitalize","off"),a.appendChild(s);let l=null;if(x){const t=document.createElement("div");t.className="dm-edit-host",l=document.createElement("pre"),l.className="dm-edit-highlight",t.appendChild(l),t.appendChild(e),a.appendChild(t)}else a.appendChild(e);r.appendChild(a);function f(){l&&(l.innerHTML=S(e.value,x))}function p(){const t=e.value.split(`
|
|
90
|
+
`).length||1,n=new Array(t);for(let i=0;i<t;i++)n[i]=String(i+1);s.textContent=n.join(`
|
|
91
|
+
`);const o=String(t).length;s.style.minWidth=`calc(${o}ch + 1.5rem)`}function u(){requestAnimationFrame(()=>{s.scrollTop=e.scrollTop,l&&(l.scrollTop=e.scrollTop,l.scrollLeft=e.scrollLeft)})}function g(){if(!k)return;const t=e.value.slice(0,e.selectionStart),n=(t.match(/\n/g)||[]).length+1,o=t.length-(t.lastIndexOf(`
|
|
92
|
+
`)+1)+1;k({line:n,col:o})}function E(t){if(t.key!=="Enter"||t.isComposing)return;t.preventDefault();const{selectionStart:n,value:o}=e,i=o.lastIndexOf(`
|
|
93
|
+
`,n-1)+1,d=(o.slice(i,n).match(/^[ \t]*/)||[""])[0],h=o.slice(0,n).trimEnd().slice(-1),m="{[(".includes(h)?" ":"";e.setRangeText(`
|
|
94
|
+
`+d+m,n,e.selectionEnd,"end"),e.dispatchEvent(new Event("input",{bubbles:!0}))}function z(t){if(t.key!=="Tab")return;t.preventDefault();const{selectionStart:n,selectionEnd:o,value:i}=e;if(!t.shiftKey)e.value=i.slice(0,n)+" "+i.slice(o),e.selectionStart=e.selectionEnd=n+2;else{const d=i.lastIndexOf(`
|
|
95
|
+
`,n-1)+1,h=i.slice(d,d+2),m=h===" "?2:h[0]===" "?1:0;m>0&&(e.value=i.slice(0,d)+i.slice(d+m),e.selectionStart=e.selectionEnd=Math.max(d,n-m))}e.dispatchEvent(new Event("input",{bubbles:!0}))}const b=[];function c(t,n,o){t.addEventListener(n,o),b.push([t,n,o])}return c(e,"input",()=>{p(),f(),u(),g(),v&&v(e.value)}),c(e,"scroll",u),c(e,"keydown",z),C&&c(e,"keydown",E),c(e,"keyup",()=>{u(),g()}),c(e,"click",g),e.value=y,p(),f(),{textarea:e,gutter:s,getValue(){return e.value},setValue(t){e.value!==t&&(e.value=t??"",p(),f(),u())},focus(){e.focus()},destroy(){for(const[t,n,o]of b)t.removeEventListener(n,o);b.length=0,a.parentNode===r&&r.removeChild(a)}}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export function escapeHtml(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const i=(e,t)=>`<span class="dm-tok-${e}">${escapeHtml(t)}</span>`;function m(e,t){let n="",s=0;const o=t.map(r=>({cls:r.cls,re:new RegExp(r.re.source,r.re.flags.replace("y","")+"y")}));for(;s<e.length;){let r=!1;for(const{cls:l,re:a}of o){a.lastIndex=s;const c=a.exec(e);if(c&&c[0].length>0){n+=i(l,c[0]),s+=c[0].length,r=!0;break}}if(!r){let l=s+1;if(/[A-Za-z0-9_]/.test(e[s]))for(;l<e.length&&/[A-Za-z0-9_]/.test(e[l]);)l++;n+=escapeHtml(e.slice(s,l)),s=l}}return n}const d=/\b(?:async|await|break|case|catch|class|const|continue|default|delete|do|else|export|extends|finally|for|from|function|if|import|in|instanceof|let|new|of|return|static|super|switch|this|throw|try|typeof|var|void|while|yield)\b/,g={js:[{cls:"comment",re:/\/\/[^\n]*/},{cls:"comment",re:/\/\*[\s\S]*?(?:\*\/|$)/},{cls:"string",re:/`(?:[^`\\]|\\[\s\S])*(?:`|$)/},{cls:"string",re:/'(?:[^'\\\n]|\\[\s\S])*(?:'|$)/},{cls:"string",re:/"(?:[^"\\\n]|\\[\s\S])*(?:"|$)/},{cls:"keyword",re:d},{cls:"literal",re:/\b(?:true|false|null|undefined|NaN|Infinity)\b/},{cls:"number",re:/\b0[xXbBoO]?[\da-fA-F_]+n?\b|\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?\b/},{cls:"func",re:/[A-Za-z_$][\w$]*(?=\s*\()/}],json:[{cls:"key",re:/"(?:[^"\\]|\\[\s\S])*"(?=\s*:)/},{cls:"string",re:/"(?:[^"\\]|\\[\s\S])*(?:"|$)/},{cls:"literal",re:/\b(?:true|false|null)\b/},{cls:"number",re:/-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/}],css:[{cls:"comment",re:/\/\*[\s\S]*?(?:\*\/|$)/},{cls:"string",re:/'(?:[^'\\\n]|\\[\s\S])*(?:'|$)|"(?:[^"\\\n]|\\[\s\S])*(?:"|$)/},{cls:"number",re:/-?\b\d+(?:\.\d+)?(?:px|rem|em|vh|vw|s|ms|deg|fr|%)?\b/},{cls:"key",re:/--?[a-zA-Z][\w-]*(?=\s*:)/},{cls:"keyword",re:/@[a-zA-Z-]+/},{cls:"func",re:/[a-zA-Z-]+(?=\()/},{cls:"literal",re:/#[0-9a-fA-F]{3,8}\b/},{cls:"tag",re:/\.[a-zA-Z_-][\w-]*|#[a-zA-Z_-][\w-]*/}],html:[{cls:"comment",re:/<!--[\s\S]*?(?:-->|$)/},{cls:"string",re:/"(?:[^"]*)(?:"|$)|'(?:[^']*)(?:'|$)/},{cls:"tag",re:/<\/?[a-zA-Z][\w-]*|\/?>/},{cls:"key",re:/\b[a-zA-Z-]+(?==)/}],md:[{cls:"keyword",re:/^#{1,6}[^\n]*/m},{cls:"comment",re:/```[\s\S]*?(?:```|$)/},{cls:"string",re:/`[^`\n]*(?:`|$)/},{cls:"literal",re:/\*\*[^*\n]+\*\*|\*[^*\n]+\*/},{cls:"func",re:/\[[^\]\n]*\]\([^)\n]*\)/},{cls:"tag",re:/^\s*[-*+]\s|^\s*\d+\.\s/m}]};export function languageForFile(e){const t=String(e||"").toLowerCase().split(".").pop();return{js:"js",json:"json",css:"css",html:"html",htm:"html",svg:"html",md:"md",markdown:"md"}[t]||null}export function highlight(e,t){const n=g[t];return(n?m(String(e??""),n):escapeHtml(String(e??"")))+`
|
|
2
|
+
`}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.pcode-layout { display: flex; gap: 1rem; align-items: stretch; min-height: 70vh; }
|
|
3
|
+
.pcode-files { flex: 0 0 250px; overflow-y: auto; }
|
|
4
|
+
.pcode-files .card-body { padding: .5rem; }
|
|
5
|
+
.pcode-dir {
|
|
6
|
+
padding: .5rem .5rem .2rem; font-size: .7rem; font-weight: 700;
|
|
7
|
+
text-transform: uppercase; letter-spacing: .04em;
|
|
8
|
+
color: var(--dm-text-muted, #6b7280);
|
|
9
|
+
}
|
|
10
|
+
.pcode-file {
|
|
11
|
+
display: flex; align-items: center; gap: .4rem; width: 100%;
|
|
12
|
+
padding: .3rem .5rem .3rem 1rem; border: 0; background: none; cursor: pointer;
|
|
13
|
+
border-radius: var(--dm-radius-md, 6px); text-align: left;
|
|
14
|
+
color: var(--dm-text, inherit); font-size: .82rem;
|
|
15
|
+
font-family: var(--dm-font-mono, ui-monospace, Menlo, Consolas, monospace);
|
|
16
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
17
|
+
}
|
|
18
|
+
.pcode-file:hover { background: var(--dm-hover-bg, rgba(128,128,128,.12)); }
|
|
19
|
+
.pcode-file.active { background: var(--dm-primary, #2563eb); color: #fff; }
|
|
20
|
+
.pcode-file .pcode-dirty { margin-left: auto; font-weight: 700; }
|
|
21
|
+
.pcode-editor-card { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
22
|
+
.pcode-editor-card .card-body { flex: 1; display: flex; flex-direction: column; padding: 0; }
|
|
23
|
+
.pcode-toolbar {
|
|
24
|
+
display: flex; align-items: center; gap: .5rem;
|
|
25
|
+
padding: .5rem .75rem; border-bottom: 1px solid var(--dm-border, rgba(128,128,128,.25));
|
|
26
|
+
}
|
|
27
|
+
.pcode-current {
|
|
28
|
+
font-family: var(--dm-font-mono, ui-monospace, Menlo, Consolas, monospace);
|
|
29
|
+
font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
30
|
+
}
|
|
31
|
+
#pcode-editor { flex: 1; display: flex; min-height: 55vh; }
|
|
32
|
+
#pcode-editor > .dm-edit-wrap { flex: 1; }
|
|
33
|
+
.pcode-statusbar {
|
|
34
|
+
display: flex; align-items: center; gap: .75rem; padding: .4rem .75rem;
|
|
35
|
+
border-top: 1px solid var(--dm-border, rgba(128,128,128,.25));
|
|
36
|
+
font-size: .78rem; color: var(--dm-text-muted, #6b7280);
|
|
37
|
+
}
|
|
38
|
+
#pcode-cursor { margin-left: auto; font-family: var(--dm-font-mono, ui-monospace, Menlo, Consolas, monospace); }
|
|
39
|
+
</style>
|
|
40
|
+
|
|
41
|
+
<div class="view-header d-flex justify-content-between align-items-center mb-4">
|
|
42
|
+
<div>
|
|
43
|
+
<h1 class="view-title"><span data-icon="code"></span> <span id="pcode-title">Plugin Code</span></h1>
|
|
44
|
+
<p class="view-subtitle text-muted" style="margin:0;font-size:.875rem">
|
|
45
|
+
<code>admin/</code> files apply on a hard refresh; everything else needs a server restart.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="d-flex gap-2">
|
|
49
|
+
<button id="pcode-back" class="btn btn-ghost"><span data-icon="arrow-left"></span> Plugins</button>
|
|
50
|
+
<button id="pcode-restart" class="btn btn-warning"><span data-icon="refresh-cw"></span> Restart Server</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="pcode-layout">
|
|
55
|
+
<div class="card pcode-files">
|
|
56
|
+
<div class="card-body">
|
|
57
|
+
<div style="display:flex;justify-content:space-between;align-items:center;padding:.25rem .5rem .5rem;">
|
|
58
|
+
<strong style="font-size:.8rem">Files</strong>
|
|
59
|
+
<button id="pcode-newfile" class="btn btn-sm btn-ghost" title="New file"><span data-icon="plus"></span></button>
|
|
60
|
+
</div>
|
|
61
|
+
<div id="pcode-filelist"></div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="card pcode-editor-card">
|
|
66
|
+
<div class="card-body">
|
|
67
|
+
<div class="pcode-toolbar">
|
|
68
|
+
<span class="pcode-current" id="pcode-current">Select a file</span>
|
|
69
|
+
<span style="flex:1"></span>
|
|
70
|
+
<button id="pcode-rename" class="btn btn-sm btn-ghost" title="Rename file" disabled><span data-icon="edit-3"></span></button>
|
|
71
|
+
<button id="pcode-delete" class="btn btn-sm btn-ghost" title="Delete file" disabled><span data-icon="trash-2"></span></button>
|
|
72
|
+
<button id="pcode-saveall" class="btn btn-sm btn-ghost" disabled>Save All</button>
|
|
73
|
+
<button id="pcode-save" class="btn btn-sm btn-primary" disabled><span data-icon="save"></span> Save</button>
|
|
74
|
+
</div>
|
|
75
|
+
<div id="pcode-editor"></div>
|
|
76
|
+
<div class="pcode-statusbar">
|
|
77
|
+
<span id="pcode-status">Ctrl+S saves the open file. Unsaved edits survive switching files.</span>
|
|
78
|
+
<span id="pcode-cursor"></span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
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{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView 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{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as n}from"./media.js";import{loginView as s}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{pluginCodeView as V}from"./plugin-code.js";import{documentationView as a}from"./documentation.js";import{tutorialsView as d}from"./tutorials.js";import{docViews as l}from"./doc-pages.js";import{apiReferenceView as E}from"./api-reference.js";import{collectionsView as u}from"./collections.js";import{collectionEditorView as g}from"./collection-editor.js";import{collectionEntriesView as v}from"./collection-entries.js";import{formsView as b}from"./forms.js";import{formEditorView as j}from"./form-editor.js";import{formSubmissionsView as k}from"./form-submissions.js";import{viewsListView as L}from"./views-list.js";import{viewEditorView as y}from"./view-editor.js";import{viewPreviewView as h}from"./view-preview.js";import{actionsListView as D}from"./actions-list.js";import{actionEditorView as P}from"./action-editor.js";import{proDocsView as R}from"./pro-docs.js";import{blocksView as S}from"./blocks.js";import{blockEditorView as C}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as T}from"./components.js";import{componentEditorView as x}from"./component-editor.js";import{myProfileView as M}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{menusView as A}from"./menus.js";import{menuEditorView as B}from"./menu-editor.js";import{menuLocationsView as F}from"./menu-locations.js";import{projectsView as G}from"./projects.js";import{projectEditorView as H}from"./project-editor.js";import{projectDetailView as I}from"./project-detail.js";import{projectSettingsView as J}from"./project-settings.js";import{apiTokensView as K}from"./api-tokens.js";import{apiEndpointsView as N}from"./api-endpoints.js";import{apiEndpointEditorView as O}from"./api-endpoint-editor.js";const Q={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:G,projectEditor:H,projectDetail:I,projectSettings:J,apiTokens:K,apiEndpoints:N,apiEndpointEditor:O,dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:p,media:n,login:s,users:f,userEditor:w,plugins:c,pluginCode:V,documentation:a,tutorials:d,apiReference:E,collections:u,collectionEditor:g,collectionEntries:v,forms:b,formEditor:j,formSubmissions:k,viewsList:L,viewEditor:y,viewPreview:h,actionsList:D,actionEditor:P,proDocs:R,blocks:S,blockEditor:C,components:T,componentEditor:x,myProfile:M,roles:U,roleEditor:q,effects:z,notifications:m,menus:A,menuEditor:B,menuLocations:F,menusRedirect:Q,...l};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{api as d}from"../api.js";import{createSimpleEditor as V}from"../lib/simple-editor.js";import{languageForFile as q}from"../lib/syntax-highlight.js";export const pluginCodeView={templateUrl:"/admin/js/templates/plugin-code.html",async onMount(o){const b=window.location.hash.match(/\/plugins\/code\/([a-z0-9-]+)/),l=b?b[1]:null;if(!l){E.toast("No plugin specified.",{type:"error"}),R.navigate("/plugins");return}o.find("#pcode-title").text(`${l} \u2014 Code`),document.title=`${l} Code - Domma CMS`;const k=o.find("#pcode-save").get(0),S=o.find("#pcode-saveall").get(0),C=o.find("#pcode-delete").get(0),x=o.find("#pcode-rename").get(0),K=o.find("#pcode-status").get(0),F=o.find("#pcode-cursor").get(0),L=o.find("#pcode-current").get(0),$=o.find("#pcode-filelist"),P=o.find("#pcode-editor").get(0);let m=[],n=null,c=null;const r=new Map,v=e=>r.has(e)&&r.get(e).content!==r.get(e).saved,p=()=>[...r.keys()].filter(v);function f(e){K.textContent=e}function g(){k.disabled=!n||!v(n),S.disabled=p().length===0,C.disabled=!n,x.disabled=!n}function h(){$.empty();const e=$.get(0),a=new Map;m.forEach(t=>{const s=t.path.includes("/")?t.path.slice(0,t.path.lastIndexOf("/")):"";a.has(s)||a.set(s,[]),a.get(s).push(t)}),[...a.keys()].sort((t,s)=>t.localeCompare(s)).forEach(t=>{if(t!==""){const s=document.createElement("div");s.className="pcode-dir",s.textContent=t+"/",e.appendChild(s)}a.get(t).forEach(s=>{const i=document.createElement("button");if(i.className="pcode-file"+(s.path===n?" active":""),i.style.paddingLeft=t===""?".5rem":"1rem",i.textContent=s.path.slice(t===""?0:t.length+1),i.title=s.path,v(s.path)){const w=document.createElement("span");w.className="pcode-dirty",w.textContent="\u25CF",i.appendChild(w)}i.addEventListener("click",()=>y(s.path)),e.appendChild(i)})})}async function u(){try{m=(await d.plugins.files(l)).files||[],h()}catch(e){E.toast(`Failed to list files: ${e.message}`,{type:"error"})}}function O(e,a){c&&c.destroy(),c=V(P,{initialValue:a,language:q(e),autoIndent:!0,onChange:t=>{r.get(e).content=t,g(),h()},onCursor:({line:t,col:s})=>{F.textContent=`Ln ${t}, Col ${s}`}}),c.textarea.addEventListener("keydown",t=>{(t.ctrlKey||t.metaKey)&&t.key==="s"&&(t.preventDefault(),t.shiftKey?N():j())}),c.focus()}async function y(e){if(e!==n){if(!r.has(e))try{const a=await d.plugins.readFile(l,e);r.set(e,{content:a.content,saved:a.content})}catch(a){E.toast(`Failed to open ${e}: ${a.message}`,{type:"error"});return}n=e,L.textContent=e+(q(e)?"":" (plain text)"),O(e,r.get(e).content),F.textContent="Ln 1, Col 1",g(),h()}}async function D(e){const a=r.get(e),t=await d.plugins.writeFile(l,e,a.content);return a.saved=a.content,t.restartRequired}async function j(){if(!(!n||!v(n))){try{const e=await D(n);f(e?`Saved ${n} \u2014 server file: restart to apply.`:`Saved ${n} \u2014 hard-refresh the admin tab to see it.`),E.toast(e?"Saved. Restart the server to apply.":"Saved.",{type:e?"warning":"success"})}catch(e){E.toast(`Save failed: ${e.message}`,{type:"error"})}g(),h(),u()}}async function N(){const e=p();if(!e.length)return;let a=!1;const t=[];for(const s of e)try{a=await D(s)||a}catch(i){t.push(`${s}: ${i.message}`)}t.length?E.toast(`Some saves failed \u2014 ${t.join("; ")}`,{type:"error"}):(f(a?`Saved ${e.length} file(s) \u2014 server files included: restart to apply.`:`Saved ${e.length} file(s) \u2014 hard-refresh the admin tab to see them.`),E.toast(`Saved ${e.length} file(s).`,{type:a?"warning":"success"})),g(),h(),u()}k.addEventListener("click",j),S.addEventListener("click",N);async function B(e,a){const t=i=>String(i||"").replace(/&/g,"&").replace(/</g,"<").replace(/"/g,""");return await E.confirm(`<form id="pcode-path-form"><div class="form-group">
|
|
2
|
+
<label class="form-label">File path (inside the plugin)</label>
|
|
3
|
+
<input class="form-control" type="text" name="path" value="${t(a)}" placeholder="admin/views/cart.js">
|
|
4
|
+
</div></form>`,{title:e,confirmText:"OK",html:!0})&&document.querySelector('#pcode-path-form input[name="path"]')?.value.trim()||null}o.find("#pcode-newfile").on("click",async()=>{const e=await B("New File");if(e)try{await d.plugins.writeFile(l,e,""),r.set(e,{content:"",saved:""}),await u(),await y(e)}catch(a){E.toast(`Could not create file: ${a.message}`,{type:"error"})}}),x.addEventListener("click",async()=>{if(!n)return;if(["plugin.json","plugin.js","config.js"].includes(n)){E.toast(`${n} is required and cannot be renamed.`,{type:"error"});return}const e=await B("Rename File",n);if(!(!e||e===n))try{await d.plugins.writeFile(l,e,r.get(n)?.content??""),await d.plugins.deleteFile(l,n),r.set(e,{content:r.get(n).content,saved:r.get(n).content}),r.delete(n),n=null,await u(),await y(e),E.toast("Renamed.",{type:"success"})}catch(a){E.toast(`Rename failed: ${a.message}`,{type:"error"}),u()}}),C.addEventListener("click",async()=>{if(n&&await E.confirm(`Delete ${n}? This cannot be undone.`))try{await d.plugins.deleteFile(l,n),r.delete(n),E.toast(`Deleted ${n}.`,{type:"success"}),n=null,L.textContent="Select a file",c&&(c.destroy(),c=null),g(),await u()}catch(e){E.toast(`Delete failed: ${e.message}`,{type:"error"})}}),o.find("#pcode-back").on("click",async()=>{p().length&&!await E.confirm(`Discard unsaved changes in ${p().length} file(s)?`)||R.navigate("/plugins")});const M=e=>{p().length&&(e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",M),window.addEventListener("hashchange",function e(){window.removeEventListener("beforeunload",M),window.removeEventListener("hashchange",e)}),o.find("#pcode-restart").on("click",async()=>{const e=p().length;if(await E.confirm((e?`${e} file(s) have unsaved changes \u2014 they will NOT be included. `:"")+"Restart the server now? Server-side plugin changes apply on boot. The admin will be unreachable for a few seconds.",{title:"Restart Server",confirmText:"Restart"}))try{(await d.plugins.restart()).supervised||E.toast("No supervisor detected \u2014 if the server does not come back, start it manually.",{type:"warning"}),f("Restarting\u2026 the page will report back when the server returns.");const s=Date.now(),i=setInterval(async()=>{try{await H.get("/api/auth/setup-status"),clearInterval(i),f("Server is back. Server-side changes are live."),E.toast("Server restarted \u2014 changes are live.",{type:"success"})}catch{Date.now()-s>3e4&&(clearInterval(i),f("Server did not come back within 30s \u2014 check it manually."),E.toast("Server not responding after 30s.",{type:"error"}))}},1500)}catch(t){E.toast(`Restart failed: ${t.message}`,{type:"error"})}}),await u();const T=m.find(e=>e.path==="plugin.js")||m[0];T&&await y(T.path),I.scan()}};
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import{api as g,getUser as
|
|
1
|
+
import{api as g,getUser as v}from"../api.js";import{pluginMarketplaceView as h}from"./plugin-marketplace.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(s){s.find("#new-plugin-btn").on("click",()=>x(s));let u=!1;s.find('[data-tab="marketplace"]').on("click",async function(){if(u)return;const a=s.find("#marketplace-mount");try{const n=await H.get(h.templateUrl),p=new DOMParser().parseFromString(n,"text/html");for(;p.body.firstChild;)a.get(0).appendChild(p.body.firstChild);await h.onMount(a),u=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(s.find("#plugin-tabs").get(0));const r=E.loader(s.get(0),{type:"dots"});let e=[];try{e=await g.plugins.list()}catch(a){E.toast(`Failed to load plugins: ${a.message}`,{type:"error"})}if(r.destroy(),!e.length){s.find("#plugins-empty").show();return}f(s,e),Domma.icons.scan()}};function x(s){const u=E.slideover({title:"New Plugin",size:"sm",position:"right"}),r=document.createElement("div");r.style.cssText="padding:.25rem 0 .5rem;";const e=v(),a=document.createElement("div");F.create({displayName:{type:"string",label:"Display Name",placeholder:"e.g. Event Manager",required:!0},slug:{type:"string",label:"Slug",placeholder:"event-manager",required:!0,help:"Folder name under plugins/ \u2014 lowercase letters, digits, hyphens."},description:{type:"string",label:"Description",placeholder:"What does this plugin do?"},author:{type:"string",label:"Author"},icon:{type:"string",label:"Icon",placeholder:"box",help:"A Domma icon name (as used in data-icon)."}},{author:e?.name||e?.username||"",icon:"box"},{showSubmitButton:!1}).renderTo(a),r.appendChild(a);const n=document.createElement("p");n.className="text-muted",n.style.cssText="font-size:.8rem;margin:.5rem 0 0;",n.textContent="The plugin is scaffolded from the built-in template with server routes, an admin view and a CLAUDE.md, and enabled automatically. Restart the server to activate it.",r.appendChild(n);const l=document.createElement("div");l.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const p=document.createElement("button");p.className="btn btn-ghost",p.textContent="Cancel";const i=document.createElement("button");i.className="btn btn-primary",i.textContent="Create Plugin",l.appendChild(p),l.appendChild(i),r.appendChild(l),u.element.appendChild(r),u.open();const m=a.querySelector('input[name="displayName"]'),t=a.querySelector('input[name="slug"]');setTimeout(()=>m?.focus(),50);let d=!1;t?.addEventListener("input",()=>{d=!0}),m?.addEventListener("input",()=>{d||!t||(t.value=m.value.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40))});async function b(){const c={};for(const y of["displayName","slug","description","author","icon"])c[y]=a.querySelector(`input[name="${y}"]`)?.value.trim()||"";if(!c.displayName||!c.slug){E.toast("Display name and slug are required.",{type:"error"});return}i.disabled=!0,i.textContent="Creating\u2026";try{await g.plugins.scaffold(c),u.close(),E.toast(`Plugin "${c.displayName}" created. Restart the server to activate it.`,{type:"success"});const y=await g.plugins.list();y.length&&(s.find("#plugins-empty").hide(),f(s,y))}catch(y){E.toast(y.message||"Failed to create plugin.",{type:"error"}),i.disabled=!1,i.textContent="Create Plugin"}}p.addEventListener("click",()=>u.close()),i.addEventListener("click",b),t?.addEventListener("keydown",c=>{c.key==="Enter"&&b()})}function f(s,u){const r=s.find("#plugins-grid").empty();u.forEach(e=>{const a=e.enabled?"badge-success":"badge-secondary",n=e.enabled?"Enabled":"Disabled",l=e.enabled?"Disable":"Enable",p=e.enabled?"btn-ghost":"btn-primary",i=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",m=e.bundled?"Bundled \u2014 click to unbundle":"Not bundled \u2014 click to bundle",t=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${m}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${i};border-color:${i};">${e.bundled?"Bundled":"Unbundled"}</button>`,d=e.author?`by ${o(e.author)}`:"",b=e.date?o(e.date):"",c=`
|
|
2
2
|
<div class="card plugin-card" data-plugin="${e.name}">
|
|
3
3
|
<div class="card-body">
|
|
4
4
|
<div class="plugin-header">
|
|
5
5
|
<div class="plugin-icon"><span data-icon="${e.icon||"package"}"></span></div>
|
|
6
6
|
<div class="plugin-meta">
|
|
7
|
-
<div class="plugin-name">${
|
|
8
|
-
<div class="plugin-version">v${
|
|
7
|
+
<div class="plugin-name">${o(e.displayName)}</div>
|
|
8
|
+
<div class="plugin-version">v${o(e.version)}${d?` · ${d}`:""}</div>
|
|
9
9
|
</div>
|
|
10
10
|
<div style="display:flex;gap:.35rem;align-items:center;">
|
|
11
11
|
${t}
|
|
12
12
|
<span class="badge ${a}">${n}</span>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
|
-
<p class="plugin-desc">${
|
|
15
|
+
<p class="plugin-desc">${o(e.description)}</p>
|
|
16
16
|
${b?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${b}</p>`:""}
|
|
17
17
|
</div>
|
|
18
18
|
<div class="plugin-footer">
|
|
19
19
|
<span class="text-muted" style="font-size:.8rem">Restart server after changes</span>
|
|
20
20
|
<div class="plugin-footer-actions">
|
|
21
|
+
<button class="btn btn-sm btn-ghost btn-plugin-code" data-name="${e.name}"><span data-icon="code"></span> Code</button>
|
|
21
22
|
${e.settingsSchema?`<button class="btn btn-sm btn-ghost btn-plugin-settings" data-name="${e.name}"><span data-icon="settings"></span> Settings</button>`:""}
|
|
22
23
|
<button class="btn btn-sm ${p} btn-toggle-plugin"
|
|
23
24
|
data-name="${e.name}"
|
|
@@ -26,13 +27,13 @@ import{api as g,getUser as h}from"../api.js";import{pluginMarketplaceView as v}f
|
|
|
26
27
|
</button>
|
|
27
28
|
</div>
|
|
28
29
|
</div>
|
|
29
|
-
</div>`;
|
|
30
|
-
<label class="form-label">${
|
|
31
|
-
<select class="form-control" name="${
|
|
30
|
+
</div>`;r.append(c)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),r.on("click",".btn-plugin-code",function(){R.navigate(`/plugins/code/${$(this).data("name")}`)}),r.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),a=u.find(t=>t.name===e);if(!a?.settingsSchema)return;const l=`<form id="plugin-settings-form">${a.settingsSchema.map(t=>{const d=a.settings?.[t.key]??t.default??"";if(t.type==="select"){const b=t.options.map(c=>`<option value="${o(c.value)}"${String(d)===String(c.value)?" selected":""}>${o(c.label)}</option>`).join("");return`<div class="form-group">
|
|
31
|
+
<label class="form-label">${o(t.label)}</label>
|
|
32
|
+
<select class="form-control" name="${o(t.key)}">${b}</select>
|
|
32
33
|
</div>`}return t.type==="number"?`<div class="form-group">
|
|
33
|
-
<label class="form-label">${
|
|
34
|
-
<input class="form-control" type="number" name="${
|
|
34
|
+
<label class="form-label">${o(t.label)}</label>
|
|
35
|
+
<input class="form-control" type="number" name="${o(t.key)}" value="${o(String(d))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
|
|
35
36
|
</div>`:`<div class="form-group">
|
|
36
|
-
<label class="form-label">${
|
|
37
|
-
<input class="form-control" type="text" name="${
|
|
38
|
-
</div>`}).join("")}</form>`;if(!await E.confirm(l,{title:`${
|
|
37
|
+
<label class="form-label">${o(t.label)}</label>
|
|
38
|
+
<input class="form-control" type="text" name="${o(t.key)}" value="${o(String(d))}">
|
|
39
|
+
</div>`}).join("")}</form>`;if(!await E.confirm(l,{title:`${o(a.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const i=document.getElementById("plugin-settings-form");if(!i)return;const m={};a.settingsSchema.forEach(t=>{const d=i.elements[t.key];d&&(m[t.key]=t.type==="number"?Number(d.value):d.value)});try{await g.plugins.update(e,{settings:m}),a.settings={...a.settings,...m},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),r.on("click",".btn-bundle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(n?"Bundling\u2026":"Unbundling\u2026");try{await g.plugins.update(a,{bundled:n}),E.toast(`Plugin ${n?"marked as bundled":"unbundled"}.`,{type:"success"});const l=await g.plugins.list();f(s,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}}),r.on("click",".btn-toggle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(n?"Enabling\u2026":"Disabling\u2026");try{await g.plugins.update(a,{enabled:n}),E.toast(`Plugin ${n?"enabled":"disabled"}. Restart the server to apply changes.`,{type:"success"});const l=await g.plugins.list();f(s,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}})}function o(s){return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}
|
package/package.json
CHANGED
|
@@ -14,11 +14,13 @@ import {
|
|
|
14
14
|
savePluginState
|
|
15
15
|
} from '../../services/plugins.js';
|
|
16
16
|
import {scaffoldPlugin} from '../../services/pluginScaffold.js';
|
|
17
|
+
import {deletePluginFile, listPluginFiles, readPluginFile, writePluginFile} from '../../services/pluginFiles.js';
|
|
17
18
|
|
|
18
19
|
export async function pluginsRoutes(fastify) {
|
|
19
|
-
const canRead
|
|
20
|
-
const canCreate
|
|
21
|
-
const canUpdate
|
|
20
|
+
const canRead = { preHandler: [authenticate, requirePermission('plugins', 'read')] };
|
|
21
|
+
const canCreate = { preHandler: [authenticate, requirePermission('plugins', 'create')] };
|
|
22
|
+
const canUpdate = { preHandler: [authenticate, requirePermission('plugins', 'update')] };
|
|
23
|
+
const canDevelop = { preHandler: [authenticate, requirePermission('plugins', 'develop')] };
|
|
22
24
|
|
|
23
25
|
// List all plugins with their current state
|
|
24
26
|
fastify.get('/plugins', canRead, async () => {
|
|
@@ -82,4 +84,50 @@ export async function pluginsRoutes(fastify) {
|
|
|
82
84
|
fastify.get('/plugins/admin-config', { preHandler: [authenticate] }, async () => {
|
|
83
85
|
return getAdminPluginConfig();
|
|
84
86
|
});
|
|
87
|
+
|
|
88
|
+
// ── In-admin code editor (plugins.develop — code execution grade) ──────
|
|
89
|
+
|
|
90
|
+
const mapErr = (reply, err) => reply.status(err.statusCode || 500).send({ error: err.message });
|
|
91
|
+
|
|
92
|
+
// List a plugin's editable files
|
|
93
|
+
fastify.get('/plugins/:name/files', canDevelop, async (request, reply) => {
|
|
94
|
+
try {
|
|
95
|
+
return { files: await listPluginFiles(request.params.name) };
|
|
96
|
+
} catch (err) { return mapErr(reply, err); }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Read one file (?path=admin/views/index.js)
|
|
100
|
+
fastify.get('/plugins/:name/file', canDevelop, async (request, reply) => {
|
|
101
|
+
try {
|
|
102
|
+
return await readPluginFile(request.params.name, request.query.path);
|
|
103
|
+
} catch (err) { return mapErr(reply, err); }
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Create or overwrite one file
|
|
107
|
+
fastify.put('/plugins/:name/file', canDevelop, async (request, reply) => {
|
|
108
|
+
const { path: relPath, content } = request.body || {};
|
|
109
|
+
try {
|
|
110
|
+
const result = await writePluginFile(request.params.name, relPath, content);
|
|
111
|
+
return { success: true, ...result };
|
|
112
|
+
} catch (err) { return mapErr(reply, err); }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Delete one file (?path=admin/views/extra.js)
|
|
116
|
+
fastify.delete('/plugins/:name/file', canDevelop, async (request, reply) => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await deletePluginFile(request.params.name, request.query.path);
|
|
119
|
+
return { success: true, ...result };
|
|
120
|
+
} catch (err) { return mapErr(reply, err); }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Restart the server so server-side plugin edits take effect. The process
|
|
124
|
+
// exits cleanly after replying; a supervisor (fleet manager fork, pm2,
|
|
125
|
+
// systemd) brings it back on the new code. `supervised` tells the UI
|
|
126
|
+
// whether that safety net was detected.
|
|
127
|
+
fastify.post('/plugins/restart', canDevelop, async () => {
|
|
128
|
+
const supervised = typeof process.send === 'function'
|
|
129
|
+
|| !!process.env.pm_id || !!process.env.PM2_HOME;
|
|
130
|
+
setTimeout(() => process.exit(0), 400);
|
|
131
|
+
return { success: true, restarting: true, supervised };
|
|
132
|
+
});
|
|
85
133
|
}
|
|
@@ -188,7 +188,8 @@ export const REGISTRY = [
|
|
|
188
188
|
{key: 'read', label: 'View', description: 'View installed plugins'},
|
|
189
189
|
{key: 'create', label: 'Install', description: 'Install new plugins'},
|
|
190
190
|
{key: 'update', label: 'Configure', description: 'Configure plugin settings'},
|
|
191
|
-
{key: 'delete', label: 'Remove', description: 'Remove plugins'}
|
|
191
|
+
{key: 'delete', label: 'Remove', description: 'Remove plugins'},
|
|
192
|
+
{key: 'develop', label: 'Code', description: 'Edit plugin source files (grants code execution on the server)'}
|
|
192
193
|
]
|
|
193
194
|
},
|
|
194
195
|
{
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Files Service
|
|
3
|
+
* Read/write access to a plugin's source files for the in-admin code editor.
|
|
4
|
+
*
|
|
5
|
+
* Every operation is confined to plugins/<name>/ — names must be discovered
|
|
6
|
+
* plugin directories (underscore-prefixed dirs like _template/_lib are never
|
|
7
|
+
* valid names), paths are normalised and containment-checked after symlink
|
|
8
|
+
* resolution, and only known text extensions are editable.
|
|
9
|
+
*
|
|
10
|
+
* Thrown errors carry `statusCode` (400 invalid, 404 missing, 413 too large)
|
|
11
|
+
* for direct mapping in the route layer.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
|
|
16
|
+
const PLUGINS_DIR = path.resolve('plugins');
|
|
17
|
+
|
|
18
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
|
|
19
|
+
|
|
20
|
+
// Editable/viewable file types. Everything else is invisible to the editor.
|
|
21
|
+
const TEXT_EXTENSIONS = new Set(['.js', '.json', '.md', '.html', '.css', '.txt', '.svg']);
|
|
22
|
+
|
|
23
|
+
// Never deletable — removing any of these breaks discovery of the plugin.
|
|
24
|
+
const REQUIRED_FILES = new Set(['plugin.json', 'plugin.js', 'config.js']);
|
|
25
|
+
|
|
26
|
+
// Directories skipped when listing.
|
|
27
|
+
const SKIP_DIRS = new Set(['node_modules']);
|
|
28
|
+
|
|
29
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
30
|
+
|
|
31
|
+
function fail(statusCode, message) {
|
|
32
|
+
const err = new Error(message);
|
|
33
|
+
err.statusCode = statusCode;
|
|
34
|
+
return err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate the plugin name and return its directory, or throw.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @returns {Promise<string>} Absolute plugin directory
|
|
42
|
+
*/
|
|
43
|
+
async function resolvePluginDir(name) {
|
|
44
|
+
if (!NAME_PATTERN.test(String(name || ''))) throw fail(400, 'Invalid plugin name.');
|
|
45
|
+
const dir = path.join(PLUGINS_DIR, name);
|
|
46
|
+
try {
|
|
47
|
+
const stat = await fs.stat(dir);
|
|
48
|
+
if (!stat.isDirectory()) throw new Error();
|
|
49
|
+
} catch {
|
|
50
|
+
throw fail(404, `Plugin "${name}" not found.`);
|
|
51
|
+
}
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a relative file path inside the plugin dir, or throw.
|
|
57
|
+
* Rejects absolute paths, traversal, hidden segments and unknown extensions.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} pluginDir - Absolute plugin directory
|
|
60
|
+
* @param {string} relPath - Caller-supplied relative path
|
|
61
|
+
* @returns {Promise<string>} Absolute, containment-checked file path
|
|
62
|
+
*/
|
|
63
|
+
async function resolveFilePath(pluginDir, relPath) {
|
|
64
|
+
const rel = String(relPath || '').replace(/\\/g, '/');
|
|
65
|
+
if (!rel || path.isAbsolute(rel)) throw fail(400, 'Invalid file path.');
|
|
66
|
+
const segments = rel.split('/');
|
|
67
|
+
if (segments.some(s => !s || s === '.' || s === '..' || s.startsWith('.'))) {
|
|
68
|
+
throw fail(400, 'Invalid file path.');
|
|
69
|
+
}
|
|
70
|
+
if (!TEXT_EXTENSIONS.has(path.extname(rel).toLowerCase())) {
|
|
71
|
+
throw fail(400, `Only ${[...TEXT_EXTENSIONS].join(', ')} files can be edited.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const abs = path.resolve(pluginDir, rel);
|
|
75
|
+
if (!(abs + path.sep).startsWith(pluginDir + path.sep) || abs === pluginDir) {
|
|
76
|
+
throw fail(400, 'Invalid file path.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If the target (or an ancestor inside the plugin) already exists, make
|
|
80
|
+
// sure symlinks don't escape the plugin directory.
|
|
81
|
+
try {
|
|
82
|
+
const real = await fs.realpath(abs);
|
|
83
|
+
const realBase = await fs.realpath(pluginDir);
|
|
84
|
+
if (!(real + path.sep).startsWith(realBase + path.sep)) {
|
|
85
|
+
throw fail(400, 'Invalid file path.');
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err.statusCode) throw err;
|
|
89
|
+
// ENOENT — new file, fine
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return abs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* List a plugin's editable files as a flat, sorted array.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} name
|
|
99
|
+
* @returns {Promise<Array<{path: string, size: number, modified: string}>>}
|
|
100
|
+
*/
|
|
101
|
+
export async function listPluginFiles(name) {
|
|
102
|
+
const pluginDir = await resolvePluginDir(name);
|
|
103
|
+
const files = [];
|
|
104
|
+
|
|
105
|
+
async function walk(dir) {
|
|
106
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
|
|
109
|
+
const abs = path.join(dir, entry.name);
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
await walk(abs);
|
|
112
|
+
} else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
113
|
+
const stat = await fs.stat(abs);
|
|
114
|
+
files.push({
|
|
115
|
+
path: path.relative(pluginDir, abs).split(path.sep).join('/'),
|
|
116
|
+
size: stat.size,
|
|
117
|
+
modified: stat.mtime.toISOString()
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await walk(pluginDir);
|
|
124
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read one plugin file.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} name
|
|
132
|
+
* @param {string} relPath
|
|
133
|
+
* @returns {Promise<{path: string, content: string}>}
|
|
134
|
+
*/
|
|
135
|
+
export async function readPluginFile(name, relPath) {
|
|
136
|
+
const pluginDir = await resolvePluginDir(name);
|
|
137
|
+
const abs = await resolveFilePath(pluginDir, relPath);
|
|
138
|
+
|
|
139
|
+
let stat;
|
|
140
|
+
try {
|
|
141
|
+
stat = await fs.stat(abs);
|
|
142
|
+
} catch {
|
|
143
|
+
throw fail(404, 'File not found.');
|
|
144
|
+
}
|
|
145
|
+
if (stat.size > MAX_FILE_BYTES) throw fail(413, 'File too large to edit.');
|
|
146
|
+
|
|
147
|
+
const content = await fs.readFile(abs, 'utf8');
|
|
148
|
+
return { path: relPath, content };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write (create or overwrite) one plugin file.
|
|
153
|
+
* JSON files must parse; plugin.json must additionally keep its `name`
|
|
154
|
+
* matching the directory so the plugin stays discoverable.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} name
|
|
157
|
+
* @param {string} relPath
|
|
158
|
+
* @param {string} content
|
|
159
|
+
* @returns {Promise<{path: string, restartRequired: boolean}>}
|
|
160
|
+
*/
|
|
161
|
+
export async function writePluginFile(name, relPath, content) {
|
|
162
|
+
const pluginDir = await resolvePluginDir(name);
|
|
163
|
+
const abs = await resolveFilePath(pluginDir, relPath);
|
|
164
|
+
|
|
165
|
+
if (typeof content !== 'string') throw fail(400, 'Content must be a string.');
|
|
166
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_FILE_BYTES) {
|
|
167
|
+
throw fail(413, `File exceeds the ${MAX_FILE_BYTES / 1024}KB editor limit.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (path.extname(abs).toLowerCase() === '.json') {
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(content);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw fail(400, `Invalid JSON: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
const rel = path.relative(pluginDir, abs).split(path.sep).join('/');
|
|
178
|
+
if (rel === 'plugin.json' && parsed.name !== name) {
|
|
179
|
+
throw fail(400, `plugin.json "name" must stay "${name}" (it must match the directory).`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
184
|
+
await fs.writeFile(abs, content, 'utf8');
|
|
185
|
+
|
|
186
|
+
const rel = path.relative(pluginDir, abs).split(path.sep).join('/');
|
|
187
|
+
// admin/** is fetched by the SPA at runtime — a tab refresh picks it up.
|
|
188
|
+
// Everything else (plugin.js, config.js, plugin.json, public/ snippets)
|
|
189
|
+
// is read or imported server-side and needs a restart.
|
|
190
|
+
const restartRequired = !rel.startsWith('admin/');
|
|
191
|
+
return { path: rel, restartRequired };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Delete one plugin file. The three required files are refused.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} name
|
|
198
|
+
* @param {string} relPath
|
|
199
|
+
* @returns {Promise<{path: string}>}
|
|
200
|
+
*/
|
|
201
|
+
export async function deletePluginFile(name, relPath) {
|
|
202
|
+
const pluginDir = await resolvePluginDir(name);
|
|
203
|
+
const abs = await resolveFilePath(pluginDir, relPath);
|
|
204
|
+
const rel = path.relative(pluginDir, abs).split(path.sep).join('/');
|
|
205
|
+
|
|
206
|
+
if (REQUIRED_FILES.has(rel)) {
|
|
207
|
+
throw fail(400, `${rel} is required — deleting it would break the plugin.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await fs.unlink(abs);
|
|
212
|
+
} catch {
|
|
213
|
+
throw fail(404, 'File not found.');
|
|
214
|
+
}
|
|
215
|
+
return { path: rel };
|
|
216
|
+
}
|