domma-cms 0.18.0 → 0.22.1

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 (110) hide show
  1. package/CLAUDE.md +37 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/api.js +1 -1
  4. package/admin/js/app.js +4 -4
  5. package/admin/js/config/sidebar-config.js +1 -1
  6. package/admin/js/lib/crud-tutorial.js +1 -0
  7. package/admin/js/lib/markdown-toolbar.js +5 -5
  8. package/admin/js/lib/project-context.js +1 -0
  9. package/admin/js/lib/sidebar-renderer.js +4 -0
  10. package/admin/js/templates/action-editor.html +7 -0
  11. package/admin/js/templates/block-editor.html +7 -0
  12. package/admin/js/templates/collection-editor.html +9 -0
  13. package/admin/js/templates/form-editor.html +9 -0
  14. package/admin/js/templates/menu-editor.html +99 -0
  15. package/admin/js/templates/menu-locations.html +14 -0
  16. package/admin/js/templates/menus.html +14 -0
  17. package/admin/js/templates/page-editor.html +9 -2
  18. package/admin/js/templates/project-detail.html +50 -0
  19. package/admin/js/templates/project-editor.html +45 -0
  20. package/admin/js/templates/project-settings.html +60 -0
  21. package/admin/js/templates/projects.html +13 -0
  22. package/admin/js/templates/role-editor.html +11 -0
  23. package/admin/js/templates/tutorials.html +335 -2
  24. package/admin/js/templates/view-editor.html +7 -0
  25. package/admin/js/views/action-editor.js +1 -1
  26. package/admin/js/views/actions-list.js +1 -1
  27. package/admin/js/views/block-editor.js +8 -8
  28. package/admin/js/views/blocks.js +2 -2
  29. package/admin/js/views/collection-editor.js +4 -4
  30. package/admin/js/views/collections.js +1 -1
  31. package/admin/js/views/form-editor.js +5 -5
  32. package/admin/js/views/forms.js +1 -1
  33. package/admin/js/views/index.js +1 -1
  34. package/admin/js/views/menu-editor.js +20 -0
  35. package/admin/js/views/menu-locations.js +1 -0
  36. package/admin/js/views/menus.js +5 -0
  37. package/admin/js/views/page-editor.js +24 -24
  38. package/admin/js/views/pages.js +3 -3
  39. package/admin/js/views/project-detail.js +4 -0
  40. package/admin/js/views/project-editor.js +1 -0
  41. package/admin/js/views/project-settings.js +1 -0
  42. package/admin/js/views/projects.js +7 -0
  43. package/admin/js/views/role-editor.js +1 -1
  44. package/admin/js/views/roles.js +3 -3
  45. package/admin/js/views/tutorials.js +1 -1
  46. package/admin/js/views/user-editor.js +1 -1
  47. package/admin/js/views/users.js +3 -3
  48. package/admin/js/views/view-editor.js +1 -1
  49. package/admin/js/views/views-list.js +1 -1
  50. package/config/menu-locations.json +5 -0
  51. package/config/menus/admin-sidebar.json +185 -0
  52. package/config/menus/footer.json +33 -0
  53. package/config/menus/main.json +35 -0
  54. package/config/menus/sproj-1779696558011-menu.json +17 -0
  55. package/config/menus/sproj-1779696960337-menu.json +18 -0
  56. package/config/menus/sproj-1779696985353-menu.json +18 -0
  57. package/config/site.json +6 -22
  58. package/package.json +4 -3
  59. package/plugins/analytics/daily.json +3 -0
  60. package/plugins/analytics/journeys.json +8 -0
  61. package/plugins/analytics/lifetime.json +1 -1
  62. package/public/css/site.css +1 -1
  63. package/public/js/collection-browser.js +4 -0
  64. package/public/js/forms.js +1 -1
  65. package/public/js/site.js +1 -1
  66. package/server/middleware/auth.js +88 -22
  67. package/server/routes/api/actions.js +58 -5
  68. package/server/routes/api/auth.js +2 -2
  69. package/server/routes/api/blocks.js +18 -3
  70. package/server/routes/api/collections.js +201 -8
  71. package/server/routes/api/forms.js +266 -21
  72. package/server/routes/api/menu-locations.js +46 -0
  73. package/server/routes/api/menus.js +115 -0
  74. package/server/routes/api/pages.js +1 -1
  75. package/server/routes/api/projects.js +107 -0
  76. package/server/routes/api/scaffold.js +86 -0
  77. package/server/routes/api/sidebar.js +23 -0
  78. package/server/routes/api/users.js +32 -7
  79. package/server/routes/api/views.js +10 -2
  80. package/server/routes/public.js +79 -6
  81. package/server/server.js +38 -0
  82. package/server/services/actions.js +137 -8
  83. package/server/services/adapters/FileAdapter.js +23 -8
  84. package/server/services/adapters/MongoAdapter.js +36 -18
  85. package/server/services/blocks.js +20 -8
  86. package/server/services/collections.js +85 -8
  87. package/server/services/content.js +23 -9
  88. package/server/services/filterEngine.js +281 -0
  89. package/server/services/hooks.js +48 -0
  90. package/server/services/markdown.js +702 -109
  91. package/server/services/menus-migration.js +107 -0
  92. package/server/services/menus.js +524 -0
  93. package/server/services/permissionRegistry.js +26 -0
  94. package/server/services/plugins.js +9 -2
  95. package/server/services/presetCollections.js +22 -0
  96. package/server/services/projects.js +429 -0
  97. package/server/services/recipes/contact-list.json +78 -0
  98. package/server/services/recipes/onboarding.json +426 -0
  99. package/server/services/references.js +174 -0
  100. package/server/services/renderer.js +253 -40
  101. package/server/services/roles.js +6 -1
  102. package/server/services/rowAccess.js +86 -13
  103. package/server/services/scaffolder.js +465 -0
  104. package/server/services/sidebar-migration.js +117 -0
  105. package/server/services/sitemap.js +112 -0
  106. package/server/services/userRoles.js +86 -0
  107. package/server/services/users.js +23 -2
  108. package/server/services/views.js +15 -4
  109. package/server/templates/page.html +7 -2
  110. /package/config/{navigation.json → navigation.json.bak} +0 -0
@@ -1 +1 @@
1
- import{apiRequest as p}from"/admin/js/api.js";function u(i){return String(i).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(i){await g(i),i.find("#create-form-btn").off("click").on("click",()=>{const r=E.modal({title:"Create Form",size:"sm"}),t=document.createElement("div");t.style.cssText="padding:.25rem 0 .5rem;";const n=document.createElement("div");F.create({title:{type:"string",label:"Form Title",placeholder:"e.g. Contact, Feedback\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(n),t.appendChild(n);const e=document.createElement("div");e.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const s=document.createElement("button");s.className="btn btn-ghost",s.textContent="Cancel";const a=document.createElement("button");a.className="btn btn-primary",a.textContent="Create",e.appendChild(s),e.appendChild(a),t.appendChild(e),r.element.appendChild(t),r.open();const o=n.querySelector('input[name="title"]');setTimeout(()=>o?.focus(),50);async function d(){const l=o?.value.trim();if(l)try{const c=await p("/forms",{method:"POST",body:JSON.stringify({title:l})});r.close(),R.navigate(`/forms/edit/${c.slug}`)}catch(c){E.toast(c.message||"Failed to create form.",{type:"error"})}}s.addEventListener("click",()=>r.close()),a.addEventListener("click",d),o?.addEventListener("keydown",l=>{l.key==="Enter"&&d()})}),i.find("#test-email-btn").off("click").on("click",async()=>{const t=(window.__CMS_SITE__?.smtp||{}).fromAddress||"",n=E.modal({title:"Send Test Email",size:"sm"}),e=document.createElement("div");e.style.cssText="padding:.25rem 0 .5rem;";const s=document.createElement("div");s.className="mb-3";const a=document.createElement("label");a.className="form-label",a.textContent="Send to";const o=document.createElement("input");o.type="email",o.className="form-input",o.value=t,o.placeholder="test@example.com",s.appendChild(a),s.appendChild(o),e.appendChild(s);const d=document.createElement("p");d.className="text-muted",d.style.cssText="font-size:.8rem;margin-bottom:.75rem;",d.textContent="SMTP settings are configured in Site Settings.",e.appendChild(d);const l=document.createElement("div");l.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const c=document.createElement("button");c.className="btn btn-ghost",c.textContent="Cancel";const m=document.createElement("button");m.className="btn btn-primary",m.textContent="Send",l.appendChild(c),l.appendChild(m),e.appendChild(l),n.element.appendChild(e),n.open(),setTimeout(()=>o?.focus(),50),c.addEventListener("click",()=>n.close()),m.addEventListener("click",async()=>{const f=o.value.trim();if(f){m.disabled=!0;try{await p("/forms/test-email",{method:"POST",body:JSON.stringify({to:f})}),n.close(),E.toast("Test email sent.",{type:"success"})}catch(y){E.toast(y.message||"Failed to send test email.",{type:"error"})}finally{m.disabled=!1}}})}),Domma.icons.scan()}};async function g(i){let r=[];try{r=await p("/forms")}catch{E.toast("Could not load forms.",{type:"error"})}T.create("#forms-table",{data:r,columns:[{key:"title",title:"Title",render:(t,n)=>{const e=document.createElement("span");e.style.cssText="display:inline-flex;align-items:center;gap:.4rem;flex-wrap:wrap;";const s=document.createElement("a");if(s.href=`#/forms/edit/${u(n.slug)}`,s.textContent=t,s.style.fontWeight="600",e.appendChild(s),n.plugin){const a=document.createElement("span");a.className="badge badge-outline",a.textContent=n.plugin,a.title=`Managed by the ${n.plugin} plugin`,a.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",e.appendChild(a)}return e.outerHTML}},{key:"slug",title:"Slug",render:t=>{const n=document.createElement("code");return n.textContent=t,n.outerHTML}},{key:"fields",title:"Field Count",render:t=>String(t?.length??0)},{key:"submissionCount",title:"Submission Count",render:t=>String(t??0)},{key:"slug",title:"Actions",render:(t,n)=>{const e=document.createElement("div");e.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const s=document.createElement("a");s.href=`#/forms/edit/${u(t)}`,s.className="btn btn-sm btn-primary",s.textContent="Edit";const a=document.createElement("a");a.href=`#/forms/${u(t)}/submissions`,a.className="btn btn-sm btn-ghost",a.textContent="Submissions";const o=document.createElement("button");return o.className="btn btn-sm btn-danger js-delete-form",o.dataset.slug=t,o.dataset.plugin=n.plugin||"",o.textContent="Delete",e.appendChild(s),e.appendChild(a),e.appendChild(o),e.outerHTML}}],emptyMessage:'No forms yet. Click "Create Form" to get started.'}),document.querySelectorAll(".js-delete-form").forEach(t=>{t.addEventListener("click",async()=>{const n=t.dataset.slug,e=t.dataset.plugin,s=e?`This form is managed by the <strong>${e}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete form "${n}" and all its submissions? This cannot be undone.`;if(await E.confirm(s))try{await p(`/forms/${n}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),await g(i)}catch{E.toast("Failed to delete form.",{type:"error"})}})}),Domma.icons.scan()}
1
+ import{apiRequest as p}from"/admin/js/api.js";import{filterByProject as b,getProjectFromHash as h}from"/admin/js/lib/project-context.js";function u(d){return String(d).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(d){await g(d),d.find("#create-form-btn").off("click").on("click",()=>{const r=E.modal({title:"Create Form",size:"sm"}),l=document.createElement("div");l.style.cssText="padding:.25rem 0 .5rem;";const e=document.createElement("div");F.create({title:{type:"string",label:"Form Title",placeholder:"e.g. Contact, Feedback\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(e),l.appendChild(e);const t=document.createElement("div");t.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const a=document.createElement("button");a.className="btn btn-primary",a.textContent="Create",t.appendChild(n),t.appendChild(a),l.appendChild(t),r.element.appendChild(l),r.open();const s=e.querySelector('input[name="title"]');setTimeout(()=>s?.focus(),50);async function o(){const c=s?.value.trim();if(c)try{const i=await p("/forms",{method:"POST",body:JSON.stringify({title:c})});r.close(),R.navigate(`/forms/edit/${i.slug}`)}catch(i){E.toast(i.message||"Failed to create form.",{type:"error"})}}n.addEventListener("click",()=>r.close()),a.addEventListener("click",o),s?.addEventListener("keydown",c=>{c.key==="Enter"&&o()})}),d.find("#test-email-btn").off("click").on("click",async()=>{const l=(window.__CMS_SITE__?.smtp||{}).fromAddress||"",e=E.modal({title:"Send Test Email",size:"sm"}),t=document.createElement("div");t.style.cssText="padding:.25rem 0 .5rem;";const n=document.createElement("div");n.className="mb-3";const a=document.createElement("label");a.className="form-label",a.textContent="Send to";const s=document.createElement("input");s.type="email",s.className="form-input",s.value=l,s.placeholder="test@example.com",n.appendChild(a),n.appendChild(s),t.appendChild(n);const o=document.createElement("p");o.className="text-muted",o.style.cssText="font-size:.8rem;margin-bottom:.75rem;",o.textContent="SMTP settings are configured in Site Settings.",t.appendChild(o);const c=document.createElement("div");c.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const i=document.createElement("button");i.className="btn btn-ghost",i.textContent="Cancel";const m=document.createElement("button");m.className="btn btn-primary",m.textContent="Send",c.appendChild(i),c.appendChild(m),t.appendChild(c),e.element.appendChild(t),e.open(),setTimeout(()=>s?.focus(),50),i.addEventListener("click",()=>e.close()),m.addEventListener("click",async()=>{const f=s.value.trim();if(f){m.disabled=!0;try{await p("/forms/test-email",{method:"POST",body:JSON.stringify({to:f})}),e.close(),E.toast("Test email sent.",{type:"success"})}catch(y){E.toast(y.message||"Failed to send test email.",{type:"error"})}finally{m.disabled=!1}}})}),Domma.icons.scan()}};async function g(d){let r=[];try{r=await p("/forms")}catch{E.toast("Could not load forms.",{type:"error"})}const l=h();l&&(r=b(r,l)),T.create("#forms-table",{data:r,columns:[{key:"title",title:"Title",render:(e,t)=>{const n=document.createElement("span");n.style.cssText="display:inline-flex;align-items:center;gap:.4rem;flex-wrap:wrap;";const a=document.createElement("a");if(a.href=`#/forms/edit/${u(t.slug)}`,a.textContent=e,a.style.fontWeight="600",n.appendChild(a),t.plugin){const s=document.createElement("span");s.className="badge badge-outline",s.textContent=t.plugin,s.title=`Managed by the ${t.plugin} plugin`,s.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",n.appendChild(s)}return n.outerHTML}},{key:"slug",title:"Slug",render:e=>{const t=document.createElement("code");return t.textContent=e,t.outerHTML}},{key:"fields",title:"Field Count",render:e=>String(e?.length??0)},{key:"submissionCount",title:"Submission Count",render:e=>String(e??0)},{key:"slug",title:"Actions",render:(e,t)=>{const n=document.createElement("div");n.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const a=document.createElement("a");a.href=`#/forms/edit/${u(e)}`,a.className="btn btn-sm btn-primary",a.textContent="Edit";const s=document.createElement("a");s.href=`#/forms/${u(e)}/submissions`,s.className="btn btn-sm btn-ghost",s.textContent="Submissions";const o=document.createElement("button");return o.className="btn btn-sm btn-danger js-delete-form",o.dataset.slug=e,o.dataset.plugin=t.plugin||"",o.textContent="Delete",n.appendChild(a),n.appendChild(s),n.appendChild(o),n.outerHTML}}],emptyMessage:'No forms yet. Click "Create Form" to get started.'}),document.querySelectorAll(".js-delete-form").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.slug,n=e.dataset.plugin,a=n?`This form is managed by the <strong>${n}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete form "${t}" and all its submissions? This cannot be undone.`;if(await E.confirm(a))try{await p(`/forms/${t}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),await g(d)}catch{E.toast("Failed to delete form.",{type:"error"})}})}),Domma.icons.scan()}
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as s}from"./layouts.js";import{mediaView as f}from"./media.js";import{loginView as p}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as n}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{apiReferenceView as a}from"./api-reference.js";import{collectionsView as d}from"./collections.js";import{collectionEditorView as E}from"./collection-editor.js";import{collectionEntriesView as u}from"./collection-entries.js";import{formsView as g}from"./forms.js";import{formEditorView as v}from"./form-editor.js";import{formSubmissionsView as b}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as y}from"./view-editor.js";import{viewPreviewView as L}from"./view-preview.js";import{actionsListView as P}from"./actions-list.js";import{actionEditorView as h}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as x}from"./components.js";import{componentEditorView as j}from"./component-editor.js";import{myProfileView as q}from"./my-profile.js";import{rolesView as z}from"./roles.js";import{roleEditorView as A}from"./role-editor.js";import{effectsView as B}from"./effects.js";export const views={dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:s,media:f,login:p,users:w,userEditor:n,plugins:c,documentation:V,tutorials:l,apiReference:a,collections:d,collectionEditor:E,collectionEntries:u,forms:g,formEditor:v,formSubmissions:b,viewsList:k,viewEditor:y,viewPreview:L,actionsList:P,actionEditor:h,proDocs:D,blocks:R,blockEditor:S,components:x,componentEditor:j,myProfile:q,roles:z,roleEditor:A,effects:B,notifications:m};
1
+ import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as s}from"./layouts.js";import{mediaView as p}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as a}from"./tutorials.js";import{apiReferenceView as l}from"./api-reference.js";import{collectionsView as d}from"./collections.js";import{collectionEditorView as u}from"./collection-editor.js";import{collectionEntriesView as E}from"./collection-entries.js";import{formsView as g}from"./forms.js";import{formEditorView as v}from"./form-editor.js";import{formSubmissionsView as b}from"./form-submissions.js";import{viewsListView as j}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as y}from"./view-preview.js";import{actionsListView as h}from"./actions-list.js";import{actionEditorView as k}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as P}from"./blocks.js";import{blockEditorView as R}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as S}from"./components.js";import{componentEditorView as x}from"./component-editor.js";import{myProfileView as M}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{menusView as A}from"./menus.js";import{menuEditorView as B}from"./menu-editor.js";import{menuLocationsView as C}from"./menu-locations.js";import{projectsView as F}from"./projects.js";import{projectEditorView as G}from"./project-editor.js";import{projectDetailView as H}from"./project-detail.js";import{projectSettingsView as I}from"./project-settings.js";const J={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:F,projectEditor:G,projectDetail:H,projectSettings:I,dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:s,media:p,login:n,users:f,userEditor:w,plugins:c,documentation:V,tutorials:a,apiReference:l,collections:d,collectionEditor:u,collectionEntries:E,forms:g,formEditor:v,formSubmissions:b,viewsList:j,viewEditor:L,viewPreview:y,actionsList:h,actionEditor:k,proDocs:D,blocks:P,blockEditor:R,components:S,componentEditor:x,myProfile:M,roles:U,roleEditor:q,effects:z,notifications:m,menus:A,menuEditor:B,menuLocations:C,menusRedirect:J};
@@ -0,0 +1,20 @@
1
+ import{api as I}from"../api.js";import{colourToCss as M}from"/public/js/menu-decor.mjs";import{openIconPicker as U}from"../lib/card-builder.js";let V=1;const z=()=>"i_"+V++,G=760;function q(t,s=null,l=[]){for(const n of t||[]){const e=z();if(n&&n.type==="separator"){l.push({id:e,parentId:s,type:"separator"});continue}l.push({id:e,parentId:s,text:n.text||"",url:n.url||"",icon:n.icon||"",visibility:n.visibility||"",permission:n.permission||"",hidden:!!n.hidden,bundled:!!n.bundled,badge:n.badge?{...n.badge}:null,pill:n.pill?{...n.pill}:null,colour:n.colour||""}),Array.isArray(n.items)&&n.items.length&&q(n.items,e,l)}return l}function K(t){const s=new Map;for(const n of t)s.has(n.parentId)||s.set(n.parentId,[]),s.get(n.parentId).push(n);function l(n){return(s.get(n)||[]).map(e=>{if(e.type==="separator")return{type:"separator"};const c=l(e.id);return{text:(e.text||"").trim(),url:(e.url||"").trim(),...e.icon&&{icon:e.icon.trim()},...e.visibility&&{visibility:e.visibility},...e.permission&&{permission:e.permission},...e.hidden&&{hidden:!0},...e.bundled&&{bundled:!0},...e.badge&&(e.badge.text||e.badge.countFrom)?{badge:{...e.badge.text&&{text:e.badge.text},...e.badge.countFrom&&{countFrom:e.badge.countFrom},...e.badge.variant&&{variant:e.badge.variant}}}:{},...e.pill&&e.pill.variant?{pill:{style:e.pill.style||"filled",variant:e.pill.variant}}:{},...e.colour&&{colour:e.colour},...c.length&&{items:c}}})}return l(null)}function N(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function X(t,s){let l=0,n=s.find(e=>e.id===t);for(;n&&n.parentId;)l++,n=s.find(e=>e.id===n.parentId);return l}function y(t,s,l=()=>""){t.html("");function n(e){for(const c of s.filter(p=>p.parentId===e)){const p=X(c.id,s);if(c.type==="separator"){t.append(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${c.id}" style="margin-left:${p*28}px">
3
+ <span class="menu-tree-grip" data-icon="grip-vertical"></span>
4
+ <hr class="me-sep-line">
5
+ <button class="btn btn-sm btn-danger me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
6
+ </div>
7
+ `);continue}const S=c.badge&&(c.badge.text||c.badge.countFrom)?`<span class="dm-menu-badge">${N(String(c.badge.text||"#"))}</span>`:"",w=M(c.colour),g=w?` style="color:${N(w)}"`:"";t.append(`
8
+ <div class="menu-tree-row menu-tree-row--slim${c.hidden?" menu-tree-row--hidden":""}" data-id="${c.id}" style="margin-left:${p*28}px">
9
+ <span class="menu-tree-grip" data-icon="grip-vertical"></span>
10
+ <span class="me-row-label"${g}>${N(c.text||"(untitled)")}${S}</span>
11
+ <span class="me-row-url">${N(c.url||"")}</span>
12
+ <button class="btn btn-sm btn-ghost me-outdent" data-tooltip="Outdent"><span data-icon="chevron-left"></span></button>
13
+ <button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
14
+ <button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
15
+ <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
16
+ <button class="btn btn-sm btn-ghost me-hidden ${c.hidden?"active":""}" data-tooltip="${c.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
17
+ <button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
18
+ <button class="btn btn-sm btn-danger me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
19
+ </div>
20
+ `),n(c.id)}}n(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}function F(t,s){t.find(".menu-tree-row").each(function(){const l=$(this).data("id"),n=s.find(g=>g.id===l);if(!n||n.type==="separator")return;const e=$(this).find(".me-text");e.length&&(n.text=e.val());const c=$(this).find(".me-url");c.length&&(n.url=c.val());const p=$(this).find(".me-icon");p.length&&(n.icon=p.val());const S=$(this).find(".me-vis");S.length&&(n.visibility=S.val());const w=$(this).find(".me-perm");w.length&&(n.permission=w.val())})}function et(t,s){const l={};for(const n of s){const e=t.querySelector(`[name="${n}"]`);e&&(l[n]=e.type==="checkbox"?e.checked:e.value)}return l}function b(t,s={},...l){const n=document.createElement(t);Object.assign(n,s);for(const e of l)e!=null&&n.append(e);return n}function x(t,s){return b("label",{className:"form-field"},b("span",{className:"form-label",textContent:t}),s)}function O(t,s){const l=b("select",{className:"form-select"});return t.forEach(([n,e])=>l.add(new Option(e,n))),l.value=s??"",l}function R(t,s){const l=typeof s=="string"&&s.startsWith("#"),n=O([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":s||""),e=b("input",{type:"color",className:"form-input me-colour-hex",value:l?s:"#000000"});e.style.display=l?"":"none",n.addEventListener("change",()=>{e.style.display=n.value==="__custom__"?"":"none"});const c=x(t,n);return c.append(e),c._read=()=>n.value==="__custom__"?e.value:n.value,c}function J(t,s){const l=b("input",{className:"form-input me-icon",name:"icon",value:s||""}),n=b("span",{className:"me-icon-preview"});s&&n.setAttribute("data-icon",s);const e=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),c=p=>{p?n.setAttribute("data-icon",p):n.removeAttribute("data-icon"),Domma.icons.scan(n.parentNode||n)};return e.addEventListener("click",()=>U(p=>{l.value=p,c(p)})),l.addEventListener("input",()=>c(l.value.trim())),x(t,b("div",{className:"me-icon-field"},l,e,n))}function T(...t){return b("div",{className:"me-field-grid"},...t)}async function Q(t,s,l,n){const e=v=>b("h4",{className:"me-form-section",textContent:v}),c=x("Label",b("input",{className:"form-input",name:"text",value:t.text||""})),p=x("URL",b("input",{className:"form-input",name:"url",value:t.url||""})),S=J("Icon",t.icon||""),w=x("Visibility",b("input",{className:"form-input",name:"visibility",value:t.visibility||""})),g=O([["","\u2014 no permission gate \u2014"],...(s||[]).map(v=>[v.key,v.label||v.key])],t.permission||"");g.name="permission";const i=x("Permission",g),m=x("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),P=O([["","\u2014 none \u2014"],...(l||[]).map(v=>[v.slug,v.title||v.slug])],t.badge?.countFrom||"");P.name="badgeCount";const A=x("Badge count from",P),j=R("Badge colour",t.badge?.variant||""),a=O([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");a.name="renderAs";const o=x("Render as",a),d=x("Pill style",O([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));d.querySelector("select").name="pillStyle";const f=R("Pill colour",t.pill?.variant||""),r=b("div",{className:"me-pill-wrap"},T(d,f));r.style.display=t.pill?"":"none",a.addEventListener("change",()=>{r.style.display=a.value==="pill"?"":"none"});const u=R("Text colour",t.colour||""),h=b("button",{className:"btn btn-primary",textContent:"Apply"}),C=b("div",{className:"me-item-form"},e("Link"),c,p,S,e("Access"),T(w,i),e("Badge"),T(m,j),A,e("Appearance"),o,r,u,h),_=E.slideover({title:"Edit menu item",size:"lg",position:"right"});_.element.appendChild(C),_.open();const B=_.element.closest(".dm-slideover")||_.element.querySelector(".dm-slideover")||_.element;B.style.setProperty("width",G+"px","important"),B.style.setProperty("max-width","95vw","important"),Domma.icons.scan(C);const k=v=>C.querySelector(`[name="${v}"]`)?.value||"";h.addEventListener("click",()=>{t.text=k("text"),t.url=k("url"),t.icon=k("icon"),t.visibility=k("visibility"),t.permission=k("permission");const v=k("badgeText"),L=k("badgeCount"),W=j._read();if(t.badge=v||L?{...v&&{text:v},...L&&{countFrom:L},...W&&{variant:W}}:null,k("renderAs")==="pill"){const D=f._read();t.pill={style:k("pillStyle")||"filled",...D&&{variant:D}}}else t.pill=null;t.colour=u._read(),_.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const s=location.hash.match(/^#\/menus\/edit\/(.+)$/),l=s?decodeURIComponent(s[1]):null,n=!l;let e;if(n)e={slug:"",name:"",description:"",items:[]};else try{e=await I.menus.get(l)}catch(a){E.toast(`Failed to load menu: ${a.message||a}`,{type:"error"}),location.hash="#/menus";return}const c=await I.projects.list().catch(()=>[]),p=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),S=await I.collections.list().catch(()=>[]),w='<option value="">\u2014 no permission gate \u2014</option>',g=a=>w+(p.resources||[]).map(o=>{const d=(a||"")===o.key?" selected":"";return`<option value="${o.key}"${d}>${o.label||o.key}</option>`}).join("");t.find("#me-title").text(n?"New menu":`Edit "${e.slug}"`);let i=q(e.items);const m=t.find("#me-items-list");y(m,i,g),t.find("#me-add-item").on("click",()=>{F(m,i),i.push({id:z(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),y(m,i,g)}),t.find("#me-add-separator").on("click",()=>{F(m,i),i.push({id:z(),parentId:null,type:"separator"}),y(m,i,g)}),t.off("click",".me-remove").on("click",".me-remove",function(){const a=$(this).closest(".menu-tree-row").data("id");i=i.filter(o=>o.id!==a&&o.parentId!==a),y(m,i,g)}),t.off("click",".me-indent").on("click",".me-indent",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(r=>r.id===a);if(!o)return;const d=i.filter(r=>r.parentId===o.parentId),f=d.findIndex(r=>r.id===a);f<=0||(o.parentId=d[f-1].id,y(m,i,g))}),t.off("click",".me-outdent").on("click",".me-outdent",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(f=>f.id===a);if(!o||o.parentId==null)return;const d=i.find(f=>f.id===o.parentId);o.parentId=d?d.parentId:null,y(m,i,g)}),t.off("click",".me-up").on("click",".me-up",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.filter(u=>u.parentId===i.find(h=>h.id===a)?.parentId),d=o.findIndex(u=>u.id===a);if(d<=0)return;const f=i.indexOf(o[d]),r=i.indexOf(o[d-1]);[i[f],i[r]]=[i[r],i[f]],y(m,i,g)}),t.off("click",".me-down").on("click",".me-down",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.filter(u=>u.parentId===i.find(h=>h.id===a)?.parentId),d=o.findIndex(u=>u.id===a);if(d===-1||d>=o.length-1)return;const f=i.indexOf(o[d]),r=i.indexOf(o[d+1]);[i[f],i[r]]=[i[r],i[f]],y(m,i,g)}),t.off("click",".me-hidden").on("click",".me-hidden",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(d=>d.id===a);o&&(o.hidden=!o.hidden),y(m,i,g)}),t.off("click",".me-edit").on("click",".me-edit",function(){const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(d=>d.id===a);!o||o.type==="separator"||Q(o,p.resources||[],S,()=>y(m,i,g))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(e.variant||""),t.find("#me-position").val(e.position||""),t.find("#me-fontFamily").val(e.style?.fontFamily||""),t.find("#me-fontSize").val(e.style?.fontSize||""),t.find("#me-fontWeight").val(e.style?.fontWeight||""),t.find("#me-letterSpacing").val(e.style?.letterSpacing||""),t.find("#me-iconSize").val(e.style?.iconSize||"");const{getProjectFromHash:P}=await import("../lib/project-context.js"),A=n?P():null,j=t.find("#me-project");j.html('<option value="">\u2014 none \u2014</option>'+c.map(a=>`<option value="${N(a.slug)}">${N(a.name||a.slug)}</option>`).join("")),t.find("#me-slug").val(e.slug||""),t.find("#me-name").val(e.name||""),t.find("#me-description").val(e.description||""),j.val(e.meta?.project||A||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{F(m,i);const a={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},o={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},d=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],f=Object.fromEntries(Object.entries(a).filter(([u,h])=>d.includes(u)&&h)),r={slug:o.slug,name:o.name,description:o.description,...a.variant&&{variant:a.variant},...a.position&&{position:a.position},...Object.keys(f).length&&{style:f},items:K(i),meta:{...e.meta||{},project:o.project||null}};try{if(n)await I.menus.create(r);else if(r.slug!==e.slug){await I.menus.create(r);const u=await I.menuLocations.get();let h=!1;for(const[C,_]of Object.entries(u))_===e.slug&&(u[C]=r.slug,h=!0);h&&await I.menuLocations.save(u),await I.menus.remove(e.slug)}else await I.menus.update(e.slug,r);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(r.slug)}catch(u){E.toast(`Save failed: ${u.message||u}`,{type:"error"})}})}};
@@ -0,0 +1 @@
1
+ import{api as i}from"../api.js";function m(s){if(!Array.isArray(s)||s.length===0)return 0;let l=0;for(const c of s){const r=Array.isArray(c.items)&&c.items.length?m(c.items):0;r>l&&(l=r)}return l+1}function a(s){return String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const menuLocationsView={templateUrl:"/admin/js/templates/menu-locations.html",async onMount(s){const[l,c,r]=await Promise.all([i.menuLocations.registry().catch(()=>[]),i.menuLocations.get().catch(()=>({})),i.menus.list().catch(()=>[])]),u=await Promise.all(r.map(e=>i.menus.get(e.slug).catch(()=>null))),p={};for(const e of u)e&&e.slug&&(p[e.slug]=m(e.items||[]));const d=(l||[]).map(e=>({slot:e.slot,label:e.label,source:e.source,description:e.description,maxDepth:e.maxDepth,menu:c[e.slot]||""}));T.create("#ml-table",{data:d,emptyMessage:"No menu slots registered.",columns:[{key:"slot",title:"Slot",render:e=>`<code>${a(e)}</code>`},{key:"label",title:"Label",render:e=>a(e)},{key:"source",title:"Source",render:e=>e?`<span class="badge badge-secondary">${a(e)}</span>`:""},{key:"description",title:"Description",render:e=>e?`<span class="text-muted">${a(e)}</span>`:""},{key:"menu",title:"Mapped menu",render:(e,t)=>{const n=['<option value="">&mdash; none &mdash;</option>'].concat(r.map(o=>{const g=o.slug===t.menu?" selected":"",h=a(o.name||o.slug);return`<option value="${a(o.slug)}"${g}>${h}</option>`}));return`<select class="form-select ml-select" data-slot="${a(t.slot)}">${n.join("")}</select>`}},{key:"_warning",title:"",render:(e,t)=>{if(!t.menu||t.maxDepth==null)return"";const n=p[t.menu]||0;if(n>t.maxDepth){const o=`This slot supports max depth ${t.maxDepth}; menu is depth ${n}`;return`<span class="badge badge-warning" data-tooltip="${a(o)}">&#9888; depth</span>`}return""}}]}),Domma.icons.scan("#ml-table"),document.querySelectorAll("#ml-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),s.find("#ml-save").on("click",async()=>{const e={};s.find(".ml-select").each(function(){const t=$(this).data("slot"),n=$(this).val();t&&n&&(e[t]=n)});try{await i.menuLocations.save(e),E.toast("Locations saved.",{type:"success"})}catch(t){E.toast(`Save failed: ${t.message||t}`,{type:"error"})}})}};
@@ -0,0 +1,5 @@
1
+ import{api as o}from"../api.js";import{filterByProject as g,getProjectFromHash as b}from"../lib/project-context.js";function l(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const menusView={templateUrl:"/admin/js/templates/menus.html",async onMount(s){const[i,d]=await Promise.all([o.menus.list().catch(()=>[]),o.menuLocations.get().catch(()=>({}))]),r=b(),p=r?g(i,r):i,a={};for(const[e,t]of Object.entries(d||{}))t&&(a[t]||(a[t]=[]),a[t].push(e));T.create("#menus-table",{data:p,emptyMessage:'No menus yet. Click "New menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${l(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${l(e||t.slug)}</a>`},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))},{key:"mapped",title:"Mapped",render:(e,t)=>{const n=a[t.slug]||[];return n.length?n.map(c=>`<span class="badge badge-info">${l(c)}</span>`).join(" "):'<span class="text-muted">\u2014</span>'}},{key:"bundled",title:"Bundled",render:(e,t)=>t.meta?.bundled?'<span class="badge badge-secondary">bundled</span>':""},{key:"_actions",title:"Actions",render:(e,t)=>{const n=(a[t.slug]||[]).length>0,c=n?'data-tooltip="Unmap this menu before deleting"':"",m=n?"disabled":"",u=l(t.slug);return`
2
+ <a class="btn btn-sm btn-ghost" href="#/menus/edit/${encodeURIComponent(t.slug)}" data-tooltip="Edit"><span data-icon="edit"></span></a>
3
+ <button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${u}" data-tooltip="Duplicate"><span data-icon="copy"></span></button>
4
+ <button class="btn btn-sm btn-danger btn-delete" data-slug="${u}" ${m} ${c}><span data-icon="trash"></span></button>
5
+ `}}]}),Domma.icons.scan("#menus-table"),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),s.off("click",".btn-duplicate").on("click",".btn-duplicate",async function(){const e=$(this).data("slug");try{const t=await o.menus.duplicate(e);E.toast(`Duplicated as "${t.slug}".`,{type:"success"}),location.reload()}catch(t){E.toast(`Duplicate failed: ${t.message||t}`,{type:"error"})}}),s.off("click",".btn-delete").on("click",".btn-delete",async function(){if(this.disabled)return;const e=$(this).data("slug");if(await E.confirm(`Delete menu "${e}"? This cannot be undone.`))try{await o.menus.remove(e),E.toast("Menu deleted.",{type:"success"}),location.reload()}catch(n){E.toast(`Delete failed: ${n.message||n}`,{type:"error"})}})}};