domma-cms 0.18.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +98 -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 +19 -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 +686 -109
  91. package/server/services/menus-migration.js +107 -0
  92. package/server/services/menus.js +422 -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 +237 -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,8 +1,8 @@
1
- import{api as p}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(n){const c=E.loader(n.get(0),{type:"dots"});let o=await p.pages.list().catch(()=>[]);c.destroy();const r=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,e)=>{const a=e.bundled?'<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-info,#2563eb);border-color:var(--dm-info,#2563eb);margin-left:.35rem;">Bundled</span>':"",i=e.plugin?`<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);margin-left:.35rem;" title="Managed by the ${e.plugin} plugin">${e.plugin}</span>`:"";return`<a href="#/pages/edit${e.urlPath}" style="font-weight:600;">${t}</a>${a}${i}`}},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(e=>`<span class="badge badge-info badge-pill badge-sm">${e}</span>`).join(" "):"\u2014"},{key:"versionCount",title:"Revisions",render:t=>Number.isFinite(t)&&t>0?`<span class="badge badge-secondary badge-pill badge-sm">${t}</span>`:'<span style="color:var(--dm-text-muted);">0</span>'},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,e)=>`
1
+ import{api as p}from"../api.js";import{filterByProject as m,getProjectFromHash as f}from"../lib/project-context.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(n){const g=E.loader(n.get(0),{type:"dots"});let o=await p.pages.list().catch(()=>[]);g.destroy();const c=f();c&&(o=m(o,c));const l=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,e)=>{const a=e.bundled?'<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-info,#2563eb);border-color:var(--dm-info,#2563eb);margin-left:.35rem;">Bundled</span>':"",i=e.plugin?`<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);margin-left:.35rem;" title="Managed by the ${e.plugin} plugin">${e.plugin}</span>`:"";return`<a href="#/pages/edit${e.urlPath}" style="font-weight:600;">${t}</a>${a}${i}`}},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(e=>`<span class="badge badge-info badge-pill badge-sm">${e}</span>`).join(" "):"\u2014"},{key:"versionCount",title:"Revisions",render:t=>Number.isFinite(t)&&t>0?`<span class="badge badge-secondary badge-pill badge-sm">${t}</span>`:'<span style="color:var(--dm-text-muted);">0</span>'},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,e)=>`
2
2
  <a href="#/pages/edit${e.urlPath}" class="btn btn-sm btn-primary">Edit</a>
3
3
  <a href="${e.urlPath}" target="_blank" class="btn btn-sm btn-ghost" data-tooltip="View"><span data-icon="external-link"></span></a>
4
4
  <button class="btn btn-sm btn-ghost btn-prune" data-path="${e.urlPath}" data-count="${e.versionCount||0}" data-tooltip="Prune revisions"${(e.versionCount||0)===0?" disabled":""}><span data-icon="archive"></span></button>
5
5
  <button class="btn btn-sm btn-danger btn-delete" data-path="${e.urlPath}" data-plugin="${e.plugin||""}">Delete</button>
6
- `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};r(o);const g=s=>{const t=n.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const e=s.map(a=>{const i=a.urlPath.split("/").filter(Boolean),l=i.length>1?"/"+i.slice(0,-1).join("/"):null,d=l&&s.some(b=>b.urlPath===l);return{id:a.urlPath,parent_id:d?l:null,name:a.title||a.urlPath,icon:a.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:e,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:a=>R.navigate(`/pages/edit${a}`)}),Domma.icons.scan(t)};n.find("#view-table-btn").on("click",function(){n.find("#pages-table").show(),n.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),n.find("#view-tree-btn").on("click",function(){n.find("#pages-table").hide(),n.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),g(o)}),n.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const u=()=>{const s=n.find("#status-filter").val(),t=n.find("#pages-search").val().toLowerCase().trim(),e=o.filter(a=>!(s&&a.status!==s||t&&!`${a.title} ${a.urlPath} ${(a.tags||[]).join(" ")}`.toLowerCase().includes(t)));r(e)};n.find("#status-filter").off("change").on("change",u),n.find("#pages-search").get(0).addEventListener("input",u),n.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path"),t=$(this).data("plugin"),e=t?`This page is managed by the <strong>${t}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete page at <strong>${s}</strong>? This cannot be undone.`;if(await E.confirm(e))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),o=o.filter(a=>a.urlPath!==s),r(o)}catch{E.toast("Failed to delete page.",{type:"error"})}}),n.off("click",".btn-prune").on("click",".btn-prune",async function(){const s=$(this).data("path"),t=Number($(this).data("count"))||0,e=window.prompt(`This page has ${t} revision${t===1?"":"s"}.
6
+ `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(o);const b=s=>{const t=n.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const e=s.map(a=>{const i=a.urlPath.split("/").filter(Boolean),r=i.length>1?"/"+i.slice(0,-1).join("/"):null,d=r&&s.some(h=>h.urlPath===r);return{id:a.urlPath,parent_id:d?r:null,name:a.title||a.urlPath,icon:a.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:e,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:a=>R.navigate(`/pages/edit${a}`)}),Domma.icons.scan(t)};n.find("#view-table-btn").on("click",function(){n.find("#pages-table").show(),n.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),n.find("#view-tree-btn").on("click",function(){n.find("#pages-table").hide(),n.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),b(o)}),n.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const u=()=>{const s=n.find("#status-filter").val(),t=n.find("#pages-search").val().toLowerCase().trim(),e=o.filter(a=>!(s&&a.status!==s||t&&!`${a.title} ${a.urlPath} ${(a.tags||[]).join(" ")}`.toLowerCase().includes(t)));l(e)};n.find("#status-filter").off("change").on("change",u),n.find("#pages-search").get(0).addEventListener("input",u),n.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path"),t=$(this).data("plugin"),e=t?`This page is managed by the <strong>${t}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete page at <strong>${s}</strong>? This cannot be undone.`;if(await E.confirm(e))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),o=o.filter(a=>a.urlPath!==s),l(o)}catch{E.toast("Failed to delete page.",{type:"error"})}}),n.off("click",".btn-prune").on("click",".btn-prune",async function(){const s=$(this).data("path"),t=Number($(this).data("count"))||0,e=window.prompt(`This page has ${t} revision${t===1?"":"s"}.
7
7
 
8
- How many of the most recent revisions do you want to keep?`,"5");if(e===null)return;const a=Number.parseInt(e,10);if(!Number.isFinite(a)||a<0){E.toast("Please enter a non-negative number.",{type:"error"});return}try{const i=await p.versions.prune(s,a);E.toast(`Pruned ${i.deleted} revision${i.deleted===1?"":"s"}; kept ${i.kept}.`,{type:"success"});const l=o.find(d=>d.urlPath===s);l&&(l.versionCount=i.kept),r(o)}catch(i){E.toast(i.message||"Failed to prune revisions.",{type:"error"})}})}};
8
+ How many of the most recent revisions do you want to keep?`,"5");if(e===null)return;const a=Number.parseInt(e,10);if(!Number.isFinite(a)||a<0){E.toast("Please enter a non-negative number.",{type:"error"});return}try{const i=await p.versions.prune(s,a);E.toast(`Pruned ${i.deleted} revision${i.deleted===1?"":"s"}; kept ${i.kept}.`,{type:"success"});const r=o.find(d=>d.urlPath===s);r&&(r.versionCount=i.kept),l(o)}catch(i){E.toast(i.message||"Failed to prune revisions.",{type:"error"})}})}};
@@ -0,0 +1,4 @@
1
+ import{api as d}from"../api.js";function o(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const j=[{key:"pages",text:"Page",icon:"file-text",path:"/pages/new"},{key:"collections",text:"Collection",icon:"database",path:"/collections/new"},{key:"forms",text:"Form",icon:"layout",path:"/forms/new"},{key:"actions",text:"Action",icon:"zap",path:"/actions/new"},{key:"menus",text:"Menu",icon:"menu",path:"/menus/new"},{key:"blocks",text:"Block",icon:"box",path:"/blocks/new"},{key:"views",text:"View",icon:"eye",path:"/views/new"},{key:"roles",text:"Role",icon:"shield",path:"/roles"},{key:"users",text:"User",icon:"users",path:"/users/new"}],h={pages:"Pages",collections:"Collections",forms:"Forms",actions:"Actions",menus:"Menus",blocks:"Blocks",views:"Views",roles:"Roles",users:"Users"};export const projectDetailView={templateUrl:"/admin/js/templates/project-detail.html",async onMount(t){const i=location.hash.split("?")[0].match(/^#\/projects\/([^/]+)$/),s=i?decodeURIComponent(i[1]):null;if(!s){location.hash="#/projects";return}const a=await d.projects.get(s).catch(()=>null);if(!a){E.toast("Project not found.",{type:"error"}),location.hash="#/projects";return}const r=encodeURIComponent(s);t.find("#pd-name").text(a.name||s),t.find("#pd-icon").attr("data-icon",a.icon||"folder"),t.find("#pd-desc").text(a.description||""),t.find("#pd-settings").attr("href","#/projects/"+r+"/settings"),Domma.icons.scan(t.find(".page-header").get(0));const l=await d.projects.artefacts(s).catch(()=>({})),m=t.find("#pd-counts"),u=Object.keys(h).map(e=>{const n=Array.isArray(l[e])?l[e].length:0,c=n>0?`#/projects/${r}/${o(e)}`:null,g=c?`<a href="${c}" class="dm-card-link">`:"",f=c?"</a>":"";return`<div class="card">${g}<div class="card-body dm-count-card">
2
+ <strong>${o(h[e])}</strong>
3
+ <span class="badge badge-${n>0?"info":"secondary"}">${n}</span>
4
+ </div>${f}</div>`});m.html(`<div class="grid grid-cols-3" style="gap:1rem;">${u.join("")}</div>`);const p=t.find("#pd-quick-add");p.html(`<div class="dm-qa-grid">${j.map(e=>`<button class="btn btn-primary pd-qa-btn" data-path="${o(e.path)}" data-project="${o(s)}"><span data-icon="${o(e.icon)}"></span> ${o(e.text)}</button>`).join("")}</div>`),Domma.icons.scan(t.get(0)),p.off("click",".pd-qa-btn").on("click",".pd-qa-btn",function(){const e=$(this).data("path"),n=$(this).data("project");try{sessionStorage.setItem("__projectContext",n)}catch{}location.hash="#"+e})}};
@@ -0,0 +1 @@
1
+ import{api as l}from"../api.js";import{makeIconInput as h}from"../lib/shortcode-modal.js";export const projectEditorView={templateUrl:"/admin/js/templates/project-editor.html",async onMount(t){const n=location.hash.match(/^#\/projects\/edit\/(.+)$/),i=n?decodeURIComponent(n[1]):null,r=!i;let e={slug:"",name:"",description:"",icon:"folder",rootUrl:"",sortOrder:0};if(!r)try{e=await l.projects.get(i)}catch(o){E.toast(`Failed to load project: ${o.message||o}`,{type:"error"}),location.hash="#/projects";return}t.find("#pe-title").text(r?"New project":`Edit "${e.slug}"`);const a=t.find("#pe-slug"),p=t.find("#pe-name"),d=t.find("#pe-description"),m=t.find("#pe-rootUrl"),u=t.find("#pe-sortOrder");a.val(e.slug||""),p.val(e.name||""),d.val(e.description||""),m.val(e.rootUrl||""),u.val(e.sortOrder??0),r||a.prop("disabled",!0);const g=t.find("#pe-icon-mount").get(0),s=h("e.g. folder, users, box",e.icon||"folder");s.input.id="pe-icon",s.input.classList.add("form-input"),g.appendChild(s.el),Domma.icons.scan(t.get(0)),t.find("#pe-save").on("click",async()=>{const o={slug:a.val().trim(),name:p.val().trim(),description:d.val().trim(),icon:s.input.value.trim()||"folder",sortOrder:Number.parseInt(u.val(),10)||0},f=m.val().trim();f&&(o.rootUrl=f);try{if(r)await l.projects.create(o);else{const{slug:c,...v}=o;await l.projects.update(i,v)}E.toast("Saved.",{type:"success"}),location.hash="#/projects"}catch(c){E.toast(`Save failed: ${c.message||c}`,{type:"error"})}})}};
@@ -0,0 +1 @@
1
+ import{api as a}from"../api.js";import{makeIconInput as h}from"../lib/shortcode-modal.js";export const projectSettingsView={templateUrl:"/admin/js/templates/project-settings.html",async onMount(e){const c=location.hash.split("?")[0].match(/^#\/projects\/([^/]+)\/settings$/),s=c?decodeURIComponent(c[1]):null;if(!s){location.hash="#/projects";return}let o;try{o=await a.projects.get(s)}catch(t){E.toast(`Failed to load project: ${t.message||t}`,{type:"error"}),location.hash="#/projects";return}const f=encodeURIComponent(s);e.find("#ps-title").text(`Settings \u2014 ${o.name||s}`),e.find("#ps-back").attr("href","#/projects/"+f);const u=e.find("#ps-slug"),n=e.find("#ps-name"),i=e.find("#ps-description"),l=e.find("#ps-rootUrl"),p=e.find("#ps-sortOrder");u.val(o.slug||""),n.val(o.name||""),i.val(o.description||""),l.val(o.rootUrl||""),p.val(o.sortOrder??0);const g=e.find("#ps-icon-mount").get(0),r=h("e.g. folder, users, box",o.icon||"folder");r.input.id="ps-icon",r.input.classList.add("form-input"),g.appendChild(r.el),Domma.icons.scan(e.get(0)),e.find("#ps-save").on("click",async()=>{const t={name:n.val().trim(),description:i.val().trim(),icon:r.input.value.trim()||"folder",sortOrder:Number.parseInt(p.val(),10)||0},d=l.val().trim();d&&(t.rootUrl=d);try{await a.projects.update(s,t),E.toast("Saved.",{type:"success"})}catch(m){E.toast(`Save failed: ${m.message||m}`,{type:"error"})}}),e.find("#ps-untag-all").on("click",async()=>{if(await E.confirm(`Untag all artefacts from project "${s}"? They will become site-wide.`))try{const t=await a.projects.untagAll(s);E.toast(`Untagged: ${JSON.stringify(t.untagged)}`,{type:"success"}),setTimeout(()=>location.reload(),600)}catch(t){E.toast(`Untag failed: ${t.message||t}`,{type:"error"})}}),e.find("#ps-delete").on("click",async()=>{if(await E.confirm(`Delete project "${s}"? The server refuses if any artefacts are still tagged.`))try{await a.projects.remove(s),E.toast("Project deleted.",{type:"success"}),location.hash="#/projects"}catch(t){E.toast(`Delete refused: ${t.message||t}`,{type:"error"})}})}};
@@ -0,0 +1,7 @@
1
+ import{api as o}from"../api.js";function s(a){return String(a??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const projectsView={templateUrl:"/admin/js/templates/projects.html",async onMount(a){const n=await o.projects.list().catch(()=>[]),c=await Promise.all(n.map(t=>o.projects.artefacts(t.slug).then(e=>Object.values(e).reduce((l,p)=>l+p.length,0)).catch(()=>0))),r=n.map((t,e)=>({...t,artefactCount:c[e]}));T.create("#projects-table",{data:r,emptyMessage:'No projects yet. Click "New project" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0},{key:"name",title:"Name",sortable:!0},{key:"icon",title:"Icon",render:t=>t?`<span data-icon="${s(t)}"></span>`:""},{key:"rootUrl",title:"Root URL",render:t=>t?`<code>${s(t)}</code>`:'<span class="text-muted">\u2014</span>'},{key:"artefactCount",title:"Artefacts",render:t=>`<span class="badge badge-info">${t??0}</span>`},{key:"_actions",title:"Actions",render:(t,e)=>`
2
+ <span style="display:inline-flex;gap:0.25rem;">
3
+ <a class="btn btn-sm btn-ghost" href="#/projects/${encodeURIComponent(e.slug)}" data-tooltip="Overview"><span data-icon="eye"></span></a>
4
+ <a class="btn btn-sm btn-ghost" href="#/projects/${encodeURIComponent(e.slug)}/settings" data-tooltip="Settings"><span data-icon="settings"></span></a>
5
+ <button class="btn btn-sm btn-danger btn-delete-project" data-slug="${s(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash"></span></button>
6
+ </span>
7
+ `}]}),Domma.icons.scan(a.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),a.off("click",".btn-delete-project").on("click",".btn-delete-project",async function(){const t=$(this).data("slug");if(await E.confirm(`Delete project "${t}"? This cannot be undone.`))try{await o.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
@@ -1 +1 @@
1
- import{api as O,getUser as K}from"../api.js";const Q=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];export const roleEditorView={templateUrl:"/admin/js/templates/role-editor.html",async onMount(i){if(!K()){R.navigate("/");return}const N=window.location.hash.match(/\/roles\/edit\/([^/?#]+)/)?.[1];if(!N){R.navigate("/roles");return}E.tabs(i.find("#role-tabs").get(0));const U=E.loader(i.get(0),{type:"dots"});let j,y;try{[j,y]=await Promise.all([O.get(`/collections/roles/entries/${N}`),O.get("/auth/permissions-registry")])}catch(e){U.destroy(),E.toast(`Failed to load role: ${e.message}`,{type:"error"}),R.navigate("/roles");return}U.destroy();const c=j.data||{},L=c.level===0,I=Object.fromEntries(y.resources.map(e=>[e.key,e])),x={};y.groups.forEach(e=>{x[e]=[]}),y.resources.forEach(e=>{x[e.group]&&x[e.group].push(e.key)}),i.find("#role-title").text(`Edit Role: ${c.label||c.name}`);const S=i.find("#role-name"),M=i.find("#role-label"),A=i.find("#role-level"),B=i.find("#role-badge");Q.forEach(e=>{const t=document.createElement("option");t.value=e.value,t.textContent=e.label,c.badgeClass===e.value&&(t.selected=!0),B.get(0).appendChild(t)}),S.val(c.name||""),M.val(c.label||""),A.val(c.level??""),L&&(S.prop("disabled",!0),A.prop("disabled",!0));const _=i.find("#permissions-container").get(0),F=c.permissions||[],C={},q={},H={},V=(e,t)=>F.includes(e)||F.includes(`${e}.${t}`);y.groups.forEach(e=>{const t=x[e];if(!t.length)return;const l=document.createElement("div");l.className="card mb-3";const r=document.createElement("div");r.className="card-header",r.style.cssText="display:flex;align-items:center;justify-content:space-between;cursor:default;";const g=document.createElement("strong");g.textContent=e;const d=document.createElement("label");d.style.cssText="display:flex;align-items:center;gap:.4rem;font-size:.85rem;font-weight:normal;cursor:pointer;";const o=document.createElement("input");o.type="checkbox",o.style.cursor="pointer",d.appendChild(o),d.appendChild(document.createTextNode("Select All")),H[e]=o,r.appendChild(g),r.appendChild(d),l.appendChild(r);const p=document.createElement("div");p.className="card-body",p.style.cssText="padding:.75rem 1rem;",t.forEach((a,v)=>{const n=I[a],m=n.actions.map(s=>s.key),b=v===t.length-1,T=document.createElement("div");T.style.cssText=`margin-bottom:${b?"0":".75rem"};padding-bottom:${b?"0":".75rem"};${b?"":"border-bottom:1px solid var(--dm-border);"}`;const k=document.createElement("div");k.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem;";const W=document.createElement("span");W.dataset.icon=n.icon;const G=document.createElement("strong");G.textContent=n.label,G.style.fontSize=".9rem";const w=document.createElement("label");w.style.cssText="margin-left:auto;font-size:.8rem;display:flex;align-items:center;gap:.35rem;cursor:pointer;";const u=document.createElement("input");u.type="checkbox",u.style.cursor="pointer";const J=m.every(s=>V(a,s));u.checked=J,q[a]=u,w.appendChild(u),w.appendChild(document.createTextNode("Full Access")),k.appendChild(W),k.appendChild(G),k.appendChild(w),T.appendChild(k);const P=document.createElement("div");P.style.cssText="display:flex;gap:1.5rem;padding-left:1.5rem;flex-wrap:wrap;";const $={};n.actions.forEach(s=>{const f=document.createElement("label");f.style.cssText="display:flex;align-items:center;gap:.3rem;font-size:.875rem;cursor:pointer;";const h=document.createElement("input");h.type="checkbox",h.style.cursor="pointer",h.checked=V(a,s.key),h.dataset.res=a,h.dataset.action=s.key,C[`${a}.${s.key}`]=h,$[s.key]=h,f.appendChild(h),f.appendChild(document.createTextNode(s.label)),P.appendChild(f)}),u.addEventListener("change",()=>{m.forEach(s=>{$[s].checked=u.checked}),z(e,t)}),m.forEach(s=>{$[s].addEventListener("change",()=>{u.checked=m.every(f=>$[f].checked),z(e,t)})}),T.appendChild(P),p.appendChild(T)}),o.addEventListener("change",()=>{t.forEach(a=>{I[a].actions.map(m=>m.key).forEach(m=>{const b=C[`${a}.${m}`];b&&(b.checked=o.checked)});const n=q[a];n&&(n.checked=o.checked)})}),z(e,t),l.appendChild(p),_.appendChild(l)}),Domma.icons.scan("#permissions-container"),i.find("#btn-save-role").on("click",async()=>{const e=S.val().trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),t=M.val().trim(),l=parseInt(A.val(),10),r=B.val();if(!t){E.toast("Display label is required.",{type:"error"});return}const g=[];y.resources.forEach(o=>{const p=o.key,a=o.actions.map(n=>n.key),v=a.filter(n=>C[`${p}.${n}`]?.checked);v.length===a.length?g.push(p):v.forEach(n=>g.push(`${p}.${n}`))});const d={data:{name:L?c.name:e||c.name,label:t,level:L||isNaN(l)?c.level:l,permissions:g,badgeClass:r}};try{await O.put(`/collections/roles/entries/${N}`,d),E.toast("Role saved.",{type:"success"}),R.navigate("/roles")}catch(o){E.toast(`Save failed: ${o.message}`,{type:"error"})}});function z(e,t){const l=H[e];l&&(l.checked=t.every(r=>I[r].actions.map(d=>d.key).every(d=>C[`${r}.${d}`]?.checked)))}}};
1
+ import{api as j,getUser as X}from"../api.js";const Y=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];export const roleEditorView={templateUrl:"/admin/js/templates/role-editor.html",async onMount(o){if(!X()){R.navigate("/");return}const N=window.location.hash.match(/\/roles\/edit\/([^/?#]+)/)?.[1];if(!N){R.navigate("/roles");return}E.tabs(o.find("#role-tabs").get(0));const O=E.loader(o.get(0),{type:"dots"});let U,y,M;try{[U,y,M]=await Promise.all([j.get(`/collections/roles/entries/${N}`),j.get("/auth/permissions-registry"),j.projects.list().catch(()=>[])])}catch(e){O.destroy(),E.toast(`Failed to load role: ${e.message}`,{type:"error"}),R.navigate("/roles");return}O.destroy();const a=U.data||{},L=a.level===0,S=Object.fromEntries(y.resources.map(e=>[e.key,e])),C={};y.groups.forEach(e=>{C[e]=[]}),y.resources.forEach(e=>{C[e.group]&&C[e.group].push(e.key)}),o.find("#role-title").text(`Edit Role: ${a.label||a.name}`);const I=o.find("#role-name"),B=o.find("#role-label"),A=o.find("#role-level"),F=o.find("#role-badge");Y.forEach(e=>{const t=document.createElement("option");t.value=e.value,t.textContent=e.label,a.badgeClass===e.value&&(t.selected=!0),F.get(0).appendChild(t)});const V=o.find("#role-project");(M||[]).forEach(e=>{const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,a.meta?.project===e.slug&&(t.selected=!0),V.get(0).appendChild(t)}),I.val(a.name||""),B.val(a.label||""),A.val(a.level??""),L&&(I.prop("disabled",!0),A.prop("disabled",!0));const K=o.find("#permissions-container").get(0),q=a.permissions||[],x={},H={},W={},_=(e,t)=>q.includes(e)||q.includes(`${e}.${t}`);y.groups.forEach(e=>{const t=C[e];if(!t.length)return;const l=document.createElement("div");l.className="card mb-3";const r=document.createElement("div");r.className="card-header",r.style.cssText="display:flex;align-items:center;justify-content:space-between;cursor:default;";const g=document.createElement("strong");g.textContent=e;const d=document.createElement("label");d.style.cssText="display:flex;align-items:center;gap:.4rem;font-size:.85rem;font-weight:normal;cursor:pointer;";const p=document.createElement("input");p.type="checkbox",p.style.cursor="pointer",d.appendChild(p),d.appendChild(document.createTextNode("Select All")),W[e]=p,r.appendChild(g),r.appendChild(d),l.appendChild(r);const m=document.createElement("div");m.className="card-body",m.style.cssText="padding:.75rem 1rem;",t.forEach((n,v)=>{const i=S[n],s=i.actions.map(c=>c.key),b=v===t.length-1,T=document.createElement("div");T.style.cssText=`margin-bottom:${b?"0":".75rem"};padding-bottom:${b?"0":".75rem"};${b?"":"border-bottom:1px solid var(--dm-border);"}`;const k=document.createElement("div");k.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem;";const J=document.createElement("span");J.dataset.icon=i.icon;const G=document.createElement("strong");G.textContent=i.label,G.style.fontSize=".9rem";const w=document.createElement("label");w.style.cssText="margin-left:auto;font-size:.8rem;display:flex;align-items:center;gap:.35rem;cursor:pointer;";const u=document.createElement("input");u.type="checkbox",u.style.cursor="pointer";const Q=s.every(c=>_(n,c));u.checked=Q,H[n]=u,w.appendChild(u),w.appendChild(document.createTextNode("Full Access")),k.appendChild(J),k.appendChild(G),k.appendChild(w),T.appendChild(k);const P=document.createElement("div");P.style.cssText="display:flex;gap:1.5rem;padding-left:1.5rem;flex-wrap:wrap;";const $={};i.actions.forEach(c=>{const f=document.createElement("label");f.style.cssText="display:flex;align-items:center;gap:.3rem;font-size:.875rem;cursor:pointer;";const h=document.createElement("input");h.type="checkbox",h.style.cursor="pointer",h.checked=_(n,c.key),h.dataset.res=n,h.dataset.action=c.key,x[`${n}.${c.key}`]=h,$[c.key]=h,f.appendChild(h),f.appendChild(document.createTextNode(c.label)),P.appendChild(f)}),u.addEventListener("change",()=>{s.forEach(c=>{$[c].checked=u.checked}),z(e,t)}),s.forEach(c=>{$[c].addEventListener("change",()=>{u.checked=s.every(f=>$[f].checked),z(e,t)})}),T.appendChild(P),m.appendChild(T)}),p.addEventListener("change",()=>{t.forEach(n=>{S[n].actions.map(s=>s.key).forEach(s=>{const b=x[`${n}.${s}`];b&&(b.checked=p.checked)});const i=H[n];i&&(i.checked=p.checked)})}),z(e,t),l.appendChild(m),K.appendChild(l)}),Domma.icons.scan("#permissions-container"),o.find("#btn-save-role").on("click",async()=>{const e=I.val().trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),t=B.val().trim(),l=parseInt(A.val(),10),r=F.val();if(!t){E.toast("Display label is required.",{type:"error"});return}const g=[];y.resources.forEach(m=>{const n=m.key,v=m.actions.map(s=>s.key),i=v.filter(s=>x[`${n}.${s}`]?.checked);i.length===v.length?g.push(n):i.forEach(s=>g.push(`${n}.${s}`))});const d=V.val()||"",p={data:{name:L?a.name:e||a.name,label:t,level:L||isNaN(l)?a.level:l,permissions:g,badgeClass:r,meta:{...a.meta||{},project:d||null}}};try{await j.put(`/collections/roles/entries/${N}`,p),E.toast("Role saved.",{type:"success"}),R.navigate("/roles")}catch(m){E.toast(`Save failed: ${m.message}`,{type:"error"})}});function z(e,t){const l=W[e];l&&(l.checked=t.every(r=>S[r].actions.map(d=>d.key).every(d=>x[`${r}.${d}`]?.checked)))}}};
@@ -1,4 +1,4 @@
1
- import{api as y,getUser as h}from"../api.js";const C=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];function N(t){if(!t||!t.length)return"<em>None</em>";const f=new Set(t.map(o=>o.split(".")[0])),b=t.every(o=>!o.includes(".")),i=f.size;return`${i} resource${i!==1?"s":""} ${b?"(full)":"(partial)"}`}function $(t){return String(t).replace(/"/g,"&quot;")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(t){if(!h()){R.navigate("/");return}const b=E.loader(t.get(0),{type:"dots"});let i=[];const o=async()=>{i=(await y.get("/collections/roles/entries?limit=100")).entries||[]},v=()=>{T.create("#roles-table",{data:i,columns:[{key:"data",title:"Label",render:e=>`<span class="badge ${e.badgeClass||"badge-secondary"}">${e.label||""}</span>`},{key:"data",title:"Name",render:e=>`<code>${e.name||""}</code>`},{key:"data",title:"Level",render:e=>e.level??""},{key:"data",title:"Permissions",render:e=>N(e.permissions)},{key:"id",title:"Actions",render:(e,a)=>{const n=a.data?.level===0;return`
1
+ import{api as y,getUser as C}from"../api.js";import{getProjectFromHash as N}from"../lib/project-context.js";const $=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];function k(a){if(!a||!a.length)return"<em>None</em>";const f=new Set(a.map(c=>c.split(".")[0])),b=a.every(c=>!c.includes(".")),l=f.size;return`${l} resource${l!==1?"s":""} ${b?"(full)":"(partial)"}`}function w(a){return String(a).replace(/"/g,"&quot;")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(a){if(!C()){R.navigate("/");return}const b=E.loader(a.get(0),{type:"dots"});let l=[];const c=N(),v=async()=>{l=(await y.get("/collections/roles/entries?limit=100")).entries||[],c&&(l=l.filter(t=>t?.data?.meta?.project===c))},h=()=>{T.create("#roles-table",{data:l,columns:[{key:"data",title:"Label",render:e=>`<span class="badge ${e.badgeClass||"badge-secondary"}">${e.label||""}</span>`},{key:"data",title:"Name",render:e=>`<code>${e.name||""}</code>`},{key:"data",title:"Level",render:e=>e.level??""},{key:"data",title:"Permissions",render:e=>k(e.permissions)},{key:"id",title:"Actions",render:(e,t)=>{const n=t.data?.level===0;return`
2
2
  <a href="#/roles/edit/${e}" class="btn btn-sm btn-primary">Edit</a>
3
- ${n?"":`<button class="btn btn-sm btn-danger btn-delete-role" data-id="${e}" data-name="${$(a.data?.label)}">Delete</button>`}
4
- `}}],emptyMessage:"No roles found."}),Domma.icons.scan()};try{await o()}finally{b.destroy()}v(),document.addEventListener("click",async function e(a){if(!document.contains(t.get(0))){document.removeEventListener("click",e);return}const n=a.target.closest(".btn-delete-role");if(!n)return;const m=n.dataset.id,u=n.dataset.name;if(await E.confirm(`Delete role <strong>${u}</strong>? This cannot be undone.`))try{await y.delete(`/collections/roles/entries/${m}`),E.toast("Role deleted.",{type:"success"}),await o(),v()}catch(p){E.toast(`Failed: ${p.message}`,{type:"error"})}}),t.find("#btn-add-role").on("click",()=>{const e=document.createElement("div");e.style.cssText="display:flex;flex-direction:column;gap:1rem;padding:1rem;";const a=(l,c)=>{const s=document.createElement("div"),r=document.createElement("label");return r.textContent=l,r.style.cssText="display:block;font-size:.85rem;margin-bottom:.25rem;font-weight:600;",s.appendChild(r),s.appendChild(c),s},n=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. moderator"}),m=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. Moderator"}),u=Object.assign(document.createElement("input"),{type:"number",className:"form-input",value:"5",min:"1"}),d=document.createElement("select");d.className="form-input",C.forEach(l=>{const c=Object.assign(document.createElement("option"),{value:l.value,textContent:l.label});d.appendChild(c)});const p=Object.assign(document.createElement("button"),{className:"btn btn-primary",textContent:"Create Role"});e.appendChild(a("Name (slug)",n)),e.appendChild(a("Display Label",m)),e.appendChild(a("Level",u)),e.appendChild(a("Badge Colour",d)),e.appendChild(p);const g=E.modal({title:"Add Role",size:"sm"});g.element.appendChild(e),g.open(),p.addEventListener("click",async()=>{const l=n.value.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),c=m.value.trim(),s=parseInt(u.value,10);if(!l||!c||isNaN(s)||s<1){E.toast("Name, label and level (\u2265 1) are required.",{type:"error"});return}try{const r=await y.post("/collections/roles/entries",{data:{name:l,label:c,level:s,permissions:[],badgeClass:d.value}});g.close(),E.toast("Role created. Set permissions in the editor.",{type:"success"}),R.navigate(`/roles/edit/${r.id}`)}catch(r){E.toast(`Error: ${r.message}`,{type:"error"})}})})}};
3
+ ${n?"":`<button class="btn btn-sm btn-danger btn-delete-role" data-id="${e}" data-name="${w(t.data?.label)}">Delete</button>`}
4
+ `}}],emptyMessage:"No roles found."}),Domma.icons.scan()};try{await v()}finally{b.destroy()}h(),document.addEventListener("click",async function e(t){if(!document.contains(a.get(0))){document.removeEventListener("click",e);return}const n=t.target.closest(".btn-delete-role");if(!n)return;const m=n.dataset.id,u=n.dataset.name;if(await E.confirm(`Delete role <strong>${u}</strong>? This cannot be undone.`))try{await y.delete(`/collections/roles/entries/${m}`),E.toast("Role deleted.",{type:"success"}),await v(),h()}catch(p){E.toast(`Failed: ${p.message}`,{type:"error"})}}),a.find("#btn-add-role").on("click",()=>{const e=document.createElement("div");e.style.cssText="display:flex;flex-direction:column;gap:1rem;padding:1rem;";const t=(r,i)=>{const s=document.createElement("div"),o=document.createElement("label");return o.textContent=r,o.style.cssText="display:block;font-size:.85rem;margin-bottom:.25rem;font-weight:600;",s.appendChild(o),s.appendChild(i),s},n=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. moderator"}),m=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. Moderator"}),u=Object.assign(document.createElement("input"),{type:"number",className:"form-input",value:"5",min:"1"}),d=document.createElement("select");d.className="form-input",$.forEach(r=>{const i=Object.assign(document.createElement("option"),{value:r.value,textContent:r.label});d.appendChild(i)});const p=Object.assign(document.createElement("button"),{className:"btn btn-primary",textContent:"Create Role"});e.appendChild(t("Name (slug)",n)),e.appendChild(t("Display Label",m)),e.appendChild(t("Level",u)),e.appendChild(t("Badge Colour",d)),e.appendChild(p);const g=E.modal({title:"Add Role",size:"sm"});g.element.appendChild(e),g.open(),p.addEventListener("click",async()=>{const r=n.value.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),i=m.value.trim(),s=parseInt(u.value,10);if(!r||!i||isNaN(s)||s<1){E.toast("Name, label and level (\u2265 1) are required.",{type:"error"});return}try{const o=await y.post("/collections/roles/entries",{data:{name:r,label:i,level:s,permissions:[],badgeClass:d.value}});g.close(),E.toast("Role created. Set permissions in the editor.",{type:"success"}),R.navigate(`/roles/edit/${o.id}`)}catch(o){E.toast(`Error: ${o.message}`,{type:"error"})}})})}};
@@ -1 +1 @@
1
- export const tutorialsView={templateUrl:"/admin/js/templates/tutorials.html",async onMount(t){E.tabs(t.find("#tutorials-tabs").get(0)),Domma.icons.scan(),Domma.syntax.scan()}};
1
+ import{mountScaffolder as a}from"../lib/crud-tutorial.js";export const tutorialsView={templateUrl:"/admin/js/templates/tutorials.html",async onMount(t){E.tabs(t.find("#tutorials-tabs").get(0));const o=t.find("#tutorial-scaffolder-mount").get(0);o&&a(o,null,null),Domma.icons.scan(),Domma.syntax.scan()}};
@@ -1 +1 @@
1
- import{api as n}from"../api.js";function k(c){return{string:"string",email:"email",tel:"string",number:"number",textarea:"textarea",select:"select",radio:"select",checkbox:"boolean","checkbox-group":"select",date:"string",time:"string",url:"string",hidden:"string"}[c]||"string"}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(c){const d=window.location.hash.match(/#\/users\/edit\/([^/]+)/),u=d?d[1]:null,r=!!u,[o,m,f]=await Promise.all([r?n.users.get(u).catch(()=>null):Promise.resolve(null),n.collections.listEntries("roles",{limit:100}).catch(()=>null),n.collections.get("user-profiles").catch(()=>null)]);if(r&&!o){E.toast("User not found.",{type:"error"}),R.navigate("/users");return}c.find("#editor-title").text(r?"Edit User":"New User"),c.find("#cancel-btn").on("click",()=>R.navigate("/users"));const h=(m?.entries||[]).map(t=>({value:t.data.name,label:t.data.label})).sort((t,a)=>{const e=l=>m.entries.find(s=>s.data.name===l)?.data?.level??99;return e(t.value)-e(a.value)}),y=[{value:"admin",label:"Admin"},{value:"manager",label:"Manager"},{value:"editor",label:"Editor"},{value:"subscriber",label:"Subscriber"}],v=h.length>0?h:y,w={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Jane Smith"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"jane@example.com"}},role:{type:"select",required:!0,label:"Role",options:v},isActive:{type:"boolean",label:"Active account"},password:{type:"password",required:!r,minLength:r?0:8,label:r?"New Password":"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:r?"Leave blank to keep current password":"Minimum 8 characters"}}},U=o?{name:o.name||"",email:o.email||"",role:o.role||"editor",isActive:o.isActive!==!1}:{role:"editor",isActive:!0};F.render("#user-form-container",w,U,{layout:"stacked",submitText:r?"Update User":"Create User",onSubmit:async t=>{if(r&&t.password&&t.password.length<8)return E.toast("Password must be at least 8 characters.",{type:"warning"}),!1;const a={name:t.name,email:t.email,role:t.role,isActive:!!t.isActive};t.password&&(a.password=t.password);try{let e;r?e=await n.users.update(u,a):e=await n.users.create(a);const l=f?.fields||[];if(l.length>0){const s={};for(const p of l)p.name in t&&(s[p.name]=t[p.name]);const i=r?u:e?.id;i&&Object.keys(s).length>0&&await n.users.update(i,{profile:s})}E.toast("User saved successfully.",{type:"success"}),R.navigate("/users")}catch(e){return E.toast(`Save failed: ${e.message||"Unknown error"}`,{type:"error"}),!1}}});const b=f?.fields||[];if(b.length>0){c.find("#profile-card").show();const t={},a={};for(const e of b){const s={type:k(e.type),label:e.label||e.name};e.required&&(s.required=!0),e.placeholder&&(s.formConfig={placeholder:e.placeholder}),e.options&&Array.isArray(e.options)&&(s.options=e.options.map(i=>typeof i=="string"?{value:i,label:i}:i)),t[e.name]=s,a[e.name]=o?.profile?.[e.name]??""}F.render("#profile-form-container",t,a,{layout:"stacked",submitText:r?"Update Profile":"Save Profile",onSubmit:async e=>{const l=u;if(!l)return E.toast("Save the user first, then update the profile.",{type:"info"}),!1;try{await n.users.update(l,{profile:e}),E.toast("Profile saved.",{type:"success"})}catch(s){return E.toast(`Profile save failed: ${s.message||"Unknown error"}`,{type:"error"}),!1}}})}Domma.icons.scan()}};
1
+ import{api as l}from"../api.js";function B(d){return{string:"string",email:"email",tel:"string",number:"number",textarea:"textarea",select:"select",radio:"select",checkbox:"boolean","checkbox-group":"select",date:"string",time:"string",url:"string",hidden:"string"}[d]||"string"}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(d){const y=window.location.hash.match(/#\/users\/edit\/([^/]+)/),u=y?y[1]:null,r=!!u,[o,b,g,x]=await Promise.all([r?l.users.get(u).catch(()=>null):Promise.resolve(null),l.collections.listEntries("roles",{limit:100}).catch(()=>null),l.collections.get("user-profiles").catch(()=>null),l.projects.list().catch(()=>[])]);if(r&&!o){E.toast("User not found.",{type:"error"}),R.navigate("/users");return}d.find("#editor-title").text(r?"Edit User":"New User"),d.find("#cancel-btn").on("click",()=>R.navigate("/users"));const w=(b?.entries||[]).map(e=>({value:e.data.name,label:e.data.label})).sort((e,m)=>{const t=n=>b.entries.find(a=>a.data.name===n)?.data?.level??99;return t(e.value)-t(m.value)}),k=[{value:"admin",label:"Admin"},{value:"manager",label:"Manager"},{value:"editor",label:"Editor"},{value:"subscriber",label:"Subscriber"}],v=w.length>0?w:k,j=(x||[]).map(e=>({value:e.slug,label:e.name||e.slug})),C=[{value:"",label:"(none)"},...j],O={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Jane Smith"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"jane@example.com"}},role:{type:"select",required:!0,label:"Primary role",options:v,formConfig:{tooltip:"Shown as the user's badge. Used for hierarchy comparisons."}},additionalRoles:{type:"multiselect",label:"Additional roles",options:v,formConfig:{placeholder:"",tooltip:"Optional \u2014 extra roles whose permissions also apply. Useful for someone who is both, say, a recruiter AND a content editor."}},ownedByProject:{type:"select",label:"Owned by project",options:C,formConfig:{tooltip:"Administrative tag \u2014 controls where this user appears in the sidebar Projects section. Mapped to meta.project."}},projects:{type:"multiselect",label:"Project access",options:j,formConfig:{placeholder:"",tooltip:"Access scope \u2014 limits which artefacts the user can see and edit. Leave empty for no restriction."}},isActive:{type:"boolean",label:"Active account"},password:{type:"password",required:!r,minLength:r?0:8,label:r?"New Password":"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:r?"Leave blank to keep current password":"Minimum 8 characters"}}},S=o?{name:o.name||"",email:o.email||"",role:o.role||"editor",additionalRoles:Array.isArray(o.additionalRoles)?o.additionalRoles:[],ownedByProject:o.meta?.project||"",projects:Array.isArray(o.projects)?o.projects:[],isActive:o.isActive!==!1}:{role:"editor",additionalRoles:[],ownedByProject:"",projects:[],isActive:!0};F.render("#user-form-container",O,S,{layout:"stacked",submitText:r?"Update User":"Create User",onSubmit:async e=>{if(r&&e.password&&e.password.length<8)return E.toast("Password must be at least 8 characters.",{type:"warning"}),!1;const t=(Array.isArray(e.additionalRoles)?e.additionalRoles:[]).filter(s=>s&&s!==e.role).filter((s,p,i)=>i.indexOf(s)===p),a=(Array.isArray(e.projects)?e.projects:[]).filter(Boolean).filter((s,p,i)=>i.indexOf(s)===p),c=e.ownedByProject||null,f={name:e.name,email:e.email,role:e.role,additionalRoles:t,projects:a,meta:{...o?.meta||{},project:c},isActive:!!e.isActive};e.password&&(f.password=e.password);try{let s;r?s=await l.users.update(u,f):s=await l.users.create(f);const p=g?.fields||[];if(p.length>0){const i={};for(const h of p)h.name in e&&(i[h.name]=e[h.name]);const P=r?u:s?.id;P&&Object.keys(i).length>0&&await l.users.update(P,{profile:i})}E.toast("User saved successfully.",{type:"success"}),R.navigate("/users")}catch(s){return E.toast(`Save failed: ${s.message||"Unknown error"}`,{type:"error"}),!1}}});const A=g?.fields||[];if(A.length>0){d.find("#profile-card").show();const e={},m={};for(const t of A){const a={type:B(t.type),label:t.label||t.name};t.required&&(a.required=!0),t.placeholder&&(a.formConfig={placeholder:t.placeholder}),t.options&&Array.isArray(t.options)&&(a.options=t.options.map(c=>typeof c=="string"?{value:c,label:c}:c)),e[t.name]=a,m[t.name]=o?.profile?.[t.name]??""}F.render("#profile-form-container",e,m,{layout:"stacked",submitText:r?"Update Profile":"Save Profile",onSubmit:async t=>{const n=u;if(!n)return E.toast("Save the user first, then update the profile.",{type:"info"}),!1;try{await l.users.update(n,{profile:t}),E.toast("Profile saved.",{type:"success"})}catch(a){return E.toast(`Profile save failed: ${a.message||"Unknown error"}`,{type:"error"}),!1}}})}Domma.icons.scan()}};
@@ -1,4 +1,4 @@
1
- import{api as c,getUser as p}from"../api.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(a){const i=p(),b=E.loader(a.get(0),{type:"dots"});let[n,g]=await Promise.all([c.users.list().catch(()=>[]),c.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);b.destroy();const o=g.entries||[],d=r=>{T.create("#users-table",{data:r,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:e=>{const t=o.find(m=>m.data?.name===e),s=t?.data?.badgeClass||"badge-secondary",l=t?.data?.label||e;return`<span class="badge ${s}">${l}</span>`}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const s=t.id===i?.id,l=Object.fromEntries(o.map(u=>[u.data?.name,u.data?.level??99]));return(l[i?.role]??99)<(l[t.role]??99)?`
1
+ import{api as d,getUser as k}from"../api.js";import{filterByProject as M,getProjectFromHash as A}from"../lib/project-context.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(r){const m=k(),y=E.loader(r.get(0),{type:"dots"});let[n,f]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);y.destroy();const u=A();u&&(n=M(n,u));const c=f.entries||[],b=l=>{T.create("#users-table",{data:l,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",g=a?.data?.label||e,p=`<span class="badge ${i}">${g}</span>`,o=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return p+o}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(o=>[o.data?.name,o.data?.level??99]));return(i[m?.role]??99)<(i[t.role]??99)?`
2
2
  <a href="#/users/edit/${t.id}" class="btn btn-sm btn-primary">Edit</a>
3
- ${s?"":`<button class="btn btn-sm btn-danger btn-delete-user" data-id="${t.id}" data-name="${y(t.name)}">Delete</button>`}
4
- `:""}}],emptyMessage:"No users found."}),Domma.icons.scan()};d(n),a.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const r=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await c.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(s=>s.id!==r),d(n)}catch(s){E.toast(`Failed to delete: ${s.message}`,{type:"error"})}})}};function y(a){return String(a).replace(/"/g,"&quot;")}
3
+ ${a?"":`<button class="btn btn-sm btn-danger btn-delete-user" data-id="${t.id}" data-name="${j(t.name)}">Delete</button>`}
4
+ `:""}}],emptyMessage:"No users found."}),Domma.icons.scan()};b(n),r.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const l=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${l}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==l),b(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function j(r){return String(r).replace(/"/g,"&quot;")}
@@ -1 +1 @@
1
- import{api as v}from"../api.js";let g=null,f=null;export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){g=null,f=null;const t=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);t&&(g=t[1]),E.tabs(e.find("#view-editor-tabs").get(0)),await T(e),await D(e),await O(e),await L(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await N(e)}),g&&(e.find("#view-editor-title").text("Edit View"),await _(e,g)),e.find("#add-stage-btn").off("click").on("click",()=>{const n=e.find("#add-stage-type").val()||"$match";$(e,{type:n,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await P(e)}),H(e),G(e),Domma.icons.scan()}};async function T(e){const t=e.find("#view-source").get(0);if(t)try{(await v.collections.list()).forEach(n=>{const a=document.createElement("option");a.value=n.slug,a.textContent=`${n.title} (${n.slug})`,t.appendChild(a)})}catch{}}async function L(e){const t=e.find("#view-block-name").get(0);if(t)try{(await v.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-connection").get(0);if(t)try{const n=await v.collections.getConnections();Object.keys(n).forEach(a=>{if(!t.querySelector(`option[value="${a}"]`)){const l=document.createElement("option");l.value=a,l.textContent=a,t.appendChild(l)}})}catch{}}async function O(e){const t=e.find("#view-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="view-role-cb",l.checked=n==="admin",a.appendChild(l),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function N(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await v.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");a&&(n==="$match"&&V(a),n==="$sort"&&I(a))}),q(e,null)}async function _(e,t){try{const n=await v.views.get(t);if(!n){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const a=n.pipeline?.source;a&&(e.find("#view-source").val(a),await N(e)),j(e,n)}catch(n){E.toast(n.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function j(e,t){e.find("#view-title").val(t.title||""),e.find("#view-slug").val(t.slug||""),e.find("#view-description").val(t.description||""),e.find("#view-source").val(t.pipeline?.source||""),e.find("#view-bundled").prop("checked",!!t.bundled),e.find("#view-connection").val(t.connection||"default"),e.find("#view-display-mode").val(t.display?.mode||"table"),e.find("#view-page-size").val(t.display?.pageSize||25),e.find("#view-block-name").val(t.display?.block||"");const n=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=n.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const a=t.access?.rowLevel;a&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(a.mode||"owner"),e.find("#view-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(a.field||"")));const l=e.find("#pipeline-stages-list").get(0);if(l)for(;l.firstChild;)l.removeChild(l.firstChild);(t.pipeline?.stages||[]).forEach(i=>$(e,i)),q(e,t.display?.columns||[]),x(e)}const z=[{value:"eq",label:"= equals"},{value:"ne",label:"\u2260 not equals"},{value:"gt",label:"> greater than"},{value:"lt",label:"< less than"},{value:"gte",label:"\u2265 greater or equal"},{value:"lte",label:"\u2264 less or equal"},{value:"contains",label:"~ contains (regex)"},{value:"in",label:"\u2208 in (comma list)"}];function y(e=!1){const t=[];return e&&t.push({value:"",label:"\u2014 select field \u2014"}),(f||[]).forEach(n=>{t.push({value:`data.${n.name}`,label:`${n.label||n.name} (${n.name})`})}),t.length===0||t.length===1&&e?(t.push({value:"__meta.createdAt",label:"Created At"}),t.push({value:"__meta.updatedAt",label:"Updated At"})):(t.push({value:"__meta.createdAt",label:"Created At (meta)"}),t.push({value:"__meta.updatedAt",label:"Updated At (meta)"})),t}function h(e,t){const n=document.createElement("select");return n.className="form-input form-input--sm",e.forEach(a=>{const l=document.createElement("option");l.value=a.value,l.textContent=a.label,String(a.value)===String(t)&&(l.selected=!0),n.appendChild(l)}),n}function k(e,t,n="text"){const a=document.createElement("input");return a.type=n,a.className="form-input form-input--sm",a.placeholder=e,a.value=t??"",a}function F(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(n=>n.name===t)||null}function b(e,t,n=""){const a=F(e);if(a?.type==="select"&&(t==="eq"||t==="ne")&&a.options?.length){const d=document.createElement("select");d.className="form-input form-input--sm match-val",d.style.flex="1";const c=document.createElement("option");return c.value="",c.textContent="\u2014 select value \u2014",d.appendChild(c),(a.options||[]).forEach(o=>{const s=typeof o=="string"?o:o.value??"",r=typeof o=="string"?o:o.label||s;if(!s||s==="undefined")return;const u=document.createElement("option");u.value=s,u.textContent=r,s===n&&(u.selected=!0),d.appendChild(u)}),d}const l=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",i=document.createElement("input");return i.type="text",i.className="form-input form-input--sm match-val",i.style.flex="1",i.placeholder=l,i.value=n,i}function S(e){const t=[],n={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([a,l])=>{if(typeof l=="object"&&l!==null&&!Array.isArray(l)){if("$regex"in l){t.push({field:a,op:"contains",val:String(l.$regex??"")});return}Object.entries(l).forEach(([i,d])=>{const c=n[i];c&&t.push({field:a,op:c,val:Array.isArray(d)?d.join(", "):String(d)})})}else t.push({field:a,op:"eq",val:String(l??"")})}),t}function C(e,t,n="",a="eq",l=""){const i=document.createElement("div");i.className="match-condition-row",i.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const d=h([{value:"",label:"\u2014 field \u2014"},...y()],n);d.className+=" match-field",d.style.minWidth="140px";const c=h(z,a);c.className+=" match-op",c.style.minWidth="160px";const o=document.createElement("div");o.className="match-val-wrap",o.style.cssText="flex:1;min-width:120px;display:flex;",o.appendChild(b(n,a,l));const s=()=>{const m=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(d.value,c.value,m))};d.addEventListener("change",s),c.addEventListener("change",s);const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost",r.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),r.appendChild(u),r.addEventListener("click",()=>i.remove()),i.appendChild(d),i.appendChild(c),i.appendChild(o),i.appendChild(r),e.insertBefore(i,t),Domma.icons.scan(i)}function B(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$match||t||{},a="$or"in n;let l=[];a?(n.$or||[]).forEach(r=>S(r).forEach(u=>l.push(u))):l=S(n);const i=document.createElement("div");i.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const d=document.createElement("label");d.className="form-label",d.style.marginBottom="0",d.textContent="Match:";const c=document.createElement("select");c.className="form-input form-input--sm match-logic",c.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(r=>{const u=document.createElement("option");u.value=r.value,u.textContent=r.label,(a?"or":"and")===r.value&&(u.selected=!0),c.appendChild(u)}),i.appendChild(d),i.appendChild(c),e.appendChild(i);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm match-add-btn";const s=document.createElement("span");s.setAttribute("data-icon","plus"),o.appendChild(s),o.appendChild(document.createTextNode(" Add Condition")),o.addEventListener("click",()=>{C(e,o),Domma.icons.scan(e)}),e.appendChild(o),l.forEach(r=>C(e,o,r.field,r.op,r.val)),l.length===0&&C(e,o),Domma.icons.scan(e)}function V(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const n=t.querySelector(".match-field");if(n){const d=n.value,c=h([{value:"",label:"\u2014 field \u2014"},...y()],d);c.className=n.className,c.style.cssText=n.style.cssText;const o=t.querySelector(".match-val-wrap"),s=t.querySelector(".match-op");o&&s&&c.addEventListener("change",()=>{const r=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(c.value,s.value,r))}),n.parentNode.replaceChild(c,n)}const a=t.querySelector(".match-val-wrap"),l=t.querySelector(".match-field")?.value,i=t.querySelector(".match-op")?.value;if(a&&l&&i){const d=a.querySelector(".match-val")?.value??"";for(;a.firstChild;)a.removeChild(a.firstChild);a.appendChild(b(l,i,d))}})}function W(e){const t=e.querySelector(".match-logic")?.value||"and",n=l=>{const i=l.querySelector(".match-field")?.value?.trim(),d=l.querySelector(".match-op")?.value,c=l.querySelector(".match-val")?.value?.trim()??"";if(!i)return null;const o=i.startsWith("__")?i.slice(2):i,s={};switch(d){case"eq":s[o]=c;break;case"ne":s[o]={$ne:c};break;case"gt":s[o]={$gt:isNaN(c)?c:Number(c)};break;case"lt":s[o]={$lt:isNaN(c)?c:Number(c)};break;case"gte":s[o]={$gte:isNaN(c)?c:Number(c)};break;case"lte":s[o]={$lte:isNaN(c)?c:Number(c)};break;case"contains":s[o]={$regex:c,$options:"i"};break;case"in":s[o]={$in:c.split(",").map(r=>r.trim()).filter(Boolean)};break}return Object.keys(s).length?s:null},a=[];return e.querySelectorAll(".match-condition-row").forEach(l=>{const i=n(l);i&&a.push(i)}),a.length===0?{}:t==="or"?{$or:a}:Object.assign({},...a)}function J(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$sort||t||{},a=Object.entries(n),l=document.createElement("button");l.type="button",l.className="btn btn-ghost btn-sm";const i=document.createElement("span");i.setAttribute("data-icon","plus"),l.appendChild(i),l.appendChild(document.createTextNode(" Add Sort")),e.appendChild(l);const d=(c="",o=1)=>{const s=document.createElement("div");s.className="sort-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=h([{value:"",label:"\u2014 field \u2014"},...y()],c);r.className+=" sort-field";const u=h([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(o));u.className+=" sort-dir";const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove()}),s.appendChild(r),s.appendChild(u),s.appendChild(m),e.insertBefore(s,l),Domma.icons.scan(s)};l.addEventListener("click",()=>{d()}),a.forEach(([c,o])=>d(c,o)),a.length===0&&d(),Domma.icons.scan(e)}function I(e){e.querySelectorAll(".sort-row").forEach(t=>{const n=t.querySelector(".sort-field"),a=n?.value,l=h([{value:"",label:"\u2014 field \u2014"},...y()],a);l.className=n.className,n.parentNode.replaceChild(l,n)})}function M(e){const t={};return e.querySelectorAll(".sort-row").forEach(n=>{const a=n.querySelector(".sort-field")?.value?.trim(),l=parseInt(n.querySelector(".sort-dir")?.value,10)||1;if(!a)return;const i=a.startsWith("__")?a.slice(2):a;t[i]=l}),t}function $(e,t){const n=e.find("#pipeline-stages-list").get(0);if(!n)return;const a=n.querySelector(".stage-empty-placeholder");a&&a.remove();const l=["$match","$sort"].includes(t.type),i=document.createElement("div");i.className="card mb-2 stage-card",i.dataset.stageType=t.type,l&&(i.dataset.guided="true");const d=document.createElement("div");d.className="card-header",d.style.cssText="display:flex;align-items:center;gap:.5rem;";const c=document.createElement("code");c.textContent=t.type,c.style.cssText="flex:1;font-size:.85rem;";const o=document.createElement("button");o.type="button",o.className="btn btn-sm btn-danger";const s=document.createElement("span");s.setAttribute("data-icon","trash-2"),o.appendChild(s),o.addEventListener("click",()=>{i.remove(),n.querySelector(".stage-card")||n.appendChild(U())}),d.appendChild(c),d.appendChild(o);const r=document.createElement("div");if(r.className="card-body",l){const u=document.createElement("div");u.className="stage-guided",r.appendChild(u),t.type==="$match"&&B(u,t.config),t.type==="$sort"&&J(u,t.config)}else{const u=document.createElement("label");u.className="form-label",u.textContent="Stage Config (JSON)";const m=document.createElement("small");m.className="text-muted",m.style.cssText="display:block;margin-bottom:.4rem;",m.textContent=`Enter the inner config for ${t.type} \u2014 e.g. for $lookup: { from, localField, foreignField, as }. Do not wrap in { "${t.type}": ... }.`;const p=document.createElement("textarea");p.className="form-input stage-config",p.rows=5,p.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",p.placeholder="{}",p.value=Object.keys(t.config||{}).length?JSON.stringify(t.config,null,2):"",r.appendChild(u),r.appendChild(m),r.appendChild(p)}i.appendChild(d),i.appendChild(r),n.appendChild(i),Domma.icons.scan(i)}function U(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function K(e){const t=[];return e.find(".stage-card").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");let l;if(a)n==="$match"&&(l=W(a)),n==="$sort"&&(l=M(a));else{const i=this.querySelector(".stage-config")?.value?.trim()||"{}";try{l=JSON.parse(i)}catch{throw new Error(`Invalid JSON in ${n} stage config`)}}t.push({type:n,config:l})}),t}function q(e,t){const n=e.find("#view-columns-builder").get(0);if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const a=t??(f||[]).map(c=>({key:`data.${c.name}`,label:c.label||c.name})),l=(c="",o="")=>{const s=document.createElement("div");s.className="col-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=f?.length?h([{value:"",label:"\u2014 field \u2014"},...y()],c):k("data.fieldName",c);r.className+=" col-key";const u=k("Display label",o);u.className+=" col-label",u.style.flex="1",f?.length&&r.tagName==="SELECT"&&r.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(A=>`data.${A.name}`===r.value);w&&(u.value=w.label||w.name)}});const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove column";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove(),Domma.icons.scan(n)}),s.appendChild(r),s.appendChild(u),s.appendChild(m),n.insertBefore(s,n.lastChild),Domma.icons.scan(s)};a.forEach(c=>l(c.key,c.label));const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm";const d=document.createElement("span");d.setAttribute("data-icon","plus"),i.appendChild(d),i.appendChild(document.createTextNode(" Add Column")),i.addEventListener("click",()=>{l(),Domma.icons.scan(n)}),n.appendChild(i),Domma.icons.scan(n)}function Y(e){const t=[];return e.find(".col-row").each(function(){const n=this.querySelector(".col-key")?.value?.trim(),a=this.querySelector(".col-label")?.value?.trim();n&&t.push({key:n,label:a||n})}),t}function G(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>x(e)),x(e))}function x(e){const t=e.find("#view-display-mode").val()||"table",n=e.find("#view-columns-section").get(0),a=e.find("#view-block-section").get(0);n&&(n.style.display=t==="table"?"":"none"),a&&(a.style.display=t==="block"?"":"none")}function H(e){const t=e.find("#view-rowlevel-enabled").get(0),n=e.find("#view-rowlevel-config").get(0),a=e.find("#view-rowlevel-mode").get(0),l=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{n&&(n.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{l&&(l.style.display=a.value==="field"?"":"none")}))}async function P(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const n=e.find("#view-source").val();if(!n){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let a;try{a=K(e)}catch(r){E.toast(r.message,{type:"error"});return}const l=Y(e),i=[];e.find(".view-role-cb:checked").each(function(){i.push(this.value)});const d=e.find("#view-rowlevel-enabled").is(":checked");let c=null;if(d){const r=e.find("#view-rowlevel-mode").val()||"owner",u=e.find("#view-rowlevel-userkey").val()||"id";if(c={mode:r,userKey:u},r==="field"){const m=e.find("#view-rowlevel-field").val().trim();if(!m){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}c.field=m}}const o={title:t,slug:e.find("#view-slug").val().trim()||void 0,description:e.find("#view-description").val().trim(),...e.find("#view-bundled").is(":checked")?{bundled:!0}:{},connection:e.find("#view-connection").val()||"default",pipeline:{source:n,stages:a},display:{mode:e.find("#view-display-mode").val()||"table",columns:l,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:i,public:e.find("#view-public").is(":checked"),rowLevel:c}},s=e.find("#save-view-btn").get(0);s&&(s.disabled=!0);try{if(g)await v.views.update(g,o),E.toast("View updated.",{type:"success"});else{const r=await v.views.create(o);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${r.slug}`)}}catch(r){E.toast(r.message||"Failed to save view.",{type:"error"})}finally{s&&(s.disabled=!1)}}
1
+ import{api as v}from"../api.js";let g=null,f=null,C={};async function T(e){const t=e.find("#view-project").get(0);if(t)try{(await v.projects.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=a.name||a.slug,t.appendChild(l)})}catch{}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){g=null,f=null,C={};const t=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);t&&(g=t[1]),E.tabs(e.find("#view-editor-tabs").get(0)),await L(e),await _(e),await O(e),await D(e),await T(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await k(e)}),g&&(e.find("#view-editor-title").text("Edit View"),await V(e,g)),e.find("#add-stage-btn").off("click").on("click",()=>{const n=e.find("#add-stage-type").val()||"$match";q(e,{type:n,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await X(e)}),Q(e),H(e),Domma.icons.scan()}};async function L(e){const t=e.find("#view-source").get(0);if(t)try{(await v.collections.list()).forEach(n=>{const a=document.createElement("option");a.value=n.slug,a.textContent=`${n.title} (${n.slug})`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-block-name").get(0);if(t)try{(await v.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function _(e){const t=e.find("#view-connection").get(0);if(t)try{const n=await v.collections.getConnections();Object.keys(n).forEach(a=>{if(!t.querySelector(`option[value="${a}"]`)){const l=document.createElement("option");l.value=a,l.textContent=a,t.appendChild(l)}})}catch{}}async function O(e){const t=e.find("#view-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="view-role-cb",l.checked=n==="admin",a.appendChild(l),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function k(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await v.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");a&&(n==="$match"&&J(a),n==="$sort"&&U(a))}),A(e,null)}async function V(e,t){try{const n=await v.views.get(t);if(!n){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const a=n.pipeline?.source;a&&(e.find("#view-source").val(a),await k(e)),z(e,n)}catch(n){E.toast(n.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function z(e,t){e.find("#view-title").val(t.title||""),e.find("#view-slug").val(t.slug||""),e.find("#view-description").val(t.description||""),e.find("#view-source").val(t.pipeline?.source||""),e.find("#view-bundled").prop("checked",!!t.bundled),C=t.meta||{},e.find("#view-project").val(t.meta?.project||""),e.find("#view-connection").val(t.connection||"default"),e.find("#view-display-mode").val(t.display?.mode||"table"),e.find("#view-page-size").val(t.display?.pageSize||25),e.find("#view-block-name").val(t.display?.block||"");const n=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=n.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const a=t.access?.rowLevel;a&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(a.mode||"owner"),e.find("#view-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(a.field||"")));const l=e.find("#pipeline-stages-list").get(0);if(l)for(;l.firstChild;)l.removeChild(l.firstChild);(t.pipeline?.stages||[]).forEach(i=>q(e,i)),A(e,t.display?.columns||[]),N(e)}const F=[{value:"eq",label:"= equals"},{value:"ne",label:"\u2260 not equals"},{value:"gt",label:"> greater than"},{value:"lt",label:"< less than"},{value:"gte",label:"\u2265 greater or equal"},{value:"lte",label:"\u2264 less or equal"},{value:"contains",label:"~ contains (regex)"},{value:"in",label:"\u2208 in (comma list)"}];function y(e=!1){const t=[];return e&&t.push({value:"",label:"\u2014 select field \u2014"}),(f||[]).forEach(n=>{t.push({value:`data.${n.name}`,label:`${n.label||n.name} (${n.name})`})}),t.length===0||t.length===1&&e?(t.push({value:"__meta.createdAt",label:"Created At"}),t.push({value:"__meta.updatedAt",label:"Updated At"})):(t.push({value:"__meta.createdAt",label:"Created At (meta)"}),t.push({value:"__meta.updatedAt",label:"Updated At (meta)"})),t}function h(e,t){const n=document.createElement("select");return n.className="form-input form-input--sm",e.forEach(a=>{const l=document.createElement("option");l.value=a.value,l.textContent=a.label,String(a.value)===String(t)&&(l.selected=!0),n.appendChild(l)}),n}function S(e,t,n="text"){const a=document.createElement("input");return a.type=n,a.className="form-input form-input--sm",a.placeholder=e,a.value=t??"",a}function B(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(n=>n.name===t)||null}function b(e,t,n=""){const a=B(e);if(a?.type==="select"&&(t==="eq"||t==="ne")&&a.options?.length){const d=document.createElement("select");d.className="form-input form-input--sm match-val",d.style.flex="1";const c=document.createElement("option");return c.value="",c.textContent="\u2014 select value \u2014",d.appendChild(c),(a.options||[]).forEach(o=>{const s=typeof o=="string"?o:o.value??"",r=typeof o=="string"?o:o.label||s;if(!s||s==="undefined")return;const u=document.createElement("option");u.value=s,u.textContent=r,s===n&&(u.selected=!0),d.appendChild(u)}),d}const l=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",i=document.createElement("input");return i.type="text",i.className="form-input form-input--sm match-val",i.style.flex="1",i.placeholder=l,i.value=n,i}function $(e){const t=[],n={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([a,l])=>{if(typeof l=="object"&&l!==null&&!Array.isArray(l)){if("$regex"in l){t.push({field:a,op:"contains",val:String(l.$regex??"")});return}Object.entries(l).forEach(([i,d])=>{const c=n[i];c&&t.push({field:a,op:c,val:Array.isArray(d)?d.join(", "):String(d)})})}else t.push({field:a,op:"eq",val:String(l??"")})}),t}function x(e,t,n="",a="eq",l=""){const i=document.createElement("div");i.className="match-condition-row",i.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const d=h([{value:"",label:"\u2014 field \u2014"},...y()],n);d.className+=" match-field",d.style.minWidth="140px";const c=h(F,a);c.className+=" match-op",c.style.minWidth="160px";const o=document.createElement("div");o.className="match-val-wrap",o.style.cssText="flex:1;min-width:120px;display:flex;",o.appendChild(b(n,a,l));const s=()=>{const m=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(d.value,c.value,m))};d.addEventListener("change",s),c.addEventListener("change",s);const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost",r.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),r.appendChild(u),r.addEventListener("click",()=>i.remove()),i.appendChild(d),i.appendChild(c),i.appendChild(o),i.appendChild(r),e.insertBefore(i,t),Domma.icons.scan(i)}function W(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$match||t||{},a="$or"in n;let l=[];a?(n.$or||[]).forEach(r=>$(r).forEach(u=>l.push(u))):l=$(n);const i=document.createElement("div");i.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const d=document.createElement("label");d.className="form-label",d.style.marginBottom="0",d.textContent="Match:";const c=document.createElement("select");c.className="form-input form-input--sm match-logic",c.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(r=>{const u=document.createElement("option");u.value=r.value,u.textContent=r.label,(a?"or":"and")===r.value&&(u.selected=!0),c.appendChild(u)}),i.appendChild(d),i.appendChild(c),e.appendChild(i);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm match-add-btn";const s=document.createElement("span");s.setAttribute("data-icon","plus"),o.appendChild(s),o.appendChild(document.createTextNode(" Add Condition")),o.addEventListener("click",()=>{x(e,o),Domma.icons.scan(e)}),e.appendChild(o),l.forEach(r=>x(e,o,r.field,r.op,r.val)),l.length===0&&x(e,o),Domma.icons.scan(e)}function J(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const n=t.querySelector(".match-field");if(n){const d=n.value,c=h([{value:"",label:"\u2014 field \u2014"},...y()],d);c.className=n.className,c.style.cssText=n.style.cssText;const o=t.querySelector(".match-val-wrap"),s=t.querySelector(".match-op");o&&s&&c.addEventListener("change",()=>{const r=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(c.value,s.value,r))}),n.parentNode.replaceChild(c,n)}const a=t.querySelector(".match-val-wrap"),l=t.querySelector(".match-field")?.value,i=t.querySelector(".match-op")?.value;if(a&&l&&i){const d=a.querySelector(".match-val")?.value??"";for(;a.firstChild;)a.removeChild(a.firstChild);a.appendChild(b(l,i,d))}})}function M(e){const t=e.querySelector(".match-logic")?.value||"and",n=l=>{const i=l.querySelector(".match-field")?.value?.trim(),d=l.querySelector(".match-op")?.value,c=l.querySelector(".match-val")?.value?.trim()??"";if(!i)return null;const o=i.startsWith("__")?i.slice(2):i,s={};switch(d){case"eq":s[o]=c;break;case"ne":s[o]={$ne:c};break;case"gt":s[o]={$gt:isNaN(c)?c:Number(c)};break;case"lt":s[o]={$lt:isNaN(c)?c:Number(c)};break;case"gte":s[o]={$gte:isNaN(c)?c:Number(c)};break;case"lte":s[o]={$lte:isNaN(c)?c:Number(c)};break;case"contains":s[o]={$regex:c,$options:"i"};break;case"in":s[o]={$in:c.split(",").map(r=>r.trim()).filter(Boolean)};break}return Object.keys(s).length?s:null},a=[];return e.querySelectorAll(".match-condition-row").forEach(l=>{const i=n(l);i&&a.push(i)}),a.length===0?{}:t==="or"?{$or:a}:Object.assign({},...a)}function I(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$sort||t||{},a=Object.entries(n),l=document.createElement("button");l.type="button",l.className="btn btn-ghost btn-sm";const i=document.createElement("span");i.setAttribute("data-icon","plus"),l.appendChild(i),l.appendChild(document.createTextNode(" Add Sort")),e.appendChild(l);const d=(c="",o=1)=>{const s=document.createElement("div");s.className="sort-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=h([{value:"",label:"\u2014 field \u2014"},...y()],c);r.className+=" sort-field";const u=h([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(o));u.className+=" sort-dir";const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove()}),s.appendChild(r),s.appendChild(u),s.appendChild(m),e.insertBefore(s,l),Domma.icons.scan(s)};l.addEventListener("click",()=>{d()}),a.forEach(([c,o])=>d(c,o)),a.length===0&&d(),Domma.icons.scan(e)}function U(e){e.querySelectorAll(".sort-row").forEach(t=>{const n=t.querySelector(".sort-field"),a=n?.value,l=h([{value:"",label:"\u2014 field \u2014"},...y()],a);l.className=n.className,n.parentNode.replaceChild(l,n)})}function K(e){const t={};return e.querySelectorAll(".sort-row").forEach(n=>{const a=n.querySelector(".sort-field")?.value?.trim(),l=parseInt(n.querySelector(".sort-dir")?.value,10)||1;if(!a)return;const i=a.startsWith("__")?a.slice(2):a;t[i]=l}),t}function q(e,t){const n=e.find("#pipeline-stages-list").get(0);if(!n)return;const a=n.querySelector(".stage-empty-placeholder");a&&a.remove();const l=["$match","$sort"].includes(t.type),i=document.createElement("div");i.className="card mb-2 stage-card",i.dataset.stageType=t.type,l&&(i.dataset.guided="true");const d=document.createElement("div");d.className="card-header",d.style.cssText="display:flex;align-items:center;gap:.5rem;";const c=document.createElement("code");c.textContent=t.type,c.style.cssText="flex:1;font-size:.85rem;";const o=document.createElement("button");o.type="button",o.className="btn btn-sm btn-danger";const s=document.createElement("span");s.setAttribute("data-icon","trash-2"),o.appendChild(s),o.addEventListener("click",()=>{i.remove(),n.querySelector(".stage-card")||n.appendChild(P())}),d.appendChild(c),d.appendChild(o);const r=document.createElement("div");if(r.className="card-body",l){const u=document.createElement("div");u.className="stage-guided",r.appendChild(u),t.type==="$match"&&W(u,t.config),t.type==="$sort"&&I(u,t.config)}else{const u=document.createElement("label");u.className="form-label",u.textContent="Stage Config (JSON)";const m=document.createElement("small");m.className="text-muted",m.style.cssText="display:block;margin-bottom:.4rem;",m.textContent=`Enter the inner config for ${t.type} \u2014 e.g. for $lookup: { from, localField, foreignField, as }. Do not wrap in { "${t.type}": ... }.`;const p=document.createElement("textarea");p.className="form-input stage-config",p.rows=5,p.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",p.placeholder="{}",p.value=Object.keys(t.config||{}).length?JSON.stringify(t.config,null,2):"",r.appendChild(u),r.appendChild(m),r.appendChild(p)}i.appendChild(d),i.appendChild(r),n.appendChild(i),Domma.icons.scan(i)}function P(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function Y(e){const t=[];return e.find(".stage-card").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");let l;if(a)n==="$match"&&(l=M(a)),n==="$sort"&&(l=K(a));else{const i=this.querySelector(".stage-config")?.value?.trim()||"{}";try{l=JSON.parse(i)}catch{throw new Error(`Invalid JSON in ${n} stage config`)}}t.push({type:n,config:l})}),t}function A(e,t){const n=e.find("#view-columns-builder").get(0);if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const a=t??(f||[]).map(c=>({key:`data.${c.name}`,label:c.label||c.name})),l=(c="",o="")=>{const s=document.createElement("div");s.className="col-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=f?.length?h([{value:"",label:"\u2014 field \u2014"},...y()],c):S("data.fieldName",c);r.className+=" col-key";const u=S("Display label",o);u.className+=" col-label",u.style.flex="1",f?.length&&r.tagName==="SELECT"&&r.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(j=>`data.${j.name}`===r.value);w&&(u.value=w.label||w.name)}});const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove column";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove(),Domma.icons.scan(n)}),s.appendChild(r),s.appendChild(u),s.appendChild(m),n.insertBefore(s,n.lastChild),Domma.icons.scan(s)};a.forEach(c=>l(c.key,c.label));const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm";const d=document.createElement("span");d.setAttribute("data-icon","plus"),i.appendChild(d),i.appendChild(document.createTextNode(" Add Column")),i.addEventListener("click",()=>{l(),Domma.icons.scan(n)}),n.appendChild(i),Domma.icons.scan(n)}function G(e){const t=[];return e.find(".col-row").each(function(){const n=this.querySelector(".col-key")?.value?.trim(),a=this.querySelector(".col-label")?.value?.trim();n&&t.push({key:n,label:a||n})}),t}function H(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>N(e)),N(e))}function N(e){const t=e.find("#view-display-mode").val()||"table",n=e.find("#view-columns-section").get(0),a=e.find("#view-block-section").get(0);n&&(n.style.display=t==="table"?"":"none"),a&&(a.style.display=t==="block"?"":"none")}function Q(e){const t=e.find("#view-rowlevel-enabled").get(0),n=e.find("#view-rowlevel-config").get(0),a=e.find("#view-rowlevel-mode").get(0),l=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{n&&(n.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{l&&(l.style.display=a.value==="field"?"":"none")}))}async function X(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const n=e.find("#view-source").val();if(!n){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let a;try{a=Y(e)}catch(u){E.toast(u.message,{type:"error"});return}const l=G(e),i=[];e.find(".view-role-cb:checked").each(function(){i.push(this.value)});const d=e.find("#view-rowlevel-enabled").is(":checked");let c=null;if(d){const u=e.find("#view-rowlevel-mode").val()||"owner",m=e.find("#view-rowlevel-userkey").val()||"id";if(c={mode:u,userKey:m},u==="field"){const p=e.find("#view-rowlevel-field").val().trim();if(!p){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}c.field=p}}const o=e.find("#view-project").val()||"",s={title:t,slug:e.find("#view-slug").val().trim()||void 0,description:e.find("#view-description").val().trim(),...e.find("#view-bundled").is(":checked")?{bundled:!0}:{},connection:e.find("#view-connection").val()||"default",pipeline:{source:n,stages:a},display:{mode:e.find("#view-display-mode").val()||"table",columns:l,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:i,public:e.find("#view-public").is(":checked"),rowLevel:c},meta:{...C||{},project:o||null}},r=e.find("#save-view-btn").get(0);r&&(r.disabled=!0);try{if(g)await v.views.update(g,s),E.toast("View updated.",{type:"success"});else{const u=await v.views.create(s);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${u.slug}`)}}catch(u){E.toast(u.message||"Failed to save view.",{type:"error"})}finally{r&&(r.disabled=!1)}}
@@ -1 +1 @@
1
- import{api as r}from"../api.js";function i(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const viewsListView={templateUrl:"/admin/js/templates/views-list.html",async onMount(s){await l(s),s.find("#create-view-btn").off("click").on("click",()=>{R.navigate("/views/new")}),Domma.icons.scan()}};async function l(s){let c=[];try{c=await r.views.list(),s.find("#views-pro-notice").hide()}catch(e){e.message?.includes("MongoDB")||e.message?.includes("pro mode")||e.message?.includes("connection")?s.find("#views-pro-notice").show():E.toast("Could not load views.",{type:"error"})}T.create("#views-table",{data:c,columns:[{key:"title",title:"Title",render:(e,t)=>{const n=document.createElement("a");return n.href=`#/views/${i(t.slug)}/preview`,n.textContent=e,n.style.fontWeight="600",n.outerHTML}},{key:"slug",title:"Slug",render:e=>`<code>${i(e)}</code>`},{key:"pipeline",title:"Source",render:e=>`<code>${i(e?.source||"\u2014")}</code>`},{key:"display",title:"Mode",render:e=>i(e?.mode||"table")},{key:"access",title:"Roles",render:e=>(e?.roles||[]).map(t=>`<span class="badge badge-secondary">${i(t)}</span>`).join(" ")},{key:"slug",title:"Actions",render:e=>{const t=document.createElement("div");t.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const n=document.createElement("a");n.href=`#/views/${i(e)}/preview`,n.className="btn btn-sm btn-ghost",n.textContent="Preview";const a=document.createElement("a");a.href=`#/views/edit/${i(e)}`,a.className="btn btn-sm btn-primary",a.textContent="Edit";const o=document.createElement("button");return o.className="btn btn-sm btn-danger js-delete-view",o.dataset.slug=e,o.textContent="Delete",t.appendChild(n),t.appendChild(a),t.appendChild(o),t.outerHTML}}],emptyMessage:'No views yet. Click "New View" to create your first view.'}),document.querySelectorAll(".js-delete-view").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.slug;if(await E.confirm(`Delete view "${t}"? This cannot be undone.`))try{await r.views.delete(t),E.toast("View deleted.",{type:"success"}),await l(s)}catch{E.toast("Failed to delete view.",{type:"error"})}})}),Domma.icons.scan()}
1
+ import{api as l}from"../api.js";import{filterByProject as m,getProjectFromHash as p}from"../lib/project-context.js";function i(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const viewsListView={templateUrl:"/admin/js/templates/views-list.html",async onMount(s){await d(s),s.find("#create-view-btn").off("click").on("click",()=>{R.navigate("/views/new")}),Domma.icons.scan()}};async function d(s){let o=[];try{o=await l.views.list(),s.find("#views-pro-notice").hide()}catch(e){e.message?.includes("MongoDB")||e.message?.includes("pro mode")||e.message?.includes("connection")?s.find("#views-pro-notice").show():E.toast("Could not load views.",{type:"error"})}const c=p();c&&(o=m(o,c)),T.create("#views-table",{data:o,columns:[{key:"title",title:"Title",render:(e,t)=>{const n=document.createElement("a");return n.href=`#/views/${i(t.slug)}/preview`,n.textContent=e,n.style.fontWeight="600",n.outerHTML}},{key:"slug",title:"Slug",render:e=>`<code>${i(e)}</code>`},{key:"pipeline",title:"Source",render:e=>`<code>${i(e?.source||"\u2014")}</code>`},{key:"display",title:"Mode",render:e=>i(e?.mode||"table")},{key:"access",title:"Roles",render:e=>(e?.roles||[]).map(t=>`<span class="badge badge-secondary">${i(t)}</span>`).join(" ")},{key:"slug",title:"Actions",render:e=>{const t=document.createElement("div");t.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const n=document.createElement("a");n.href=`#/views/${i(e)}/preview`,n.className="btn btn-sm btn-ghost",n.textContent="Preview";const a=document.createElement("a");a.href=`#/views/edit/${i(e)}`,a.className="btn btn-sm btn-primary",a.textContent="Edit";const r=document.createElement("button");return r.className="btn btn-sm btn-danger js-delete-view",r.dataset.slug=e,r.textContent="Delete",t.appendChild(n),t.appendChild(a),t.appendChild(r),t.outerHTML}}],emptyMessage:'No views yet. Click "New View" to create your first view.'}),document.querySelectorAll(".js-delete-view").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.slug;if(await E.confirm(`Delete view "${t}"? This cannot be undone.`))try{await l.views.delete(t),E.toast("View deleted.",{type:"success"}),await d(s)}catch{E.toast("Failed to delete view.",{type:"error"})}})}),Domma.icons.scan()}
@@ -0,0 +1,5 @@
1
+ {
2
+ "navbar": "main",
3
+ "footer-primary": "footer",
4
+ "admin-sidebar": "admin-sidebar"
5
+ }
@@ -0,0 +1,185 @@
1
+ {
2
+ "slug": "admin-sidebar",
3
+ "name": "Admin sidebar",
4
+ "description": "Auto-seeded by sidebar migration on first boot",
5
+ "variant": "transparent",
6
+ "items": [
7
+ {
8
+ "text": "Overview",
9
+ "url": "",
10
+ "icon": "home",
11
+ "items": [
12
+ {
13
+ "text": "Dashboard",
14
+ "url": "#/",
15
+ "icon": "home"
16
+ }
17
+ ]
18
+ },
19
+ {
20
+ "text": "Projects",
21
+ "url": "",
22
+ "icon": "folder",
23
+ "permission": "projects",
24
+ "items": [
25
+ {
26
+ "text": "Manage Projects",
27
+ "url": "#/projects",
28
+ "icon": "folder",
29
+ "permission": "projects"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "text": "Content",
35
+ "url": "",
36
+ "icon": "edit",
37
+ "items": [
38
+ {
39
+ "text": "Pages",
40
+ "url": "#/pages",
41
+ "icon": "file-text",
42
+ "permission": "pages"
43
+ },
44
+ {
45
+ "text": "Media",
46
+ "url": "#/media",
47
+ "icon": "image",
48
+ "permission": "media"
49
+ },
50
+ {
51
+ "text": "Menus",
52
+ "url": "#/menus",
53
+ "icon": "menu",
54
+ "permission": "menus"
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "text": "Data",
60
+ "url": "",
61
+ "icon": "database",
62
+ "items": [
63
+ {
64
+ "text": "Collections",
65
+ "url": "#/collections",
66
+ "icon": "database",
67
+ "permission": "collections"
68
+ },
69
+ {
70
+ "text": "Forms",
71
+ "url": "#/forms",
72
+ "icon": "layout",
73
+ "permission": "collections"
74
+ },
75
+ {
76
+ "text": "Views",
77
+ "url": "#/views",
78
+ "icon": "eye",
79
+ "permission": "views"
80
+ },
81
+ {
82
+ "text": "Actions",
83
+ "url": "#/actions",
84
+ "icon": "zap",
85
+ "permission": "actions"
86
+ },
87
+ {
88
+ "text": "Blocks",
89
+ "url": "#/blocks",
90
+ "icon": "box",
91
+ "permission": "pages"
92
+ },
93
+ {
94
+ "text": "Components",
95
+ "url": "#/components",
96
+ "icon": "component",
97
+ "permission": "components"
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ "text": "System",
103
+ "url": "",
104
+ "icon": "settings",
105
+ "items": [
106
+ {
107
+ "text": "Notifications",
108
+ "url": "#/system/notifications",
109
+ "icon": "bell",
110
+ "permission": "notifications"
111
+ },
112
+ {
113
+ "text": "Roles",
114
+ "url": "#/roles",
115
+ "icon": "shield",
116
+ "permission": "plugins"
117
+ },
118
+ {
119
+ "text": "Users",
120
+ "url": "#/users",
121
+ "icon": "users",
122
+ "permission": "users"
123
+ },
124
+ {
125
+ "text": "Site Settings",
126
+ "url": "#/settings",
127
+ "icon": "settings",
128
+ "permission": "settings"
129
+ },
130
+ {
131
+ "text": "Effects",
132
+ "url": "#/effects",
133
+ "icon": "sparkles",
134
+ "permission": "settings"
135
+ },
136
+ {
137
+ "text": "Layouts",
138
+ "url": "#/layouts",
139
+ "icon": "layout",
140
+ "permission": "layouts"
141
+ },
142
+ {
143
+ "text": "Plugins",
144
+ "url": "#/plugins",
145
+ "icon": "package",
146
+ "permission": "plugins"
147
+ },
148
+ {
149
+ "text": "My Profile",
150
+ "url": "#/my-profile",
151
+ "icon": "user"
152
+ }
153
+ ]
154
+ },
155
+ {
156
+ "text": "Documentation",
157
+ "url": "",
158
+ "icon": "book",
159
+ "items": [
160
+ {
161
+ "text": "Usage",
162
+ "url": "#/documentation",
163
+ "icon": "book"
164
+ },
165
+ {
166
+ "text": "Tutorials",
167
+ "url": "#/tutorials",
168
+ "icon": "document"
169
+ },
170
+ {
171
+ "text": "API Reference",
172
+ "url": "#/api-reference",
173
+ "icon": "code"
174
+ }
175
+ ]
176
+ }
177
+ ],
178
+ "meta": {
179
+ "createdAt": "2026-05-25T11:11:15.223Z",
180
+ "updatedAt": "2026-05-25T13:48:21.541Z",
181
+ "bundled": false,
182
+ "presetOwner": null,
183
+ "project": null
184
+ }
185
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "slug": "footer",
3
+ "name": "Footer",
4
+ "description": "Migrated from site.json footer.links",
5
+ "items": [
6
+ {
7
+ "text": "About",
8
+ "url": "/about"
9
+ },
10
+ {
11
+ "text": "Resources",
12
+ "url": "/resources"
13
+ },
14
+ {
15
+ "text": "Contact",
16
+ "url": "/contact"
17
+ },
18
+ {
19
+ "text": "Privacy Policy",
20
+ "url": "/privacy"
21
+ },
22
+ {
23
+ "text": "GDPR",
24
+ "url": "/gdpr"
25
+ }
26
+ ],
27
+ "meta": {
28
+ "createdAt": "2026-05-24T14:51:22.419Z",
29
+ "updatedAt": "2026-05-24T14:51:22.419Z",
30
+ "bundled": false,
31
+ "presetOwner": null
32
+ }
33
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "slug": "main",
3
+ "name": "Main navigation",
4
+ "description": "Migrated from config/navigation.json",
5
+ "variant": "dark",
6
+ "position": "sticky",
7
+ "items": [
8
+ {
9
+ "text": "Home",
10
+ "url": "/",
11
+ "icon": "home"
12
+ },
13
+ {
14
+ "text": "About",
15
+ "url": "/about",
16
+ "icon": "info"
17
+ },
18
+ {
19
+ "text": "Resources",
20
+ "url": "/resources",
21
+ "icon": "book-open"
22
+ },
23
+ {
24
+ "text": "Contact",
25
+ "url": "/contact",
26
+ "icon": "mail"
27
+ }
28
+ ],
29
+ "meta": {
30
+ "createdAt": "2026-05-24T14:51:22.419Z",
31
+ "updatedAt": "2026-05-24T14:51:22.419Z",
32
+ "bundled": false,
33
+ "presetOwner": null
34
+ }
35
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "slug": "sproj-1779696558011-menu",
3
+ "name": "Test menu",
4
+ "description": "",
5
+ "items": [
6
+ {
7
+ "text": "X",
8
+ "url": "/x"
9
+ }
10
+ ],
11
+ "meta": {
12
+ "createdAt": "2026-05-25T08:09:18.015Z",
13
+ "updatedAt": "2026-05-25T08:09:18.015Z",
14
+ "bundled": true,
15
+ "presetOwner": "test-scaff-project"
16
+ }
17
+ }