domma-cms 0.36.10 → 0.36.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/css/admin.css +1 -1
- package/admin/js/lib/table-icons.js +1 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoints.js +5 -5
- package/admin/js/views/api-tokens.js +6 -6
- package/admin/js/views/blocks.js +5 -5
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/media.js +1 -1
- package/admin/js/views/menu-editor.js +2 -2
- package/admin/js/views/menus.js +5 -5
- package/admin/js/views/navigation.js +2 -2
- package/admin/js/views/pages.js +4 -4
- package/admin/js/views/projects.js +3 -3
- package/admin/js/views/roles.js +4 -4
- package/admin/js/views/users.js +4 -4
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/package.json +1 -1
- package/server/routes/public.js +6 -2
- package/server/services/content.js +19 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{apiRequest as x}from"/admin/js/api.js";let c=null,k=[],r=[];export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(
|
|
1
|
+
import{apiRequest as x}from"/admin/js/api.js";import{keepTableIconsAlive as A}from"/admin/js/lib/table-icons.js";let c=null,k=[],r=[];export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(e){const l=window.location.hash.match(/\/forms\/([^/?#]+)\/submissions/);if(c=l?l[1]:null,!c){E.toast("No form selected.",{type:"error"});return}const u=E.loader(e.get(0),{type:"dots"});try{const o=await x(`/forms/${c}`);k=(o.fields||[]).filter(s=>s.type!=="page-break"),e.find("#submissions-title").get(0).textContent=`${o.title} \u2014 Submissions`}catch{E.toast("Could not load form definition.",{type:"error"})}try{await M(e)}finally{u.destroy()}A(e.get(0)),e.off("click",".js-delete-submission").on("click",".js-delete-submission",async function(o){o.stopPropagation();const s=this.dataset.id;if(await E.confirm("Delete this submission?"))try{await x(`/forms/${c}/submissions/${s}`,{method:"DELETE"}),r=r.filter(d=>d.id!==s),E.toast("Submission deleted.",{type:"success"}),v(r,e)}catch{E.toast("Failed to delete submission.",{type:"error"})}}),e.find("#export-btn").off("click").on("click",()=>{const o=E.slideover({title:"Export Submissions",size:"sm",position:"right"}),s=document.createElement("div");s.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;";function i(d,y,f,g){const C=document.createElement("div");C.style.cssText="display:flex;flex-direction:column;gap:.4rem;";const b=document.createElement("button");b.className="btn btn-ghost",b.style.cssText="justify-content:flex-start;gap:.5rem;";const L=document.createElement("span");return L.setAttribute("data-icon",y),b.appendChild(L),b.appendChild(document.createTextNode(" "+d)),b.addEventListener("click",()=>{fetch(f,{headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}}).then(w=>w.blob()).then(w=>{const h=document.createElement("a");h.href=URL.createObjectURL(w),h.download=g,document.body.appendChild(h),h.click(),document.body.removeChild(h),URL.revokeObjectURL(h.href),o.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),C.appendChild(b),C}s.appendChild(i("Export as CSV","file-text",`/api/forms/${c}/submissions/export`,`${c}-submissions.csv`)),s.appendChild(i("Export as JSON","code",`/api/forms/${c}/submissions/export/json`,`${c}-submissions.json`)),o.setContent(s),Domma.icons.scan(s),o.open()}),e.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete all submissions? This cannot be undone."))try{await x(`/forms/${c}/submissions`,{method:"DELETE"}),r=[],E.toast("All submissions cleared.",{type:"success"}),v([],e)}catch{E.toast("Failed to clear submissions.",{type:"error"})}});const m=e.find("#sub-search").get(0),n=e.find("#sub-date-from").get(0),t=e.find("#sub-date-to").get(0);function a(){const o=m?.value.toLowerCase()||"",s=n?.value?new Date(n.value):null,i=t?.value?new Date(t.value+"T23:59:59"):null,d=r.filter(y=>{if(o&&!Object.values(y.data||{}).map(g=>String(g).toLowerCase()).some(g=>g.includes(o)))return!1;if(s||i){const f=y.meta?.createdAt?new Date(y.meta.createdAt):null;if(!f||s&&f<s||i&&f>i)return!1}return!0});v(d,e)}m&&m.addEventListener("input",a),n&&n.addEventListener("change",a),t&&t.addEventListener("change",a),Domma.icons.scan()}};async function M(e){try{r=await x(`/forms/${c}/submissions`)}catch{r=[],E.toast("Could not load submissions.",{type:"error"})}v(r,e)}function v(e,p){const l=p.find("#sub-count").get(0);l&&(e.length===r.length?l.textContent=`${r.length} submission${r.length!==1?"s":""}`:l.textContent=`Showing ${e.length} of ${r.length}`);const m=[...k.map(t=>({key:`data.${t.name}`,title:t.label||t.name,render:(a,o)=>{const s=o.data?.[t.name]??"",i=String(s),d=document.createElement("span");return d.title=i,d.textContent=i.length>80?i.slice(0,80)+"\u2026":i,d.outerHTML}})),{key:"meta",title:"Date",render:t=>D(t?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:t=>{const a=document.createElement("button");return a.className="btn btn-sm btn-ghost js-delete-submission",a.dataset.id=t,a.setAttribute("data-tooltip","Delete"),a.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',a.outerHTML}}];T.create("#submissions-table",{data:e,columns:m,emptyMessage:"No submissions yet."});const n=document.querySelector("#submissions-table");n&&(n.querySelectorAll("tbody tr").forEach((t,a)=>{const o=e[a];o&&(t.style.cursor="pointer",t.addEventListener("click",s=>{s.target.closest(".js-delete-submission")||j(o)}))}),Domma.icons.scan(n),n.querySelectorAll("[data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}))}function j(e){const p=document.createElement("div"),l=document.createElement("div");l.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",k.forEach(n=>{const t=e.data?.[n.name];if(t==null||t==="")return;const a=document.createElement("div");a.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const o=document.createElement("strong");o.textContent=n.label||n.name,o.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const s=document.createElement("p");s.textContent=String(t),s.style.cssText="margin:0;word-break:break-word;",a.appendChild(o),a.appendChild(s),l.appendChild(a)});const u=document.createElement("div");if(u.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",e.meta?.createdAt){const n=document.createElement("span");n.textContent=`Submitted: ${D(e.meta.createdAt).format("DD MMM YYYY HH:mm")}`,u.appendChild(n)}if(e.meta?.ip){const n=document.createElement("span");n.textContent=`IP: ${e.meta.ip}`,u.appendChild(n)}p.appendChild(l),p.appendChild(u);const m=E.modal({title:"Submission Details",size:"md"});m.element.appendChild(p),m.open()}
|
package/admin/js/views/forms.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{apiRequest as p}from"/admin/js/api.js";import{filterByProject as b,getProjectFromHash as h}from"/admin/js/lib/project-context.js";function u(
|
|
1
|
+
import{apiRequest as p}from"/admin/js/api.js";import{filterByProject as b,getProjectFromHash as h}from"/admin/js/lib/project-context.js";import{keepTableIconsAlive as C}from"/admin/js/lib/table-icons.js";function u(d){return String(d).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(d){await g(d),C(d.get(0)),d.off("click",".js-delete-form").on("click",".js-delete-form",async function(){const r=this.dataset.slug,e=this.dataset.plugin,c=e?`This form is managed by the <strong>${e}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete form "${r}" and all its submissions? This cannot be undone.`;if(await E.confirm(c))try{await p(`/forms/${r}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),await g(d)}catch{E.toast("Failed to delete form.",{type:"error"})}}),d.off("click",".js-clone-form").on("click",".js-clone-form",async function(){const r=this.dataset.slug;try{const e=await p(`/forms/${r}`),{collection:c,...t}=e.actions||{},n=await p("/forms",{method:"POST",body:JSON.stringify({title:`${e.title} (Copy)`,description:e.description||"",fields:e.fields||[],settings:e.settings||{},actions:t})});E.toast(`Form duplicated as "${n.title}".`,{type:"success"}),R.navigate(`/forms/edit/${n.slug}`)}catch(e){E.toast(e.message||"Failed to duplicate form.",{type:"error"})}}),d.find("#create-form-btn").off("click").on("click",()=>{const r=E.modal({title:"Create Form",size:"sm"}),e=document.createElement("div");e.style.cssText="padding:.25rem 0 .5rem;";const c=document.createElement("div");F.create({title:{type:"string",label:"Form Title",placeholder:"e.g. Contact, Feedback\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(c),e.appendChild(c);const t=document.createElement("div");t.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const o=document.createElement("button");o.className="btn btn-primary",o.textContent="Create",t.appendChild(n),t.appendChild(o),e.appendChild(t),r.element.appendChild(e),r.open();const s=c.querySelector('input[name="title"]');setTimeout(()=>s?.focus(),50);async function a(){const i=s?.value.trim();if(i)try{const l=await p("/forms",{method:"POST",body:JSON.stringify({title:i})});r.close(),R.navigate(`/forms/edit/${l.slug}`)}catch(l){E.toast(l.message||"Failed to create form.",{type:"error"})}}n.addEventListener("click",()=>r.close()),o.addEventListener("click",a),s?.addEventListener("keydown",i=>{i.key==="Enter"&&a()})}),d.find("#test-email-btn").off("click").on("click",async()=>{const e=(window.__CMS_SITE__?.smtp||{}).fromAddress||"",c=E.modal({title:"Send Test Email",size:"sm"}),t=document.createElement("div");t.style.cssText="padding:.25rem 0 .5rem;";const n=document.createElement("div");n.className="mb-3";const o=document.createElement("label");o.className="form-label",o.textContent="Send to";const s=document.createElement("input");s.type="email",s.className="form-input",s.value=e,s.placeholder="test@example.com",n.appendChild(o),n.appendChild(s),t.appendChild(n);const a=document.createElement("p");a.className="text-muted",a.style.cssText="font-size:.8rem;margin-bottom:.75rem;",a.textContent="SMTP settings are configured in Site Settings.",t.appendChild(a);const i=document.createElement("div");i.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const l=document.createElement("button");l.className="btn btn-ghost",l.textContent="Cancel";const m=document.createElement("button");m.className="btn btn-primary",m.textContent="Send",i.appendChild(l),i.appendChild(m),t.appendChild(i),c.element.appendChild(t),c.open(),setTimeout(()=>s?.focus(),50),l.addEventListener("click",()=>c.close()),m.addEventListener("click",async()=>{const f=s.value.trim();if(f){m.disabled=!0;try{await p("/forms/test-email",{method:"POST",body:JSON.stringify({to:f})}),c.close(),E.toast("Test email sent.",{type:"success"})}catch(y){E.toast(y.message||"Failed to send test email.",{type:"error"})}finally{m.disabled=!1}}})}),Domma.icons.scan()}};async function g(d){const r=E.loader(d.get(0),{type:"dots"});let e=[];try{e=await p("/forms")}catch{E.toast("Could not load forms.",{type:"error"})}finally{r.destroy()}const c=h();c&&(e=b(e,c)),T.create("#forms-table",{data:e,columns:[{key:"title",title:"Title",render:(t,n)=>{const o=document.createElement("span");o.style.cssText="display:inline-flex;align-items:center;gap:.4rem;flex-wrap:wrap;";const s=document.createElement("a");if(s.href=`#/forms/edit/${u(n.slug)}`,s.textContent=t,s.style.fontWeight="600",o.appendChild(s),n.plugin){const a=document.createElement("span");a.className="badge badge-outline",a.textContent=n.plugin,a.title=`Managed by the ${n.plugin} plugin`,a.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",o.appendChild(a)}return o.outerHTML}},{key:"slug",title:"Slug",render:t=>{const n=document.createElement("code");return n.textContent=t,n.outerHTML}},{key:"fields",title:"Field Count",render:t=>String(t?.length??0)},{key:"submissionCount",title:"Submission Count",render:t=>String(t??0)},{key:"slug",title:"Actions",render:(t,n)=>{const o=document.createElement("div");o.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const s=document.createElement("a");s.href=`#/forms/edit/${u(t)}`,s.className="btn btn-sm btn-ghost",s.setAttribute("data-tooltip","Edit"),s.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const a=document.createElement("a");a.href=`#/forms/${u(t)}/submissions`,a.className="btn btn-sm btn-ghost",a.setAttribute("data-tooltip","Submissions"),a.innerHTML='<span data-icon="inbox" data-icon-size="14"></span>';const i=document.createElement("button");i.className="btn btn-sm btn-ghost js-clone-form",i.dataset.slug=t,i.setAttribute("data-tooltip","Duplicate"),i.innerHTML='<span data-icon="copy" data-icon-size="14"></span>';const l=document.createElement("button");return l.className="btn btn-sm btn-ghost js-delete-form",l.dataset.slug=t,l.dataset.plugin=n.plugin||"",l.setAttribute("data-tooltip","Delete"),l.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',o.appendChild(s),o.appendChild(a),o.appendChild(i),o.appendChild(l),o.outerHTML}}],emptyMessage:'No forms yet. Click "Create Form" to get started.'}),Domma.icons.scan(),document.querySelectorAll("#forms-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}
|
package/admin/js/views/media.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as m}from"../api.js";import{openImageEditor as B}from"../lib/image-editor.js";export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(t){const i=c=>{const
|
|
1
|
+
import{api as m}from"../api.js";import{openImageEditor as B}from"../lib/image-editor.js";export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(t){const i=c=>{const s=t.find("#media-grid").empty().get(0);if(!c.length){const e=document.createElement("p");e.className="text-muted",e.textContent="No media files yet. Upload one above.",s.appendChild(e);return}c.forEach(e=>{const r=/\.(png|jpe?g|gif|webp|svg)$/i.test(e.name),p=/\.(png|jpe?g|gif|webp|tiff?)$/i.test(e.name),n=document.createElement("div");n.className="media-card";const u=document.createElement("div");if(u.className="media-preview",r){const o=document.createElement("img");o.src=e.url+"?t="+Date.now(),o.alt=e.name,o.className="media-thumb",u.appendChild(o)}else{const o=document.createElement("div");o.className="media-thumb media-thumb--file";const w=document.createElement("span");w.setAttribute("data-icon","file"),o.appendChild(w),u.appendChild(o)}n.appendChild(u);const C=document.createElement("div");C.className="media-info";const l=document.createElement("span");l.className="media-name",l.title="Double-click to rename",l.textContent=e.name;const v=document.createElement("span");v.className="media-size",v.textContent=L(e.size),C.appendChild(l),C.appendChild(v),n.appendChild(C),l.addEventListener("dblclick",()=>D(l,e,i));const h=document.createElement("div");h.className="media-actions";const b=document.createElement("button");b.className="btn btn-sm btn-ghost";const N=document.createElement("span");N.setAttribute("data-icon","copy"),b.appendChild(N),b.addEventListener("click",()=>{navigator.clipboard.writeText(window.location.origin+e.url).then(()=>E.toast("URL copied.",{type:"success"})).catch(()=>E.toast("Copy failed.",{type:"error"}))});const g=document.createElement("button");g.className="btn btn-sm btn-ghost";const k=document.createElement("span");k.setAttribute("data-icon","type"),g.appendChild(k),g.addEventListener("click",()=>D(l,e,i));let d=null;if(p){d=document.createElement("button"),d.className="btn btn-sm btn-ghost";const o=document.createElement("span");o.setAttribute("data-icon","edit"),d.appendChild(o),d.addEventListener("click",async()=>{await B(e)&&i(await m.media.list().catch(()=>[]))})}const y=document.createElement("button");y.className="btn btn-sm btn-ghost";const x=document.createElement("span");x.setAttribute("data-icon","trash"),y.appendChild(x),y.addEventListener("click",async()=>{if(await E.confirm(`Delete "${e.name}"?`))try{await m.media.delete(e.name),E.toast("File deleted.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Delete failed.",{type:"error"})}}),h.appendChild(b),h.appendChild(g),d&&h.appendChild(d),h.appendChild(y),n.appendChild(h),s.appendChild(n),E.tooltip(b,{content:"Copy URL",position:"top"}),E.tooltip(g,{content:"Rename",position:"top"}),d&&E.tooltip(d,{content:"Edit image",position:"top"}),E.tooltip(y,{content:"Delete file",position:"top"})}),Domma.icons.scan(),Domma.effects.reveal(".media-card",{animation:"fade",stagger:40,duration:350})},f=E.loader(t.get(0),{type:"dots"}),a=await m.media.list().catch(()=>[]);f.destroy(),i(a),Domma.effects.ripple("#upload-btn"),t.find("#upload-btn").on("click",()=>t.find("#file-input").get(0).click()),t.find("#file-input").on("change",async function(){if(!this.files.length)return;const c=new FormData;for(const r of this.files)c.append("file",r);const s=t.find("#upload-btn").get(0),e=s.lastChild;s.disabled=!0,e.textContent=" Uploading\u2026";try{if(!(await fetch("/api/media",{method:"POST",body:c,headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}})).ok)throw new Error("Upload failed");E.toast("File uploaded.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Upload failed.",{type:"error"})}finally{s.disabled=!1,e.textContent=" Upload",this.value=""}})}};function D(t,i,f){if(t.querySelector("input"))return;const a=i.name,c=a.lastIndexOf("."),s=c>0?a.slice(0,c):a,e=document.createElement("input");e.type="text",e.className="media-rename-input",e.value=a,e.title="",t.textContent="",t.appendChild(e),e.focus(),e.setSelectionRange(0,s.length);async function r(){const n=e.value.trim();if(!n||n===a)return p();e.disabled=!0;try{await m.media.rename(a,n),E.toast("File renamed.",{type:"success"}),f(await m.media.list().catch(()=>[]))}catch(u){E.toast(u.message||"Rename failed.",{type:"error"}),p()}}function p(){t.textContent=a,t.title="Double-click to rename"}e.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),r()),n.key==="Escape"&&(n.preventDefault(),p())}),e.addEventListener("blur",()=>{setTimeout(()=>{t.contains(e)&&p()},150)})}function L(t){if(!t)return"0 B";const i=1024,f=["B","KB","MB","GB"],a=Math.floor(Math.log(t)/Math.log(i));return`${(t/Math.pow(i,a)).toFixed(1)} ${f[a]}`}
|
|
@@ -8,7 +8,7 @@ import{api as k}from"../api.js";import{colourToCss as U}from"/public/js/menu-dec
|
|
|
8
8
|
<button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
|
|
9
9
|
<button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
|
|
10
10
|
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
11
|
-
<button class="btn btn-sm btn-
|
|
11
|
+
<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
12
12
|
</div>
|
|
13
13
|
`);continue}const y=i.badge&&(i.badge.text||i.badge.countFrom)?`<span class="dm-menu-badge">${O(String(i.badge.text||"#"))}</span>`:"",w=U(i.colour),x=w?` style="color:${O(w)}"`:"";t.append(`
|
|
14
14
|
<div class="menu-tree-row menu-tree-row--slim${i.hidden?" menu-tree-row--hidden":""}" data-id="${i.id}" style="margin-left:${b*28}px">
|
|
@@ -23,6 +23,6 @@ import{api as k}from"../api.js";import{colourToCss as U}from"/public/js/menu-dec
|
|
|
23
23
|
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
24
24
|
<button class="btn btn-sm btn-ghost me-hidden ${i.hidden?"active":""}" data-tooltip="${i.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
|
|
25
25
|
<button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
|
|
26
|
-
<button class="btn btn-sm btn-
|
|
26
|
+
<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
27
27
|
</div>
|
|
28
28
|
`),e(i.id)}}e(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function S(t,r){t.find(".menu-tree-row").each(function(){const l=$(this).data("id"),e=r.find(x=>x.id===l);if(!e||e.type==="separator")return;const n=$(this).find(".me-text");n.length&&(e.text=n.val());const i=$(this).find(".me-url");i.length&&(e.url=i.val());const b=$(this).find(".me-icon");b.length&&(e.icon=b.val());const y=$(this).find(".me-vis");y.length&&(e.visibility=y.val());const w=$(this).find(".me-perm");w.length&&(e.permission=w.val())})}function it(t,r){const l={};for(const e of r){const n=t.querySelector(`[name="${e}"]`);n&&(l[e]=n.type==="checkbox"?n.checked:n.value)}return l}function g(t,r={},...l){const e=document.createElement(t);Object.assign(e,r);for(const n of l)n!=null&&e.append(n);return e}function I(t,r,l){const e=g("label",{className:"form-field"},g("span",{className:"form-label",textContent:t}),r);return l&&e.append(g("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),e}function Q(t,r){const l=[["","\u2014 Everyone \u2014"],["public","Public \u2014 anyone, incl. logged-out"],...(r||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private \u2014 super-admin only"]];t&&!l.some(([n])=>n===t)&&l.push([t,`${t} (custom)`]);const e=P(l,t||"");return e.name="visibility",e}function P(t,r){const l=g("select",{className:"form-select"});return t.forEach(([e,n])=>l.add(new Option(n,e))),l.value=r??"",l}function W(t,r){const l=typeof r=="string"&&r.startsWith("#"),e=P([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":r||""),n=g("input",{type:"color",className:"form-input me-colour-hex",value:l?r:"#000000"});n.style.display=l?"":"none",e.addEventListener("change",()=>{n.style.display=e.value==="__custom__"?"":"none"});const i=I(t,e);return i.append(n),i._read=()=>e.value==="__custom__"?n.value:e.value,i}function Y(t,r){const l=g("input",{className:"form-input me-icon",name:"icon",value:r||""}),e=g("span",{className:"me-icon-preview"});r&&e.setAttribute("data-icon",r);const n=g("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),i=b=>{b?e.setAttribute("data-icon",b):e.removeAttribute("data-icon"),Domma.icons.scan(e.parentNode||e)};return n.addEventListener("click",()=>V(b=>{l.value=b,i(b)})),l.addEventListener("input",()=>i(l.value.trim())),I(t,g("div",{className:"me-icon-field"},l,n,e))}function M(...t){return g("div",{className:"me-field-grid"},...t)}async function Z(t,r,l,e,n){const i=m=>g("h4",{className:"me-form-section",textContent:m}),b=I("Label",g("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),y=I("URL",g("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),w=Y("Icon",t.icon||""),x=I("Visibility",Q(t.visibility||"",e),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),z=P([["","\u2014 no permission gate \u2014"],...(r||[]).map(m=>[m.key,m.label||m.key])],t.permission||"");z.name="permission";const T=I("Permission",z,"Hide unless the viewer holds this specific permission. Independent of Visibility \u2014 both must pass."),B=I("Badge text",g("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),h=P([["","\u2014 none \u2014"],...(l||[]).map(m=>[m.slug,m.title||m.slug])],t.badge?.countFrom||"");h.name="badgeCount";const o=I("Badge count from",h,"Show a live count of this collection\u2019s entries as the badge."),u=W("Badge colour",t.badge?.variant||""),_=P([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");_.name="renderAs";const j=I("Render as",_,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),C=I("Pill style",P([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));C.querySelector("select").name="pillStyle";const L=W("Pill colour",t.pill?.variant||""),F=g("div",{className:"me-pill-wrap"},M(C,L));F.style.display=t.pill?"":"none",_.addEventListener("change",()=>{F.style.display=_.value==="pill"?"":"none"});const s=W("Text colour",t.colour||""),a=g("button",{className:"btn btn-primary",textContent:"Apply"}),d=g("div",{className:"me-item-form"},i("Link"),b,y,w,i("Access"),M(x,T),i("Badge"),M(B,u),o,i("Appearance"),j,F,s,a),p=E.slideover({title:"Edit menu item",size:"lg",position:"right"});p.element.appendChild(d),p.open();const f=p.element.closest(".dm-slideover")||p.element.querySelector(".dm-slideover")||p.element;f.style.setProperty("width",K+"px","important"),f.style.setProperty("max-width","95vw","important"),Domma.icons.scan(d);const c=m=>d.querySelector(`[name="${m}"]`)?.value||"";a.addEventListener("click",()=>{t.text=c("text"),t.url=c("url"),t.icon=c("icon"),t.visibility=c("visibility"),t.permission=c("permission");const m=c("badgeText"),A=c("badgeCount"),R=u._read();if(t.badge=m||A?{...m&&{text:m},...A&&{countFrom:A},...R&&{variant:R}}:null,c("renderAs")==="pill"){const D=L._read();t.pill={style:c("pillStyle")||"filled",...D&&{variant:D}}}else t.pill=null;t.colour=s._read(),p.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const r=location.hash.match(/^#\/menus\/edit\/(.+)$/),l=r?decodeURIComponent(r[1]):null,e=!l,n=E.loader(t.get(0),{type:"dots"});let i,b,y,w,x;try{if(e)i={slug:"",name:"",description:"",items:[]};else try{i=await k.menus.get(l)}catch(s){E.toast(`Failed to load menu: ${s.message||s}`,{type:"error"}),location.hash="#/menus";return}b=await k.projects.list().catch(()=>[]),y=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),w=await k.collections.list().catch(()=>[]),x=await k.get("/collections/roles/entries?limit=100").catch(()=>null)}finally{n.destroy()}const T=(Array.isArray(x)?x:x?.entries||[]).map(s=>({name:s.data?.name,label:s.data?.label,level:s.data?.level??99})).filter(s=>s.name).sort((s,a)=>a.level-s.level),B='<option value="">\u2014 no permission gate \u2014</option>',h=s=>B+(y.resources||[]).map(a=>{const d=(s||"")===a.key?" selected":"";return`<option value="${a.key}"${d}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(e?"New menu":`Edit "${i.slug}"`);let o=q(i.items);const u=t.find("#me-items-list");v(u,o,h),t.find("#me-add-item").on("click",()=>{S(u,o),o.push({id:N(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),v(u,o,h)}),t.find("#me-add-separator").on("click",()=>{S(u,o),o.push({id:N(),parentId:null,type:"separator"}),v(u,o,h)});function _(s){return function(){S(u,o);const a=$(this).closest(".menu-tree-row").data("id"),d=o.findIndex(c=>c.id===a);if(d===-1)return;const p=o[d],f=s==="separator"?{id:N(),parentId:p.parentId,type:"separator"}:{id:N(),parentId:p.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};o.splice(d+1,0,f),v(u,o,h)}}t.off("click",".me-ins-item").on("click",".me-ins-item",_("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",_("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const s=$(this).closest(".menu-tree-row").data("id");o=o.filter(a=>a.id!==s&&a.parentId!==s),v(u,o,h)}),t.off("click",".me-indent").on("click",".me-indent",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(f=>f.id===s);if(!a)return;const d=o.filter(f=>f.parentId===a.parentId),p=d.findIndex(f=>f.id===s);p<=0||d[p-1].type!=="separator"&&(a.parentId=d[p-1].id,v(u,o,h))}),t.off("click",".me-outdent").on("click",".me-outdent",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(p=>p.id===s);if(!a||a.parentId==null)return;const d=o.find(p=>p.id===a.parentId);a.parentId=d?d.parentId:null,v(u,o,h)}),t.off("click",".me-up").on("click",".me-up",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.filter(c=>c.parentId===o.find(m=>m.id===s)?.parentId),d=a.findIndex(c=>c.id===s);if(d<=0)return;const p=o.indexOf(a[d]),f=o.indexOf(a[d-1]);[o[p],o[f]]=[o[f],o[p]],v(u,o,h)}),t.off("click",".me-down").on("click",".me-down",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.filter(c=>c.parentId===o.find(m=>m.id===s)?.parentId),d=a.findIndex(c=>c.id===s);if(d===-1||d>=a.length-1)return;const p=o.indexOf(a[d]),f=o.indexOf(a[d+1]);[o[p],o[f]]=[o[f],o[p]],v(u,o,h)}),t.off("click",".me-hidden").on("click",".me-hidden",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(d=>d.id===s);a&&(a.hidden=!a.hidden),v(u,o,h)}),t.off("click",".me-edit").on("click",".me-edit",function(){const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(d=>d.id===s);!a||a.type==="separator"||Z(a,y.resources||[],w,T,()=>v(u,o,h))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(i.variant||""),t.find("#me-position").val(i.position||"");const j=t.find("#me-appearOnHover").get(0);j&&(j.checked=i.appearOnHover===!0),t.find("#me-fontFamily").val(i.style?.fontFamily||""),t.find("#me-fontSize").val(i.style?.fontSize||""),t.find("#me-fontWeight").val(i.style?.fontWeight||""),t.find("#me-letterSpacing").val(i.style?.letterSpacing||""),t.find("#me-iconSize").val(i.style?.iconSize||"");const{getProjectFromHash:C}=await import("../lib/project-context.js"),L=e?C():null,F=t.find("#me-project");F.html('<option value="">\u2014 none \u2014</option>'+b.map(s=>`<option value="${O(s.slug)}">${O(s.name||s.slug)}</option>`).join("")),t.find("#me-slug").val(i.slug||""),t.find("#me-name").val(i.name||""),t.find("#me-description").val(i.description||""),F.val(i.meta?.project||L||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{S(u,o);const s={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},d=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],p=Object.fromEntries(Object.entries(s).filter(([c,m])=>d.includes(c)&&m)),f={slug:a.slug,name:a.name,description:a.description,...s.variant&&{variant:s.variant},...s.position&&{position:s.position},...s.appearOnHover&&{appearOnHover:!0},...Object.keys(p).length&&{style:p},items:X(o),meta:{...i.meta||{},project:a.project||null}};try{if(e)await k.menus.create(f);else if(f.slug!==i.slug){await k.menus.create(f);const c=await k.menuLocations.get();let m=!1;for(const[A,R]of Object.entries(c))R===i.slug&&(c[A]=f.slug,m=!0);m&&await k.menuLocations.save(c),await k.menus.remove(i.slug)}else await k.menus.update(i.slug,f);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(f.slug)}catch(c){E.toast(`Save failed: ${c.message||c}`,{type:"error"})}})}};
|
package/admin/js/views/menus.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{api as o}from"../api.js";import{filterByProject as b,getProjectFromHash as y}from"../lib/project-context.js";function l(
|
|
2
|
-
<a class="btn btn-sm btn-ghost" href="#/menus/edit/${encodeURIComponent(t.slug)}" data-tooltip="Edit"><span data-icon="edit"></span></a>
|
|
3
|
-
<button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${u}" data-tooltip="Duplicate"><span data-icon="copy"></span></button>
|
|
4
|
-
<button class="btn btn-sm btn-
|
|
5
|
-
`}}]}),Domma.icons.scan("#menus-table"),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),
|
|
1
|
+
import{api as o}from"../api.js";import{filterByProject as b,getProjectFromHash as y}from"../lib/project-context.js";import{keepTableIconsAlive as f}from"../lib/table-icons.js";function l(s){return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}export const menusView={templateUrl:"/admin/js/templates/menus.html",async onMount(s){const p=E.loader(s.get(0),{type:"dots"});let c,r;try{[c,r]=await Promise.all([o.menus.list().catch(()=>[]),o.menuLocations.get().catch(()=>({}))])}finally{p.destroy()}const d=y(),m=d?b(c,d):c,n={};for(const[e,t]of Object.entries(r||{}))t&&(n[t]||(n[t]=[]),n[t].push(e));T.create("#menus-table",{data:m,emptyMessage:'No menus yet. Click "New menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${l(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${l(e||t.slug)}</a>`},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))},{key:"mapped",title:"Mapped",render:(e,t)=>{const a=n[t.slug]||[];return a.length?a.map(i=>`<span class="badge badge-info">${l(i)}</span>`).join(" "):'<span class="text-muted">\u2014</span>'}},{key:"bundled",title:"Bundled",render:(e,t)=>t.meta?.bundled?'<span class="badge badge-secondary">bundled</span>':""},{key:"_actions",title:"Actions",render:(e,t)=>{const a=(n[t.slug]||[]).length>0,i=a?'data-tooltip="Unmap this menu before deleting"':"",g=a?"disabled":"",u=l(t.slug);return`
|
|
2
|
+
<a class="btn btn-sm btn-ghost" href="#/menus/edit/${encodeURIComponent(t.slug)}" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
|
|
3
|
+
<button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${u}" data-tooltip="Duplicate"><span data-icon="copy" data-icon-size="14"></span></button>
|
|
4
|
+
<button class="btn btn-sm btn-ghost btn-delete" data-slug="${u}" ${g} ${i}><span data-icon="trash" data-icon-size="14"></span></button>
|
|
5
|
+
`}}]}),Domma.icons.scan("#menus-table"),f(s.get(0)),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),s.off("click",".btn-duplicate").on("click",".btn-duplicate",async function(){const e=$(this).data("slug");try{const t=await o.menus.duplicate(e);E.toast(`Duplicated as "${t.slug}".`,{type:"success"}),location.reload()}catch(t){E.toast(`Duplicate failed: ${t.message||t}`,{type:"error"})}}),s.off("click",".btn-delete").on("click",".btn-delete",async function(){if(this.disabled)return;const e=$(this).data("slug");if(await E.confirm(`Delete menu "${e}"? This cannot be undone.`))try{await o.menus.remove(e),E.toast("Menu deleted.",{type:"success"}),location.reload()}catch(a){E.toast(`Delete failed: ${a.message||a}`,{type:"error"})}})}};
|
|
@@ -8,7 +8,7 @@ import{api as x}from"../api.js";let k=1;const v=n=>String(n||"").replace(/&/g,"&
|
|
|
8
8
|
<span class="nav-col-action">
|
|
9
9
|
<button class="btn btn-sm btn-ghost btn-toggle-bundled${t.bundled?" active":""}" data-id="${t._id}" data-bundled="${!!t.bundled}" data-tooltip="${t.bundled?"Unbundle":"Bundle (seed on install)"}"><span data-icon="package"></span></button>
|
|
10
10
|
<button class="btn btn-sm btn-ghost btn-toggle-hidden${c?" active":""}" data-id="${t._id}" data-hidden="${c}" data-tooltip="${c?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
|
|
11
|
-
<button class="btn btn-sm btn-
|
|
11
|
+
<button class="btn btn-sm btn-ghost btn-remove-item" data-id="${t._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
12
12
|
</span>
|
|
13
13
|
</div>
|
|
14
14
|
`)}),Domma.icons.scan("#nav-items-list"),document.querySelectorAll("#nav-items-list [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})},m=()=>{const d=n.find("#footer-links-list").empty();e.forEach((i,t)=>{const s=i.hidden===!0;d.append(`
|
|
@@ -17,7 +17,7 @@ import{api as x}from"../api.js";let k=1;const v=n=>String(n||"").replace(/&/g,"&
|
|
|
17
17
|
<input type="text" class="form-input footer-link-url nav-col-main" value="${v(i.url)}" placeholder="/url">
|
|
18
18
|
<span class="nav-col-action">
|
|
19
19
|
<button class="btn btn-sm btn-ghost btn-toggle-hidden${s?" active":""}" data-idx="${t}" data-hidden="${s}" data-tooltip="${s?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
|
|
20
|
-
<button class="btn btn-sm btn-
|
|
20
|
+
<button class="btn btn-sm btn-ghost btn-remove-footer" data-idx="${t}" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
21
21
|
</span>
|
|
22
22
|
</div>
|
|
23
23
|
`)}),Domma.icons.scan("#footer-links-list"),document.querySelectorAll("#footer-links-list [data-tooltip]").forEach(i=>{E.tooltip(i,{content:i.getAttribute("data-tooltip"),position:"top"})})},h=()=>{e=[],n.find("#footer-links-list .nav-item-row").each(function(){const d=$(this).find(".btn-toggle-hidden").attr("data-hidden")==="true",i={text:$(this).find(".footer-link-text").val().trim(),url:$(this).find(".footer-link-url").val().trim()};d&&(i.hidden=!0),e.push(i)})};n.find("#add-footer-link").on("click",()=>{h(),e.push({text:"",url:"",hidden:!1}),m()}),n.off("click",".btn-remove-footer").on("click",".btn-remove-footer",function(){h();const d=parseInt($(this).data("idx"),10);e.splice(d,1),m()}),n.find("#field-brand-text").val(a.brand?.text||""),n.find("#field-brand-url").val(a.brand?.url||"/"),n.find("#field-brand-icon").val(a.brand?.icon||""),n.find("#field-nav-variant").val(a.variant||"dark"),n.find("#field-brand-tagline").val(a.brand?.tagline||""),n.find("#field-brand-logo").val(a.brand?.logo||""),n.find("#field-nav-position").val(a.position||"sticky"),n.find("#field-brand-size").val(a.brand?.size||"md"),n.find("#field-nav-font-family").val(a.style?.fontFamily||""),n.find("#field-nav-font-size").val(a.style?.fontSize||""),n.find("#field-nav-font-weight").val(a.style?.fontWeight||""),n.find("#field-nav-letter-spacing").val(a.style?.letterSpacing||""),n.find("#field-nav-icon-size").val(a.style?.iconSize||""),n.find("#field-admin-brand-title").val(l.adminBrand?.title||""),n.find("#field-admin-brand-icon").val(l.adminBrand?.icon||""),n.find("#field-admin-brand-logo").val(l.adminBrand?.logo||""),p(),m(),E.tabs(n.find("#nav-tabs").get(0)),n.find("#add-nav-item").on("click",()=>{u(),o.push({_id:k++,text:"",url:"",icon:"",hidden:!1,parentId:null}),p()}),n.off("click",".btn-remove-item").on("click",".btn-remove-item",function(){u();const d=parseInt($(this).data("id"),10);o=o.filter(i=>i._id!==d&&i.parentId!==d),p()}),n.off("click","#nav-items-list .btn-toggle-bundled").on("click","#nav-items-list .btn-toggle-bundled",function(d){d.stopPropagation(),u();const i=parseInt($(this).data("id"),10),t=o.find(s=>s._id===i);t&&(t.bundled=!t.bundled),p()}),n.off("click","#nav-items-list .btn-toggle-hidden").on("click","#nav-items-list .btn-toggle-hidden",function(d){d.stopPropagation(),u();const i=parseInt($(this).data("id"),10),t=o.find(s=>s._id===i);t&&(t.hidden=!t.hidden),p()}),n.off("click","#footer-links-list .btn-toggle-hidden").on("click","#footer-links-list .btn-toggle-hidden",function(d){d.stopPropagation(),h();const i=parseInt($(this).data("idx"),10);e[i]&&(e[i].hidden=!e[i].hidden),m()}),n.off("change",".item-parent").on("change",".item-parent",function(){u(),p()}),n.find("#save-nav-btn").on("click",async()=>{u(),h();const d=B(o.map(r=>({...r,text:r.text.trim(),url:r.url.trim(),icon:r.icon.trim()}))).filter(r=>r.text||r.url),i=n.find("#field-brand-icon").val().trim(),t=n.find("#field-brand-tagline").val().trim(),s=n.find("#field-brand-logo").val().trim(),c=n.find("#field-brand-size").val(),g={brand:{text:n.find("#field-brand-text").val().trim(),url:n.find("#field-brand-url").val().trim()||"/",...s&&{logo:s},...i&&!s&&{icon:i},...t&&{tagline:t},...c!=="md"&&{size:c}},items:d,variant:n.find("#field-nav-variant").val(),position:n.find("#field-nav-position").val()||"sticky",style:{...n.find("#field-nav-font-family").val()&&{fontFamily:n.find("#field-nav-font-family").val()},...n.find("#field-nav-font-size").val()&&{fontSize:n.find("#field-nav-font-size").val()},...n.find("#field-nav-font-weight").val()&&{fontWeight:n.find("#field-nav-font-weight").val()},...n.find("#field-nav-letter-spacing").val()&&{letterSpacing:n.find("#field-nav-letter-spacing").val()},...n.find("#field-nav-icon-size").val()&&{iconSize:n.find("#field-nav-icon-size").val()}}},f=e.filter(r=>r.text||r.url);try{await x.navigation.save(g),a=g,o=S(a.items),p(),E.toast("Navigation saved.",{type:"success"})}catch(r){console.error("[navigation] save failed:",r),E.toast("Failed to save navigation.",{type:"error"});return}const y=n.find("#field-admin-brand-logo").val().trim(),I=n.find("#field-admin-brand-icon").val().trim(),_=n.find("#field-admin-brand-title").val().trim(),b={..._&&{title:_},...y&&{logo:y},...!y&&I&&{icon:I}};try{await x.settings.save({adminBrand:b,footer:{links:f}}),l={...l,adminBrand:b},e=f,m();const r=b.title||"CMS Admin",A=b.icon||"layout",w=b.logo||"",z=$("#admin-topbar .topbar-brand");w?z.html(`<img src="${w}" class="topbar-brand-logo" alt="${r}"><span class="topbar-brand-text">${r}</span>`):z.html(`<span data-icon="${A}"></span><span class="topbar-brand-text">${r}</span>`),Domma.icons.scan("#admin-topbar .topbar-brand")}catch(r){console.error("[navigation] admin brand / footer save failed:",r),E.toast(`Admin brand save failed: ${r.message||r}`,{type:"error"})}})}};
|
package/admin/js/views/pages.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
|
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 i=await p.pages.list().catch(()=>[]);g.destroy();const c=f();c&&(i=m(i,c));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>':"",o=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}${o}`}},{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-ghost" data-tooltip="Edit"><span data-icon="edit"></span></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
|
-
<button class="btn btn-sm btn-
|
|
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(
|
|
5
|
+
<button class="btn btn-sm btn-ghost btn-delete" data-path="${e.urlPath}" data-plugin="${e.plugin||""}" data-tooltip="Delete"><span data-icon="trash"></span></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(i);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 o=a.urlPath.split("/").filter(Boolean),l=o.length>1?"/"+o.slice(0,-1).join("/"):null,d=l&&s.some(h=>h.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"),b(i)}),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=i.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"}),i=i.filter(a=>a.urlPath!==s),r(i)}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
|
|
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 o=await p.versions.prune(s,a);E.toast(`Pruned ${o.deleted} revision${o.deleted===1?"":"s"}; kept ${o.kept}.`,{type:"success"});const l=i.find(d=>d.urlPath===s);l&&(l.versionCount=o.kept),r(i)}catch(o){E.toast(o.message||"Failed to prune revisions.",{type:"error"})}})}};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="settings"></span></span>`:`<a class="btn btn-sm btn-ghost" href="#/projects/${a}" data-tooltip="Overview"><span data-icon="eye"></span></a>
|
|
3
|
-
<a class="btn btn-sm btn-ghost" href="#/projects/${a}/settings" data-tooltip="Settings"><span data-icon="settings"></span></a>`,g=e.slug==="core"?"":o(e)?`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="1" data-tooltip="Enable"><span class="text-success" data-icon="play"></span></button>`:`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="0" data-tooltip="Disable"><span class="text-warning" data-icon="pause"></span></button>`,u=e.protected?"":`<button class="btn btn-sm btn-
|
|
1
|
+
import{api as l}from"../api.js";import{keepTableIconsAlive as m}from"../lib/table-icons.js";function s(n){return String(n??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}export const projectsView={templateUrl:"/admin/js/templates/projects.html",async onMount(n){const d=E.loader(n.get(0),{type:"dots"});let i,r;try{i=await l.projects.list().catch(()=>[]),r=await Promise.all(i.map(t=>l.projects.artefacts(t.slug).then(e=>Object.values(e).reduce((a,c)=>a+c.length,0)).catch(()=>0)))}finally{d.destroy()}const b=i.map((t,e)=>({...t,artefactCount:r[e]})),o=t=>t.enabled===!1;T.create("#projects-table",{data:b,emptyMessage:'No projects yet. Click "New project" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:(t,e)=>o(e)?`<span class="text-muted">${s(t)}</span>`:s(t)},{key:"name",title:"Name",sortable:!0,render:(t,e)=>{const a=o(e)?`<span class="text-muted">${s(t)}</span>`:s(t),c=e.protected?' <span class="badge badge-secondary" data-tooltip="Built-in project \u2014 cannot be deleted">System</span>':"",p=o(e)?' <span class="badge badge-warning" data-tooltip="Disabled \u2014 public pages and external API are off">Disabled</span>':"";return a+c+p}},{key:"icon",title:"Icon",render:(t,e)=>t?`<span class="${o(e)?"text-muted":""}" 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)=>{const a=encodeURIComponent(e.slug),c=o(e)?`<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="eye" data-icon-size="14"></span></span>
|
|
2
|
+
<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="settings" data-icon-size="14"></span></span>`:`<a class="btn btn-sm btn-ghost" href="#/projects/${a}" data-tooltip="Overview"><span data-icon="eye" data-icon-size="14"></span></a>
|
|
3
|
+
<a class="btn btn-sm btn-ghost" href="#/projects/${a}/settings" data-tooltip="Settings"><span data-icon="settings" data-icon-size="14"></span></a>`,g=e.slug==="core"?"":o(e)?`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="1" data-tooltip="Enable"><span class="text-success" data-icon="play" data-icon-size="14"></span></button>`:`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="0" data-tooltip="Disable"><span class="text-warning" data-icon="pause" data-icon-size="14"></span></button>`,u=e.protected?"":`<button class="btn btn-sm btn-ghost btn-delete-project" data-slug="${s(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash" data-icon-size="14"></span></button>`;return`<span style="display:inline-flex;gap:0.25rem;">${c}${g}${u}</span>`}}]}),Domma.icons.scan(n.get(0)),m(n.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),n.off("click",".btn-toggle-project").on("click",".btn-toggle-project",async function(){const t=$(this).data("slug"),e=String($(this).data("enable"))==="1";if(!(!e&&!await E.confirm(`Disable project "${t}"? Its public pages will 404 and its external API will stop responding. Admin editing is unaffected.`)))try{await l.projects.update(t,{enabled:e}),E.toast(e?"Project enabled.":"Project disabled.",{type:"success"}),location.reload()}catch(a){E.toast(`Failed: ${a.message||a}`,{type:"error"})}}),n.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 l.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
|
package/admin/js/views/roles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
2
|
-
<a href="#/roles/edit/${e}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit"></span></a>
|
|
3
|
-
${n?"":`<button class="btn btn-sm btn-
|
|
4
|
-
`}}],emptyMessage:"No roles found."}),Domma.icons.scan(),document.querySelectorAll("#roles-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};try{await v()}finally{b.destroy()}h(),document.addEventListener("click",async function e(
|
|
1
|
+
import{api as y,getUser as C}from"../api.js";import{getProjectFromHash as N}from"../lib/project-context.js";import{keepTableIconsAlive as k}from"../lib/table-icons.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 w(t){if(!t||!t.length)return"<em>None</em>";const f=new Set(t.map(c=>c.split(".")[0])),b=t.every(c=>!c.includes(".")),o=f.size;return`${o} resource${o!==1?"s":""} ${b?"(full)":"(partial)"}`}function x(t){return String(t).replace(/"/g,""")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(t){if(!C()){R.navigate("/");return}const b=E.loader(t.get(0),{type:"dots"});let o=[];const c=N(),v=async()=>{o=(await y.get("/collections/roles/entries?limit=100")).entries||[],c&&(o=o.filter(a=>(a?.data?.meta?.project||"core")===c))},h=()=>{T.create("#roles-table",{data:o,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=>w(e.permissions)},{key:"id",title:"Actions",render:(e,a)=>{const n=a.data?.level===0;return`
|
|
2
|
+
<a href="#/roles/edit/${e}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
|
|
3
|
+
${n?"":`<button class="btn btn-sm btn-ghost btn-delete-role" data-id="${e}" data-name="${x(a.data?.label)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
|
|
4
|
+
`}}],emptyMessage:"No roles found."}),Domma.icons.scan(),document.querySelectorAll("#roles-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};try{await v()}finally{b.destroy()}h(),k(t.get(0)),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,p=n.dataset.name;if(await E.confirm(`Delete role <strong>${p}</strong>? This cannot be undone.`))try{await y.delete(`/collections/roles/entries/${m}`),E.toast("Role deleted.",{type:"success"}),await v(),h()}catch(u){E.toast(`Failed: ${u.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,i)=>{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(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"}),p=Object.assign(document.createElement("input"),{type:"number",className:"form-input",value:"5",min:"1"}),d=document.createElement("select");d.className="form-input",$.forEach(l=>{const i=Object.assign(document.createElement("option"),{value:l.value,textContent:l.label});d.appendChild(i)});const u=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",p)),e.appendChild(a("Badge Colour",d)),e.appendChild(u);const g=E.modal({title:"Add Role",size:"sm"});g.element.appendChild(e),g.open(),u.addEventListener("click",async()=>{const l=n.value.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),i=m.value.trim(),s=parseInt(p.value,10);if(!l||!i||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:i,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"})}})})}};
|
package/admin/js/views/users.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(
|
|
2
|
-
<a href="#/users/edit/${t.id}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit"></span></a>
|
|
3
|
-
${a?"":`<button class="btn btn-sm btn-
|
|
4
|
-
`:""}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};
|
|
1
|
+
import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){const m=A(),f=E.loader(o.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=r=>{T.create("#users-table",{data:r,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",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=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 g+l}},{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(l=>[l.data?.name,l.data?.level??99]));return(i[m?.role]??99)<(i[t.role]??99)?`
|
|
2
|
+
<a href="#/users/edit/${t.id}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
|
|
3
|
+
${a?"":`<button class="btn btn-sm btn-ghost btn-delete-user" data-id="${t.id}" data-name="${v(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
|
|
4
|
+
`:""}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(o.get(0)),o.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 d.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==r),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(o){return String(o).replace(/"/g,""")}
|
|
@@ -1 +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));const n=E.loader(e.get(0),{type:"dots"});try{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))}finally{n.destroy()}e.find("#add-stage-btn").off("click").on("click",()=>{const a=e.find("#add-stage-type").val()||"$match";q(e,{type:a,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"),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
|
+
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));const n=E.loader(e.get(0),{type:"dots"});try{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))}finally{n.destroy()}e.find("#add-stage-btn").off("click").on("click",()=>{const a=e.find("#add-stage-type").val()||"$match";q(e,{type:a,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-ghost";const s=document.createElement("span");s.setAttribute("data-icon","trash"),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
|
|
1
|
+
import{api as r}from"../api.js";import{filterByProject as m,getProjectFromHash as u}from"../lib/project-context.js";import{keepTableIconsAlive as w}from"../lib/table-icons.js";function s(a){return String(a).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}export const viewsListView={templateUrl:"/admin/js/templates/views-list.html",async onMount(a){await d(a),a.find("#create-view-btn").off("click").on("click",()=>{R.navigate("/views/new")}),Domma.icons.scan(),w(a.get(0))}};async function d(a){const p=E.loader(a.get(0),{type:"dots"});let c=[];try{c=await r.views.list()}catch{E.toast("Could not load views.",{type:"error"})}finally{p.destroy()}const l=u();l&&(c=m(c,l)),T.create("#views-table",{data:c,columns:[{key:"title",title:"Title",render:(e,t)=>{const i=document.createElement("a");return i.href=`#/views/${s(t.slug)}/preview`,i.textContent=e,i.style.fontWeight="600",i.outerHTML}},{key:"slug",title:"Slug",render:e=>`<code>${s(e)}</code>`},{key:"pipeline",title:"Source",render:e=>`<code>${s(e?.source||"\u2014")}</code>`},{key:"display",title:"Mode",render:e=>s(e?.mode||"table")},{key:"access",title:"Roles",render:e=>(e?.roles||[]).map(t=>`<span class="badge badge-secondary">${s(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 i=document.createElement("a");i.href=`#/views/${s(e)}/preview`,i.className="btn btn-sm btn-ghost",i.setAttribute("data-tooltip","Preview"),i.innerHTML='<span data-icon="eye" data-icon-size="14"></span>';const o=document.createElement("a");o.href=`#/views/edit/${s(e)}`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Edit"),o.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const n=document.createElement("button");return n.className="btn btn-sm btn-ghost js-delete-view",n.dataset.slug=e,n.setAttribute("data-tooltip","Delete"),n.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',t.appendChild(i),t.appendChild(o),t.appendChild(n),t.outerHTML}}],emptyMessage:'No views yet. Click "New View" to create your first view.'}),a.off("click",".js-delete-view").on("click",".js-delete-view",async function(){const e=this.dataset.slug;if(await E.confirm(`Delete view "${e}"? This cannot be undone.`))try{await r.views.delete(e),E.toast("View deleted.",{type:"success"}),await d(a)}catch{E.toast("Failed to delete view.",{type:"error"})}}),Domma.icons.scan(),document.querySelectorAll("#views-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}
|
package/package.json
CHANGED
package/server/routes/public.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Draft pages are not served publicly.
|
|
5
5
|
* The admin panel is excluded (handled by static serving).
|
|
6
6
|
*/
|
|
7
|
-
import {getPage} from '../services/content.js';
|
|
7
|
+
import {getPage, getPageMtime} from '../services/content.js';
|
|
8
8
|
import {getProjectForPage, isProjectEnabled} from '../services/projects.js';
|
|
9
9
|
import {renderPage} from '../services/renderer.js';
|
|
10
10
|
import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.js';
|
|
@@ -155,7 +155,11 @@ export async function publicRoutes(fastify) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const baseUrl = getBaseUrl(request);
|
|
158
|
-
|
|
158
|
+
// mtime in the key: any change to the backing file — editor save,
|
|
159
|
+
// script, git pull — yields a fresh entry even if no invalidation
|
|
160
|
+
// hook fired. Stale-mtime entries age out via TTL/LRU.
|
|
161
|
+
const pageMtime = await getPageMtime(page.urlPath || urlPath);
|
|
162
|
+
const cacheKey = `page:${urlPath}:m${pageMtime}:role${userRole ?? 'anon'}:o${baseUrl}`;
|
|
159
163
|
const cacheTags = [`page:${urlPath}`, ...(page.tags || []), 'nav', 'site'];
|
|
160
164
|
const html = await cache.wrap(
|
|
161
165
|
cacheKey,
|
|
@@ -48,6 +48,25 @@ export async function listPages() {
|
|
|
48
48
|
* that gate output by role (currently `[menu]`) can filter correctly.
|
|
49
49
|
* @returns {Promise<object|null>}
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* Millisecond mtime of the page's backing file. The public route folds this
|
|
53
|
+
* into the response-cache key so out-of-band file edits (scripts, git pulls,
|
|
54
|
+
* rsync deploys) invalidate cached pages naturally, without depending on
|
|
55
|
+
* service-layer hooks having fired.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} urlPath
|
|
58
|
+
* @returns {Promise<number>} 0 when the file cannot be resolved/statted.
|
|
59
|
+
*/
|
|
60
|
+
export async function getPageMtime(urlPath) {
|
|
61
|
+
try {
|
|
62
|
+
const filePath = await resolveExistingFilePath(urlPath);
|
|
63
|
+
if (!filePath) return 0;
|
|
64
|
+
return Math.trunc((await fs.stat(filePath)).mtimeMs);
|
|
65
|
+
} catch {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
export async function getPage(urlPath, opts = {}) {
|
|
52
71
|
const filePath = await resolveExistingFilePath(urlPath);
|
|
53
72
|
try {
|