domma-cms 1.10.15 → 1.10.17

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/index.html +2 -2
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/lib/collection-editor-arrange.js +1 -1
  8. package/admin/js/lib/collection-entries-arrange.js +1 -1
  9. package/admin/js/lib/form-submissions-arrange.js +1 -1
  10. package/admin/js/lib/hotkeys-ui.js +7 -16
  11. package/admin/js/lib/hotkeys.js +1 -1
  12. package/admin/js/lib/media-arrange.js +1 -1
  13. package/admin/js/lib/media-picker.js +17 -5
  14. package/admin/js/lib/media-upload.js +1 -0
  15. package/admin/js/lib/palette-commands.js +1 -0
  16. package/admin/js/templates/collection-entries.html +4 -0
  17. package/admin/js/templates/form-submissions.html +3 -0
  18. package/admin/js/templates/media.html +19 -10
  19. package/admin/js/views/collection-entries.js +7 -7
  20. package/admin/js/views/form-editor.js +6 -6
  21. package/admin/js/views/form-submissions.js +2 -2
  22. package/admin/js/views/media.js +3 -3
  23. package/docs/DommaFeatures.md +5 -4
  24. package/docs/api-reference.md +6 -1
  25. package/docs/configuration.md +57 -10
  26. package/docs/guides/multi-page-forms.md +1 -1
  27. package/docs/markdown-shortcodes.md +3 -2
  28. package/docs/plugin-development.md +3 -0
  29. package/package.json +2 -2
  30. package/public/css/forms.css +1 -1
  31. package/public/css/site.css +1 -1
  32. package/public/js/collection-browser.js +2 -2
  33. package/public/js/collection-range.mjs +1 -0
  34. package/public/js/form-files.mjs +1 -0
  35. package/public/js/form-logic-engine.js +1 -1
  36. package/public/js/forms.js +2 -2
  37. package/public/js/site.js +1 -1
  38. package/server/routes/api/forms.js +211 -60
  39. package/server/routes/api/media.js +25 -2
  40. package/server/server.js +8 -2
  41. package/server/services/collections.js +3 -0
  42. package/server/services/email.js +15 -4
  43. package/server/services/forms.js +11 -3
  44. package/server/services/markdown.js +20 -9
  45. package/server/services/mediaTypes.js +22 -0
  46. package/server/templates/page.html +8 -6
package/admin/js/api.js CHANGED
@@ -1 +1 @@
1
- import{claimDrafts as $}from"./lib/draft-owner.js";import{loginHashFor as O}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 i=r=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...r?{Authorization:`Bearer ${r}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:i(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:i(n)})}catch{return}if(!s.ok){const r=await s.json().catch(()=>({error:"Request failed"})),d=new Error(r.error||r.message||`HTTP ${s.status}`);throw Array.isArray(r.validation)&&(d.validation=r.validation),d.status=s.status,d.body=r,d}return s.status===204?null:s.json()}async function b(e,o){const n=a(),i=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:i,body:o});if(!s.ok){const r=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(r.error||r.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,i)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n,...i&&Object.keys(i).length?{blocks:i}:{}})}),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(i=>i.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=>b("/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"}),policy:()=>t("/plugins/_policy",{method:"GET"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),i=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${i}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const r=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(r)?.[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:i=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),i&&s.append("confirmDowngrade","true"),s.append("file",e);const r=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:r?{Authorization:`Bearer ${r}`}:{},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 i=await n.blob(),s=URL.createObjectURL(i),r=document.createElement("a");r.href=s,r.download=`${e}.dmblock.json`,document.body.appendChild(r),r.click(),r.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 i=await n.json().catch(()=>({})),s=new Error(i.error||"Block already exists");throw s.code="CONFLICT",s.name=i.name,s}if(!n.ok){const i=await n.json().catch(()=>({}));throw new Error(i.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 i=await n.blob(),s=URL.createObjectURL(i),r=document.createElement("a");r.href=s,r.download=`${e}.dmcomponent.json`,document.body.appendChild(r),r.click(),r.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 i=await n.json().catch(()=>({})),s=new Error(i.error||"Component already exists");throw s.code="CONFLICT",s.name=i.name,s}if(!n.ok){const i=await n.json().catch(()=>({}));throw new Error(i.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&&($(n.id),S.set("auth_user",n))}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=O(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,P(),!0):!1}function P(){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
+ import{claimDrafts as $}from"./lib/draft-owner.js";import{loginHashFor as O}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 i=r=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...r?{Authorization:`Bearer ${r}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:i(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:i(n)})}catch{return}if(!s.ok){const r=await s.json().catch(()=>({error:"Request failed"})),d=new Error(r.error||r.message||`HTTP ${s.status}`);throw Array.isArray(r.validation)&&(d.validation=r.validation),d.status=s.status,d.body=r,d}return s.status===204?null:s.json()}async function b(e,o){const n=a(),i=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:i,body:o});if(!s.ok){const r=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(r.error||r.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,i)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n,...i&&Object.keys(i).length?{blocks:i}:{}})}),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(i=>i.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=>b("/media",e),limits:()=>t("/media/_limits",{method:"GET"}),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"}),policy:()=>t("/plugins/_policy",{method:"GET"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),i=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${i}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const r=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(r)?.[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:i=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),i&&s.append("confirmDowngrade","true"),s.append("file",e);const r=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:r?{Authorization:`Bearer ${r}`}:{},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 i=await n.blob(),s=URL.createObjectURL(i),r=document.createElement("a");r.href=s,r.download=`${e}.dmblock.json`,document.body.appendChild(r),r.click(),r.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 i=await n.json().catch(()=>({})),s=new Error(i.error||"Block already exists");throw s.code="CONFLICT",s.name=i.name,s}if(!n.ok){const i=await n.json().catch(()=>({}));throw new Error(i.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 i=await n.blob(),s=URL.createObjectURL(i),r=document.createElement("a");r.href=s,r.download=`${e}.dmcomponent.json`,document.body.appendChild(r),r.click(),r.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 i=await n.json().catch(()=>({})),s=new Error(i.error||"Component already exists");throw s.code="CONFLICT",s.name=i.name,s}if(!n.ok){const i=await n.json().catch(()=>({}));throw new Error(i.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&&($(n.id),S.set("auth_user",n))}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=O(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,P(),!0):!1}function P(){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,3 +1,3 @@
1
- export const FIELD_TYPES=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],OPTION_TYPES=new Set(["select","radio","checkbox-group"]),ADDON_TYPES=new Set(["string","email","tel","number","textarea","select","date","time","url"]);const f=/^[a-z0-9-]+$/i;export const EXTRA_TYPES={clear:new Set(["string","email","tel","url","textarea"]),stepper:new Set(["number"]),counter:new Set(["string","textarea"])};const m=24;function p(e){return e?typeof e=="string"?{icon:"",text:e}:{icon:e.icon||"",text:e.icon?"":String(e.text??"")}:{icon:"",text:""}}export function addonFromBoxes(e,r){const n=String(e??"").trim(),t=String(r??"").trim();return n&&f.test(n)?{icon:n}:t?{text:t.slice(0,m)}:null}export const OPERATIONS=["create","read","update","delete"],EXPORT_FORMATS=["csv","json"],FIELD_NAME_RE=/^[A-Za-z_][A-Za-z0-9_-]*$/;export function slugify(e){return String(e??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export const nameFromLabel=e=>slugify(e).replace(/-/g,"_"),typeLabel=e=>FIELD_TYPES.find(r=>r.value===e)?.label||e||"";export function optionsText(e){return(Array.isArray(e)?e:[]).map(r=>typeof r=="string"?`${r}: ${r}`:`${r?.value??""}: ${r?.label??r?.value??""}`).join(`
1
+ export const FIELD_TYPES=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"},{value:"file",label:"Files (uploaded with a form)"}],OPTION_TYPES=new Set(["select","radio","checkbox-group"]),ADDON_TYPES=new Set(["string","email","tel","number","textarea","select","date","time","url"]);const f=/^[a-z0-9-]+$/i;export const EXTRA_TYPES={clear:new Set(["string","email","tel","url","textarea"]),stepper:new Set(["number"]),counter:new Set(["string","textarea"])};const m=24;function p(e){return e?typeof e=="string"?{icon:"",text:e}:{icon:e.icon||"",text:e.icon?"":String(e.text??"")}:{icon:"",text:""}}export function addonFromBoxes(e,r){const n=String(e??"").trim(),t=String(r??"").trim();return n&&f.test(n)?{icon:n}:t?{text:t.slice(0,m)}:null}export const OPERATIONS=["create","read","update","delete"],EXPORT_FORMATS=["csv","json"],FIELD_NAME_RE=/^[A-Za-z_][A-Za-z0-9_-]*$/;export function slugify(e){return String(e??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export const nameFromLabel=e=>slugify(e).replace(/-/g,"_"),typeLabel=e=>FIELD_TYPES.find(r=>r.value===e)?.label||e||"";export function optionsText(e){return(Array.isArray(e)?e:[]).map(r=>typeof r=="string"?`${r}: ${r}`:`${r?.value??""}: ${r?.label??r?.value??""}`).join(`
2
2
  `)}export function parseOptions(e){return String(e??"").split(`
3
3
  `).filter(r=>r.trim()).map(r=>{const[n,...t]=r.split(":");return{value:n.trim(),label:t.join(":").trim()||n.trim()}})}const g=()=>({create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}}),b=e=>({enabled:!0,access:e||"",formats:["csv","json"]});export function roleLadder(e){return(Array.isArray(e)?e:e?.entries||[]).map(n=>({name:n?.data?.name,label:n?.data?.label||n?.data?.name,level:Number(n?.data?.level)})).filter(n=>typeof n.name=="string"&&n.name&&Number.isFinite(n.level)).sort((n,t)=>n.level-t.level||n.name.localeCompare(t.name))}export function accessOptions(e,r,{token:n=!0}={}){const t=[{value:"public",label:"Public - anyone"}];n&&t.push({value:"token",label:"API token"});for(const l of[...e||[]].reverse())t.push({value:l.name,label:l.level===0?`${l.label} only`:`${l.label} and above`});return typeof r=="string"&&r&&!t.some(l=>l.value===r)&&t.push(e?.length?{value:r,label:`${r} (role not found)`,missing:!0}:{value:r,label:r}),t}let x=0;export const fieldKey=()=>`f${Date.now().toString(36)}${(x++).toString(36)}`;export function formFromCollection(e=null,{project:r="",defaultRole:n=""}={}){const t=e||{},l={...g(),...t.api||{}},o={...b(n),...t.export||{}},a={title:String(t.title??""),slug:String(t.slug??""),description:String(t.description??""),layout:t.layout==="grid"?"grid":"stacked",columns:String(Number.parseInt(t.columns,10)||2),project:String(t.meta?.project??r??""),bundled:t.bundled===!0,storageAdapter:t.storage?.adapter==="mongodb"?"mongodb":"file",storageConnection:String(t.storage?.connection||"default"),fields:(Array.isArray(t.fields)?t.fields:[]).map(i=>({...i,_key:fieldKey()})),readFields:Array.isArray(l.read?.fields)?l.read.fields.join(", "):"",exportEnabled:o.enabled!==!1,exportAccess:typeof o.access=="string"&&o.access?o.access:String(n||""),exportCsv:(Array.isArray(o.formats)?o.formats:EXPORT_FORMATS).includes("csv"),exportJson:(Array.isArray(o.formats)?o.formats:EXPORT_FORMATS).includes("json")};for(const i of OPERATIONS){a[`${i}Enabled`]=l[i]?.enabled===!0;const s=l[i]?.access;a[`${i}Access`]=typeof s=="string"&&s?s:"admin"}return a}export function cleanField(e){const{_key:r,...n}=e||{};return n}export function payloadFromForm(e,{isNew:r=!1,meta:n={}}={}){const t=e||{},l={};for(const i of OPERATIONS)l[i]={enabled:t[`${i}Enabled`]===!0,access:t[`${i}Access`]||"admin"};const o=String(t.readFields??"").split(",").map(i=>i.trim()).filter(Boolean);o.length&&(l.read.fields=o);const a=Math.min(6,Math.max(1,Number.parseInt(t.columns,10)||2));return{title:String(t.title??"").trim(),...r&&{slug:slugify(t.slug||t.title)},description:String(t.description??"").trim(),layout:t.layout==="grid"?"grid":"stacked",columns:a,fields:(t.fields||[]).map(cleanField),api:l,export:{enabled:t.exportEnabled!==!1,...t.exportAccess&&{access:t.exportAccess},formats:EXPORT_FORMATS.filter(i=>i==="csv"?t.exportCsv:t.exportJson)},storage:t.storageAdapter==="mongodb"?{adapter:"mongodb",connection:t.storageConnection||"default"}:{adapter:"file"},meta:{...n||{},project:String(t.project??"")||null},bundled:t.bundled===!0}}export function snapshot(e){const{fields:r=[],...n}=e||{};return JSON.stringify({...n,fields:r.map(cleanField)})}export function checkForm(e,{isNew:r=!1,taken:n=[],original:t={}}={}){const l=e||{},o={};if(String(l.title??"").trim()||(o.title="A collection needs a title."),r){const c=slugify(l.slug||l.title);c?n.includes(c)&&(o.slug=`"${c}" is already a collection.`):o.slug="The slug needs a letter or a number."}const a=Number.parseInt(l.columns,10);l.layout==="grid"&&!(a>=1&&a<=6)&&(o.columns="Between 1 and 6 columns.");const i={},s=new Map;return(l.fields||[]).forEach(c=>{const d=fieldProblem(c,t[c._key]);d&&(i[c._key]=d);const u=String(c.name??"").trim();u&&(s.has(u)?i[c._key]=i[c._key]||`"${u}" is used by another field.`:s.set(u,c._key))}),Object.keys(i).length&&(o.fields=i),l.exportEnabled&&!l.exportCsv&&!l.exportJson&&(o.export="Pick at least one export format, or turn export off."),o}export function fieldProblem(e,r){const n=String(e?.name??"").trim();return n?n!==r&&!FIELD_NAME_RE.test(n)?"Names start with a letter or _, then letters, digits, _ or -.":OPTION_TYPES.has(e.type)&&!(Array.isArray(e.options)&&e.options.length)?`A ${typeLabel(e.type).toLowerCase()} needs at least one option.`:"":"A field needs a name - it is what each entry is keyed by."}export const errorCount=e=>Object.keys(e||{}).filter(r=>r!=="fields").length+Object.keys(e?.fields||{}).length;export function fieldRow(e,r={},{grid:n=!1}={}){const t=[e.name||"(no name)",typeLabel(e.type)];return OPTION_TYPES.has(e.type)&&t.push(`${(e.options||[]).length} options`),n&&t.push(e.fullWidth?"full width":`span ${e.span>1?e.span:1}`),{key:e._key,label:e.label||"(Untitled field)",meta:t.join(" \xB7 "),required:e.required===!0,hidden:e.type==="hidden",problem:r[e._key]||"",rowCls:r[e._key]?"is-bad":""}}export function moveField(e,r,n){const t=[...e];if(r<0||r>=t.length||n<0||n>=t.length||r===n)return t;const[l]=t.splice(r,1);return t.splice(n,0,l),t}export function duplicateField(e,r){const n=e[r];if(!n)return[...e];const t=new Set(e.map(s=>s.name)),l=`${n.name||"field"}_copy`;let o=l;for(let s=2;t.has(o);s++)o=`${l}_${s}`;const a={...JSON.parse(JSON.stringify(cleanField(n))),name:o,label:`${n.label||"Field"} (copy)`,_key:fieldKey()};delete a.id;const i=[...e];return i.splice(r+1,0,a),i}export const newField=()=>({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null,_key:fieldKey()});export function panelForm(e){return{label:String(e.label??""),name:String(e.name??""),type:e.type||"string",required:e.required===!0,placeholder:String(e.placeholder??""),helper:String(e.helper??""),options:optionsText(e.options),span:String(e.span>1?e.span:1),fullWidth:e.fullWidth===!0,prefixIcon:p(e.prefix).icon,prefixText:p(e.prefix).text,suffixIcon:p(e.suffix).icon,suffixText:p(e.suffix).text,clear:e.clear===!0,stepper:e.stepper===!0,counter:e.counter===!0}}export function fieldFromPanel(e,r){const n={...e,label:String(r.label??"").trim(),name:String(r.name??"").trim(),type:r.type||"string",required:r.required===!0,placeholder:String(r.placeholder??"").trim(),helper:String(r.helper??"").trim()};OPTION_TYPES.has(n.type)&&(n.options=parseOptions(r.options));for(const[t,l]of Object.entries(EXTRA_TYPES)){if(!l.has(n.type)){delete n[t];continue}t in r&&(r[t]===!0?n[t]=!0:delete n[t])}for(const t of["prefix","suffix"]){const l=`${t}Icon`in r||`${t}Text`in r;if(!ADDON_TYPES.has(n.type)){delete n[t];continue}if(!l)continue;const o=addonFromBoxes(r[`${t}Icon`],r[`${t}Text`]);o?n[t]=o:delete n[t]}if(r.fullWidth)n.fullWidth=!0,delete n.span;else{delete n.fullWidth;const t=Number.parseInt(r.span,10);t>1?n.span=Math.min(6,t):delete n.span}return n}export function storageChange(e,r){const n=e||"file",t=r||"file";return n===t?"":`${n} \u2192 ${t}`}export function draftOf(e){const{fields:r=[],...n}=e||{};return{...n,fields:r.map(cleanField)}}export function formFromDraft(e,r){const n=e||{};return{...r,...n,fields:(Array.isArray(n.fields)?n.fields:r.fields).map(t=>({...cleanField(t),_key:fieldKey()}))}}
@@ -1 +1 @@
1
- import{fmtDate as b,fmtDateTime as d}from"./dates.js";export const OP_LABELS={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"},MAX_COLUMNS=5,PAGE_SIZE=500,SHOW_STEP=100;export function matchesFilter(e,t){const r=e?.data?.[t.field],n=String(r??"");switch(t.op){case"eq":return n===String(t.value);case"ne":return n!==String(t.value);case"contains":return n.toLowerCase().includes(String(t.value).toLowerCase());case"gt":case"lt":{const i=s=>String(s).trim()!==""&&Number.isFinite(Number(s))?Number(s):NaN,o=i(n),c=i(t.value);if(!Number.isNaN(o)&&!Number.isNaN(c))return t.op==="gt"?o>c:o<c;const a=Date.parse(n),u=Date.parse(t.value);return!Number.isNaN(a)&&!Number.isNaN(u)?t.op==="gt"?a>u:a<u:t.op==="gt"?n>String(t.value):n<String(t.value)}default:return!0}}export function valueText(e){return e==null?"":typeof e=="boolean"?e?"Yes":"No":Array.isArray(e)?e.map(t=>t&&typeof t=="object"?JSON.stringify(t):String(t)).join(", "):typeof e=="object"?JSON.stringify(e):String(e)}export function shownValue(e,t){return t==null||t===""?t:e?.type==="date"?b(t):e?.type==="datetime"?d(t):t}export function cellText(e,t=60){const r=valueText(e).replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r}export function arrangeEntries(e,{filters:t=[],query:r="",sort:n={key:"created",dir:"desc"}}={}){const i=String(r||"").toLowerCase().trim();let o=(e||[]).filter(s=>t.every(p=>matchesFilter(s,p)));i&&(o=o.filter(s=>Object.values(s?.data||{}).some(p=>valueText(p).toLowerCase().includes(i))));const c=n?.dir==="asc"?1:-1,a=n?.key||"created",u=s=>a==="created"?String(s?.meta?.createdAt||""):s?.data?.[a];return[...o].sort((s,p)=>{const l=u(s),m=u(p),f=typeof l=="number"?l:parseFloat(l),g=typeof m=="number"?m:parseFloat(m);return a!=="created"&&!Number.isNaN(f)&&!Number.isNaN(g)&&String(f)===String(l??"").trim()&&String(g)===String(m??"").trim()?(f-g)*c:valueText(l).localeCompare(valueText(m),void 0,{numeric:!0,sensitivity:"base"})*c})}export function filterChoices(e,t){const r=valueText(t);if(r==="")return[];const n=valueText(shownValue(e,t)),i=n.length>28?`${n.slice(0,27)}\u2026`:n,o=[{op:"eq",value:r,label:`= \u201C${i}\u201D`},{op:"ne",value:r,label:`\u2260 \u201C${i}\u201D`}];return["number","date","time","datetime"].includes(e?.type)&&o.push({op:"gt",value:r,label:`> \u201C${i}\u201D`},{op:"lt",value:r,label:`< \u201C${i}\u201D`}),o}export function filterText(e){const t=String(e.value),r=/^\d{4}-\d{2}-\d{2}$/.test(t)?b(t):/^\d{4}-\d{2}-\d{2}T\d/.test(t)?d(t):t;return`${e.label} ${OP_LABELS[e.op]||e.op} \u201C${r.length>24?`${r.slice(0,23)}\u2026`:r}\u201D`}export function typedValue(e,t){if(e?.type==="number"&&typeof t=="string"&&t.trim()!==""){const r=Number(t);if(Number.isFinite(r))return r}return t}export function isSecretField(e){return e?.type==="password"||/pass(word)?|secret|token|api[-_]?key|private|credential/i.test(String(e?.name||""))}export function draftable(e,t=[]){const r=new Set(t.filter(isSecretField).map(n=>n.name));return Object.fromEntries(Object.entries(e||{}).filter(([n])=>!r.has(n)))}export function snapshot(e){const t=Object.keys(e||{}).sort();return JSON.stringify(t.map(r=>[r,typeof e[r]=="boolean"?e[r]:valueText(e[r])]))}export const hasForm=e=>!["roles","user-profiles"].includes(e),canClear=e=>e!=="roles";export function countText(e,t,r){const n=c=>c.toLocaleString("en-GB"),i=c=>`entr${c===1?"y":"ies"}`,o=e===t?`${n(t)} ${i(t)}`:`Showing ${n(e)} of ${n(t)}`;return r>t?`${o} \xB7 ${n(t)} of ${n(r)} loaded`:o}
1
+ import{fmtDate as y,fmtDateTime as d}from"./dates.js";export const OP_LABELS={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"},MAX_COLUMNS=5,PAGE_SIZE=500,SHOW_STEP=100;export function matchesFilter(t,e){const r=t?.data?.[e.field],n=String(r??"");switch(e.op){case"eq":return n===String(e.value);case"ne":return n!==String(e.value);case"contains":return n.toLowerCase().includes(String(e.value).toLowerCase());case"gt":case"lt":{const o=s=>String(s).trim()!==""&&Number.isFinite(Number(s))?Number(s):NaN,i=o(n),c=o(e.value);if(!Number.isNaN(i)&&!Number.isNaN(c))return e.op==="gt"?i>c:i<c;const a=Date.parse(n),u=Date.parse(e.value);return!Number.isNaN(a)&&!Number.isNaN(u)?e.op==="gt"?a>u:a<u:e.op==="gt"?n>String(e.value):n<String(e.value)}default:return!0}}export const isFileRef=t=>!!t&&typeof t=="object"&&!Array.isArray(t)&&typeof t.url=="string"&&(typeof t.name=="string"||t.name===void 0),fileRefsOf=t=>(Array.isArray(t)?t:[t]).filter(isFileRef),isFileValue=t=>{const e=fileRefsOf(t);return e.length>0&&e.length===(Array.isArray(t)?t.length:1)};const b=t=>t.name||t.url.split("/").pop()||"file";export const safeFileHref=t=>/^(https?:\/\/|\/(?!\/))/i.test(String(t||""))?String(t):"";export function valueText(t){return t==null?"":typeof t=="boolean"?t?"Yes":"No":isFileRef(t)?b(t):Array.isArray(t)?t.map(e=>isFileRef(e)?b(e):e&&typeof e=="object"?JSON.stringify(e):String(e)).join(", "):typeof t=="object"?JSON.stringify(t):String(t)}export function shownValue(t,e){return e==null||e===""?e:t?.type==="date"?y(e):t?.type==="datetime"?d(e):e}export function cellText(t,e=60){const r=valueText(t).replace(/\s+/g," ").trim();return r.length>e?`${r.slice(0,e-1)}\u2026`:r}export function arrangeEntries(t,{filters:e=[],query:r="",sort:n={key:"created",dir:"desc"}}={}){const o=String(r||"").toLowerCase().trim();let i=(t||[]).filter(s=>e.every(p=>matchesFilter(s,p)));o&&(i=i.filter(s=>Object.values(s?.data||{}).some(p=>valueText(p).toLowerCase().includes(o))));const c=n?.dir==="asc"?1:-1,a=n?.key||"created",u=s=>a==="created"?String(s?.meta?.createdAt||""):s?.data?.[a];return[...i].sort((s,p)=>{const l=u(s),f=u(p),m=typeof l=="number"?l:parseFloat(l),g=typeof f=="number"?f:parseFloat(f);return a!=="created"&&!Number.isNaN(m)&&!Number.isNaN(g)&&String(m)===String(l??"").trim()&&String(g)===String(f??"").trim()?(m-g)*c:valueText(l).localeCompare(valueText(f),void 0,{numeric:!0,sensitivity:"base"})*c})}export function filterChoices(t,e){const r=valueText(e);if(r==="")return[];const n=valueText(shownValue(t,e)),o=n.length>28?`${n.slice(0,27)}\u2026`:n,i=[{op:"eq",value:r,label:`= \u201C${o}\u201D`},{op:"ne",value:r,label:`\u2260 \u201C${o}\u201D`}];return["number","date","time","datetime"].includes(t?.type)&&i.push({op:"gt",value:r,label:`> \u201C${o}\u201D`},{op:"lt",value:r,label:`< \u201C${o}\u201D`}),i}export function filterText(t){const e=String(t.value),r=/^\d{4}-\d{2}-\d{2}$/.test(e)?y(e):/^\d{4}-\d{2}-\d{2}T\d/.test(e)?d(e):e;return`${t.label} ${OP_LABELS[t.op]||t.op} \u201C${r.length>24?`${r.slice(0,23)}\u2026`:r}\u201D`}export function typedValue(t,e){if(t?.type==="number"&&typeof e=="string"&&e.trim()!==""){const r=Number(e);if(Number.isFinite(r))return r}return e}export function isSecretField(t){return t?.type==="password"||/pass(word)?|secret|token|api[-_]?key|private|credential/i.test(String(t?.name||""))}export function draftable(t,e=[]){const r=new Set(e.filter(isSecretField).map(n=>n.name));return Object.fromEntries(Object.entries(t||{}).filter(([n])=>!r.has(n)))}export function snapshot(t){const e=Object.keys(t||{}).sort();return JSON.stringify(e.map(r=>[r,typeof t[r]=="boolean"?t[r]:valueText(t[r])]))}export const hasForm=t=>!["roles","user-profiles"].includes(t),canClear=t=>t!=="roles";export function countText(t,e,r){const n=c=>c.toLocaleString("en-GB"),o=c=>`entr${c===1?"y":"ies"}`,i=t===e?`${n(e)} ${o(e)}`:`Showing ${n(t)} of ${n(e)}`;return r>e?`${i} \xB7 ${n(e)} of ${n(r)} loaded`:i}
@@ -1,2 +1,2 @@
1
- export const FILTERS=["inbox","new","spam"],WEEK_MS=6048e5;const g=new Set(["page-break","heading","paragraph","html","divider","spacer","banner"]);export function answerFields(e){return(e?.fields||[]).filter(t=>t&&t.name&&!g.has(t.type)).map(t=>({name:String(t.name),label:String(t.label||t.name),type:String(t.type||"text")}))}export function answerText(e){return e==null?"":Array.isArray(e)?e.map(answerText).filter(Boolean).join(", "):typeof e=="boolean"?e?"Yes":"No":typeof e=="object"?JSON.stringify(e):String(e).trim()}export const isSpam=e=>e?.data?.spam===!0,createdAt=e=>e?.meta?.createdAt||"",isNew=(e,t=Date.now())=>{const n=Date.parse(createdAt(e));return Number.isFinite(n)&&t-n<6048e5};const u=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;export function emailOf(e,t){const n=e?.data||{},r=t.find(a=>a.type==="email"&&u.test(answerText(n[a.name])));if(r)return answerText(n[r.name]);const i=t.find(a=>/e-?mail/i.test(a.name)&&u.test(answerText(n[a.name])));if(i)return answerText(n[i.name]);const c=t.find(a=>u.test(answerText(n[a.name])));return c?answerText(n[c.name]):""}const d=(e,t)=>e.length>t?`${e.slice(0,t-1)}\u2026`:e;export function submissionRow(e,t,n=Date.now()){const r=e?.data||{},i=t.filter(m=>answerText(r[m.name])!==""),c=i.find(m=>/(^|_|-)(full[-_ ]?)?name$|^name/i.test(m.name)&&m.type!=="email"),a=emailOf(e,t),o=c||i.find(m=>answerText(r[m.name])===a&&a)||i[0],p=o?d(answerText(r[o.name]),80):"Empty submission",s=i.filter(m=>m!==o).slice(0,3).map(m=>`${m.label}: ${d(answerText(r[m.name]),60)}`),l=isSpam(e),f=isNew(e,n);return{id:String(e?.id??""),title:p,meta:s.join(" \xB7 ")||"No other answers",email:a,at:createdAt(e),spam:l,fresh:f&&!l,rowCls:[l&&"is-spam",f&&!l&&"is-new"].filter(Boolean).join(" ")}}export function arrangeSubmissions(e,{filter:t="inbox",query:n="",from:r="",to:i="",now:c=Date.now()}={}){const a=String(n).trim().toLowerCase(),o=r?Date.parse(`${r}T00:00:00`):null,p=i?Date.parse(`${i}T23:59:59.999`):null;return[...e||[]].filter(s=>t==="spam"?isSpam(s):!isSpam(s)).filter(s=>t!=="new"||isNew(s,c)).filter(s=>!a||Object.values(s?.data||{}).some(l=>answerText(l).toLowerCase().includes(a))).filter(s=>{if(o===null&&p===null)return!0;const l=Date.parse(createdAt(s));return Number.isFinite(l)?(o===null||l>=o)&&(p===null||l<=p):!1}).sort((s,l)=>String(createdAt(l)).localeCompare(String(createdAt(s))))}export function submissionCounts(e,t=Date.now()){const n=e||[],r=n.filter(isSpam).length;return{inbox:n.length-r,new:n.filter(i=>!isSpam(i)&&isNew(i,t)).length,spam:r,all:n.length}}export function submissionText(e,t,{formTitle:n="",when:r=""}={}){const i=e?.data||{},c=t.filter(o=>answerText(i[o.name])!=="").map(o=>`${o.label}: ${answerText(i[o.name])}`),a=[n&&`${n} - submission`,r&&`Submitted ${r}`].filter(Boolean);return[...a,...a.length?[""]:[],...c].join(`
1
+ export const FILTERS=["inbox","new","spam"],WEEK_MS=6048e5;const g=new Set(["page-break","heading","paragraph","html","divider","spacer","banner"]);export function answerFields(e){return(e?.fields||[]).filter(t=>t&&t.name&&!g.has(t.type)).map(t=>({name:String(t.name),label:String(t.label||t.name),type:String(t.type||"text")}))}export const isFileRef=e=>!!e&&typeof e=="object"&&!Array.isArray(e)&&typeof e.url=="string"&&(typeof e.name=="string"||e.name===void 0),fileRefsOf=e=>(Array.isArray(e)?e:[e]).filter(isFileRef),safeFileHref=e=>/^(https?:\/\/|\/(?!\/))/i.test(String(e||""))?String(e):"";export function answerText(e){return e==null?"":Array.isArray(e)?e.map(answerText).filter(Boolean).join(", "):typeof e=="boolean"?e?"Yes":"No":isFileRef(e)?e.name||e.url.split("/").pop()||"file":typeof e=="object"?JSON.stringify(e):String(e).trim()}export const isSpam=e=>e?.data?.spam===!0,createdAt=e=>e?.meta?.createdAt||"",isNew=(e,t=Date.now())=>{const n=Date.parse(createdAt(e));return Number.isFinite(n)&&t-n<6048e5};const u=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;export function emailOf(e,t){const n=e?.data||{},a=t.find(r=>r.type==="email"&&u.test(answerText(n[r.name])));if(a)return answerText(n[a.name]);const i=t.find(r=>/e-?mail/i.test(r.name)&&u.test(answerText(n[r.name])));if(i)return answerText(n[i.name]);const c=t.find(r=>u.test(answerText(n[r.name])));return c?answerText(n[c.name]):""}const d=(e,t)=>e.length>t?`${e.slice(0,t-1)}\u2026`:e;export function submissionRow(e,t,n=Date.now()){const a=e?.data||{},i=t.filter(m=>answerText(a[m.name])!==""),c=i.find(m=>/(^|_|-)(full[-_ ]?)?name$|^name/i.test(m.name)&&m.type!=="email"),r=emailOf(e,t),o=c||i.find(m=>answerText(a[m.name])===r&&r)||i[0],p=o?d(answerText(a[o.name]),80):"Empty submission",s=i.filter(m=>m!==o).slice(0,3).map(m=>`${m.label}: ${d(answerText(a[m.name]),60)}`),l=isSpam(e),f=isNew(e,n);return{id:String(e?.id??""),title:p,meta:s.join(" \xB7 ")||"No other answers",email:r,at:createdAt(e),spam:l,fresh:f&&!l,rowCls:[l&&"is-spam",f&&!l&&"is-new"].filter(Boolean).join(" ")}}export function arrangeSubmissions(e,{filter:t="inbox",query:n="",from:a="",to:i="",now:c=Date.now()}={}){const r=String(n).trim().toLowerCase(),o=a?Date.parse(`${a}T00:00:00`):null,p=i?Date.parse(`${i}T23:59:59.999`):null;return[...e||[]].filter(s=>t==="spam"?isSpam(s):!isSpam(s)).filter(s=>t!=="new"||isNew(s,c)).filter(s=>!r||Object.values(s?.data||{}).some(l=>answerText(l).toLowerCase().includes(r))).filter(s=>{if(o===null&&p===null)return!0;const l=Date.parse(createdAt(s));return Number.isFinite(l)?(o===null||l>=o)&&(p===null||l<=p):!1}).sort((s,l)=>String(createdAt(l)).localeCompare(String(createdAt(s))))}export function submissionCounts(e,t=Date.now()){const n=e||[],a=n.filter(isSpam).length;return{inbox:n.length-a,new:n.filter(i=>!isSpam(i)&&isNew(i,t)).length,spam:a,all:n.length}}export function submissionText(e,t,{formTitle:n="",when:a=""}={}){const i=e?.data||{},c=t.filter(o=>answerText(i[o.name])!=="").map(o=>`${o.label}: ${answerText(i[o.name])}`),r=[n&&`${n} - submission`,a&&`Submitted ${a}`].filter(Boolean);return[...r,...r.length?[""]:[],...c].join(`
2
2
  `)}
@@ -1,23 +1,14 @@
1
- import{assignHotkeys as q,chipEntries as D,CHIP_DELAY_MS as H,createSequence as S,frequentKeys as P,hotkeyLabel as g,isPaletteKey as A,isTypingTarget as T,matchTools as M,paletteKeyLabel as G,SEQUENCE_MS as j}from"./hotkeys.js";let d={tools:[],byKey:new Map,byUrl:new Map,conflicts:[],frequent:[]},v=!1,u=null;const E=()=>/Mac|iPhone|iPad/.test(navigator.userAgentData?.platform||navigator.platform||navigator.userAgent||"");function p(e){return String(e??"").replace(/[&<>"']/g,o=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[o])}function r(...e){return e.map(o=>`<kbd>${p(String(o).toUpperCase())}</kbd>`).join("")}export function setHotkeyTree(e){const o=q(e);d={...o,frequent:P(e)},o.conflicts.length&&console.info("[hotkeys] Letters already taken:",o.conflicts.map(i=>`${i.text} wanted G ${i.key.toUpperCase()} (${i.takenBy} has it)`).join("; "))}export function hotkeyFor(e){return d.byUrl.get(e)||null}export function frequentNumberFor(e){return d.frequent.find(o=>o.url===e)?.n||null}export function currentHotkeys(){return d}export function hotkeyTitle(e,o,{inFrequent:i=!1}={}){const t=[],a=i?frequentNumberFor(e):null;a&&t.push(g(a));const n=hotkeyFor(e);return n&&t.push(g(n)),t.length?`${o} - ${t.join(" or ")}`:""}function x(e){e&&location.hash!==e&&(location.hash=e)}const f=S(()=>d),K=()=>d.tools.length>0&&!!document.querySelector(".dm-admin-sidebar");let h=null,k=null,$=null;function m(){clearTimeout(k),clearTimeout($),k=$=null,h?.remove(),h=null}function N(){const{frequent:e,letters:o,more:i}=D(d),t=(a,n)=>`<span class="dm-hk-chip-item">${r(a)}<span>${p(n)}</span></span>`;h=document.createElement("div"),h.className="dm-hk-chip",h.setAttribute("role","status"),h.setAttribute("aria-live","polite"),h.innerHTML=`<span class="dm-hk-chip-lead">${r("G")} then</span>`+e.map(a=>t(a.n,a.text)).join("")+(e.length&&o.length?'<span class="dm-hk-chip-sep" aria-hidden="true"></span>':"")+o.map(a=>t(a.key,a.text)).join("")+`<span class="dm-hk-chip-more">${i?`+${i} more \xB7 `:""}${r("?")} for all</span>`,document.body.appendChild(h)}function U(){m(),k=setTimeout(()=>{k=null,f.pending(Date.now())&&N()},H),$=setTimeout(m,j)}function F(e){if(!K()){m();return}const o=(e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey,i=String(e.key||"").toLowerCase();if(u&&o&&(i==="k"||i==="d")){e.preventDefault(),e.stopImmediatePropagation(),u.close();return}if(u)return;if(A(e)){m(),f.reset(),e.preventDefault(),e.stopImmediatePropagation(),C();return}if(e.ctrlKey||e.metaKey||e.altKey||e.shiftKey||e.isComposing){f.reset(),m();return}if(T(e.target)){f.reset(),m();return}const t=f.press(e.key,Date.now());if(t?.type==="start"){U();return}m(),t&&(e.preventDefault(),e.stopImmediatePropagation(),t.type==="go"&&x(t.url))}function z(e){e.key!=="?"||e.defaultPrevented||u||!K()||e.ctrlKey||e.metaKey||e.altKey||T(e.target)||(e.preventDefault(),b())}export function installHotkeys(){v||typeof window>"u"||(v=!0,window.addEventListener("keydown",F,!0),window.addEventListener("keydown",z,!1))}export function showPalette(){u||C()}export function showKeyList(){u||b()}function L(e,o,i){const t=document.createElement("div");t.className="dm-hk-backdrop",t.innerHTML=`<div class="dm-hk-panel ${e}" role="dialog" aria-modal="true" aria-label="${p(o)}">${i}</div>`;const a=document.activeElement,n=()=>{!u||u.el!==t||(u=null,t.remove(),a&&typeof a.focus=="function"&&a.isConnected&&a.focus())};if(t.addEventListener("mousedown",l=>{l.target===t&&n()}),t.addEventListener("keydown",l=>{l.key==="Escape"&&(l.preventDefault(),l.stopPropagation(),n())}),document.body.appendChild(t),u={el:t,close:n},typeof I<"u"&&I.scan)try{I.scan(t)}catch{}return{el:t.firstElementChild,close:n}}function C(){const e=E(),{el:o,close:i}=L("dm-hk-palette","Go to a Tool",`
2
- <div class="dm-hk-search"><span data-icon="search" data-icon-size="16"></span>
3
- <input type="text" placeholder="Go to a Tool\u2026" aria-label="Tool name" autocomplete="off" spellcheck="false"></div>
4
- <ul class="dm-hk-list" role="listbox" aria-label="Tools"></ul>
5
- <div class="dm-hk-foot">${r("\u2191","\u2193")} move \xB7 ${r("Enter")} open \xB7 ${r("Esc")} close \xB7 ${r("?")} every hotkey
6
- <span class="dm-hk-foot-end">${p(G(e))}</span></div>`),t=o.querySelector("input"),a=o.querySelector(".dm-hk-list");let n=[],l=0;const y=()=>{if(n=M(d.tools,t.value),l=Math.min(l,Math.max(n.length-1,0)),a.innerHTML=n.length?n.map((s,c)=>`<li role="option" class="dm-hk-item${c===l?" is-active":""}" aria-selected="${c===l}" data-at="${c}">
7
- <span class="dm-hk-icon">${s.icon?`<span data-icon="${p(s.icon)}" data-icon-size="16"></span>`:""}</span>
8
- <span class="dm-hk-name">${p(s.text)}${s.path.length?`<small>${p(s.path.join(" \u203A "))}</small>`:""}</span>
9
- ${s.key?`<span class="dm-hk-keys" title="${p(g(s.key))}">${r("G",s.key)}</span>`:""}
10
- </li>`).join(""):'<li class="dm-hk-empty">No Tool by that name.</li>',typeof I<"u"&&I.scan)try{I.scan(a)}catch{}a.querySelector(".is-active")?.scrollIntoView({block:"nearest"})},w=s=>{const c=n[s];c&&(i(),x(c.url))};t.addEventListener("input",()=>{l=0,y()}),t.addEventListener("keydown",s=>{if(s.key==="ArrowDown"||s.key==="ArrowUp"){if(s.preventDefault(),!n.length)return;l=(l+(s.key==="ArrowDown"?1:-1)+n.length)%n.length,y()}else s.key==="Enter"?(s.preventDefault(),w(l)):s.key==="?"&&!t.value&&(s.preventDefault(),i(),b())}),a.addEventListener("mousemove",s=>{const c=s.target.closest("[data-at]");c&&Number(c.dataset.at)!==l&&(l=Number(c.dataset.at),y())}),a.addEventListener("click",s=>{const c=s.target.closest("[data-at]");c&&w(Number(c.dataset.at))}),y(),t.focus()}function b(){const e=E(),o=d.tools.filter(n=>n.key).sort((n,l)=>n.text.localeCompare(l.text)),i=(n,l)=>`<li><span class="dm-hk-keys">${n}</span><span>${p(l)}</span></li>`,{el:t,close:a}=L("dm-hk-keylist","Hotkeys",`
1
+ import{api as g,getUser as P}from"../api.js";import{assignHotkeys as D,chipEntries as H,CHIP_DELAY_MS as v,createSequence as S,frequentKeys as N,hotkeyLabel as T,isPaletteKey as A,isTypingTarget as K,SEQUENCE_MS as G}from"./hotkeys.js";import{paletteCommands as M,recentKeyFor as j}from"./palette-commands.js";let l={tools:[],byKey:new Map,byUrl:new Map,conflicts:[],frequent:[]},C=!1,r=null;const F=()=>/Mac|iPhone|iPad/.test(navigator.userAgentData?.platform||navigator.platform||navigator.userAgent||"");function h(e){return String(e??"").replace(/[&<>"']/g,o=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[o])}function c(...e){return e.map(o=>`<kbd>${h(String(o).toUpperCase())}</kbd>`).join("")}export function setHotkeyTree(e){const o=D(e);l={...o,frequent:N(e)},s&&s.setCommands(w()),o.conflicts.length&&console.info("[hotkeys] Letters already taken:",o.conflicts.map(t=>`${t.text} wanted G ${t.key.toUpperCase()} (${t.takenBy} has it)`).join("; "))}export function hotkeyFor(e){return l.byUrl.get(e)||null}export function frequentNumberFor(e){return l.frequent.find(o=>o.url===e)?.n||null}export function currentHotkeys(){return l}export function hotkeyTitle(e,o,{inFrequent:t=!1}={}){const n=[],i=t?frequentNumberFor(e):null;i&&n.push(T(i));const a=hotkeyFor(e);return a&&n.push(T(a)),n.length?`${o} - ${n.join(" or ")}`:""}function x(e){e&&location.hash!==e&&(location.hash=e)}const f=S(()=>l),$=()=>l.tools.length>0&&!!document.querySelector(".dm-admin-sidebar");let p=null,y=null,b=null;function d(){clearTimeout(y),clearTimeout(b),y=b=null,p?.remove(),p=null}function U(){const{frequent:e,letters:o,more:t}=H(l),n=(i,a)=>`<span class="dm-hk-chip-item">${c(i)}<span>${h(a)}</span></span>`;p=document.createElement("div"),p.className="dm-hk-chip",p.setAttribute("role","status"),p.setAttribute("aria-live","polite"),p.innerHTML=`<span class="dm-hk-chip-lead">${c("G")} then</span>`+e.map(i=>n(i.n,i.text)).join("")+(e.length&&o.length?'<span class="dm-hk-chip-sep" aria-hidden="true"></span>':"")+o.map(i=>n(i.key,i.text)).join("")+`<span class="dm-hk-chip-more">${t?`+${t} more \xB7 `:""}${c("?")} for all</span>`,document.body.appendChild(p)}function O(){d(),y=setTimeout(()=>{y=null,f.pending(Date.now())&&U()},v),b=setTimeout(d,G)}function _(e){if(!$()){d(),m()&&s.close();return}const o=(e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey,t=String(e.key||"").toLowerCase();if((r||m())&&o&&(t==="k"||t==="d")){e.preventDefault(),e.stopImmediatePropagation(),r?r.close():s.close();return}if(r||m())return;if(A(e)){d(),f.reset(),e.preventDefault(),e.stopImmediatePropagation(),q();return}if(e.ctrlKey||e.metaKey||e.altKey||e.shiftKey||e.isComposing){f.reset(),d();return}if(K(e.target)){f.reset(),d();return}const n=f.press(e.key,Date.now());if(n?.type==="start"){O();return}d(),n&&(e.preventDefault(),e.stopImmediatePropagation(),n.type==="go"&&x(n.url))}function B(e){e.key!=="?"||e.defaultPrevented||r||m()||!$()||e.ctrlKey||e.metaKey||e.altKey||K(e.target)||(e.preventDefault(),k())}export function installHotkeys(){C||typeof window>"u"||(C=!0,window.addEventListener("keydown",_,!0),window.addEventListener("keydown",B,!1))}export function showPalette(){!r&&!m()&&q()}export function showKeyList(){!r&&!m()&&k()}function R(e,o,t){const n=document.createElement("div");n.className="dm-hk-backdrop",n.innerHTML=`<div class="dm-hk-panel ${e}" role="dialog" aria-modal="true" aria-label="${h(o)}">${t}</div>`;const i=document.activeElement,a=()=>{!r||r.el!==n||(r=null,n.remove(),i&&typeof i.focus=="function"&&i.isConnected&&i.focus())};if(n.addEventListener("mousedown",u=>{u.target===n&&a()}),n.addEventListener("keydown",u=>{u.key==="Escape"&&(u.preventDefault(),u.stopPropagation(),a())}),document.body.appendChild(n),r={el:n,close:a},typeof I<"u"&&I.scan)try{I.scan(n)}catch{}return{el:n.firstElementChild,close:a}}let s=null,L=null;const z={pages:()=>g.pages.list(),collections:()=>g.collections.list(),forms:()=>g.forms.list()},w=()=>M(l.tools,{load:z,showKeys:()=>k()});function Q(e,{newTab:o}={}){if(o){window.open(e,"_blank","noopener");return}let t;try{t=new URL(e,location.href)}catch{return}t.origin===location.origin&&t.pathname===location.pathname&&t.hash?x(t.hash):location.assign(t.href)}function Y(){const e=j(P());if(s&&L===e)return s;s?.destroy(),L=e,s=E.commandPalette({commands:w(),hotkey:!1,label:"Go to a Tool",placeholder:"Go to a Tool, or find a page\u2026",recentKey:e,recentLimit:5,maxResults:500,emptyText:"Nothing by that name.",errorText:"Could not load that list. Try again.",navigate:Q,className:"dm-hk-palette"}),s.element.addEventListener("keydown",t=>{t.key!=="?"||t.ctrlKey||t.metaKey||t.altKey||s.query()!==""||(t.preventDefault(),t.stopPropagation(),s.close(),k())},!0);const o=s.element.querySelector(".dm-command-palette-footer");if(o){const t=document.createElement("span");t.className="dm-command-palette-hint";const n=document.createElement("kbd");n.textContent="?",t.append(n,document.createTextNode(" every hotkey")),o.appendChild(t)}return s}function J(){const e=typeof E<"u"&&E.focusTrap?.top?E.focusTrap.top():null;return e?!(s&&s.element&&e.element===s.element):!1}const m=()=>!!s?.isOpen();function q(){if(J())return;const e=Y();e.setCommands(w()),e.open()}function k(){const e=F(),o=l.tools.filter(a=>a.key).sort((a,u)=>a.text.localeCompare(u.text)),t=(a,u)=>`<li><span class="dm-hk-keys">${a}</span><span>${h(u)}</span></li>`,{el:n,close:i}=R("dm-hk-keylist","Hotkeys",`
11
2
  <div class="dm-hk-head"><strong>Hotkeys</strong>
12
3
  <button type="button" class="dm-hk-close" aria-label="Close"><span data-icon="x" data-icon-size="16"></span></button></div>
13
4
  <div class="dm-hk-cols">
14
5
  <section><h4>Go to a Tool</h4><p class="dm-hk-note">Press G, let go, then the letter.</p>
15
- <ul>${o.map(n=>i(r("G",n.key),n.text)).join("")||'<li class="dm-hk-empty">No Tools with a letter yet.</li>'}</ul></section>
6
+ <ul>${o.map(a=>t(c("G",a.key),a.text)).join("")||'<li class="dm-hk-empty">No Tools with a letter yet.</li>'}</ul></section>
16
7
  <section><h4>Frequent</h4>
17
- <ul>${d.frequent.map(n=>i(r("G",n.n),n.text)).join("")||'<li class="dm-hk-empty">Right-click a Tool and choose Add to Frequent: the first nine get G then 1 to 9.</li>'}</ul>
8
+ <ul>${l.frequent.map(a=>t(c("G",a.n),a.text)).join("")||'<li class="dm-hk-empty">Right-click a Tool and choose Add to Frequent: the first nine get G then 1 to 9.</li>'}</ul>
18
9
  <h4>Anywhere</h4>
19
- <ul>${i(r(e?"\u2318":"Ctrl","K"),"Find any Tool by name")}
20
- ${i(r(e?"\u2318":"Ctrl","D"),"The same - whichever is handier")}
21
- ${i(r("?"),"This list")}</ul>
10
+ <ul>${t(c(e?"\u2318":"Ctrl","K"),"Find any Tool, page, collection or form by name")}
11
+ ${t(c(e?"\u2318":"Ctrl","D"),"The same - whichever is handier")}
12
+ ${t(c("?"),"This list")}</ul>
22
13
  <p class="dm-hk-note">Hotkeys wait while you are typing in a field. Hover over a Tool in the sidebar to see its key.</p></section>
23
- </div>`);t.querySelector(".dm-hk-close")?.addEventListener("click",a),t.setAttribute("tabindex","-1"),t.focus()}
14
+ </div>`);n.querySelector(".dm-hk-close")?.addEventListener("click",i),n.setAttribute("tabindex","-1"),n.focus()}
@@ -1 +1 @@
1
- export const CORE_HOTKEYS=Object.freeze({"#/":"d","#/pages":"p","#/media":"m","#/collections":"l","#/forms":"f","#/views":"v","#/users":"u","#/analytics":"a","#/notes":"n","#/todo":"t","#/contacts":"c","#/settings":","}),LEADER="g",SEQUENCE_MS=1500,FREQUENT_KEYS=9;export function normaliseHotkey(e){if(typeof e!="string")return null;const r=e.trim().toLowerCase();return r.length!==1||r===LEADER?null:/^[a-z,]$/.test(r)?r:null}export function hotkeyLabel(e){return`G then ${String(e).toUpperCase()}`}export function paletteKeyLabel(e=!1){return e?"\u2318K or \u2318D":"Ctrl+K or Ctrl+D"}export function sidebarTools(e){const r=[],o=new Set,n=(c,s)=>{for(const t of c||[]){if(!t||typeof t!="object"||t.hidden||t.frequent||t.type==="separator"||t.type==="spacer"||t.type==="theme-switcher")continue;const i=String(t.text||"").replace(/\s*\(\d+\)\s*$/,"").trim(),l=typeof t.url=="string"?t.url:"";if(l.startsWith("#/")&&i&&!o.has(l)){o.add(l);const u=normaliseHotkey(t.hotkey),f=CORE_HOTKEYS[l]||null;r.push({url:l,text:i,icon:t.icon||null,path:s,wants:f||u,core:!!f})}Array.isArray(t.items)&&t.items.length&&n(t.items,i?[...s,i]:s)}};return n(e,[]),r}export function assignHotkeys(e){const r=sidebarTools(e),o=new Map,n=new Map,c=[],s=t=>{if(!t.wants)return;const i=o.get(t.wants);if(i){c.push({url:t.url,text:t.text,key:t.wants,takenBy:i.text});return}o.set(t.wants,t),n.set(t.url,t.wants)};return r.filter(t=>t.core).forEach(s),r.filter(t=>!t.core).forEach(s),{tools:r.map(t=>({...t,key:n.get(t.url)||null})),byKey:o,byUrl:n,conflicts:c}}export function frequentKeys(e){return((e||[]).find(o=>o&&o.frequent)?.items||[]).filter(o=>o&&typeof o.url=="string"&&o.url.startsWith("#/")).slice(0,FREQUENT_KEYS).map((o,n)=>({n:n+1,url:o.url,text:String(o.text||"")}))}export const CHIP_DELAY_MS=300;export function chipEntries(e,{max:r=14}={}){const o=(e?.frequent||[]).slice(0,r).map(s=>({n:s.n,text:s.text})),n=[...(e?.byKey||new Map).entries()].map(([s,t])=>({key:s,text:t.text})).sort((s,t)=>(s.key===","?1:0)-(t.key===","?1:0)||s.key.localeCompare(t.key)),c=Math.max(r-o.length,0);return{frequent:o,letters:n.slice(0,c),more:Math.max(n.length-c,0)}}export function createSequence(e){let r=null;const o=n=>r!==null&&n-r<=SEQUENCE_MS;return{pending:o,reset(){r=null},press(n,c){const s=typeof n=="string"&&n.length===1?n.toLowerCase():null;if(o(c)){if(r=null,!s)return{type:"miss"};const{byKey:t,frequent:i}=e();if(/^[1-9]$/.test(s)){const u=(i||[]).find(f=>f.n===Number(s));return u?{type:"go",url:u.url}:{type:"miss"}}const l=t?.get(s);return l?{type:"go",url:l.url}:{type:"miss"}}return r=null,s===LEADER?(r=c,{type:"start"}):null}}}export function matchTools(e,r,o=50){const n=String(r||"").trim().toLowerCase(),c=(t,i)=>(i.t.key?1:0)-(t.t.key?1:0)||t.t.path.length-i.t.path.length||t.at-i.at;if(!n)return e.map((t,i)=>({t,at:i})).sort(c).slice(0,o).map(t=>t.t);const s=[];return e.forEach((t,i)=>{const l=t.text.toLowerCase(),u=l.split(/[\s\-_/&]+/).filter(Boolean),f=u.map(p=>p[0]).join(""),y=t.path.join(" ").toLowerCase();let a=0;n.length===1&&t.key===n?a=120:l===n?a=110:l.startsWith(n)?a=100:u.some(p=>p.startsWith(n))?a=80:n.length>1&&f.startsWith(n)?a=70:l.includes(n)?a=50:y.includes(n)&&(a=20),a&&s.push({t,score:a,at:i})}),s.sort((t,i)=>i.score-t.score||c(t,i)),s.slice(0,o).map(t=>t.t)}export function isTypingTarget(e){if(!e||typeof e!="object")return!1;const r=String(e.tagName||"").toUpperCase();if(r==="TEXTAREA"||r==="SELECT")return!0;if(r==="INPUT"){const o=String(e.type||"text").toLowerCase();return!["button","submit","reset","checkbox","radio","range","color","file","image"].includes(o)}return e.isContentEditable?!0:!!(e.closest&&e.closest('[contenteditable=""], [contenteditable="true"], .cm-editor, .CodeMirror, .monaco-editor, .ProseMirror, [data-own-keys]'))}export function isPaletteKey(e){if(!e||e.altKey||e.shiftKey||!(e.ctrlKey||e.metaKey)||e.ctrlKey&&e.metaKey)return!1;const r=String(e.key||"").toLowerCase();return r==="d"?!isTypingTarget(e.target):r==="k"?!m(e.target):!1}function m(e){return!e||typeof e!="object"?!1:e.isContentEditable?!0:!!(e.closest&&e.closest('[contenteditable=""], [contenteditable="true"], .cm-editor, .CodeMirror, .monaco-editor, .ProseMirror'))}
1
+ export const CORE_HOTKEYS=Object.freeze({"#/":"d","#/pages":"p","#/media":"m","#/collections":"l","#/forms":"f","#/views":"v","#/users":"u","#/analytics":"a","#/notes":"n","#/todo":"t","#/contacts":"c","#/settings":","}),LEADER="g",SEQUENCE_MS=1500,FREQUENT_KEYS=9;export function normaliseHotkey(t){if(typeof t!="string")return null;const r=t.trim().toLowerCase();return r.length!==1||r===LEADER?null:/^[a-z,]$/.test(r)?r:null}export function hotkeyLabel(t){return`G then ${String(t).toUpperCase()}`}export function sidebarTools(t){const r=[],n=new Set,s=(i,o)=>{for(const e of i||[]){if(!e||typeof e!="object"||e.hidden||e.frequent||e.type==="separator"||e.type==="spacer"||e.type==="theme-switcher")continue;const c=String(e.text||"").replace(/\s*\(\d+\)\s*$/,"").trim(),u=typeof e.url=="string"?e.url:"";if(u.startsWith("#/")&&c&&!n.has(u)){n.add(u);const l=normaliseHotkey(e.hotkey),a=CORE_HOTKEYS[u]||null;r.push({url:u,text:c,icon:e.icon||null,path:o,wants:a||l,core:!!a})}Array.isArray(e.items)&&e.items.length&&s(e.items,c?[...o,c]:o)}};return s(t,[]),r}export function assignHotkeys(t){const r=sidebarTools(t),n=new Map,s=new Map,i=[],o=e=>{if(!e.wants)return;const c=n.get(e.wants);if(c){i.push({url:e.url,text:e.text,key:e.wants,takenBy:c.text});return}n.set(e.wants,e),s.set(e.url,e.wants)};return r.filter(e=>e.core).forEach(o),r.filter(e=>!e.core).forEach(o),{tools:r.map(e=>({...e,key:s.get(e.url)||null})),byKey:n,byUrl:s,conflicts:i}}export function frequentKeys(t){return((t||[]).find(n=>n&&n.frequent)?.items||[]).filter(n=>n&&typeof n.url=="string"&&n.url.startsWith("#/")).slice(0,FREQUENT_KEYS).map((n,s)=>({n:s+1,url:n.url,text:String(n.text||"")}))}export const CHIP_DELAY_MS=300;export function chipEntries(t,{max:r=14}={}){const n=(t?.frequent||[]).slice(0,r).map(o=>({n:o.n,text:o.text})),s=[...(t?.byKey||new Map).entries()].map(([o,e])=>({key:o,text:e.text})).sort((o,e)=>(o.key===","?1:0)-(e.key===","?1:0)||o.key.localeCompare(e.key)),i=Math.max(r-n.length,0);return{frequent:n,letters:s.slice(0,i),more:Math.max(s.length-i,0)}}export function createSequence(t){let r=null;const n=s=>r!==null&&s-r<=SEQUENCE_MS;return{pending:n,reset(){r=null},press(s,i){const o=typeof s=="string"&&s.length===1?s.toLowerCase():null;if(n(i)){if(r=null,!o)return{type:"miss"};const{byKey:e,frequent:c}=t();if(/^[1-9]$/.test(o)){const l=(c||[]).find(a=>a.n===Number(o));return l?{type:"go",url:l.url}:{type:"miss"}}const u=e?.get(o);return u?{type:"go",url:u.url}:{type:"miss"}}return r=null,o===LEADER?(r=i,{type:"start"}):null}}}export function isTypingTarget(t){if(!t||typeof t!="object")return!1;const r=String(t.tagName||"").toUpperCase();if(r==="TEXTAREA"||r==="SELECT")return!0;if(r==="INPUT"){const n=String(t.type||"text").toLowerCase();return!["button","submit","reset","checkbox","radio","range","color","file","image"].includes(n)}return t.isContentEditable?!0:!!(t.closest&&t.closest('[contenteditable=""], [contenteditable="true"], .cm-editor, .CodeMirror, .monaco-editor, .ProseMirror, [data-own-keys]'))}export function isPaletteKey(t){if(!t||t.altKey||t.shiftKey||!(t.ctrlKey||t.metaKey)||t.ctrlKey&&t.metaKey)return!1;const r=String(t.key||"").toLowerCase();return r==="d"?!isTypingTarget(t.target):r==="k"?!f(t.target):!1}function f(t){return!t||typeof t!="object"?!1:t.isContentEditable?!0:!!(t.closest&&t.closest('[contenteditable=""], [contenteditable="true"], .cm-editor, .CodeMirror, .monaco-editor, .ProseMirror'))}
@@ -1 +1 @@
1
- import{fmtDate as c}from"./dates.js";export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const u={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},m=new Set(["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp"]),p=new Set(["jpg","jpeg","png","webp","gif","tiff"]);export const extOf=e=>{const t=/\.([a-z0-9]+)$/i.exec(String(e??""));return t?t[1].toLowerCase():""};export function kindOf(e){const t=extOf(e);for(const[o,n]of Object.entries(u))if(n.includes(t))return o;return"other"}export const isPreviewable=e=>m.has(extOf(e)),isEditable=e=>p.has(extOf(e));export function formatBytes(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const o=["KB","MB","GB","TB"];let n=t/1024,r=0;for(;n>=1024&&r<o.length-1;)n/=1024,r++;return`${n.toFixed(1)} ${o[r]}`}export function ago(e,t=Date.now()){const o=new Date(e).getTime();if(!Number.isFinite(o))return"";const n=Math.max(0,Math.round((t-o)/1e3));if(n<60)return"just now";if(n<3600)return`${Math.round(n/60)} min ago`;if(n<86400){const r=Math.round(n/3600);return`${r} hour${r===1?"":"s"} ago`}if(n<86400*14){const r=Math.round(n/86400);return`${r} day${r===1?"":"s"} ago`}return c(o)}export function safeName(e){return String(e??"").split(/[\\/]/).pop().replace(/[^a-zA-Z0-9._-]/g,"_")}export function clashes(e,t){const o=new Set(t);return[...new Set(e.map(safeName))].filter(n=>o.has(n))}export function altFromName(e){return String(e??"").replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()}export function markdownFor(e){return isPreviewable(e.name)?`![${altFromName(e.name)}](${e.url})`:`[${e.name}](${e.url})`}export function kindCounts(e){const t=Object.fromEntries(KINDS.map(o=>[o,0]));for(const o of e||[])t.all++,t[kindOf(o.name)]++;return t}export function arrangeMedia(e,{kind:t="all",query:o="",sort:n="newest"}={}){const r=String(o).trim().toLowerCase(),s={newest:(a,i)=>String(i.createdAt).localeCompare(String(a.createdAt)),oldest:(a,i)=>String(a.createdAt).localeCompare(String(i.createdAt)),name:(a,i)=>a.name.localeCompare(i.name,void 0,{numeric:!0,sensitivity:"base"}),size:(a,i)=>(i.size||0)-(a.size||0)}[n]||(()=>0);return[...e||[]].filter(a=>t==="all"||kindOf(a.name)===t).filter(a=>!r||a.name.toLowerCase().includes(r)).sort((a,i)=>s(a,i)||a.name.localeCompare(i.name))}export function mediaTile(e,{now:t=Date.now(),stamp:o=""}={}){const n=kindOf(e.name),r=isPreviewable(e.name);return{name:e.name,url:e.url,thumb:r?`${e.url}${o?`?t=${encodeURIComponent(o)}`:""}`:null,preview:r,icon:{video:"video",audio:"music",document:"file-text"}[n]||(n==="image"?"image":"paperclip"),kind:n,kindLabel:(extOf(e.name)||n).toUpperCase(),meta:[formatBytes(e.size),ago(e.createdAt,t)].filter(Boolean).join(" \xB7 ")}}export function normalisePrefs(e){const t=e&&typeof e=="object"?e:{};return{kind:KINDS.includes(t.kind)?t.kind:"all",sort:SORTS.includes(t.sort)?t.sort:"newest"}}
1
+ import{fmtDate as u}from"./dates.js";export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const c={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},l=new Set(["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp"]),m=new Set(["jpg","jpeg","png","webp","gif","tiff"]);export const extOf=e=>{const t=/\.([a-z0-9]+)$/i.exec(String(e??""));return t?t[1].toLowerCase():""};export function kindOf(e){const t=extOf(e);for(const[n,r]of Object.entries(c))if(r.includes(t))return n;return"other"}export const isPreviewable=e=>l.has(extOf(e)),isEditable=e=>m.has(extOf(e));export function formatBytes(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const n=["KB","MB","GB","TB"];let r=t/1024,o=0;for(;r>=1024&&o<n.length-1;)r/=1024,o++;return`${r.toFixed(1)} ${n[o]}`}export function ago(e,t=Date.now()){const n=new Date(e).getTime();if(!Number.isFinite(n))return"";const r=Math.max(0,Math.round((t-n)/1e3));if(r<60)return"just now";if(r<3600)return`${Math.round(r/60)} min ago`;if(r<86400){const o=Math.round(r/3600);return`${o} hour${o===1?"":"s"} ago`}if(r<86400*14){const o=Math.round(r/86400);return`${o} day${o===1?"":"s"} ago`}return u(n)}export function safeName(e){return String(e??"").split(/[\\/]/).pop().replace(/[^a-zA-Z0-9._-]/g,"_")}export function clashes(e,t){const n=new Set(t);return[...new Set(e.map(safeName))].filter(r=>n.has(r))}export function altFromName(e){return String(e??"").replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()}export function markdownFor(e){return isPreviewable(e.name)?`![${altFromName(e.name)}](${e.url})`:`[${e.name}](${e.url})`}export function kindCounts(e){const t=Object.fromEntries(KINDS.map(n=>[n,0]));for(const n of e||[])t.all++,t[kindOf(n.name)]++;return t}export function arrangeMedia(e,{kind:t="all",query:n="",sort:r="newest"}={}){const o=String(n).trim().toLowerCase(),a={newest:(i,s)=>String(s.createdAt).localeCompare(String(i.createdAt)),oldest:(i,s)=>String(i.createdAt).localeCompare(String(s.createdAt)),name:(i,s)=>i.name.localeCompare(s.name,void 0,{numeric:!0,sensitivity:"base"}),size:(i,s)=>(s.size||0)-(i.size||0)}[r]||(()=>0);return[...e||[]].filter(i=>t==="all"||kindOf(i.name)===t).filter(i=>!o||i.name.toLowerCase().includes(o)).sort((i,s)=>a(i,s)||i.name.localeCompare(s.name))}export function mediaTile(e,{now:t=Date.now(),stamp:n=""}={}){const r=kindOf(e.name),o=isPreviewable(e.name);return{name:e.name,url:e.url,thumb:o?`${e.url}${n?`?t=${encodeURIComponent(n)}`:""}`:null,preview:o,icon:{video:"video",audio:"music",document:"file-text"}[r]||(r==="image"?"image":"paperclip"),kind:r,kindLabel:(extOf(e.name)||r).toUpperCase(),meta:[formatBytes(e.size),ago(e.createdAt,t)].filter(Boolean).join(" \xB7 ")}}export function normalisePrefs(e){const t=e&&typeof e=="object"?e:{};return{kind:KINDS.includes(t.kind)?t.kind:"all",sort:SORTS.includes(t.sort)?t.sort:"newest"}}export function normaliseLimits(e){const t=e&&typeof e=="object"?e:{},n=Array.isArray(t.types)?t.types.filter(a=>typeof a=="string"&&/^[\w.+-]+\/[\w.+-]+$/.test(a)):[],r=Number(t.maxFileSize),o=Number(t.maxNameLength);return{types:n,maxFileSize:Number.isFinite(r)&&r>0?r:null,maxNameLength:Number.isInteger(o)&&o>0?o:null}}export function dropzoneRules(e){const t=normaliseLimits(e);return{accept:t.types.length?t.types.join(","):null,maxSize:t.maxFileSize}}export function uploadHint(e){const t=normaliseLimits(e),n=[],r=a=>t.types.some(i=>a.test(i));r(/^image\//)&&n.push("images"),r(/^(application|text)\//)&&n.push("documents"),r(/^audio\//)&&n.push("audio"),r(/^video\//)&&n.push("video");const o=[];if(n.length){const a=n.length===1?n[0]:`${n.slice(0,-1).join(", ")} and ${n.at(-1)}`;o.push(a[0].toUpperCase()+a.slice(1))}return t.maxFileSize&&o.push(`up to ${formatBytes(t.maxFileSize)} each`),o.join(" \xB7 ")}export function nameRefusal(e,t){const n=normaliseLimits(t),r=safeName(e);return r?n.maxNameLength&&r.length>n.maxNameLength?`its name is longer than ${n.maxNameLength} characters`:null:"it has no name"}export function shortName(e,t=60){const n=String(e??"");return n.length>t?`${n.slice(0,t-15)}\u2026${n.slice(-12)}`:n}export function rejectText(e,t,n){const r=normaliseLimits(n),o=shortName(e);return t==="type"?`${o}: that type of file is not taken here`:t==="size"?`${o}: it is larger than ${r.maxFileSize?formatBytes(r.maxFileSize):"the limit"}`:`${o}: not added`}export function uploadErrorText(e,t){const n=e||{},r=n.response&&typeof n.response=="object"?n.response:null,o=r&&(typeof r.error=="string"?r.error:typeof r.message=="string"?r.message:"");if(n.status===413){const a=normaliseLimits(t);return`larger than ${a.maxFileSize?formatBytes(a.maxFileSize):"the server takes"}`}return o||(n.status===0?"the connection failed":n.status===401?"signed out - sign in and retry":n.status===403?"you may not upload media":n.message||"failed")}export function narrowLimits(e,t=null){const n=normaliseLimits(e);return t instanceof RegExp?{...n,types:n.types.filter(r=>t.test(r))}:n}export function uploadedFile(e){const t=Array.isArray(e)?e[0]:e;return!t||typeof t!="object"||typeof t.name!="string"||!t.name?null:{name:t.name,url:typeof t.url=="string"&&t.url?t.url:`/media/${t.name}`}}export function pickerOutcome({files:e=[],failed:t=0,waiting:n=0,offered:r=e.length,open:o=!0,single:a=!0}={}){return o?a&&e.length===1&&!t&&!n&&r<=1&&e[0]?.url?{choose:e[0].url,reload:!1}:{choose:null,reload:e.length>0}:{choose:null,reload:!1}}
@@ -1,8 +1,12 @@
1
- import{api as q}from"../api.js";import{onceClosed as S}from"./tool-kit.js";const $=/\.(png|jpe?g|gif|webp|avif|svg)$/i;export function pickable(l,{query:s="",accept:p=$}={}){const r=String(s).trim().toLowerCase();return(Array.isArray(l)?l:[]).map(e=>({name:e.name||e.filename||"",url:e.url||(e.name?`/media/${e.name}`:"")})).filter(e=>e.url&&p.test(e.name||e.url)&&(!r||e.name.toLowerCase().includes(r)))}export function pickMedia({title:l="Choose an image",current:s=""}={}){return new Promise(p=>{let r=!1;const e=a=>{r||(r=!0,p(a))},i=E.slideover({title:l,size:"lg",position:"right"}),m=document.createElement("template");m.innerHTML=`<div class="mp-picker">
1
+ import{api as $}from"../api.js";import{onceClosed as A,sidebarCount as D}from"./tool-kit.js";import{catchFiles as N,mediaUploader as P}from"./media-upload.js";import{pickerOutcome as T}from"./media-arrange.js";const U=/\.(png|jpe?g|gif|webp|avif|svg)$/i;export const UPLOAD_TYPES=/^image\/(png|jpe?g|gif|webp|avif|svg\+xml)$/;export function pickable(s,{query:d="",accept:p=U}={}){const o=String(d).trim().toLowerCase();return(Array.isArray(s)?s:[]).map(t=>({name:t.name||t.filename||"",url:t.url||(t.name?`/media/${t.name}`:"")})).filter(t=>t.url&&p.test(t.name||t.url)&&(!o||t.name.toLowerCase().includes(o)))}export function pickMedia({title:s="Choose an image",current:d=""}={}){return new Promise(p=>{let o=!1;const t=e=>{o||(o=!0,p(e))},a=E.slideover({title:s,size:"lg",position:"right"}),c=document.createElement("template");c.innerHTML=`<div class="mp-picker">
2
2
  <div class="mp-bar">
3
3
  <input type="search" class="form-input" placeholder="Search Media\u2026" aria-label="Search Media" data-model="query.value">
4
- <label class="btn btn-sm btn-ghost mp-upload"><span data-icon="upload"></span> Upload
5
- <input type="file" accept="image/*" hidden></label>
4
+ </div>
5
+ <input class="mp-file-input" type="file" multiple hidden>
6
+ <div class="mp-zone">
7
+ <span class="mp-zone-icon" data-icon="upload" data-icon-size="18" aria-hidden="true"></span>
8
+ <span class="mp-zone-text"><b>Upload: drop pictures here, paste, or click to choose</b>
9
+ <span class="mp-zone-hint" data-bind-text="hint.value" data-bind-hidden="!hint.value"></span></span>
6
10
  </div>
7
11
  <p class="mp-note" data-bind-hidden="!note.value" data-bind-text="note.value"></p>
8
12
  <div class="media-picker-grid mp-grid" data-each="shown.value key=url">
@@ -15,10 +19,18 @@ import{api as q}from"../api.js";import{onceClosed as S}from"./tool-kit.js";const
15
19
  .mp-picker { padding: 1rem; display: flex; flex-direction: column; gap: .75rem; }
16
20
  .mp-bar { display: flex; gap: .5rem; align-items: center; }
17
21
  .mp-bar .form-input { flex: 1; }
18
- .mp-upload { cursor: pointer; white-space: nowrap; }
22
+ /* A one-line strip rather than Domma's tall default area, as on the Media screen. */
23
+ .mp-zone.dm-dropzone { flex-direction: row; justify-content: flex-start; text-align: left; min-height: 0; gap: .65rem;
24
+ padding: .55rem .8rem; font-size: var(--dm-text-sm); }
25
+ .mp-picker.is-drop .mp-zone.dm-dropzone { border-color: var(--dm-primary); background: var(--dm-primary-light, transparent); }
26
+ .mp-zone-icon { color: var(--dm-primary); flex-shrink: 0; display: inline-flex; }
27
+ .mp-zone-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
28
+ .mp-zone-text b { font-weight: 600; }
29
+ .mp-zone-hint { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
30
+ .mp-picker .dm-dropzone-files { margin: 0; }
19
31
  .mp-note { margin: 0; font-size: .85rem; opacity: .75; }
20
32
  .mp-picker [hidden] { display: none !important; }
21
33
  .mp-grid { max-height: none; }
22
34
  .mp-grid .media-picker-item { background: none; padding: 0; text-align: left; color: inherit; font: inherit; }
23
35
  .mp-grid .media-picker-item.is-current { border-color: var(--dm-primary); }
24
- </style>`;const o=m.content.firstElementChild,h=m.content.querySelector("style"),d=M.observable([]),u=M.observable(""),t=M.observable("Loading\u2026"),g=M.computed(()=>pickable(d.value,{query:u.value})),x=M.effect(()=>{t.peek()==="Loading\u2026"&&!d.value.length||(t.value=g.value.length?"":u.value.trim()?"No picture matches.":"No pictures in Media yet - upload one.")}),b=async()=>{try{d.value=await q.media.list(),t.value="",pickable(d.peek()).length||(t.value="No pictures in Media yet - upload one.")}catch(a){t.value=`Could not load Media: ${a.message||a}`}},y=o.querySelector(".mp-grid"),k=a=>{const n=a.target.closest("[data-url]");n&&(e(n.dataset.url),i.close())};y.addEventListener("click",k);const v=o.querySelector("input[type=file]");v.addEventListener("change",async()=>{const a=v.files?.[0];if(a){t.value=`Uploading ${a.name}\u2026`;try{const n=new FormData;n.append("file",a);const f=await q.media.upload(n),c=Array.isArray(f)?f[0]:f,w=c?.url||(c?.name?`/media/${c.name}`:"");if(E.toast(`${c?.name||a.name} is in Media.`,{type:"success"}),w){e(w),i.close();return}await b()}catch(n){t.value="",E.toast(`Not uploaded: ${n.message||n}`,{type:"error"})}finally{v.value=""}}}),i.element.appendChild(o),h&&i.element.appendChild(h);const L=M.applyBindings({query:u,note:t,shown:g},o),C=M.effect(()=>{g.value,queueMicrotask(()=>y.querySelectorAll("[data-url]").forEach(a=>a.classList.toggle("is-current",!!s&&a.dataset.url===s)))});S(i,()=>{L.dispose(),x?.(),C?.(),y.removeEventListener("click",k),e(null)}),Domma.icons?.scan?.(o),i.open(),b(),setTimeout(()=>o.querySelector("input[type=search]")?.focus(),50)})}
36
+ </style>`;const i=c.content.firstElementChild,y=c.content.querySelector("style"),l=M.observable([]),m=M.observable(""),b=M.observable(""),r=M.observable("Loading\u2026"),u=M.computed(()=>pickable(l.value,{query:m.value})),z=M.effect(()=>{r.peek()==="Loading\u2026"&&!l.value.length||(r.value=u.value.length?"":m.value.trim()?"No picture matches.":"No pictures in Media yet - upload one.")}),x=async()=>{try{l.value=await $.media.list(),r.value="",pickable(l.peek()).length||(r.value="No pictures in Media yet - upload one.")}catch(e){r.value=`Could not load Media: ${e.message||e}`}},g=i.querySelector(".mp-grid"),k=e=>{const n=e.target.closest("[data-url]");n&&(t(n.dataset.url),a.close())};g.addEventListener("click",k),a.element.appendChild(i),y&&a.element.appendChild(y);const f=()=>!o,h=P(i.querySelector(".mp-zone"),{input:i.querySelector(".mp-file-input"),accept:"image/*",only:UPLOAD_TYPES,exists:e=>l.peek().some(n=>(n.name||n.filename)===e),showing:f,onHint:e=>{b.value=e},doneSuffix:()=>f()?"":" to Media",onUploaded({files:e,failed:n,waiting:C,offered:L}){D("#/media");const v=T({files:e,failed:n,waiting:C,offered:L,open:f()});if(v.choose){t(v.choose),setTimeout(()=>a.close(),0);return}v.reload&&x()}}),w=N(a.element,h.addFiles,{onDragging:e=>i.classList.toggle("is-drop",e)}),S=M.applyBindings({query:m,note:r,shown:u,hint:b},i),q=M.effect(()=>{u.value,queueMicrotask(()=>g.querySelectorAll("[data-url]").forEach(e=>e.classList.toggle("is-current",!!d&&e.dataset.url===d)))});A(a,()=>{const e=h.pending();S.dispose(),z?.(),q?.(),w(),g.removeEventListener("click",k),t(null),h.release()&&e&&E.toast(`Still uploading ${e} file${e===1?"":"s"} - ${e===1?"it goes":"they go"} into Media when done.`,{type:"info"})}),Domma.icons?.scan?.(i),a.open(),x(),setTimeout(()=>i.querySelector("input[type=search]")?.focus(),50)})}
@@ -0,0 +1 @@
1
+ import{api as M,getToken as V,refreshAccessToken as W}from"../api.js";import{dropzoneRules as G,nameRefusal as I,narrowLimits as P,rejectText as J,safeName as v,shortName as _,uploadErrorText as K,uploadHint as Q,uploadedFile as X}from"./media-arrange.js";const Y=f=>String(f??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),U=f=>setTimeout(f,0);export function mediaUploader(f,{input:$,multiple:h=!0,accept:p=null,only:d=null,exists:T=()=>!1,showing:w=()=>!0,onHint:L=()=>{},onUploaded:j=()=>{},doneSuffix:q=()=>""}={}){let l=P(null,d);const t=new WeakSet;let o=[],m=[],b=[],y=0,i=!1,g=0,x=0,R=0,k=!1,c=null;const B=()=>{if(!m.length)return;const e=m;m=[],E.toast(`Not added - ${e.slice(0,4).join("; ")}${e.length>4?` and ${e.length-4} more`:""}`,{type:"error",duration:9e3})},F=e=>{m.length||U(B),m.push(e)};async function C(){const e=o;if(o=[],!(!e.length||!w())){x=e.length;try{await H(e)}finally{x=0}}}async function H(e){const n=[...new Set(e.map(a=>v(a.name)))],u=n.slice(0,5).map(a=>`"${Y(a)}"`).join(", ")+(n.length>5?` and ${n.length-5} more`:""),s=n.length===1;await E.confirm(`${s?"A file":`${n.length} files`} with the same name ${s?"is":"are"} already here: ${u}. Replace ${s?"it":"them"}?`)&&(i||c||(e.forEach(a=>t.add(a)),r.addFiles(e)))}const r=E.dropzone(f,{input:$,multiple:h,previews:!0,accept:p,upload:{url:"/api/media",field:"file",concurrency:3,headers:()=>{const e=V();return e?{Authorization:`Bearer ${e}`}:{}}},labels:{cancel:"Cancel the upload of {name}",remove:"Remove {name} from the list"},onBeforeAdd(e){const n=I(e.name,l);if(n)return F(`${_(e.name)}: ${n}`),!1;if(t.has(e.file))return t.delete(e.file),!0;const u=v(e.name),s=r.getEntries().filter(a=>v(a.name)===u);return s.some(a=>a.status==="uploading"||a.status==="queued")?(F(`${_(e.name)}: a file of that name is uploading already`),!1):(s.forEach(a=>{a.status!=="done"&&r.removeFile(a.id)}),k||(k=!0,queueMicrotask(()=>{k=!1}),!O()&&!r.getEntries().some(a=>a.status!=="done")&&(R=0)),R++,T(u)?(o.length||U(C),o.push(e.file),!1):!0)},onReject(e,n){F(J(e.name,n,l))},onSuccess(e,n){b.push(X(n)||{name:v(e.name),url:`/media/${v(e.name)}`})},onError(e,n){if(n.status===401&&!e._mdxRefreshed&&S.get("auth_refresh_token")){e._mdxRefreshed=!0,n.message="signing in again\u2026",g++,W().then(()=>{i||r.retry(e.id)}).catch(()=>{}).finally(()=>{g--,c?.()});return}y++,n.message=K(n,l)},onComplete(){const e=b,n=y;b=[],y=0;const u=e.length;if(u&&E.toast(`Uploaded ${u} file${u===1?"":"s"}${q()}.`,{type:"success"}),n){const s=`${n} file${n===1?"":"s"} not uploaded`;E.toast(w()?`${s} - Retry or remove ${n===1?"it":"them"} in the list.`:`${s}.`,{type:"error",duration:9e3})}r.getEntries().filter(s=>s.status==="done").forEach(s=>r.removeFile(s.id)),u&&j({files:e,failed:n,waiting:O(),offered:R})}});function O(){return o.length+x+g}const z=()=>!i&&r.getEntries().some(e=>e.status==="uploading"||e.status==="queued"),D=()=>z()||g>0,A=e=>{D()&&(e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",A);const N=()=>{i||(i=!0,window.removeEventListener("beforeunload",A),r.destroy())};return M.media.limits().then(e=>{if(i||c||!w())return;l=P(e,d);const n=G(l);r.setOptions({...n,accept:n.accept??p}),L(Q(l))}).catch(()=>{}),{dz:r,addFiles:e=>{!i&&!c&&r.addFiles(e)},browse:()=>{!i&&!c&&r.browse()},uploading:z,pending:()=>i?0:r.getEntries().filter(e=>e.status==="uploading"||e.status==="queued").length+g,release(){return i||c?!1:(c=()=>{D()||N()},D()?(r.on("complete",c),!0):(N(),!1))}}}export function catchFiles(f,$,{onDragging:h=()=>{}}={}){const p=t=>[...t.dataTransfer?.types||[]].includes("Files");let d=0;const l={dragenter:t=>{p(t)&&(t.preventDefault(),d++,h(!0))},dragover:t=>{p(t)&&(t.preventDefault(),t.dataTransfer.dropEffect="copy")},dragleave:t=>{p(t)&&(d=Math.max(0,d-1),d||h(!1))},drop:t=>{p(t)&&(d=0,h(!1),!t.defaultPrevented&&(t.preventDefault(),$(t.dataTransfer.files)))},paste:t=>{const o=[...t.clipboardData?.files||[]];t.defaultPrevented||!o.length||t.target?.closest?.('input, textarea, [contenteditable=""], [contenteditable="true"]')&&[...t.clipboardData.types||[]].includes("text/plain")||(t.preventDefault(),$(o))}};return Object.entries(l).forEach(([t,o])=>f.addEventListener(t,o)),()=>Object.entries(l).forEach(([t,o])=>f.removeEventListener(t,o))}
@@ -0,0 +1 @@
1
+ import{hotkeyLabel as a}from"./hotkeys.js";export function recentKeyFor(r){const e=r&&(r.id??r._id??r.email);return`admin.palette.recent.${e==null||e===""?"anon":String(e)}`}export function toolCommands(r){return(r||[]).filter(e=>e&&e.url&&e.text).map((e,t)=>({t:e,at:t,path:Array.isArray(e.path)?e.path:[]})).sort((e,t)=>(t.t.key?1:0)-(e.t.key?1:0)||e.path.length-t.path.length||e.at-t.at).map(({t:e,path:t})=>{const o=[t.join(" \u203A "),e.key?a(e.key):""].filter(Boolean).join(" \xB7 ");return{id:`tool:${e.url}`,title:e.text,...o?{subtitle:o}:{},...e.icon?{icon:e.icon}:{},keywords:t.slice(),href:e.url}})}const i=(r,e)=>r.title.localeCompare(e.title,void 0,{sensitivity:"base"});export function pageCommands(r,e="file-text"){return(Array.isArray(r)?r:[]).filter(t=>t&&typeof t.urlPath=="string").map(t=>({id:`page:${t.urlPath}`,title:String(t.title||t.urlPath),subtitle:t.status==="draft"?`${t.urlPath} \xB7 Draft`:t.urlPath,icon:e,keywords:[t.urlPath],href:`#/pages/edit${t.urlPath}`})).sort(i)}export function collectionCommands(r,e="database"){return(Array.isArray(r)?r:[]).filter(t=>t&&t.slug).map(t=>({id:`collection:${t.slug}`,title:String(t.title||t.slug),subtitle:String(t.slug),icon:e,keywords:[String(t.slug)],href:`#/collections/${encodeURIComponent(t.slug)}/entries`})).sort(i)}export function formCommands(r,e="clipboard"){return(Array.isArray(r)?r:[]).filter(t=>t&&t.slug).map(t=>({id:`form:${t.slug}`,title:String(t.title||t.slug),subtitle:String(t.slug),icon:e,keywords:[String(t.slug)],href:`#/forms/edit/${encodeURIComponent(t.slug)}`})).sort(i)}const s=[{url:"#/pages",id:"find:pages",title:"Open page\u2026",placeholder:"Find a page\u2026",load:"pages",make:pageCommands,icon:"file-text",words:["edit","page"]},{url:"#/collections",id:"find:collections",title:"Open collection\u2026",placeholder:"Find a collection\u2026",load:"collections",make:collectionCommands,icon:"database",words:["entries","data"]},{url:"#/forms",id:"find:forms",title:"Open form\u2026",placeholder:"Find a form\u2026",load:"forms",make:formCommands,icon:"clipboard",words:["edit","form"]}];export function findCommands(r,e={}){const t=new Map((r||[]).map(o=>[o.url,o]));return s.filter(o=>t.has(o.url)&&typeof e[o.load]=="function").map(o=>{const n=t.get(o.url).icon||o.icon;return{id:o.id,title:o.title,group:"Find",icon:n,keywords:o.words,placeholder:o.placeholder,children:async(d,l)=>o.make(await e[o.load](l),n)}})}export function paletteCommands(r,{load:e={},showKeys:t=null}={}){const o=[...toolCommands(r),...findCommands(r,e)];return typeof t=="function"&&o.push({id:"help:hotkeys",title:"Every hotkey",subtitle:"Or press ? with nothing typed",group:"Help",icon:"keyboard",keywords:["shortcuts","keys","keyboard","?"],action:()=>t()}),o}
@@ -97,6 +97,10 @@
97
97
  .cex-detail { display: flex; flex-direction: column; gap: .6rem; }
98
98
  .cex-detail dt { font-size: var(--dm-text-xs); color: var(--dm-text-muted); font-weight: 600; }
99
99
  .cex-detail dd { margin: 0 0 .5rem; word-break: break-word; white-space: pre-wrap; padding-bottom: .5rem; border-bottom: 1px solid var(--dm-border); }
100
+ .cex-files { margin: 0; padding: 0; list-style: none; white-space: normal; display: grid; gap: .2rem; }
101
+ .cex-files a { color: var(--dm-primary); text-decoration: underline; }
102
+ .cex-files-field { display: grid; gap: .25rem; margin-bottom: .75rem; }
103
+ .cex-files-field > span { font-weight: 600; font-size: .875rem; }
100
104
  .cex-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); display: flex; flex-direction: column; gap: .2rem; }
101
105
  .cex-import { width: 100%; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); min-height: 12rem; resize: vertical; }
102
106
  .cex-export { display: flex; flex-direction: column; gap: .5rem; }
@@ -73,6 +73,9 @@
73
73
  .fsx-answer { padding: .55rem 0; border-bottom: 1px solid var(--dm-border); }
74
74
  .fsx-answer dt { font-size: var(--dm-text-xs); font-weight: 600; color: var(--dm-text-muted); margin-bottom: .15rem; }
75
75
  .fsx-answer dd { margin: 0; white-space: pre-wrap; word-break: break-word; }
76
+ .fsx-files { margin: 0; padding: 0; list-style: none; white-space: normal; display: grid; gap: .2rem; }
77
+ .fsx-files small { color: var(--dm-text-muted); }
78
+ .fsx-files a { color: var(--dm-primary); text-decoration: underline; }
76
79
  .fsx-facts { display: flex; flex-wrap: wrap; gap: .35rem .9rem; color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
77
80
  .fsx-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
78
81
  .fsx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
@@ -2,11 +2,17 @@
2
2
  (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool
3
3
  banner above is the heading, and Upload is a banner action. -->
4
4
  <div class="mdx">
5
- <input class="mdx-file-input" type="file" multiple accept="image/*,video/*,audio/*,.pdf,.svg,.txt,.csv,.doc,.docx,.xls,.xlsx" hidden>
6
- <p class="mdx-note">Files for your pages, stored in <code>content/media/</code>. Drop files anywhere on this screen to
7
- upload them.
8
- <span data-help-title="Media" data-help="Upload with the button in the banner, or drop files onto this screen. Click a file for its details - its address, a snippet to paste into a page, a rename box, and the image editor for pictures. Right-click a file (or ⋮, or Shift+F10) for the same actions without opening it. Uploading a file with the same name as one already here replaces it; you are asked first."></span></p>
9
- <p class="mdx-busy" data-bind-hidden="!busyText.value"><span class="mdx-spinner" aria-hidden="true"></span><span data-bind-text="busyText.value"></span></p>
5
+ <!-- The dropzone's input: its accept and size rules come from GET /api/media/_limits (views/media.js). -->
6
+ <input class="mdx-file-input" type="file" multiple hidden>
7
+ <p class="mdx-note">Files for your pages, stored in <code>content/media/</code>. Drop or paste files anywhere on this
8
+ screen to upload them.
9
+ <span data-help-title="Media" data-help="Upload with the drop area or the button in the banner, drop files anywhere on this screen, or paste them (Ctrl+V). Each file shows its progress; cancel one with its ×, and Retry one that failed. A type or size the site does not take is refused before it is sent. Click a file for its details - its address, a snippet to paste into a page, a rename box, and the image editor for pictures. Right-click a file (or ⋮, or Shift+F10) for the same actions without opening it. Uploading a file with the same name as one already here replaces it; you are asked first."></span></p>
10
+ <!-- E.dropzone: the area is one button (click, Enter or Space opens the picker); its file list is drawn after it. -->
11
+ <div class="mdx-zone">
12
+ <span class="mdx-zone-icon" data-icon="upload" data-icon-size="20" aria-hidden="true"></span>
13
+ <span class="mdx-zone-text"><b>Drop files here, paste, or click to choose</b>
14
+ <span class="mdx-zone-hint" data-bind-text="hintText.value" data-bind-hidden="!hintText.value"></span></span>
15
+ </div>
10
16
 
11
17
  <div class="mdx-card" data-bind-class="dragging.value && 'is-drop'">
12
18
  <div class="mdx-bar">
@@ -43,11 +49,14 @@
43
49
  .mdx [hidden], .mdx-panel [hidden] { display: none !important; }
44
50
  .mdx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
45
51
  .mdx-note.is-bad { color: var(--dm-danger); }
46
- .mdx-busy { margin: 0; display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem; border-radius: var(--dm-radius-md);
47
- background: color-mix(in srgb, var(--dm-primary) 12%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
48
- .mdx-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--dm-primary) 30%, transparent);
49
- border-top-color: var(--dm-primary); animation: mdx-spin .8s linear infinite; flex-shrink: 0; }
50
- @keyframes mdx-spin { to { transform: rotate(360deg); } }
52
+ /* A one-line strip rather than Domma's tall default area: the grid is the screen's subject. */
53
+ .mdx-zone.dm-dropzone { flex-direction: row; justify-content: flex-start; text-align: left; min-height: 0; gap: .75rem;
54
+ padding: .65rem .9rem; font-size: var(--dm-text-sm); }
55
+ .mdx-zone-icon { color: var(--dm-primary); flex-shrink: 0; display: inline-flex; }
56
+ .mdx-zone-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
57
+ .mdx-zone-text b { font-weight: 600; }
58
+ .mdx-zone-hint { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
59
+ .mdx .dm-dropzone-files { margin: 0; }
51
60
  .mdx-card { position: relative; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .75rem; }
52
61
  .mdx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
53
62
  .mdx-search { flex: 1; min-width: 10rem; max-width: 20rem; }