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.
- package/admin/js/templates/action-editor.html +5 -0
- package/admin/js/templates/block-editor.html +5 -0
- package/admin/js/templates/collection-editor.html +7 -0
- package/admin/js/templates/form-editor.html +7 -0
- package/admin/js/templates/page-editor.html +5 -0
- package/admin/js/templates/view-editor.html +5 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +1 -1
- package/admin/js/views/navigation.js +13 -12
- package/admin/js/views/page-editor.js +11 -11
- package/admin/js/views/pages.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/package.json +1 -1
- package/plugins/contacts/collections/user-contact-groups/schema.json +35 -0
- package/plugins/contacts/collections/user-contacts/schema.json +71 -0
- package/plugins/contacts/plugin.js +1 -55
- package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
- package/plugins/garage/plugin.js +0 -40
- package/plugins/notes/collections/user-notes/schema.json +53 -0
- package/plugins/notes/plugin.js +1 -47
- package/plugins/todo/collections/todos/schema.json +59 -0
- package/plugins/todo/plugin.js +1 -48
- package/server/routes/api/blocks.js +2 -2
- package/server/services/blocks.js +22 -2
- package/server/services/collections.js +17 -3
- package/server/services/forms.js +2 -1
- package/server/services/plugins.js +166 -1
package/admin/js/views/pages.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{api as p}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(
|
|
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(
|
|
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
|
@@ -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
|
+
}
|
package/plugins/garage/plugin.js
CHANGED
|
@@ -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
|
+
}
|
package/plugins/notes/plugin.js
CHANGED
|
@@ -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;
|