domma-cms 1.6.49 → 1.6.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/js/api.js +1 -1
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/list-tags-arrange.js +1 -0
- package/admin/js/lib/list-tags.js +37 -0
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/templates/blocks.html +9 -1
- package/admin/js/templates/page-editor.html +7 -0
- package/admin/js/templates/pages.html +7 -1
- package/admin/js/views/blocks.js +5 -4
- package/admin/js/views/page-editor.js +7 -7
- package/admin/js/views/pages.js +4 -3
- package/package.json +1 -1
- package/server/routes/api/list-tags.js +85 -0
- package/server/routes/api/pages.js +2 -0
- package/server/server.js +2 -0
- package/server/services/blocks.js +33 -0
- package/server/services/content.js +49 -0
- package/server/services/listTags.js +220 -0
- package/server/services/versions.js +24 -1
package/admin/js/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{loginHashFor as $}from"./lib/login-arrange.js";const c="/api";function a(){return S.get("auth_token")}function u(){return S.get("auth_refresh_token")}function y(e){S.set("auth_token",e)}function f(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function T(){const e=u();if(!e)throw new Error("No refresh token");const o=await fetch(`${c}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw f(),sendToLogin({expired:!0}),new Error("Token refresh failed");const{token:n}=await o.json();return y(n),n}async function t(e,o={}){let n=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${c}${e}`,{...o,headers:r(n)});if(s.status===401&&u())try{n=await T(),s=await fetch(`${c}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),d=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(d.validation=i.validation),d.status=s.status,d.body=i,d}return s.status===204?null:s.json()}async function O(e,o){const n=a(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${c}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),verifyLogin:e=>t("/auth/login/verify",{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})}),checkReset:(e,o)=>t("/auth/reset-password/check",{method:"POST",body:JSON.stringify({token:e,...o&&{password:o}})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:e=>t(`/theme/admin${e?`?id=${encodeURIComponent(e)}`:""}`,{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"}),removeContents:(e,o)=>t(`/projects/${encodeURIComponent(e)}/remove-contents`,{method:"POST",body:JSON.stringify({confirm:o})})},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=>O("/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"}),passwordReset:(e,o)=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"POST",body:JSON.stringify({via:o})}),cancelPasswordReset:e=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"DELETE"})},tools:{list:()=>t("/tools",{method:"GET"}),update:(e,o)=>t(`/tools/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),r=o?"?includeData=1":"",s=await fetch(`${c}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(i)?.[1],m=await s.blob(),h=URL.createObjectURL(m),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=a(),d=await fetch(`${c}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),m=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(m.error||"Plugin already installed");throw h.code="CONFLICT",h.name=m.name,h.installedVersion=m.installedVersion,h.incomingVersion=m.incomingVersion,h}if(!d.ok)throw new Error(m.error||`Install failed (${d.status})`);return m}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${c}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${c}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${c}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${c}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&y(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}let p=!1,g=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=$(location.hash,{expired:e}))}export function logout(){!g&&window.M?.subscribe&&(g=!0,M.subscribe("router:afterChange",({to:e}={})=>{/^\/login(\?|$)/.test(e?.path||"")||(p=!1)})),p=!0,R.navigate("/login")}export function takePendingLogout(){return p?(p=!1,b(),!0):!1}function b(){const e=u();e&&fetch(`${c}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),f()}export{t as apiRequest,T as refreshAccessToken,a as getToken,f as clearAuth};
|
|
1
|
+
import{loginHashFor as $}from"./lib/login-arrange.js";const m="/api";function a(){return S.get("auth_token")}function u(){return S.get("auth_refresh_token")}function f(e){S.set("auth_token",e)}function y(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function g(){const e=u();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw y(),sendToLogin({expired:!0}),new Error("Token refresh failed");const{token:n}=await o.json();return f(n),n}async function t(e,o={}){let n=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:r(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),d=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(d.validation=i.validation),d.status=s.status,d.body=i,d}return s.status===204?null:s.json()}async function O(e,o){const n=a(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),verifyLogin:e=>t("/auth/login/verify",{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})}),checkReset:(e,o)=>t("/auth/reset-password/check",{method:"POST",body:JSON.stringify({token:e,...o&&{password:o}})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},listTags:{list:e=>t(`/list-tags/${e}`,{method:"GET"}).then(o=>o.tags||[]),add:(e,o)=>t(`/list-tags/${e}`,{method:"POST",body:JSON.stringify({name:o})}),changeItem:(e,o,n)=>t(`/list-tags/${e}/item`,{method:"PUT",body:JSON.stringify({id:o,...n})}).then(r=>r.tags||[]),rename:(e,o,n)=>t(`/list-tags/${e}/rename`,{method:"POST",body:JSON.stringify({from:o,to:n})}),remove:(e,o)=>t(`/list-tags/${e}/delete`,{method:"POST",body:JSON.stringify({name:o})})},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:e=>t(`/theme/admin${e?`?id=${encodeURIComponent(e)}`:""}`,{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"}),removeContents:(e,o)=>t(`/projects/${encodeURIComponent(e)}/remove-contents`,{method:"POST",body:JSON.stringify({confirm:o})})},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=>O("/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"}),passwordReset:(e,o)=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"POST",body:JSON.stringify({via:o})}),cancelPasswordReset:e=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"DELETE"})},tools:{list:()=>t("/tools",{method:"GET"}),update:(e,o)=>t(`/tools/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!d.ok)throw new Error(c.error||`Install failed (${d.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&f(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=$(location.hash,{expired:e}))}export function logout(){!T&&window.M?.subscribe&&(T=!0,M.subscribe("router:afterChange",({to:e}={})=>{/^\/login(\?|$)/.test(e?.path||"")||(p=!1)})),p=!0,R.navigate("/login")}export function takePendingLogout(){return p?(p=!1,b(),!0):!1}function b(){const e=u();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),y()}export{t as apiRequest,g as refreshAccessToken,a as getToken,y as clearAuth};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{fmtDate as M}from"./dates.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,MAX_CSS=50*1024,FORM_FIELDS=["name","project","bundled","content","css"];export function formFromBlock(e,{project:r=""}={}){return e?{name:String(e.name||""),project:String(e.meta?.project||""),bundled:e.bundled===!0,content:String(e.content??""),css:String(e.css??"")}:{name:"",project:String(r||""),bundled:!1,content:"",css:""}}export function snapshot(e){const r=e||{};return JSON.stringify(FORM_FIELDS.map(n=>n==="bundled"?r[n]===!0:String(r[n]??"")))}export function checkForm(e,{isNew:r,taken:n=[]}={}){const t=String(e?.name||"").trim(),o={};r&&(t?NAME_RE.test(t)?n.includes(t)&&(o.name=`"${t}" is already a block - open it from the list, or choose another name.`):o.name="Lowercase letters, digits and hyphens, starting with a letter or digit.":o.name="A block needs a name.");const i=String(e?.css??"");return i.length>MAX_CSS&&(o.css=`The CSS is ${Math.ceil(i.length/1024)} KB - a block's CSS is limited to 50 KB.`),{errors:o,name:t}}export function payloadFromForm(e,r={}){return{content:String(e?.content??""),bundled:e?.bundled===!0,css:String(e?.css??""),meta:{...
|
|
1
|
+
import{fmtDate as M}from"./dates.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,MAX_CSS=50*1024,FORM_FIELDS=["name","project","bundled","content","css"];export function formFromBlock(e,{project:r=""}={}){return e?{name:String(e.name||""),project:String(e.meta?.project||""),bundled:e.bundled===!0,content:String(e.content??""),css:String(e.css??"")}:{name:"",project:String(r||""),bundled:!1,content:"",css:""}}export function snapshot(e){const r=e||{};return JSON.stringify(FORM_FIELDS.map(n=>n==="bundled"?r[n]===!0:String(r[n]??"")))}export function checkForm(e,{isNew:r,taken:n=[]}={}){const t=String(e?.name||"").trim(),o={};r&&(t?NAME_RE.test(t)?n.includes(t)&&(o.name=`"${t}" is already a block - open it from the list, or choose another name.`):o.name="Lowercase letters, digits and hyphens, starting with a letter or digit.":o.name="A block needs a name.");const i=String(e?.css??"");return i.length>MAX_CSS&&(o.css=`The CSS is ${Math.ceil(i.length/1024)} KB - a block's CSS is limited to 50 KB.`),{errors:o,name:t}}export function payloadFromForm(e,r={}){const{adminTags:n,...t}=r||{};return{content:String(e?.content??""),bundled:e?.bundled===!0,css:String(e?.css??""),meta:{...t,project:String(e?.project||"")||null}}}export function extractPlaceholders(e){const r=new Set;for(const[,n]of y(e).matchAll(/\{\{([\w_]+)(?::[^{}]+)?\}\}/g))r.add(n);return[...r]}export function rawPlaceholders(e){return new Set([...y(e).matchAll(/\{\{\{(\w+)\}\}\}/g)].map(r=>r[1]))}const y=e=>String(e??"").replace(/<!--[\s\S]*?-->/g,""),d=e=>String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");export{d as escapeHtml};export function formatDate(e,r){if(e==null||e==="")return null;const n=e instanceof Date?e:new Date(e);if(Number.isNaN(n.getTime()))return null;const t=(a,l=2)=>String(a).padStart(l,"0"),o=["January","February","March","April","May","June","July","August","September","October","November","December"],i=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],s=()=>(n.getHours()+11)%12+1,c={YYYY:()=>n.getFullYear(),YY:()=>t(n.getFullYear()%100),MMMM:()=>o[n.getMonth()],MMM:()=>o[n.getMonth()].slice(0,3),MM:()=>t(n.getMonth()+1),M:()=>n.getMonth()+1,DD:()=>t(n.getDate()),D:()=>n.getDate(),dddd:()=>i[n.getDay()],ddd:()=>i[n.getDay()].slice(0,3),HH:()=>t(n.getHours()),H:()=>n.getHours(),hh:()=>t(s()),h:()=>s(),mm:()=>t(n.getMinutes()),m:()=>n.getMinutes(),ss:()=>t(n.getSeconds()),s:()=>n.getSeconds(),A:()=>n.getHours()<12?"AM":"PM",a:()=>n.getHours()<12?"am":"pm"};return String(r).replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g,a=>String(c[a]()))}const S={icon:"box","facebook-icon":"facebook","instagram-icon":"instagram"};export const SAMPLE_IMAGE="data:image/svg+xml,"+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500"><rect width="800" height="500" fill="#94a3b8"/><path d="M0 500 L260 230 L420 390 L540 290 L800 500 Z" fill="#64748b"/><circle cx="610" cy="140" r="52" fill="#cbd5e1"/></svg>');const v=/(image|img|photo|picture|avatar|logo|thumb|thumbnail|cover|banner)(url|src)?$/;export function sampleValue(e,r=new Date,{raw:n=!1}={}){const t=String(e).toLowerCase();return S[t]?S[t]:/(class|style)$/.test(t)?"":t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"||t==="_updatedat"?r.toISOString():t.replace(/update(?!d)/g,"").includes("date")||/(published|created|updated)at$/.test(t)?M(r):v.test(t)?n?t.includes("avatar")?`<img src="${SAMPLE_IMAGE}" alt="" width="40" height="40">`:`<img src="${SAMPLE_IMAGE}" alt="">`:SAMPLE_IMAGE:/(url|href|link)$/.test(t)?"#":t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":["message","content","description","summary","excerpt"].some(o=>t.includes(o))?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("categor")?n?'<a href="#">General</a>':"general":`Sample ${e}`}export const scopeName=e=>String(e||"").trim()||"preview",PREVIEW_REPEATS=[1,3];export function renderPreview(e,r={},{repeat:n=1,sanitize:t=null}={}){if(typeof t!="function")return"";const o=u=>r[u]??`[${u}]`,i=String(e?.content??"").replace(/<!--[\s\S]*?-->/g,"").replace(/\{\{\{(\w+)\}\}\}/g,(u,g)=>String(o(g))).replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g,(u,g,p)=>{const f=o(g);if(p){const h=formatDate(f,p);if(h!==null)return d(h)}return d(f)}),s=d(scopeName(e?.name)),c=String(e?.css??""),a=c.trim()?`<style>[data-block="${s}"] { ${c.replace(/<\/style/gi,"<\\/style")} }</style>`:"",l=PREVIEW_REPEATS.includes(Number(n))?Number(n):1,m=String(t(i)??"");if(l===1)return`${a}<div class="dm-static-block" data-block="${s}">${m}</div>`;const b=`<div data-block="${s}">${m}</div>`;return`${a}<div class="dm-collection-display dm-collection-blocks grid grid-cols-1 grid-cols-md-2 grid-cols-lg-3 gap-4">`+Array.from({length:l},()=>b).join("")+"</div>"}export function previewBridge(e,r){const n=JSON.stringify(Number(e)||0),t=JSON.stringify(String(r)).replace(/</g,"\\u003c");return`<script>(function () {
|
|
2
2
|
var P = window.parent, TICK = ${n}, HOST = ${t};
|
|
3
3
|
function host() { return document.getElementById(HOST); }
|
|
4
4
|
function up(msg) { msg.source = 'dm-block-preview'; msg.tick = TICK; P.postMessage(msg, '*'); }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fmtDate as u}from"./dates.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,FILTERS=["all","used","unused"];const i=(e,t,n=`${t}s`)=>`${e} ${e===1?t:n}`;export function formatSize(e){const t=Number(e)||0;return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(t<10*1024?1:0)} KB`:`${(t/1024/1024).toFixed(1)} MB`}export function usageCount(e){return e?.usage?(e.usage.pages?.length||0)+(e.usage.views?.length||0):null}export function usageText(e){const t=usageCount(e);if(t===null)return"";if(!t)return"Not used on any page or view";const n=e.usage.pages?.length||0,
|
|
1
|
+
import{fmtDate as u}from"./dates.js";import{matchesTag as l,tagsOf as c,tagsText as d}from"./list-tags-arrange.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,FILTERS=["all","used","unused"];const i=(e,t,n=`${t}s`)=>`${e} ${e===1?t:n}`;export function formatSize(e){const t=Number(e)||0;return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(t<10*1024?1:0)} KB`:`${(t/1024/1024).toFixed(1)} MB`}export function usageCount(e){return e?.usage?(e.usage.pages?.length||0)+(e.usage.views?.length||0):null}export function usageText(e){const t=usageCount(e);if(t===null)return"";if(!t)return"Not used on any page or view";const n=e.usage.pages?.length||0,o=e.usage.views?.length||0;return`Used on ${[n&&i(n,"page"),o&&i(o,"view")].filter(Boolean).join(" and ")}`}export function shortcodeFor(e){return e?.example||`[block template="${e?.name??""}" /]`}export function arrangeBlocks(e,{filter:t="all",query:n="",tag:o=""}={}){const a=String(n).trim().toLowerCase();return[...e||[]].filter(r=>t==="used"?usageCount(r)>0:t==="unused"?usageCount(r)===0:!0).filter(r=>l(r,o)).filter(r=>!a||[r.name,r.description,r.meta?.project,...c(r)].some(s=>String(s||"").toLowerCase().includes(a))).sort((r,s)=>String(r.name).localeCompare(String(s.name)))}export function blockRow(e,t=u){const n=usageCount(e);return{name:e.name,description:e.description||"",meta:[`${e.name}.html`,formatSize(e.size),e.updatedAt?`changed ${t(e.updatedAt)}`:"",usageText(e)].filter(Boolean).join(" \xB7 "),hasCss:!!e.hasCss,bundled:!!e.bundled,project:e.meta?.project||"",listTags:d(e),unused:n===0,uses:n===null?"":String(n)}}export function checkNewName(e,t=[]){const n=String(e??"").trim();return n?NAME_RE.test(n)?t.includes(n)?`"${n}" already exists.`:"":"Lowercase letters, digits and hyphens; starting with a letter or digit.":"A block needs a name."}export function copyName(e,t=[]){const n=`${e}-copy`;if(!t.includes(n))return n;for(let o=2;o<1e3;o++)if(!t.includes(`${n}-${o}`))return`${n}-${o}`;return n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const UNTAGGED=" untagged",MAX_TAG=40,same=(t,e)=>String(t).toLowerCase()===String(e).toLowerCase();export function normaliseTag(t){return typeof t!="string"?"":t.replace(/[\u0000-\u001f\u007f]/g,"").replace(/\s+/g," ").trim().slice(0,40)}export function normaliseActive(t){return t===UNTAGGED?UNTAGGED:normaliseTag(t)}export function tagsOf(t){const e=Array.isArray(t?.adminTags)?t.adminTags:t?.meta?.adminTags;return Array.isArray(e)?e.filter(o=>typeof o=="string"&&o):[]}export function matchesTag(t,e){if(!e)return!0;const o=tagsOf(t);return e===UNTAGGED?o.length===0:o.some(r=>same(r,e))}export const tagsText=t=>tagsOf(t).join(", ");export function tagCounts(t,e){const o=[],r=n=>{n&&!o.some(i=>same(i,n))&&o.push(n)};return(t||[]).forEach(r),(e||[]).forEach(n=>tagsOf(n).forEach(r)),o.sort((n,i)=>n.localeCompare(i,void 0,{sensitivity:"base"})).map(n=>({name:n,count:(e||[]).filter(i=>tagsOf(i).some(s=>same(s,n))).length}))}export function activeNote(t,e,o){if(!t)return"";const r=`${e} ${e===1?o.replace(/s$/,""):o}`;return t===UNTAGGED?`Showing ${r} with no tag.`:`Showing ${r} tagged "${t}".`}export function tagMenuItems({all:t,current:e,change:o,newTag:r}){const n=(t||[]).filter(a=>!e.some(l=>same(l,a))),s=[{label:"Add tag",icon:"tag",submenu:[...n.map(a=>({label:a,icon:"tag",action:()=>o({add:[a]})})),...n.length?[{type:"divider"}]:[],{label:"New tag\u2026",icon:"plus",action:r}]}];return e.length?s.push({label:"Remove tag",icon:"x",submenu:[...e.map(a=>({label:a,icon:"tag",action:()=>o({remove:[a]})})),...e.length>1?[{type:"divider"},{label:"Remove all tags",icon:"x",action:()=>o({tags:[]})}]:[]]}):s.push({label:"Remove tag",icon:"x",disabled:!0}),s}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import{api as x}from"../api.js";import{helpPopovers as H}from"./help-popover.js";import{onceClosed as P}from"./tool-kit.js";import{MAX_TAG as S,UNTAGGED as U,normaliseActive as K,normaliseTag as C,same as N}from"./list-tags-arrange.js";export*from"./list-tags-arrange.js";const u=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");export async function askTagName({title:a="New tag",value:l="",confirmText:f="Add"}={}){const p=await E.prompt(`Up to ${S} characters. Tags are only seen here in the admin.`,{title:a,inputPlaceholder:"e.g. Spring campaign",inputValue:u(l),confirmText:f});return p==null?"":C(String(p))}let w=!1;function V(){if(w||document.getElementById("ltg-styles")){w=!0;return}const a=document.createElement("style");a.id="ltg-styles",a.textContent=`
|
|
2
|
+
.ltg-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
|
|
3
|
+
.ltg-panel .form-label { display: flex; align-items: center; gap: .3rem; font-weight: 500; margin-bottom: .3rem; }
|
|
4
|
+
.ltg-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
|
|
5
|
+
.ltg-list li { display: flex; align-items: center; gap: .5rem; padding: .4rem .6rem; border-bottom: 1px solid var(--dm-border); }
|
|
6
|
+
.ltg-list li:last-child { border-bottom: 0; }
|
|
7
|
+
.ltg-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
8
|
+
.ltg-count { color: var(--dm-text-muted); font-size: var(--dm-text-xs); font-variant-numeric: tabular-nums; }
|
|
9
|
+
.ltg-list button { border: 0; background: none; cursor: pointer; color: var(--dm-text-muted); padding: .15rem .3rem; border-radius: var(--dm-radius-sm); display: inline-flex; }
|
|
10
|
+
.ltg-list button:hover { color: var(--dm-primary); }
|
|
11
|
+
.ltg-list button.is-danger:hover { color: var(--dm-danger); }
|
|
12
|
+
.ltg-empty { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
|
|
13
|
+
.ltg-add { display: flex; gap: .4rem; }
|
|
14
|
+
.ltg-add .form-input { flex: 1; }
|
|
15
|
+
.ltg-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
16
|
+
.ltg-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
|
|
17
|
+
.ltg-actions .spacer { flex: 1; }`,document.head.appendChild(a),w=!0}export function openTagSettings({kind:a,noun:l,title:f,tags:p,active:i,reload:A}){V();const d=E.slideover({title:f,size:"sm",position:"right"}),$=document.createElement("template");$.innerHTML=`
|
|
18
|
+
<div class="ltg-panel">
|
|
19
|
+
<div>
|
|
20
|
+
<label class="form-label" for="ltg-show">Show
|
|
21
|
+
<span data-help="Narrow the list to the ${l} wearing one tag. Remembered in this browser; everyone else keeps their own choice."></span></label>
|
|
22
|
+
<select id="ltg-show" class="form-input"></select>
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<label class="form-label">Tags
|
|
26
|
+
<span data-help="Tags link ${l} together - an item can wear several. They are seen only here in the admin, never on the site. Right-click any of the ${l} to add or remove one. Deleting a tag takes it off its ${l}; nothing else changes."></span></label>
|
|
27
|
+
<ul class="ltg-list"></ul>
|
|
28
|
+
<p class="ltg-empty">No tags yet.</p>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="ltg-add">
|
|
31
|
+
<input type="text" class="form-input" maxlength="${S}" placeholder="New tag" aria-label="New tag">
|
|
32
|
+
<button type="button" class="btn btn-sm btn-primary">Add</button>
|
|
33
|
+
</div>
|
|
34
|
+
<small class="ltg-meta">Tags are shared by everyone who edits ${l}.</small>
|
|
35
|
+
<div class="ltg-actions"><span class="spacer"></span>
|
|
36
|
+
<button type="button" class="btn btn-sm btn-primary ltg-done">Done</button></div>
|
|
37
|
+
</div>`;const o=$.content.firstElementChild,g=o.querySelector("#ltg-show"),h=o.querySelector(".ltg-list"),q=o.querySelector(".ltg-empty"),m=o.querySelector(".ltg-add input");let y=!1;const v=(e,n)=>{const t=document.createElement("option");return t.value=e,t.textContent=n,t},T=(e,n,t,c)=>{const r=document.createElement("button");r.type="button",r.title=n,r.setAttribute("aria-label",n),t&&(r.className=t);const s=document.createElement("span");return s.setAttribute("data-icon",e),s.setAttribute("data-icon-size","14"),r.appendChild(s),r.addEventListener("click",c),r};async function b(e,n){if(!y){y=!0;try{const t=await e();return await A(),n&&E.toast(n(t),{type:"success"}),!0}catch(t){return E.toast(t.message||"That did not work.",{type:"error"}),!1}finally{y=!1}}}async function D(e){const n=await askTagName({title:`Rename "${u(e)}"`,value:e,confirmText:"Rename"});if(!n||n===e)return;await b(()=>x.listTags.rename(a,e,n),c=>`Renamed - ${c.changed} ${l} updated.`)&&N(i.peek(),e)&&(i.value=n)}async function R(e){const n=e.count?`Delete the tag "${u(e.name)}"? It comes off ${e.count} ${e.count===1?l.replace(/s$/,""):l}; nothing else about them changes.`:`Delete the tag "${u(e.name)}"?`;if(!await E.confirm(n))return;await b(()=>x.listTags.remove(a,e.name),()=>"Tag deleted.")&&N(i.peek(),e.name)&&(i.value="")}async function k(){const e=C(m.value);if(!e){m.focus();return}await b(()=>x.listTags.add(a,e)),m.value="",m.focus()}const L=M.effect(()=>{const e=p.value,n=i.value;g.replaceChildren(v("",`All ${l}`),v(U,"No tag"),...e.map(t=>v(t.name,`${t.name} (${t.count})`))),g.value=n,h.replaceChildren(...e.map(t=>{const c=document.createElement("li"),r=document.createElement("span");r.className="ltg-name",r.textContent=t.name;const s=document.createElement("span");return s.className="ltg-count",s.textContent=String(t.count),c.append(r,s,T("edit",`Rename ${t.name}`,"",()=>D(t.name)),T("trash",`Delete ${t.name}`,"is-danger",()=>R(t))),c})),h.hidden=!e.length,q.hidden=e.length>0,I.scan(h)}),z=()=>{i.value=K(g.value)},G=e=>{e.key==="Enter"&&e.target===m&&(e.preventDefault(),k())};g.addEventListener("change",z),o.addEventListener("keydown",G),o.querySelector(".ltg-add button").addEventListener("click",k),o.querySelector(".ltg-done").addEventListener("click",()=>d.close()),d.element.appendChild(o);const B=H(o);return P(d,()=>{L(),B()}),I.scan(o),d.open(),d}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fmtDate as
|
|
1
|
+
import{fmtDate as d}from"./dates.js";import{matchesTag as c,normaliseActive as f,tagsOf as h,tagsText as g}from"./list-tags-arrange.js";export const STATUSES=["all","published","draft"],SORTS=["order","updated","title","url"],MODES=["list","tree"],DEFAULT_SETTINGS={status:"all",sort:"order",mode:"list",tag:""};const u=(t,e,r=`${e}s`)=>`${t} ${t===1?e:r}`;export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{};return{status:STATUSES.includes(e.status)?e.status:DEFAULT_SETTINGS.status,sort:SORTS.includes(e.sort)?e.sort:DEFAULT_SETTINGS.sort,mode:MODES.includes(e.mode)?e.mode:DEFAULT_SETTINGS.mode,tag:f(e.tag)}}export function ago(t,e=Date.now()){const r=new Date(t).getTime();if(!Number.isFinite(r))return"";const o=Math.max(0,Math.round((e-r)/1e3));return o<60?"just now":o<3600?`${Math.round(o/60)} min ago`:o<86400?`${u(Math.round(o/3600),"hour")} ago`:o<86400*14?`${u(Math.round(o/86400),"day")} ago`:d(r)}export function visibilityLabel(t){const e=String(t??"public").trim();return!e||e==="public"?"":e==="private"?"Private":e.startsWith("=")?`Only ${e.slice(1)}`:`${e}+`}const m=t=>`${t.title||""} ${t.urlPath||""} ${(t.tags||[]).join(" ")} ${h(t).join(" ")} ${t.layout||""}`.toLowerCase();export function arrangePages(t,{status:e="all",query:r="",sort:o="order",tag:s=""}={}){const n=String(r).trim().toLowerCase(),a=(t||[]).filter(i=>e==="all"||(e==="draft"?i.status!=="published":i.status==="published")).filter(i=>c(i,s)).filter(i=>!n||m(i).includes(n));return o==="updated"?[...a].sort((i,l)=>String(l.updatedAt||"").localeCompare(String(i.updatedAt||""))):o==="title"?[...a].sort((i,l)=>String(i.title||"").localeCompare(String(l.title||""))):o==="url"?[...a].sort((i,l)=>String(i.urlPath).localeCompare(String(l.urlPath))):a}export function statusCounts(t){const e=(t||[]).length,r=(t||[]).filter(o=>o.status==="published").length;return{all:e,published:r,draft:e-r}}export function pageRow(t,e=Date.now()){const r=t.status!=="published",o=Number.isFinite(t.versionCount)?t.versionCount:0,s=t.resolvedProject&&t.resolvedProject!=="core"?t.resolvedProject:"";return{urlPath:t.urlPath,title:t.title||t.urlPath,statusText:t.status==="review"?"Review":r?"Draft":"Published",statusCls:r?"is-warn":"is-ok",visibility:visibilityLabel(t.visibility),project:s,plugin:t.plugin?String(t.plugin):"",bundled:t.bundled===!0,tagsText:(t.tags||[]).filter(Boolean).map(n=>`#${n}`).join(" "),listTags:g(t),meta:[t.urlPath,t.layout||"default",u(o,"revision"),t.updatedAt?`updated ${ago(t.updatedAt,e)}`:""].filter(Boolean).join(" \xB7 "),revisions:o,rowCls:r?"is-draft":""}}export function treeRows(t){const e=new Set((t||[]).map(r=>r.urlPath));return(t||[]).map(r=>{const o=String(r.urlPath).split("/").filter(Boolean);let s=null;for(let n=o.length-1;n>0&&!s;n--){const a="/"+o.slice(0,n).join("/");e.has(a)&&(s=a)}return{id:r.urlPath,parent_id:s,name:r.title||r.urlPath,icon:r.status==="published"?"check-circle":"file-text"}})}export function checkPrune(t,e){const r=String(t??"").trim();if(!/^\d+$/.test(r))return{error:"A whole number, 0 or more.",keep:0,deletes:0};const o=Number.parseInt(r,10);return{error:"",keep:o,deletes:Math.max(0,(Number(e)||0)-o)}}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
<!-- Hidden file picker driven by the Import banner action -->
|
|
10
10
|
<input class="blx-import-file" type="file" accept=".dmblock.json,application/json" hidden>
|
|
11
11
|
|
|
12
|
+
<p class="blx-notice" data-bind-hidden="!tagNote.value">
|
|
13
|
+
<span data-bind-text="tagNote.value"></span> <button type="button" class="blx-link" data-on-click="showAllTags">Show all blocks</button>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
12
16
|
<div class="blx-card">
|
|
13
17
|
<div class="blx-bar">
|
|
14
18
|
<div class="blx-seg" role="group" aria-label="Show">
|
|
@@ -17,7 +21,7 @@
|
|
|
17
21
|
<button type="button" data-on-click="showUnused" data-bind-class="filter.value === 'unused' && 'is-on'">Unused <b data-bind-text="counts.value.unused"></b></button>
|
|
18
22
|
</div>
|
|
19
23
|
<input type="search" class="form-input blx-search" placeholder="Search blocks…" aria-label="Search blocks" data-model="query.value">
|
|
20
|
-
<span data-help-title="The list" data-help="Click a block to edit it. Right-click it (or ⋮, or Shift+F10) to copy its shortcode, see where it is used, duplicate, export or delete it. In use counts pages and views that place it; a block a plugin uses from its own code does not count, so check before deleting one marked unused."></span>
|
|
24
|
+
<span data-help-title="The list" data-help="Click a block to edit it. Right-click it (or ⋮, or Shift+F10) to copy its shortcode, see where it is used, duplicate, export or delete it. In use counts pages and views that place it; a block a plugin uses from its own code does not count, so check before deleting one marked unused. Tags link blocks together - add or remove one from the right-click menu, and pick which tag to show (or manage the tags) with the cog above."></span>
|
|
21
25
|
</div>
|
|
22
26
|
<p class="blx-note" data-bind-hidden="!loading.value">Loading…</p>
|
|
23
27
|
<p class="blx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
|
|
@@ -55,6 +59,10 @@
|
|
|
55
59
|
.blx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
|
|
56
60
|
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
|
|
57
61
|
.blx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
|
|
62
|
+
.blx-chip.is-tag { background: color-mix(in srgb, var(--dm-primary) 12%, transparent); color: var(--dm-primary); max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
|
|
63
|
+
.blx-notice { margin: 0; padding: .5rem .75rem; border-radius: var(--dm-radius-md); font-size: var(--dm-text-sm);
|
|
64
|
+
background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 12%, transparent); color: var(--dm-text); }
|
|
65
|
+
.blx-link { border: 0; background: none; padding: 0; font: inherit; color: var(--dm-primary); text-decoration: underline; cursor: pointer; }
|
|
58
66
|
.blx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
|
|
59
67
|
.blx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
|
|
60
68
|
.blx-uses { min-width: 2.5rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
|
|
@@ -134,6 +134,13 @@
|
|
|
134
134
|
<small class="form-hint">Comma-separated keywords for filtering and search.</small>
|
|
135
135
|
</div>
|
|
136
136
|
</div>
|
|
137
|
+
<div class="row mb-3">
|
|
138
|
+
<div class="col">
|
|
139
|
+
<label class="form-label">Admin tags <span data-help="Labels that link pages together in the Pages list - pick one with the list's cog to show only those pages. Seen only here in the admin, never on the site; the Pages list's right-click menu adds and removes them too."></span></label>
|
|
140
|
+
<input id="field-admin-tags" type="text" class="form-input" />
|
|
141
|
+
<small class="form-hint">Only seen in the admin - for grouping pages in the Pages list.</small>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
137
144
|
<div class="row">
|
|
138
145
|
<div class="col-auto">
|
|
139
146
|
<label class="form-check-label">
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
<span data-bind-text="projectNote.value"></span> <a href="#/pages">Show all pages</a>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
+
<p class="pgx-notice" data-bind-hidden="!tagNote.value">
|
|
10
|
+
<span data-bind-text="tagNote.value"></span> <button type="button" class="pgx-link" data-on-click="showAllTags">Show all pages</button>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
9
13
|
<div class="pgx-card">
|
|
10
14
|
<div class="pgx-bar">
|
|
11
15
|
<div class="pgx-seg" role="group" aria-label="Status">
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
<button type="button" data-on-click="showList" data-bind-class="mode.value === 'list' && 'is-on'" title="List">List</button>
|
|
26
30
|
<button type="button" data-on-click="showTree" data-bind-class="mode.value === 'tree' && 'is-on'" title="Tree - pages under the page whose URL they sit beneath">Tree</button>
|
|
27
31
|
</div>
|
|
28
|
-
<span data-help-title="Pages" data-help="Click a page to edit it. Right-click it (or ⋮, or Shift+F10) to view it on the site, copy its address, transition between Draft, Published and Review, trim its revision history or delete it. Drafts are not public: visitors get a 404, while anyone who can edit pages sees them with a Draft banner. Tree shows each page under the page whose address it sits beneath. Your filter, sort and
|
|
32
|
+
<span data-help-title="Pages" data-help="Click a page to edit it. Right-click it (or ⋮, or Shift+F10) to view it on the site, copy its address, transition between Draft, Published and Review, trim its revision history or delete it. Drafts are not public: visitors get a 404, while anyone who can edit pages sees them with a Draft banner. Tree shows each page under the page whose address it sits beneath. Tags link pages together - add or remove one from the right-click menu, and pick which tag to show (or manage the tags) with the cog above. Your filter, sort, view and tag are remembered."></span>
|
|
29
33
|
</div>
|
|
30
34
|
<div class="pgx-note dm-loading-slot" data-bind-hidden="!loading.value">Loading…</div>
|
|
31
35
|
<p class="pgx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
|
|
@@ -67,6 +71,8 @@
|
|
|
67
71
|
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
|
|
68
72
|
.pgx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
|
|
69
73
|
.pgx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
|
|
74
|
+
.pgx-chip.is-tag { background: color-mix(in srgb, var(--dm-primary) 12%, transparent); color: var(--dm-primary); max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
|
|
75
|
+
.pgx-link { border: 0; background: none; padding: 0; font: inherit; color: var(--dm-primary); text-decoration: underline; cursor: pointer; }
|
|
70
76
|
.pgx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
|
|
71
77
|
.pgx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
|
|
72
78
|
.pgx-kebab:hover { color: var(--dm-primary); }
|
package/admin/js/views/blocks.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as h,apiRequest as Fe}from"../api.js";import{addBannerAction as A}from"../lib/plugin-chrome.js";import{helpPopovers as ie}from"../lib/help-popover.js";import{onceClosed as re,openMenuAt as de,sidebarCount as Re,viewLifetime as qe}from"../lib/tool-kit.js";import{filterByProject as Ue,getProjectFromHash as He,renderProjectFilterNotice as Oe}from"../lib/project-context.js";import{FILTERS as Pe,arrangeBlocks as Ke,blockRow as We,checkNewName as _e,copyName as ue,shortcodeFor as pe,usageText as me}from"../lib/blocks-arrange.js";import{fmtDate as ze}from"../lib/dates.js";import{UNTAGGED as Ge,activeNote as Ye,askTagName as Je,normaliseActive as Ve,openTagSettings as Qe,same as be,tagCounts as Xe,tagMenuItems as Ze,tagsOf as D}from"../lib/list-tags.js";const he=/^#\/(blocks|projects\/[^/?#]+\/blocks)\/?(\?.*)?$/,et="#/blocks",F="blocks.draft",_="blocks.filter",fe="blocks.tag",tt=`
|
|
2
2
|
<div class="blx-row" data-name="{{name}}" tabindex="0" data-bind-class="unused && 'is-unused'">
|
|
3
3
|
<span class="blx-icon" aria-hidden="true"><span data-icon="box" data-icon-size="16"></span></span>
|
|
4
4
|
<span class="blx-main"><strong>{{name}}</strong><span data-bind-hidden="!description">{{description}}</span><small>{{meta}}</small></span>
|
|
5
5
|
<span class="blx-chips">
|
|
6
6
|
<span class="blx-chip is-info" data-bind-hidden="!hasCss" title="Has its own CSS">CSS</span>
|
|
7
|
+
<span class="blx-chip is-tag" data-bind-hidden="!listTags" title="Tags">{{listTags}}</span>
|
|
7
8
|
<span class="blx-chip" data-bind-hidden="!project" title="Project">{{project}}</span>
|
|
8
9
|
<span class="blx-chip" data-bind-hidden="!bundled" title="Ships with the CMS">Built in</span>
|
|
9
10
|
<span class="blx-chip is-warn" data-bind-hidden="!unused" title="No page or view places it">Unused</span>
|
|
10
11
|
</span>
|
|
11
12
|
<span class="blx-uses" title="Pages and views that use it">{{uses}}</span>
|
|
12
13
|
<button type="button" class="blx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
13
|
-
</div>`,
|
|
14
|
+
</div>`,k=l=>String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function ge(l){const n=document.createElement("template");return n.innerHTML=l.trim(),n.content.firstElementChild}const i=qe(he);async function at(l){try{return await navigator.clipboard.writeText(l),!0}catch{const n=document.createElement("textarea");n.value=l,n.style.cssText="position:fixed;opacity:0;",document.body.appendChild(n),n.select();let f=!1;try{f=document.execCommand("copy")}catch{}return n.remove(),f}}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(l){i.reset();const n=l.get(0).querySelector(".blx")||l.get(0),f=()=>he.test(location.hash)&&n.isConnected,w=He(),u=M.observable([]),z=M.observable(!0),R=M.observable("");let G="all";try{G=Pe.includes(S.get(_))?S.get(_):"all"}catch{}const y=M.observable(G),Y=M.observable("");i.add(M.effect(()=>{const e=y.value;try{S.set(_,e)}catch{}}));let J="";try{J=Ve(S.get(fe))}catch{}const g=M.observable(J),x=M.observable([]);i.add(M.effect(()=>{const e=g.value;try{S.set(fe,e)}catch{}}));const ve=ze,q=M.observable([]);i.add(M.effect(()=>{q.value=Ke(u.value,{filter:y.value,query:Y.value,tag:g.value})}));const U=M.observable([]);i.add(M.effect(()=>{U.value=q.value.map(e=>We(e,ve))}));const H=M.computed(()=>Xe(x.value,u.value)),ye=M.computed(()=>Ye(g.value,q.value.length,"blocks")),ke=M.computed(()=>{const e=u.value,t=e.filter(a=>(a.usage?.pages?.length||0)+(a.usage?.views?.length||0)>0).length;return{all:e.length,used:t,unused:e.length-t}}),we=M.computed(()=>u.value.length?"No blocks match.":"No blocks yet - New block in the banner makes one, or Import brings one in."),O=e=>u.peek().find(t=>t.name===e),xe=w?`#/blocks/new?project=${encodeURIComponent(w)}`:"#/blocks/new",V=e=>`#/blocks/edit/${encodeURIComponent(e)}`;async function P(){try{let e=await Fe("/blocks?usage=1",{method:"GET"});if(!f())return;if(e=Array.isArray(e)?e:[],w){const t=e.length;e=Ue(e,w),Oe(n,{total:t,shown:e.length,slug:w,allHref:"#/blocks",noun:"blocks"})}u.value=e,R.value="",await Ee()}catch(e){R.value=`Could not load the blocks: ${e.message||e}`}finally{z.value=!1}}async function Ee(){try{x.value=(await h.listTags.list("blocks")).map(t=>t.name)}catch{}const e=g.peek();e&&e!==Ge&&!H.peek().some(t=>be(t.name,e))&&(g.value="")}async function Q(e,t){try{const a=await h.listTags.changeItem("blocks",e.name,t);u.value=u.peek().map(s=>s.name===e.name?{...s,meta:{...s.meta||{},adminTags:a}}:s),a.some(s=>!x.peek().some(p=>be(p,s)))&&(x.value=[...x.peek(),...a])}catch(a){E.toast(`Tags not saved: ${a.message||a}`,{type:"error"})}}const Se=()=>Qe({kind:"blocks",noun:"blocks",title:"Blocks settings - tags",tags:H,active:g,reload:P}),j=async()=>{Re(et),f()&&await P()};async function Ce(e){const a=(e.usage?.pages?.length||0)+(e.usage?.views?.length||0)?` It is ${k(me(e).toLowerCase())} - they will show nothing where it was.`:"";if(await E.confirm(`Delete the block "${k(e.name)}"?${a} This cannot be undone.`))try{await h.blocks.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),await j()}catch(s){E.toast(s.message||"Failed to delete block.",{type:"error"})}}async function Te(e){try{await h.blocks.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}async function Me(e){const t=pe(e),a=await at(t);E.toast(a?`Copied ${t}`:`Could not copy - your browser refused. The shortcode is ${t}`,{type:a?"success":"error"})}const C=n.querySelector(".blx-import-file");async function X(){const e=C.files?.[0];if(!e)return;C.value="";let t;try{t=JSON.parse(await e.text())}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!t||typeof t!="object"||!t.name||typeof t.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const a=await h.blocks.importBundle(t);E.toast(`Imported "${a.name}".`,{type:"success"}),await j()}catch(a){if(a.code==="CONFLICT"){if(!await E.confirm(`A block named "${k(a.name)}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const s=await h.blocks.importBundle(t,{overwrite:!0});E.toast(`Overwrote "${s.name}".`,{type:"success"}),await j()}catch(s){E.toast(s.message||"Import failed.",{type:"error"})}return}E.toast(a.message||"Import failed.",{type:"error"})}}C.addEventListener("change",X),i.add(()=>C.removeEventListener("change",X));const T=new Set;let v=null,Z=!1;function Ne(e){const t=E.slideover({title:`Where ${k(e.name)} is used`,size:"sm",position:"right"}),a=ge(`
|
|
14
15
|
<div class="blx-form">
|
|
15
16
|
<p class="blx-note blx-where-sum"></p>
|
|
16
17
|
<ul class="blx-where"></ul>
|
|
17
18
|
<div><div class="form-label">Shortcode</div><div class="blx-code"></div>
|
|
18
19
|
<small class="blx-hint">Right-click the block in the list and Copy shortcode to paste it into a page.</small></div>
|
|
19
|
-
</div>`);
|
|
20
|
+
</div>`);a.querySelector(".blx-where-sum").textContent=me(e)+".",a.querySelector(".blx-code").textContent=pe(e);const s=a.querySelector(".blx-where"),p=(r,B,$)=>{const L=document.createElement("li"),o=document.createElement("a");o.href=r,o.textContent=B,o.addEventListener("click",()=>t.close());const m=document.createElement("small");m.textContent=$,L.append(o,m),s.appendChild(L)};for(const r of e.usage?.pages||[])p(`#/pages/edit${r}`,r,"page");for(const r of e.usage?.views||[])p(`#/views/edit/${encodeURIComponent(r)}`,r,"view");t.element.appendChild(a),T.add(t),re(t,()=>T.delete(t)),t.open()}function ee(e,t=null){const a=()=>u.peek().map(d=>d.name),s=t?.name??ue(e.name,a()),p=M.observable(s),r=M.observable(!1),B=M.computed(()=>_e(p.value,u.value.map(d=>d.name))),$={name:p,saving:r,err:B,cannot:M.computed(()=>r.value||!!B.value)},L=()=>p.peek()!==ue(e.name,a())||!!t,o=E.slideover({title:`Duplicate ${k(e.name)}`,size:"sm",position:"right"}),m=ge(`
|
|
20
21
|
<div class="blx-form">
|
|
21
22
|
<div>
|
|
22
23
|
<label class="form-label" for="blx-dup-name">Name of the copy
|
|
@@ -31,4 +32,4 @@ import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as A}fro
|
|
|
31
32
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
|
|
32
33
|
<span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
|
|
33
34
|
</div>
|
|
34
|
-
</div>`);let
|
|
35
|
+
</div>`);let K=!1;async function oe(){if($.cannot.peek())return;r.value=!0;const d=p.peek().trim();try{const c=await h.blocks.get(e.name);await h.blocks.put(d,{content:c.content,css:c.css||"",...(c.meta?.project||D(c).length)&&{meta:{...c.meta?.project&&{project:c.meta.project},...D(c).length&&{adminTags:D(c)}}}}),E.toast(`Made "${d}".`,{type:"success"}),K=!0,o.close(),await j()}catch(c){E.toast(`Not duplicated: ${c.message||c}`,{type:"error"})}finally{r.value=!1}}o.element.appendChild(m);const Be=ie(m),$e=M.applyBindings($,m,{methods:{save:oe,cancel:()=>o.close()}}),Ae=d=>{d.key==="Enter"&&d.target.matches("input")&&(d.preventDefault(),oe())};m.addEventListener("keydown",Ae);const le={dirty:L,values:()=>({name:p.peek()}),source:e.name};v=le,T.add(o),re(o,()=>{$e.dispose(),Be(),T.delete(o),v===le&&(v=null)});const De=o.close.bind(o);let W=!1;o.close=async d=>{if(!K&&!Z&&d!==!0&&L()){if(W)return;W=!0;const c=await E.confirm("Discard the name you typed?");if(W=!1,!c)return}return K=!0,De()},I.scan(m),o.open();const ce=m.querySelector("#blx-dup-name");ce?.focus(),ce?.select()}function Ie(e){const t=O(e);if(!t)return[];const a=(t.usage?.pages?.length||0)+(t.usage?.views?.length||0);return[{header:t.name},{label:"Edit",icon:"edit",action:()=>{location.hash=V(t.name)}},{label:"Copy shortcode",icon:"copy",action:()=>Me(t)},{label:a?`Where it is used (${a})\u2026`:"Where it is used\u2026",icon:"link",action:()=>Ne(t)},{type:"divider"},...Ze({all:H.peek().map(s=>s.name),current:D(t),change:s=>Q(t,s),newTag:async()=>{const s=await Je();s&&await Q(t,{add:[s]})}}),{type:"divider"},{label:"Duplicate\u2026",icon:"layers",action:()=>ee(t)},{label:"Export (.dmblock.json)",icon:"download",action:()=>Te(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>Ce(t)}]}const b=n.querySelector(".blx-list");b.innerHTML=tt;const Le=M.applyBindings({blocks:u,loading:z,loadError:R,filter:y,query:Y,rows:U,counts:ke,emptyText:we,tagNote:ye},n,{methods:{showAllTags:()=>{g.value=""},showAll:()=>{y.value="all"},showUsed:()=>{y.value="used"},showUnused:()=>{y.value="unused"}}});i.add(()=>Le.dispose()),i.add(ie(n,{position:"bottom"})),i.add(M.effect(()=>{U.value,requestAnimationFrame(()=>{b.isConnected&&I.scan(b)})}));const je=E.contextMenu(b,{cursor:"glyph",match:".blx-row",inherit:!1,density:"compact",items:e=>Ie(e.dataset.name)});i.add(()=>je?.destroy?.());const te=e=>{O(e)&&(location.hash=V(e))},ae=e=>{const t=e.target.closest(".blx-row");if(!t)return;const a=e.target.closest(".blx-kebab");if(a){de(a);return}te(t.dataset.name)},se=e=>{const t=e.target.closest(".blx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),de(t)):e.key==="Enter"&&e.target===t&&te(t.dataset.name))};b.addEventListener("click",ae),b.addEventListener("keydown",se),i.add(()=>{b.removeEventListener("click",ae),b.removeEventListener("keydown",se)}),A(l,{id:"blocks-settings",icon:"settings",title:"Blocks settings - tags",onClick:Se}),A(l,{id:"blocks-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),A(l,{id:"blocks-import",icon:"upload",title:"Import a .dmblock.json bundle",onClick:()=>C.click()}),A(l,{id:"blocks-new",icon:"plus",title:"New block",onClick:()=>{location.hash=xe}});const ne=e=>{v?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",ne),i.add(()=>window.removeEventListener("beforeunload",ne)),i.add(()=>{if(v?.dirty())try{S.set(F,{source:v.source,values:v.values(),at:new Date().toISOString()})}catch{}Z=!0,[...T].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(n),await P(),!f())return;let N=null;try{N=S.get(F)}catch{}if(N?.values&&N.source){try{S.remove?S.remove(F):S.set(F,null)}catch{}const e=O(N.source);e&&await E.confirm(`You were duplicating "${k(e.name)}" and left it unfinished. Pick up where you left off?`)&&f()&&ee(e,N.values)}}};
|