domma-cms 0.8.10 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import{api as p}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(e){const c=E.loader(e.get(0),{type:"dots"});let i=await p.pages.list().catch(()=>[]);c.destroy();const l=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,a)=>`<a href="#/pages/edit${a.urlPath}">${t}</a>`},{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(a=>`<span class="badge badge-info badge-pill badge-sm">${a}</span>`).join(" "):"\u2014"},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,a)=>`
1
+ import{api as p}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(t){const c=E.loader(t.get(0),{type:"dots"});let i=await p.pages.list().catch(()=>[]);c.destroy();const l=n=>{T.create("#pages-table",{data:n,columns:[{key:"title",title:"Title",render:(e,a)=>{const s=a.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>':"";return`<a href="#/pages/edit${a.urlPath}" style="font-weight:600;">${e}</a>${s}`}},{key:"urlPath",title:"URL",render:e=>`<code>${e}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:e=>`<span class="badge badge-${e==="published"?"success":"warning"}">${e}</span>`},{key:"tags",title:"Tags",render:e=>Array.isArray(e)&&e.length?e.map(a=>`<span class="badge badge-info badge-pill badge-sm">${a}</span>`).join(" "):"\u2014"},{key:"updatedAt",title:"Updated",render:e=>e?D(e).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(e,a)=>`
2
2
  <a href="#/pages/edit${a.urlPath}" class="btn btn-sm btn-primary">Edit</a>
3
3
  <a href="${a.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-danger btn-delete" data-path="${a.urlPath}">Delete</button>
5
- `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(i);const u=s=>{const t=e.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const a=s.map(n=>{const d=n.urlPath.split("/").filter(Boolean),r=d.length>1?"/"+d.slice(0,-1).join("/"):null,g=r&&s.some(f=>f.urlPath===r);return{id:n.urlPath,parent_id:g?r:null,name:n.title||n.urlPath,icon:n.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:a,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:n=>{R.navigate(`/pages/edit${n}`)}}),Domma.icons.scan(t)};e.find("#view-table-btn").on("click",function(){e.find("#pages-table").show(),e.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),e.find("#view-tree-btn").on("click",function(){e.find("#pages-table").hide(),e.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),u(i)}),e.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const o=()=>{const s=e.find("#status-filter").val(),t=e.find("#pages-search").val().toLowerCase().trim(),a=i.filter(n=>!(s&&n.status!==s||t&&!`${n.title} ${n.urlPath} ${(n.tags||[]).join(" ")}`.toLowerCase().includes(t)));l(a)};e.find("#status-filter").off("change").on("change",o),e.find("#pages-search").get(0).addEventListener("input",o),e.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path");if(await E.confirm(`Delete page at <strong>${s}</strong>? This cannot be undone.`))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),i=i.filter(a=>a.urlPath!==s),l(i)}catch{E.toast("Failed to delete page.",{type:"error"})}})}};
5
+ `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(i);const g=n=>{const e=t.find("#pages-tree").empty().get(0);if(!n.length){e.textContent="No pages found.";return}const a=n.map(s=>{const d=s.urlPath.split("/").filter(Boolean),r=d.length>1?"/"+d.slice(0,-1).join("/"):null,u=r&&n.some(b=>b.urlPath===r);return{id:s.urlPath,parent_id:u?r:null,name:s.title||s.urlPath,icon:s.status==="published"?"check-circle":"file-text"}});E.treeView(e,{data:a,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:s=>{R.navigate(`/pages/edit${s}`)}}),Domma.icons.scan(e)};t.find("#view-table-btn").on("click",function(){t.find("#pages-table").show(),t.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),t.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),t.find("#view-tree-btn").on("click",function(){t.find("#pages-table").hide(),t.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),t.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),g(i)}),t.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const o=()=>{const n=t.find("#status-filter").val(),e=t.find("#pages-search").val().toLowerCase().trim(),a=i.filter(s=>!(n&&s.status!==n||e&&!`${s.title} ${s.urlPath} ${(s.tags||[]).join(" ")}`.toLowerCase().includes(e)));l(a)};t.find("#status-filter").off("change").on("change",o),t.find("#pages-search").get(0).addEventListener("input",o),t.off("click",".btn-delete").on("click",".btn-delete",async function(){const n=$(this).data("path");if(await E.confirm(`Delete page at <strong>${n}</strong>? This cannot be undone.`))try{await p.pages.delete(n),E.toast("Page deleted.",{type:"success"}),i=i.filter(a=>a.urlPath!==n),l(i)}catch{E.toast("Failed to delete page.",{type:"error"})}})}};
@@ -1 +1 @@
1
- import{api as h}from"../api.js";let b=null,f=null;export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){b=null,f=null;const l=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);l&&(b=l[1]),E.tabs(e.find("#view-editor-tabs").get(0)),await O(e),await D(e),await j(e),await T(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await x(e)}),b&&(e.find("#view-editor-title").text("Edit View"),await I(e,b)),e.find("#add-stage-btn").off("click").on("click",()=>{const n=e.find("#add-stage-type").val()||"$match";q(e,{type:n,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await Q(e)}),H(e),Y(e),Domma.icons.scan()}};async function O(e){const t=e.find("#view-source").get(0);if(t)try{(await h.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 T(e){const t=e.find("#view-block-name").get(0);if(t)try{(await h.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-connection").get(0);if(t)try{const l=await h.collections.getConnections();Object.keys(l).forEach(n=>{if(!t.querySelector(`option[value="${n}"]`)){const a=document.createElement("option");a.value=n,a.textContent=n,t.appendChild(a)}})}catch{}}async function j(e){const t=e.find("#view-roles-checkboxes").get(0);if(!t)return;["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const s=document.createElement("input");s.type="checkbox",s.value=n,s.dataset.role=n,s.className="view-role-cb",s.checked=n==="admin",a.appendChild(s),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function x(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await h.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const l=this.dataset.stageType,n=this.querySelector(".stage-guided");n&&(l==="$match"&&M(n),l==="$sort"&&z(n))}),A(e,null)}async function I(e,t){try{const l=await h.views.get(t);if(!l){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const n=l.pipeline?.source;n&&(e.find("#view-source").val(n),await x(e)),F(e,l)}catch(l){E.toast(l.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function F(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-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 l=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=l.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const n=t.access?.rowLevel;n&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(n.mode||"owner"),e.find("#view-rowlevel-userkey").val(n.userKey||"id"),n.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(n.field||"")));const a=e.find("#pipeline-stages-list").get(0);if(a)for(;a.firstChild;)a.removeChild(a.firstChild);(t.pipeline?.stages||[]).forEach(s=>q(e,s)),A(e,t.display?.columns||[]),S(e)}const _=[{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 g(e=!1){const t=[];return e&&t.push({value:"",label:"\u2014 select field \u2014"}),(f||[]).forEach(l=>{t.push({value:`data.${l.name}`,label:`${l.label||l.name} (${l.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 v(e,t){const l=document.createElement("select");return l.className="form-input form-input--sm",e.forEach(n=>{const a=document.createElement("option");a.value=n.value,a.textContent=n.label,String(n.value)===String(t)&&(a.selected=!0),l.appendChild(a)}),l}function N(e,t,l="text"){const n=document.createElement("input");return n.type=l,n.className="form-input form-input--sm",n.placeholder=e,n.value=t??"",n}function B(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(l=>l.name===t)||null}function y(e,t,l=""){const n=B(e);if(n?.type==="select"&&(t==="eq"||t==="ne")&&n.options?.length){const i=document.createElement("select");i.className="form-input form-input--sm match-val",i.style.flex="1";const o=document.createElement("option");return o.value="",o.textContent="\u2014 select value \u2014",i.appendChild(o),(n.options||[]).forEach(c=>{const d=typeof c=="string"?c:c.value??"",u=typeof c=="string"?c:c.label||d;if(!d||d==="undefined")return;const m=document.createElement("option");m.value=d,m.textContent=u,d===l&&(m.selected=!0),i.appendChild(m)}),i}const s=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",r=document.createElement("input");return r.type="text",r.className="form-input form-input--sm match-val",r.style.flex="1",r.placeholder=s,r.value=l,r}function k(e){const t=[],l={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([n,a])=>{if(typeof a=="object"&&a!==null&&!Array.isArray(a)){if("$regex"in a){t.push({field:n,op:"contains",val:String(a.$regex??"")});return}Object.entries(a).forEach(([s,r])=>{const i=l[s];i&&t.push({field:n,op:i,val:Array.isArray(r)?r.join(", "):String(r)})})}else t.push({field:n,op:"eq",val:String(a??"")})}),t}function C(e,t,l="",n="eq",a=""){const s=document.createElement("div");s.className="match-condition-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const r=v([{value:"",label:"\u2014 field \u2014"},...g()],l);r.className+=" match-field",r.style.minWidth="140px";const i=v(_,n);i.className+=" match-op",i.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(y(l,n,a));const c=()=>{const m=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(y(r.value,i.value,m))};r.addEventListener("change",c),i.addEventListener("change",c);const d=document.createElement("button");d.type="button",d.className="btn btn-sm btn-ghost",d.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),d.appendChild(u),d.addEventListener("click",()=>s.remove()),s.appendChild(r),s.appendChild(i),s.appendChild(o),s.appendChild(d),e.insertBefore(s,t),Domma.icons.scan(s)}function W(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const l=t.$match||t||{},n="$or"in l;let a=[];n?(l.$or||[]).forEach(d=>k(d).forEach(u=>a.push(u))):a=k(l);const s=document.createElement("div");s.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const r=document.createElement("label");r.className="form-label",r.style.marginBottom="0",r.textContent="Match:";const i=document.createElement("select");i.className="form-input form-input--sm match-logic",i.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(d=>{const u=document.createElement("option");u.value=d.value,u.textContent=d.label,(n?"or":"and")===d.value&&(u.selected=!0),i.appendChild(u)}),s.appendChild(r),s.appendChild(i),e.appendChild(s);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm match-add-btn";const c=document.createElement("span");c.setAttribute("data-icon","plus"),o.appendChild(c),o.appendChild(document.createTextNode(" Add Condition")),o.addEventListener("click",()=>{C(e,o),Domma.icons.scan(e)}),e.appendChild(o),a.forEach(d=>C(e,o,d.field,d.op,d.val)),a.length===0&&C(e,o),Domma.icons.scan(e)}function M(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const l=t.querySelector(".match-field");if(l){const r=l.value,i=v([{value:"",label:"\u2014 field \u2014"},...g()],r);i.className=l.className,i.style.cssText=l.style.cssText;const o=t.querySelector(".match-val-wrap"),c=t.querySelector(".match-op");o&&c&&i.addEventListener("change",()=>{const d=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(y(i.value,c.value,d))}),l.parentNode.replaceChild(i,l)}const n=t.querySelector(".match-val-wrap"),a=t.querySelector(".match-field")?.value,s=t.querySelector(".match-op")?.value;if(n&&a&&s){const r=n.querySelector(".match-val")?.value??"";for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(y(a,s,r))}})}function U(e){const t=e.querySelector(".match-logic")?.value||"and",l=a=>{const s=a.querySelector(".match-field")?.value?.trim(),r=a.querySelector(".match-op")?.value,i=a.querySelector(".match-val")?.value?.trim()??"";if(!s)return null;const o=s.startsWith("__")?s.slice(2):s,c={};switch(r){case"eq":c[o]=i;break;case"ne":c[o]={$ne:i};break;case"gt":c[o]={$gt:isNaN(i)?i:Number(i)};break;case"lt":c[o]={$lt:isNaN(i)?i:Number(i)};break;case"gte":c[o]={$gte:isNaN(i)?i:Number(i)};break;case"lte":c[o]={$lte:isNaN(i)?i:Number(i)};break;case"contains":c[o]={$regex:i,$options:"i"};break;case"in":c[o]={$in:i.split(",").map(d=>d.trim()).filter(Boolean)};break}return Object.keys(c).length?c:null},n=[];return e.querySelectorAll(".match-condition-row").forEach(a=>{const s=l(a);s&&n.push(s)}),n.length===0?{}:t==="or"?{$or:n}:Object.assign({},...n)}function V(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const l=t.$sort||t||{},n=Object.entries(l),a=document.createElement("button");a.type="button",a.className="btn btn-ghost btn-sm";const s=document.createElement("span");s.setAttribute("data-icon","plus"),a.appendChild(s),a.appendChild(document.createTextNode(" Add Sort")),e.appendChild(a);const r=(i="",o=1)=>{const c=document.createElement("div");c.className="sort-row",c.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const d=v([{value:"",label:"\u2014 field \u2014"},...g()],i);d.className+=" sort-field";const u=v([{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",()=>{c.remove()}),c.appendChild(d),c.appendChild(u),c.appendChild(m),e.insertBefore(c,a),Domma.icons.scan(c)};a.addEventListener("click",()=>{r()}),n.forEach(([i,o])=>r(i,o)),n.length===0&&r(),Domma.icons.scan(e)}function z(e){e.querySelectorAll(".sort-row").forEach(t=>{const l=t.querySelector(".sort-field"),n=l?.value,a=v([{value:"",label:"\u2014 field \u2014"},...g()],n);a.className=l.className,l.parentNode.replaceChild(a,l)})}function J(e){const t={};return e.querySelectorAll(".sort-row").forEach(l=>{const n=l.querySelector(".sort-field")?.value?.trim(),a=parseInt(l.querySelector(".sort-dir")?.value,10)||1;if(!n)return;const s=n.startsWith("__")?n.slice(2):n;t[s]=a}),t}function q(e,t){const l=e.find("#pipeline-stages-list").get(0);if(!l)return;const n=l.querySelector(".stage-empty-placeholder");n&&n.remove();const a=["$match","$sort"].includes(t.type),s=document.createElement("div");s.className="card mb-2 stage-card",s.dataset.stageType=t.type,a&&(s.dataset.guided="true");const r=document.createElement("div");r.className="card-header",r.style.cssText="display:flex;align-items:center;gap:.5rem;";const i=document.createElement("code");i.textContent=t.type,i.style.cssText="flex:1;font-size:.85rem;";const o=document.createElement("button");o.type="button",o.className="btn btn-sm btn-danger";const c=document.createElement("span");c.setAttribute("data-icon","trash-2"),o.appendChild(c),o.addEventListener("click",()=>{s.remove(),l.querySelector(".stage-card")||l.appendChild(G())}),r.appendChild(i),r.appendChild(o);const d=document.createElement("div");if(d.className="card-body",a){const u=document.createElement("div");u.className="stage-guided",d.appendChild(u),t.type==="$match"&&W(u,t.config),t.type==="$sort"&&V(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):"",d.appendChild(u),d.appendChild(m),d.appendChild(p)}s.appendChild(r),s.appendChild(d),l.appendChild(s),Domma.icons.scan(s)}function G(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function K(e){const t=[];return e.find(".stage-card").each(function(){const l=this.dataset.stageType,n=this.querySelector(".stage-guided");let a;if(n)l==="$match"&&(a=U(n)),l==="$sort"&&(a=J(n));else{const s=this.querySelector(".stage-config")?.value?.trim()||"{}";try{a=JSON.parse(s)}catch{throw new Error(`Invalid JSON in ${l} stage config`)}}t.push({type:l,config:a})}),t}function A(e,t){const l=e.find("#view-columns-builder").get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);const n=t??(f||[]).map(i=>({key:`data.${i.name}`,label:i.label||i.name})),a=(i="",o="")=>{const c=document.createElement("div");c.className="col-row",c.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const d=f?.length?v([{value:"",label:"\u2014 field \u2014"},...g()],i):N("data.fieldName",i);d.className+=" col-key";const u=N("Display label",o);u.className+=" col-label",u.style.flex="1",f?.length&&d.tagName==="SELECT"&&d.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(L=>`data.${L.name}`===d.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",()=>{c.remove(),Domma.icons.scan(l)}),c.appendChild(d),c.appendChild(u),c.appendChild(m),l.insertBefore(c,l.lastChild),Domma.icons.scan(c)};n.forEach(i=>a(i.key,i.label));const s=document.createElement("button");s.type="button",s.className="btn btn-ghost btn-sm";const r=document.createElement("span");r.setAttribute("data-icon","plus"),s.appendChild(r),s.appendChild(document.createTextNode(" Add Column")),s.addEventListener("click",()=>{a(),Domma.icons.scan(l)}),l.appendChild(s),Domma.icons.scan(l)}function P(e){const t=[];return e.find(".col-row").each(function(){const l=this.querySelector(".col-key")?.value?.trim(),n=this.querySelector(".col-label")?.value?.trim();l&&t.push({key:l,label:n||l})}),t}function Y(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>S(e)),S(e))}function S(e){const t=e.find("#view-display-mode").val()||"table",l=e.find("#view-columns-section").get(0),n=e.find("#view-block-section").get(0);l&&(l.style.display=t==="table"?"":"none"),n&&(n.style.display=t==="block"?"":"none")}function H(e){const t=e.find("#view-rowlevel-enabled").get(0),l=e.find("#view-rowlevel-config").get(0),n=e.find("#view-rowlevel-mode").get(0),a=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{l&&(l.style.display=t.checked?"flex":"none")}),n&&n.addEventListener("change",()=>{a&&(a.style.display=n.value==="field"?"":"none")}))}async function Q(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const l=e.find("#view-source").val();if(!l){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let n;try{n=K(e)}catch(d){E.toast(d.message,{type:"error"});return}const a=P(e),s=[];e.find(".view-role-cb:checked").each(function(){s.push(this.value)});const r=e.find("#view-rowlevel-enabled").is(":checked");let i=null;if(r){const d=e.find("#view-rowlevel-mode").val()||"owner",u=e.find("#view-rowlevel-userkey").val()||"id";if(i={mode:d,userKey:u},d==="field"){const m=e.find("#view-rowlevel-field").val().trim();if(!m){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}i.field=m}}const o={title:t,slug:e.find("#view-slug").val().trim()||void 0,description:e.find("#view-description").val().trim(),connection:e.find("#view-connection").val()||"default",pipeline:{source:l,stages:n},display:{mode:e.find("#view-display-mode").val()||"table",columns:a,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:s,public:e.find("#view-public").is(":checked"),rowLevel:i}},c=e.find("#save-view-btn").get(0);c&&(c.disabled=!0);try{if(b)await h.views.update(b,o),E.toast("View updated.",{type:"success"});else{const d=await h.views.create(o);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${d.slug}`)}}catch(d){E.toast(d.message||"Failed to save view.",{type:"error"})}finally{c&&(c.disabled=!1)}}
1
+ import{api as h}from"../api.js";let b=null,f=null;export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){b=null,f=null;const l=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);l&&(b=l[1]),E.tabs(e.find("#view-editor-tabs").get(0)),await O(e),await D(e),await j(e),await T(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await x(e)}),b&&(e.find("#view-editor-title").text("Edit View"),await I(e,b)),e.find("#add-stage-btn").off("click").on("click",()=>{const n=e.find("#add-stage-type").val()||"$match";q(e,{type:n,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await Q(e)}),H(e),Y(e),Domma.icons.scan()}};async function O(e){const t=e.find("#view-source").get(0);if(t)try{(await h.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 T(e){const t=e.find("#view-block-name").get(0);if(t)try{(await h.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-connection").get(0);if(t)try{const l=await h.collections.getConnections();Object.keys(l).forEach(n=>{if(!t.querySelector(`option[value="${n}"]`)){const a=document.createElement("option");a.value=n,a.textContent=n,t.appendChild(a)}})}catch{}}async function j(e){const t=e.find("#view-roles-checkboxes").get(0);if(!t)return;["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const s=document.createElement("input");s.type="checkbox",s.value=n,s.dataset.role=n,s.className="view-role-cb",s.checked=n==="admin",a.appendChild(s),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function x(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await h.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const l=this.dataset.stageType,n=this.querySelector(".stage-guided");n&&(l==="$match"&&M(n),l==="$sort"&&z(n))}),A(e,null)}async function I(e,t){try{const l=await h.views.get(t);if(!l){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const n=l.pipeline?.source;n&&(e.find("#view-source").val(n),await x(e)),F(e,l)}catch(l){E.toast(l.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function F(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-bundled").prop("checked",!!t.bundled),e.find("#view-source").val(t.pipeline?.source||""),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 l=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=l.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const n=t.access?.rowLevel;n&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(n.mode||"owner"),e.find("#view-rowlevel-userkey").val(n.userKey||"id"),n.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(n.field||"")));const a=e.find("#pipeline-stages-list").get(0);if(a)for(;a.firstChild;)a.removeChild(a.firstChild);(t.pipeline?.stages||[]).forEach(s=>q(e,s)),A(e,t.display?.columns||[]),S(e)}const _=[{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 g(e=!1){const t=[];return e&&t.push({value:"",label:"\u2014 select field \u2014"}),(f||[]).forEach(l=>{t.push({value:`data.${l.name}`,label:`${l.label||l.name} (${l.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 v(e,t){const l=document.createElement("select");return l.className="form-input form-input--sm",e.forEach(n=>{const a=document.createElement("option");a.value=n.value,a.textContent=n.label,String(n.value)===String(t)&&(a.selected=!0),l.appendChild(a)}),l}function N(e,t,l="text"){const n=document.createElement("input");return n.type=l,n.className="form-input form-input--sm",n.placeholder=e,n.value=t??"",n}function B(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(l=>l.name===t)||null}function y(e,t,l=""){const n=B(e);if(n?.type==="select"&&(t==="eq"||t==="ne")&&n.options?.length){const i=document.createElement("select");i.className="form-input form-input--sm match-val",i.style.flex="1";const c=document.createElement("option");return c.value="",c.textContent="\u2014 select value \u2014",i.appendChild(c),(n.options||[]).forEach(o=>{const d=typeof o=="string"?o:o.value??"",u=typeof o=="string"?o:o.label||d;if(!d||d==="undefined")return;const m=document.createElement("option");m.value=d,m.textContent=u,d===l&&(m.selected=!0),i.appendChild(m)}),i}const s=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",r=document.createElement("input");return r.type="text",r.className="form-input form-input--sm match-val",r.style.flex="1",r.placeholder=s,r.value=l,r}function k(e){const t=[],l={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([n,a])=>{if(typeof a=="object"&&a!==null&&!Array.isArray(a)){if("$regex"in a){t.push({field:n,op:"contains",val:String(a.$regex??"")});return}Object.entries(a).forEach(([s,r])=>{const i=l[s];i&&t.push({field:n,op:i,val:Array.isArray(r)?r.join(", "):String(r)})})}else t.push({field:n,op:"eq",val:String(a??"")})}),t}function C(e,t,l="",n="eq",a=""){const s=document.createElement("div");s.className="match-condition-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const r=v([{value:"",label:"\u2014 field \u2014"},...g()],l);r.className+=" match-field",r.style.minWidth="140px";const i=v(_,n);i.className+=" match-op",i.style.minWidth="160px";const c=document.createElement("div");c.className="match-val-wrap",c.style.cssText="flex:1;min-width:120px;display:flex;",c.appendChild(y(l,n,a));const o=()=>{const m=c.querySelector(".match-val")?.value??"";for(;c.firstChild;)c.removeChild(c.firstChild);c.appendChild(y(r.value,i.value,m))};r.addEventListener("change",o),i.addEventListener("change",o);const d=document.createElement("button");d.type="button",d.className="btn btn-sm btn-ghost",d.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),d.appendChild(u),d.addEventListener("click",()=>s.remove()),s.appendChild(r),s.appendChild(i),s.appendChild(c),s.appendChild(d),e.insertBefore(s,t),Domma.icons.scan(s)}function W(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const l=t.$match||t||{},n="$or"in l;let a=[];n?(l.$or||[]).forEach(d=>k(d).forEach(u=>a.push(u))):a=k(l);const s=document.createElement("div");s.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const r=document.createElement("label");r.className="form-label",r.style.marginBottom="0",r.textContent="Match:";const i=document.createElement("select");i.className="form-input form-input--sm match-logic",i.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(d=>{const u=document.createElement("option");u.value=d.value,u.textContent=d.label,(n?"or":"and")===d.value&&(u.selected=!0),i.appendChild(u)}),s.appendChild(r),s.appendChild(i),e.appendChild(s);const c=document.createElement("button");c.type="button",c.className="btn btn-ghost btn-sm match-add-btn";const o=document.createElement("span");o.setAttribute("data-icon","plus"),c.appendChild(o),c.appendChild(document.createTextNode(" Add Condition")),c.addEventListener("click",()=>{C(e,c),Domma.icons.scan(e)}),e.appendChild(c),a.forEach(d=>C(e,c,d.field,d.op,d.val)),a.length===0&&C(e,c),Domma.icons.scan(e)}function M(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const l=t.querySelector(".match-field");if(l){const r=l.value,i=v([{value:"",label:"\u2014 field \u2014"},...g()],r);i.className=l.className,i.style.cssText=l.style.cssText;const c=t.querySelector(".match-val-wrap"),o=t.querySelector(".match-op");c&&o&&i.addEventListener("change",()=>{const d=c.querySelector(".match-val")?.value??"";for(;c.firstChild;)c.removeChild(c.firstChild);c.appendChild(y(i.value,o.value,d))}),l.parentNode.replaceChild(i,l)}const n=t.querySelector(".match-val-wrap"),a=t.querySelector(".match-field")?.value,s=t.querySelector(".match-op")?.value;if(n&&a&&s){const r=n.querySelector(".match-val")?.value??"";for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(y(a,s,r))}})}function U(e){const t=e.querySelector(".match-logic")?.value||"and",l=a=>{const s=a.querySelector(".match-field")?.value?.trim(),r=a.querySelector(".match-op")?.value,i=a.querySelector(".match-val")?.value?.trim()??"";if(!s)return null;const c=s.startsWith("__")?s.slice(2):s,o={};switch(r){case"eq":o[c]=i;break;case"ne":o[c]={$ne:i};break;case"gt":o[c]={$gt:isNaN(i)?i:Number(i)};break;case"lt":o[c]={$lt:isNaN(i)?i:Number(i)};break;case"gte":o[c]={$gte:isNaN(i)?i:Number(i)};break;case"lte":o[c]={$lte:isNaN(i)?i:Number(i)};break;case"contains":o[c]={$regex:i,$options:"i"};break;case"in":o[c]={$in:i.split(",").map(d=>d.trim()).filter(Boolean)};break}return Object.keys(o).length?o:null},n=[];return e.querySelectorAll(".match-condition-row").forEach(a=>{const s=l(a);s&&n.push(s)}),n.length===0?{}:t==="or"?{$or:n}:Object.assign({},...n)}function V(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const l=t.$sort||t||{},n=Object.entries(l),a=document.createElement("button");a.type="button",a.className="btn btn-ghost btn-sm";const s=document.createElement("span");s.setAttribute("data-icon","plus"),a.appendChild(s),a.appendChild(document.createTextNode(" Add Sort")),e.appendChild(a);const r=(i="",c=1)=>{const o=document.createElement("div");o.className="sort-row",o.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const d=v([{value:"",label:"\u2014 field \u2014"},...g()],i);d.className+=" sort-field";const u=v([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(c));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",()=>{o.remove()}),o.appendChild(d),o.appendChild(u),o.appendChild(m),e.insertBefore(o,a),Domma.icons.scan(o)};a.addEventListener("click",()=>{r()}),n.forEach(([i,c])=>r(i,c)),n.length===0&&r(),Domma.icons.scan(e)}function z(e){e.querySelectorAll(".sort-row").forEach(t=>{const l=t.querySelector(".sort-field"),n=l?.value,a=v([{value:"",label:"\u2014 field \u2014"},...g()],n);a.className=l.className,l.parentNode.replaceChild(a,l)})}function J(e){const t={};return e.querySelectorAll(".sort-row").forEach(l=>{const n=l.querySelector(".sort-field")?.value?.trim(),a=parseInt(l.querySelector(".sort-dir")?.value,10)||1;if(!n)return;const s=n.startsWith("__")?n.slice(2):n;t[s]=a}),t}function q(e,t){const l=e.find("#pipeline-stages-list").get(0);if(!l)return;const n=l.querySelector(".stage-empty-placeholder");n&&n.remove();const a=["$match","$sort"].includes(t.type),s=document.createElement("div");s.className="card mb-2 stage-card",s.dataset.stageType=t.type,a&&(s.dataset.guided="true");const r=document.createElement("div");r.className="card-header",r.style.cssText="display:flex;align-items:center;gap:.5rem;";const i=document.createElement("code");i.textContent=t.type,i.style.cssText="flex:1;font-size:.85rem;";const c=document.createElement("button");c.type="button",c.className="btn btn-sm btn-danger";const o=document.createElement("span");o.setAttribute("data-icon","trash-2"),c.appendChild(o),c.addEventListener("click",()=>{s.remove(),l.querySelector(".stage-card")||l.appendChild(G())}),r.appendChild(i),r.appendChild(c);const d=document.createElement("div");if(d.className="card-body",a){const u=document.createElement("div");u.className="stage-guided",d.appendChild(u),t.type==="$match"&&W(u,t.config),t.type==="$sort"&&V(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):"",d.appendChild(u),d.appendChild(m),d.appendChild(p)}s.appendChild(r),s.appendChild(d),l.appendChild(s),Domma.icons.scan(s)}function G(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function K(e){const t=[];return e.find(".stage-card").each(function(){const l=this.dataset.stageType,n=this.querySelector(".stage-guided");let a;if(n)l==="$match"&&(a=U(n)),l==="$sort"&&(a=J(n));else{const s=this.querySelector(".stage-config")?.value?.trim()||"{}";try{a=JSON.parse(s)}catch{throw new Error(`Invalid JSON in ${l} stage config`)}}t.push({type:l,config:a})}),t}function A(e,t){const l=e.find("#view-columns-builder").get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);const n=t??(f||[]).map(i=>({key:`data.${i.name}`,label:i.label||i.name})),a=(i="",c="")=>{const o=document.createElement("div");o.className="col-row",o.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const d=f?.length?v([{value:"",label:"\u2014 field \u2014"},...g()],i):N("data.fieldName",i);d.className+=" col-key";const u=N("Display label",c);u.className+=" col-label",u.style.flex="1",f?.length&&d.tagName==="SELECT"&&d.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(L=>`data.${L.name}`===d.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",()=>{o.remove(),Domma.icons.scan(l)}),o.appendChild(d),o.appendChild(u),o.appendChild(m),l.insertBefore(o,l.lastChild),Domma.icons.scan(o)};n.forEach(i=>a(i.key,i.label));const s=document.createElement("button");s.type="button",s.className="btn btn-ghost btn-sm";const r=document.createElement("span");r.setAttribute("data-icon","plus"),s.appendChild(r),s.appendChild(document.createTextNode(" Add Column")),s.addEventListener("click",()=>{a(),Domma.icons.scan(l)}),l.appendChild(s),Domma.icons.scan(l)}function P(e){const t=[];return e.find(".col-row").each(function(){const l=this.querySelector(".col-key")?.value?.trim(),n=this.querySelector(".col-label")?.value?.trim();l&&t.push({key:l,label:n||l})}),t}function Y(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>S(e)),S(e))}function S(e){const t=e.find("#view-display-mode").val()||"table",l=e.find("#view-columns-section").get(0),n=e.find("#view-block-section").get(0);l&&(l.style.display=t==="table"?"":"none"),n&&(n.style.display=t==="block"?"":"none")}function H(e){const t=e.find("#view-rowlevel-enabled").get(0),l=e.find("#view-rowlevel-config").get(0),n=e.find("#view-rowlevel-mode").get(0),a=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{l&&(l.style.display=t.checked?"flex":"none")}),n&&n.addEventListener("change",()=>{a&&(a.style.display=n.value==="field"?"":"none")}))}async function Q(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const l=e.find("#view-source").val();if(!l){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let n;try{n=K(e)}catch(d){E.toast(d.message,{type:"error"});return}const a=P(e),s=[];e.find(".view-role-cb:checked").each(function(){s.push(this.value)});const r=e.find("#view-rowlevel-enabled").is(":checked");let i=null;if(r){const d=e.find("#view-rowlevel-mode").val()||"owner",u=e.find("#view-rowlevel-userkey").val()||"id";if(i={mode:d,userKey:u},d==="field"){const m=e.find("#view-rowlevel-field").val().trim();if(!m){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}i.field=m}}const c={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:l,stages:n},display:{mode:e.find("#view-display-mode").val()||"table",columns:a,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:s,public:e.find("#view-public").is(":checked"),rowLevel:i}},o=e.find("#save-view-btn").get(0);o&&(o.disabled=!0);try{if(b)await h.views.update(b,c),E.toast("View updated.",{type:"success"});else{const d=await h.views.create(c);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${d.slug}`)}}catch(d){E.toast(d.message||"Failed to save view.",{type:"error"})}finally{o&&(o.disabled=!1)}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.8.10",
3
+ "version": "0.9.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1,35 @@
1
+ {
2
+ "slug": "user-contact-groups",
3
+ "title": "Contact Groups",
4
+ "description": "Groups for the Contacts plugin.",
5
+ "plugin": "contacts",
6
+ "fields": [
7
+ {
8
+ "name": "name",
9
+ "label": "Group Name",
10
+ "type": "text",
11
+ "required": true
12
+ }
13
+ ],
14
+ "api": {
15
+ "create": {
16
+ "enabled": true,
17
+ "access": "admin"
18
+ },
19
+ "read": {
20
+ "enabled": true,
21
+ "access": "admin"
22
+ },
23
+ "update": {
24
+ "enabled": false,
25
+ "access": "admin"
26
+ },
27
+ "delete": {
28
+ "enabled": false,
29
+ "access": "admin"
30
+ }
31
+ },
32
+ "storage": {
33
+ "adapter": "file"
34
+ }
35
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "slug": "user-contacts",
3
+ "title": "Contacts",
4
+ "description": "Contacts managed by the Contacts plugin.",
5
+ "plugin": "contacts",
6
+ "fields": [
7
+ {
8
+ "name": "name",
9
+ "label": "Name",
10
+ "type": "text",
11
+ "required": true
12
+ },
13
+ {
14
+ "name": "email",
15
+ "label": "Email",
16
+ "type": "text",
17
+ "required": false
18
+ },
19
+ {
20
+ "name": "phone",
21
+ "label": "Phone",
22
+ "type": "text",
23
+ "required": false
24
+ },
25
+ {
26
+ "name": "groups",
27
+ "label": "Groups",
28
+ "type": "text",
29
+ "required": false
30
+ },
31
+ {
32
+ "name": "notes",
33
+ "label": "Notes",
34
+ "type": "textarea",
35
+ "required": false
36
+ },
37
+ {
38
+ "name": "favourite",
39
+ "label": "Favourite",
40
+ "type": "text",
41
+ "required": false
42
+ },
43
+ {
44
+ "name": "userId",
45
+ "label": "User ID",
46
+ "type": "text",
47
+ "required": false
48
+ }
49
+ ],
50
+ "api": {
51
+ "create": {
52
+ "enabled": true,
53
+ "access": "admin"
54
+ },
55
+ "read": {
56
+ "enabled": true,
57
+ "access": "admin"
58
+ },
59
+ "update": {
60
+ "enabled": false,
61
+ "access": "admin"
62
+ },
63
+ "delete": {
64
+ "enabled": false,
65
+ "access": "admin"
66
+ }
67
+ },
68
+ "storage": {
69
+ "adapter": "file"
70
+ }
71
+ }
@@ -4,28 +4,12 @@ import {
4
4
  createEntry,
5
5
  updateEntry,
6
6
  deleteEntry,
7
- getEntry,
8
- getCollection,
9
- createCollection
7
+ getEntry
10
8
  } from '../../server/services/collections.js';
11
9
 
12
10
  const CONTACTS_SLUG = 'user-contacts';
13
11
  const GROUPS_SLUG = 'user-contact-groups';
14
12
 
15
- const CONTACT_FIELDS = [
16
- {name: 'name', label: 'Name', type: 'text', required: true},
17
- {name: 'email', label: 'Email', type: 'text', required: false},
18
- {name: 'phone', label: 'Phone', type: 'text', required: false},
19
- {name: 'groups', label: 'Groups', type: 'text', required: false},
20
- {name: 'notes', label: 'Notes', type: 'textarea', required: false},
21
- {name: 'favourite', label: 'Favourite', type: 'text', required: false},
22
- {name: 'userId', label: 'User ID', type: 'text', required: false}
23
- ];
24
-
25
- const GROUP_FIELDS = [
26
- {name: 'name', label: 'Group Name', type: 'text', required: true}
27
- ];
28
-
29
13
  /** Map a collection entry to the shape the admin view expects. */
30
14
  function toContact(entry) {
31
15
  let groups = entry.data.groups;
@@ -54,48 +38,10 @@ function toGroupName(entry) {
54
38
  return entry.data.name ?? '';
55
39
  }
56
40
 
57
- /**
58
- * Lifecycle: create both collections on plugin enable.
59
- */
60
- export async function onEnable({services: {collections}}) {
61
- for (const [slug, title, fields, desc] of [
62
- [CONTACTS_SLUG, 'Contacts', CONTACT_FIELDS, 'Contacts managed by the Contacts plugin.'],
63
- [GROUPS_SLUG, 'Contact Groups', GROUP_FIELDS, 'Groups for the Contacts plugin.']
64
- ]) {
65
- const existing = await collections.getCollection(slug).catch(() => null);
66
- if (!existing) {
67
- await collections.createCollection({title, slug, description: desc, fields});
68
- }
69
- }
70
- }
71
-
72
- /**
73
- * Lifecycle: remove both collections on plugin disable.
74
- */
75
- export async function onDisable({services: {collections}}) {
76
- await collections.deleteCollection(CONTACTS_SLUG).catch(() => {
77
- });
78
- await collections.deleteCollection(GROUPS_SLUG).catch(() => {
79
- });
80
- }
81
-
82
41
  export default async function contactsPlugin(fastify, options) {
83
42
  const {authenticate} = options.auth;
84
43
  const config = {...defaultConfig, ...(options.settings || {})};
85
44
  const scope = config.scope ?? 'user';
86
- const storage = config.storage ?? {adapter: 'file'};
87
-
88
- // Auto-create collections if missing.
89
- for (const [slug, title, fields, desc] of [
90
- [CONTACTS_SLUG, 'Contacts', CONTACT_FIELDS, 'Contacts managed by the Contacts plugin.'],
91
- [GROUPS_SLUG, 'Contact Groups', GROUP_FIELDS, 'Groups for the Contacts plugin.']
92
- ]) {
93
- const existing = await getCollection(slug).catch(() => null);
94
- if (!existing) {
95
- await createCollection({title, slug, description: desc, fields, storage})
96
- .catch(err => fastify.log.warn(`[contacts] Collection setup: ${err.message}`));
97
- }
98
- }
99
45
 
100
46
  function userId(request) {
101
47
  return scope === 'user' ? (request.user?.id ?? request.user?.sub ?? null) : null;
@@ -0,0 +1,101 @@
1
+ {
2
+ "slug": "garage-vehicles",
3
+ "title": "Garage Vehicles",
4
+ "description": "Vehicle registrations managed by the Garage plugin.",
5
+ "plugin": "garage",
6
+ "fields": [
7
+ {
8
+ "name": "userId",
9
+ "label": "User ID",
10
+ "type": "text",
11
+ "required": true
12
+ },
13
+ {
14
+ "name": "registrationNumber",
15
+ "label": "Registration",
16
+ "type": "text",
17
+ "required": true
18
+ },
19
+ {
20
+ "name": "make",
21
+ "label": "Make",
22
+ "type": "text"
23
+ },
24
+ {
25
+ "name": "colour",
26
+ "label": "Colour",
27
+ "type": "text"
28
+ },
29
+ {
30
+ "name": "yearOfManufacture",
31
+ "label": "Year",
32
+ "type": "text"
33
+ },
34
+ {
35
+ "name": "fuelType",
36
+ "label": "Fuel Type",
37
+ "type": "text"
38
+ },
39
+ {
40
+ "name": "engineCapacity",
41
+ "label": "Engine Capacity",
42
+ "type": "text"
43
+ },
44
+ {
45
+ "name": "co2Emissions",
46
+ "label": "CO2 Emissions",
47
+ "type": "text"
48
+ },
49
+ {
50
+ "name": "motStatus",
51
+ "label": "MOT Status",
52
+ "type": "text"
53
+ },
54
+ {
55
+ "name": "motExpiryDate",
56
+ "label": "MOT Expiry",
57
+ "type": "text"
58
+ },
59
+ {
60
+ "name": "taxStatus",
61
+ "label": "Tax Status",
62
+ "type": "text"
63
+ },
64
+ {
65
+ "name": "taxDueDate",
66
+ "label": "Tax Due",
67
+ "type": "text"
68
+ },
69
+ {
70
+ "name": "isSaved",
71
+ "label": "Saved",
72
+ "type": "text"
73
+ },
74
+ {
75
+ "name": "lookupDate",
76
+ "label": "Lookup Date",
77
+ "type": "text"
78
+ }
79
+ ],
80
+ "api": {
81
+ "create": {
82
+ "enabled": false,
83
+ "access": "admin"
84
+ },
85
+ "read": {
86
+ "enabled": false,
87
+ "access": "admin"
88
+ },
89
+ "update": {
90
+ "enabled": false,
91
+ "access": "admin"
92
+ },
93
+ "delete": {
94
+ "enabled": false,
95
+ "access": "admin"
96
+ }
97
+ },
98
+ "storage": {
99
+ "adapter": "file"
100
+ }
101
+ }
@@ -17,10 +17,8 @@ import fs from 'fs';
17
17
  import path from 'path';
18
18
  import {fileURLToPath} from 'url';
19
19
  import {
20
- createCollection,
21
20
  createEntry,
22
21
  deleteEntry,
23
- getCollection,
24
22
  getEntry,
25
23
  listEntries,
26
24
  updateEntry
@@ -34,42 +32,6 @@ const VEHICLES_DATA = path.join(__dirname, '../../content/collections/garage-veh
34
32
  /** In-memory rate limit tracker: Map<userId, { count: number, windowStart: number }> */
35
33
  const rateLimits = new Map();
36
34
 
37
- /**
38
- * Ensure the garage-vehicles collection schema exists.
39
- * Creates it on first plugin load; no-op on subsequent starts.
40
- */
41
- async function ensureCollection() {
42
- const existing = await getCollection(COLLECTION);
43
- if (existing) return;
44
- await createCollection({
45
- slug: COLLECTION,
46
- title: 'Garage Vehicles',
47
- description: 'Vehicle registrations managed by the Garage plugin.',
48
- fields: [
49
- {name: 'userId', label: 'User ID', type: 'text', required: true},
50
- {name: 'registrationNumber', label: 'Registration', type: 'text', required: true},
51
- {name: 'make', label: 'Make', type: 'text'},
52
- {name: 'colour', label: 'Colour', type: 'text'},
53
- {name: 'yearOfManufacture', label: 'Year', type: 'text'},
54
- {name: 'fuelType', label: 'Fuel Type', type: 'text'},
55
- {name: 'engineCapacity', label: 'Engine Capacity', type: 'text'},
56
- {name: 'co2Emissions', label: 'CO2 Emissions', type: 'text'},
57
- {name: 'motStatus', label: 'MOT Status', type: 'text'},
58
- {name: 'motExpiryDate', label: 'MOT Expiry', type: 'text'},
59
- {name: 'taxStatus', label: 'Tax Status', type: 'text'},
60
- {name: 'taxDueDate', label: 'Tax Due', type: 'text'},
61
- {name: 'isSaved', label: 'Saved', type: 'text'},
62
- {name: 'lookupDate', label: 'Lookup Date', type: 'text'}
63
- ],
64
- api: {
65
- create: {enabled: false, access: 'admin'},
66
- read: {enabled: false, access: 'admin'},
67
- update: {enabled: false, access: 'admin'},
68
- delete: {enabled: false, access: 'admin'}
69
- },
70
- storage: {adapter: 'mongodb', connection: 'default'}
71
- });
72
- }
73
35
 
74
36
  /**
75
37
  * Flatten a collection entry into the vehicle model the frontend expects.
@@ -213,8 +175,6 @@ export default async function garagePlugin(fastify, options) {
213
175
  const cfg = {...defaultConfig, ...(options.settings || {})};
214
176
  cfg.dvlaApiKey = process.env.DVLA_API_KEY || cfg.dvlaApiKey;
215
177
 
216
- await ensureCollection();
217
-
218
178
  // --- [garage-vehicles /] shortcode --------------------------------------
219
179
 
220
180
  /**
@@ -0,0 +1,53 @@
1
+ {
2
+ "slug": "user-notes",
3
+ "title": "Notes",
4
+ "description": "Notes managed by the Notes plugin.",
5
+ "plugin": "notes",
6
+ "fields": [
7
+ {
8
+ "name": "title",
9
+ "label": "Title",
10
+ "type": "text",
11
+ "required": false
12
+ },
13
+ {
14
+ "name": "content",
15
+ "label": "Content",
16
+ "type": "textarea",
17
+ "required": false
18
+ },
19
+ {
20
+ "name": "categories",
21
+ "label": "Categories",
22
+ "type": "text",
23
+ "required": false
24
+ },
25
+ {
26
+ "name": "userId",
27
+ "label": "User ID",
28
+ "type": "text",
29
+ "required": false
30
+ }
31
+ ],
32
+ "api": {
33
+ "create": {
34
+ "enabled": true,
35
+ "access": "admin"
36
+ },
37
+ "read": {
38
+ "enabled": true,
39
+ "access": "admin"
40
+ },
41
+ "update": {
42
+ "enabled": false,
43
+ "access": "admin"
44
+ },
45
+ "delete": {
46
+ "enabled": false,
47
+ "access": "admin"
48
+ }
49
+ },
50
+ "storage": {
51
+ "adapter": "file"
52
+ }
53
+ }
@@ -4,43 +4,10 @@ import {
4
4
  createEntry,
5
5
  updateEntry,
6
6
  deleteEntry,
7
- getEntry,
8
- getCollection,
9
- createCollection
7
+ getEntry
10
8
  } from '../../server/services/collections.js';
11
9
 
12
10
  const SLUG = 'user-notes';
13
- const STORAGE = defaultConfig.storage ?? {adapter: 'file'};
14
-
15
- const FIELDS = [
16
- {name: 'title', label: 'Title', type: 'text', required: false},
17
- {name: 'content', label: 'Content', type: 'textarea', required: false},
18
- {name: 'categories', label: 'Categories', type: 'text', required: false},
19
- {name: 'userId', label: 'User ID', type: 'text', required: false}
20
- ];
21
-
22
- /**
23
- * Lifecycle: create the notes collection (MongoDB-backed) on plugin enable.
24
- */
25
- export async function onEnable({services: {collections}}) {
26
- const existing = await collections.getCollection(SLUG).catch(() => null);
27
- if (existing) return;
28
- await collections.createCollection({
29
- title: 'Notes',
30
- slug: SLUG,
31
- description: 'Notes managed by the Notes plugin.',
32
- fields: FIELDS,
33
- storage: STORAGE
34
- });
35
- }
36
-
37
- /**
38
- * Lifecycle: remove the notes collection on plugin disable.
39
- */
40
- export async function onDisable({services: {collections}}) {
41
- await collections.deleteCollection(SLUG).catch(() => {
42
- });
43
- }
44
11
 
45
12
  /** Flatten a collection entry into the shape the admin view expects. */
46
13
  function toNote(entry) {
@@ -57,19 +24,6 @@ export default async function notesPlugin(fastify, options) {
57
24
  const { authenticate } = options.auth;
58
25
  const config = {...defaultConfig, ...(options.settings || {})};
59
26
  const scope = config.scope ?? 'user';
60
- const storage = config.storage ?? STORAGE;
61
-
62
- // Auto-create the collection if it doesn't exist yet.
63
- const existing = await getCollection(SLUG).catch(() => null);
64
- if (!existing) {
65
- await createCollection({
66
- title: 'Notes',
67
- slug: SLUG,
68
- description: 'Notes managed by the Notes plugin.',
69
- fields: FIELDS,
70
- storage
71
- }).catch(err => fastify.log.warn(`[notes] Collection setup: ${err.message}`));
72
- }
73
27
 
74
28
  function userId(request) {
75
29
  return scope === 'user' ? (request.user?.id ?? request.user?.sub ?? null) : null;