domma-cms 0.32.4 → 0.33.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,17 +1,17 @@
1
- <div class="view-header">
2
- <h1><span data-icon="layout"></span> Forms</h1>
3
- <div style="display:flex;gap:.5rem;">
4
- <button id="test-email-btn" class="btn btn-ghost btn-sm">
5
- <span data-icon="mail"></span> Test Email
6
- </button>
7
- <button id="create-form-btn" class="btn btn-primary">
8
- <span data-icon="plus"></span> Create Form
9
- </button>
10
- </div>
11
- </div>
12
-
13
- <div class="card">
14
- <div class="card-body">
15
- <div id="forms-table"></div>
16
- </div>
17
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="layout"></span> Forms</h1>
3
+ <div style="display:flex;gap:.5rem;">
4
+ <button id="test-email-btn" class="btn btn-ghost btn-sm">
5
+ <span data-icon="mail"></span> Test Email
6
+ </button>
7
+ <button id="create-form-btn" class="btn btn-primary">
8
+ <span data-icon="plus"></span> Create Form
9
+ </button>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="card">
14
+ <div class="card-body">
15
+ <div id="forms-table"></div>
16
+ </div>
17
+ </div>
@@ -1,17 +1,17 @@
1
- <div class="view-header">
2
- <h1><span data-icon="user"></span> My Profile</h1>
3
- </div>
4
-
5
- <div class="card">
6
- <div class="card-header"><h2>Account</h2></div>
7
- <div class="card-body">
8
- <div id="account-form-container"></div>
9
- </div>
10
- </div>
11
-
12
- <div class="card mt-4" id="profile-card" style="display:none;">
13
- <div class="card-header"><h2>Profile</h2></div>
14
- <div class="card-body">
15
- <div id="profile-form-container"></div>
16
- </div>
17
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="user"></span> My Profile</h1>
3
+ </div>
4
+
5
+ <div class="card">
6
+ <div class="card-header"><h2>Account</h2></div>
7
+ <div class="card-body">
8
+ <div id="account-form-container"></div>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="card mt-4" id="profile-card" style="display:none;">
13
+ <div class="card-header"><h2>Profile</h2></div>
14
+ <div class="card-body">
15
+ <div id="profile-form-container"></div>
16
+ </div>
17
+ </div>
@@ -1,70 +1,70 @@
1
- <div class="view-header">
2
- <h1><span data-icon="shield"></span> <span id="role-title">Edit Role</span></h1>
3
- <div>
4
- <a href="#/roles" class="btn btn-secondary"><span data-icon="arrow-left"></span> Back</a>
5
- <button id="btn-save-role" class="btn btn-primary"><span data-icon="save"></span> Save</button>
6
- </div>
7
- </div>
8
-
9
- <div class="tabs" id="role-tabs">
10
- <div class="tab-list">
11
- <button class="tab-item active">General</button>
12
- <button class="tab-item">Permissions</button>
13
- </div>
14
- <div class="tab-content">
15
- <!-- General Tab -->
16
- <div class="tab-panel active" id="panel-general">
17
- <div class="card">
18
- <div class="card-body">
19
- <div class="row">
20
- <div class="col-6">
21
- <div class="mb-3">
22
- <label class="form-label">Name (slug)</label>
23
- <input id="role-name" type="text" class="form-input" placeholder="e.g. moderator">
24
- <p class="form-hint">Lowercase identifier. Cannot be changed for the root admin
25
- role.</p>
26
- </div>
27
- </div>
28
- <div class="col-6">
29
- <div class="mb-3">
30
- <label class="form-label">Display Label</label>
31
- <input id="role-label" type="text" class="form-input" placeholder="e.g. Moderator">
32
- </div>
33
- </div>
34
- </div>
35
- <div class="row">
36
- <div class="col-6">
37
- <div class="mb-3">
38
- <label class="form-label">Level</label>
39
- <input id="role-level" type="number" class="form-input" min="0">
40
- <p class="form-hint">Lower = more privileged. 0 is reserved for the root admin.</p>
41
- </div>
42
- </div>
43
- <div class="col-6">
44
- <div class="mb-3">
45
- <label class="form-label">Badge Colour</label>
46
- <select id="role-badge" class="form-input"></select>
47
- </div>
48
- </div>
49
- </div>
50
- <div class="row">
51
- <div class="col-6">
52
- <div class="mb-3">
53
- <label class="form-label">Project</label>
54
- <select id="role-project" class="form-input">
55
- <option value="">— none —</option>
56
- </select>
57
- <p class="form-hint">Tag this role to a project for grouping.</p>
58
- </div>
59
- </div>
60
- </div>
61
- </div>
62
- </div>
63
- </div>
64
-
65
- <!-- Permissions Tab -->
66
- <div class="tab-panel" id="panel-permissions">
67
- <div id="permissions-container"></div>
68
- </div>
69
- </div>
70
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="shield"></span> <span id="role-title">Edit Role</span></h1>
3
+ <div>
4
+ <a href="#/roles" class="btn btn-secondary"><span data-icon="arrow-left"></span> Back</a>
5
+ <button id="btn-save-role" class="btn btn-primary"><span data-icon="save"></span> Save</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="tabs" id="role-tabs">
10
+ <div class="tab-list">
11
+ <button class="tab-item active">General</button>
12
+ <button class="tab-item">Permissions</button>
13
+ </div>
14
+ <div class="tab-content">
15
+ <!-- General Tab -->
16
+ <div class="tab-panel active" id="panel-general">
17
+ <div class="card">
18
+ <div class="card-body">
19
+ <div class="row">
20
+ <div class="col-6">
21
+ <div class="mb-3">
22
+ <label class="form-label">Name (slug)</label>
23
+ <input id="role-name" type="text" class="form-input" placeholder="e.g. moderator">
24
+ <p class="form-hint">Lowercase identifier. Cannot be changed for the root admin
25
+ role.</p>
26
+ </div>
27
+ </div>
28
+ <div class="col-6">
29
+ <div class="mb-3">
30
+ <label class="form-label">Display Label</label>
31
+ <input id="role-label" type="text" class="form-input" placeholder="e.g. Moderator">
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="row">
36
+ <div class="col-6">
37
+ <div class="mb-3">
38
+ <label class="form-label">Level</label>
39
+ <input id="role-level" type="number" class="form-input" min="0">
40
+ <p class="form-hint">Lower = more privileged. 0 is reserved for the root admin.</p>
41
+ </div>
42
+ </div>
43
+ <div class="col-6">
44
+ <div class="mb-3">
45
+ <label class="form-label">Badge Colour</label>
46
+ <select id="role-badge" class="form-input"></select>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div class="row">
51
+ <div class="col-6">
52
+ <div class="mb-3">
53
+ <label class="form-label">Project</label>
54
+ <select id="role-project" class="form-input">
55
+ <option value="">— none —</option>
56
+ </select>
57
+ <p class="form-hint">Tag this role to a project for grouping.</p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Permissions Tab -->
66
+ <div class="tab-panel" id="panel-permissions">
67
+ <div id="permissions-container"></div>
68
+ </div>
69
+ </div>
70
+ </div>
@@ -1,10 +1,10 @@
1
- <div class="view-header">
2
- <h1><span data-icon="shield"></span> Roles &amp; Permissions</h1>
3
- <button id="btn-add-role" class="btn btn-primary"><span data-icon="plus"></span> Add Role</button>
4
- </div>
5
-
6
- <div class="card">
7
- <div class="card-body">
8
- <div id="roles-table"></div>
9
- </div>
10
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="shield"></span> Roles &amp; Permissions</h1>
3
+ <button id="btn-add-role" class="btn btn-primary"><span data-icon="plus"></span> Add Role</button>
4
+ </div>
5
+
6
+ <div class="card">
7
+ <div class="card-body">
8
+ <div id="roles-table"></div>
9
+ </div>
10
+ </div>
@@ -1 +1,3 @@
1
- import{api as y}from"../api.js";const D=[["_eq","equals"],["_ne","not equal"],["_gt",">"],["_gte",">="],["_lt","<"],["_lte","<="],["_in","in list"],["_nin","not in list"],["_contains","contains"],["_starts","starts with"],["_ends","ends with"],["_exists","exists"]],V=["id","createdBy"];function X(u){return String(u??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function G(u){for(const[h]of D)if(u.endsWith(h))return{field:u.slice(0,-h.length),op:h};return{field:u,op:"_eq"}}export const apiEndpointEditorView={templateUrl:"/admin/js/templates/api-endpoint-editor.html",async onMount(u){const h=u.get(0),o=e=>h.querySelector("#"+e)||document.getElementById(e),O=location.hash.split("?")[0].match(/\/api-endpoints\/edit\/([^/?#]+)/),g=O?decodeURIComponent(O[1]):null;o("ep-title").textContent=g?"Edit API Endpoint":"New API Endpoint";const[F,U,J,r]=await Promise.all([y.projects.list().catch(()=>[]),y.collections.list().catch(()=>[]),y.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]})),g?y.apiEndpoints.get(g).catch(()=>null):Promise.resolve(null)]);if(g&&!r){E.toast("Endpoint not found.",{type:"error"}),location.hash="#/api-endpoints";return}const M=U.filter(e=>!e.systemManaged&&!e.preset),H=(J.entries||[]).map(e=>e.data?.name).filter(Boolean),v=o("endpoint-project");for(const e of F){const t=document.createElement("option");t.value=e.slug,t.textContent=`${e.name} (${e.slug})`,v.appendChild(t)}const p=o("ep-collection");function T(e){return e.meta&&e.meta.project||"core"}function N(e){const t=e!==void 0?e:p.value,l=v.value,a=M.filter(n=>{const i=T(n);return i===l||i==="core"});p.textContent="";for(const n of a){const i=document.createElement("option");i.value=n.slug,i.textContent=`${n.title} (${n.slug})${T(n)==="core"&&l!=="core"?" \u2014 core":""}`,p.appendChild(i)}if(!a.length){const n=document.createElement("option");n.value="",n.textContent="No collections available in this project",p.appendChild(n)}t&&[...p.options].some(n=>n.value===t)&&(p.value=t)}N();const x=o("ep-auth");for(const[e,t]of[["public","Public \u2014 no credentials"],["token","Token \u2014 project API token"]]){const l=document.createElement("option");l.value=e,l.textContent=t,x.appendChild(l)}for(const e of H){const t=document.createElement("option");t.value=e,t.textContent=`Role: ${e}`,x.appendChild(t)}let b=[];async function S(){const e=p.value;if(b=[],e)try{b=((await y.collections.get(e))?.fields||[]).map(l=>l.name).filter(Boolean)}catch{}P();for(const t of $.querySelectorAll(".ep-filter-row"))k(t.querySelector(".ep-f-field"));R()}function A(){return[...b,...V]}function P(e=[]){const t=o("ep-fields-list");t.textContent="";for(const l of b){const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.9rem;";const n=document.createElement("input");n.type="checkbox",n.className="ep-field-cb",n.value=l,n.checked=e.includes(l),n.addEventListener("change",d),a.appendChild(n),a.appendChild(document.createTextNode(l)),t.appendChild(a)}if(!b.length){const l=document.createElement("p");l.className="text-muted",l.style.cssText="font-size:.85rem;margin:0;",l.textContent="No fields found on this collection.",t.appendChild(l)}}function R(e){const t=o("ep-sort"),l=e!==void 0?e:t.value;t.textContent="";for(const a of["createdAt",...A()]){const n=document.createElement("option");n.value=a==="createdAt"?"":a,n.textContent=a==="createdAt"?"createdAt (default)":a,t.appendChild(n)}t.value=l||""}function k(e,t){const l=t!==void 0?t:e.value;e.textContent="";for(const n of A()){const i=document.createElement("option");i.value=n,i.textContent=n,e.appendChild(i)}const a=document.createElement("option");if(a.value="__custom__",a.textContent="custom dot-path\u2026",e.appendChild(a),l&&[...e.options].some(n=>n.value===l))e.value=l;else if(l){const n=document.createElement("option");n.value=l,n.textContent=l,e.insertBefore(n,a),e.value=l}}const $=o("ep-filter-rows");function L(e="",t="_eq",l=""){const a=document.createElement("div");a.className="ep-filter-row",a.style.cssText="display:grid;grid-template-columns:2fr 1.2fr 2fr auto;gap:.5rem;margin-bottom:.5rem;align-items:center;";const n=document.createElement("select");n.className="form-input ep-f-field",k(n,e||void 0),n.addEventListener("change",()=>{if(n.value==="__custom__"){const f=prompt("Dot-path field (e.g. address.city):","");f&&f.trim()?k(n,f.trim()):n.value=A()[0]||""}d()});const i=document.createElement("select");i.className="form-input ep-f-op";for(const[f,q]of D){const C=document.createElement("option");C.value=f,C.textContent=q,i.appendChild(C)}i.value=t,i.addEventListener("change",d);const s=document.createElement("input");s.type="text",s.className="form-input ep-f-value",s.placeholder="e.g. {{params.date}} or {{query.team}} or a literal",s.value=l,s.addEventListener("input",d);const c=document.createElement("button");c.className="btn btn-sm btn-ghost",c.innerHTML='<span data-icon="trash"></span>',c.addEventListener("click",()=>{a.remove(),d()}),a.appendChild(n),a.appendChild(i),a.appendChild(s),a.appendChild(c),$.appendChild(a),Domma.icons.scan(a)}function W(){const e={};for(const t of $.querySelectorAll(".ep-filter-row")){const l=t.querySelector(".ep-f-field").value,a=t.querySelector(".ep-f-op").value,n=t.querySelector(".ep-f-value").value;!l||l==="__custom__"||(e[a==="_eq"?l:l+a]=n)}return e}function w(){return{name:o("ep-name").value.trim(),project:v.value,path:o("ep-path").value.trim(),collection:p.value,auth:x.value,mode:o("ep-mode").value,filter:W(),sort:o("ep-sort").value||null,order:o("ep-order").value,limit:parseInt(o("ep-limit").value,10)||0,fields:[...h.querySelectorAll(".ep-field-cb:checked")].map(e=>e.value),enabled:o("ep-enabled").checked}}let _=null,m=null;function z(){return!_||JSON.stringify(w())!==_}function d(){B(),j()}function B(){o("ep-url-preview").textContent=`/api/x/${v.value||"<project>"}${o("ep-path").value||"/\u2026"}`}function Z(e){return(e.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g)||[]).map(t=>t.slice(1))}function j(){const e=z();o("tryit-dirty-note").style.display=e?"":"none",o("tryit-send").disabled=e,o("tryit-token-group").style.display=m?.auth==="token"?"":"none";const t=o("tryit-params"),l={};for(const a of t.querySelectorAll("input"))l[a.dataset.param]=a.value;t.textContent="";for(const a of Z(m?.path||"")){const n=document.createElement("div");n.className="form-group";const i=document.createElement("label");i.className="form-label",i.textContent=`:${a}`;const s=document.createElement("input");s.type="text",s.className="form-input",s.dataset.param=a,s.value=l[a]||"",n.appendChild(i),n.appendChild(s),t.appendChild(n)}}let I="";async function K(){if(!m)return;let e=m.path;for(const c of o("tryit-params").querySelectorAll("input"))e=e.replace(":"+c.dataset.param,encodeURIComponent(c.value));const t=o("tryit-query").value.trim(),l=`/api/x/${m.project}${e}${t?"?"+t:""}`,a={},n=o("tryit-token").value.trim();m.auth==="token"&&n&&(a.Authorization=`Bearer ${n}`);const i=location.origin+l;I=`curl ${a.Authorization?`-H 'Authorization: ${a.Authorization}' `:""}'${i}'`;const s=o("tryit-send");s.disabled=!0;try{const c=await fetch(l,{headers:a}),f=await c.text();let q=f;try{q=JSON.stringify(JSON.parse(f),null,2)}catch{}o("tryit-result").style.display="";const C=o("tryit-status");C.textContent=`${c.status} ${c.statusText}`,C.className="badge "+(c.ok?"badge-success":c.status>=500?"badge-danger":"badge-warning"),o("tryit-body").textContent=q}catch(c){o("tryit-result").style.display="",o("tryit-status").textContent="Network error",o("tryit-status").className="badge badge-danger",o("tryit-body").textContent=String(c.message||c)}finally{s.disabled=z()}}if(r){if(o("ep-name").value=r.name||"",v.value=r.project,v.disabled=!0,o("ep-path").value=r.path||"",N(r.collection),p.value!==r.collection){const e=document.createElement("option");e.value=r.collection,e.textContent=`${r.collection} (other project \u2014 no longer allowed)`,p.appendChild(e),p.value=r.collection}x.value=r.auth||"public",o("ep-mode").value=r.mode||"list",o("ep-order").value=r.order||"desc",o("ep-limit").value=r.limit??50,o("ep-enabled").checked=r.enabled!==!1,await S(),P(r.fields||[]),R(r.sort||"");for(const[e,t]of Object.entries(r.filter||{})){const{field:l,op:a}=G(e);L(l,a,String(t))}m=r,_=JSON.stringify(w())}else await S(),L();B(),j();for(const e of["ep-name","ep-path","ep-limit"])o(e).addEventListener("input",d);for(const e of["ep-mode","ep-order","ep-sort"])o(e).addEventListener("change",d);o("ep-enabled").addEventListener("change",d),v.addEventListener("change",async()=>{N(),await S(),d()}),x.addEventListener("change",d),p.addEventListener("change",async()=>{await S(),d()}),o("ep-add-filter").addEventListener("click",()=>{L(),d()}),o("tryit-send").addEventListener("click",K),o("tryit-copy-curl").addEventListener("click",async()=>{if(!I){E.toast("Send a request first.",{type:"info"});return}try{await navigator.clipboard.writeText(I),E.toast("curl command copied.",{type:"success"})}catch{E.toast("Copy failed.",{type:"error"})}}),o("ep-save").addEventListener("click",async()=>{const e=w();if(!e.name){E.toast("A name is required.",{type:"error"});return}if(!e.path.startsWith("/")){E.toast('The path must start with "/".',{type:"error"});return}try{if(g)m=await y.apiEndpoints.update(g,e),_=JSON.stringify(w()),E.toast("Endpoint saved.",{type:"success"}),j();else{const t=await y.apiEndpoints.create(e);E.toast("Endpoint created.",{type:"success"}),location.hash="#/api-endpoints/edit/"+encodeURIComponent(t.id)}}catch(t){E.toast(`Save failed: ${t.message||t}`,{type:"error"})}}),Domma.icons.scan(h)}};
1
+ import{api as _}from"../api.js";const de=[["_eq","equals"],["_ne","not equal"],["_gt",">"],["_gte",">="],["_lt","<"],["_lte","<="],["_in","in list"],["_nin","not in list"],["_contains","contains"],["_starts","starts with"],["_ends","ends with"],["_exists","exists"]],xe=["id","createdBy"],we=/\{\{\s*(params|query)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;function Se(C){const b=new Set;for(const n of Object.values(C?.filter||{}))if(typeof n=="string")for(const N of n.matchAll(we))N[1]==="query"&&b.add(N[2]);return[...b]}function je(C){return String(C??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let O=null;function qe(C){for(const[b]of de)if(C.endsWith(b))return{field:C.slice(0,-b.length),op:b};return{field:C,op:"_eq"}}export const apiEndpointEditorView={templateUrl:"/admin/js/templates/api-endpoint-editor.html",async onMount(C){const b=C.get(0),n=e=>b.querySelector("#"+e)||document.getElementById(e),N=location.hash.split("?")[0].match(/\/api-endpoints\/edit\/([^/?#]+)/),q=N?decodeURIComponent(N[1]):null;n("ep-title").textContent=q?"Edit API Endpoint":"New API Endpoint",E.tabs(n("ep-editor-tabs"));const[pe,ue,me,u]=await Promise.all([_.projects.list().catch(()=>[]),_.collections.list().catch(()=>[]),_.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]})),q?_.apiEndpoints.get(q).catch(()=>null):Promise.resolve(null)]);if(q&&!u){E.toast("Endpoint not found.",{type:"error"}),location.hash="#/api-endpoints";return}const fe=ue.filter(e=>!e.systemManaged&&!e.preset),ye=(me.entries||[]).map(e=>e.data?.name).filter(Boolean),k=n("endpoint-project");for(const e of pe){const t=document.createElement("option");t.value=e.slug,t.textContent=`${e.name} (${e.slug})`,k.appendChild(t)}const y=n("ep-collection");function G(e){return e.meta&&e.meta.project||"core"}function z(e){const t=e!==void 0?e:y.value,r=k.value,o=fe.filter(a=>{const s=G(a);return s===r||s==="core"});y.textContent="";for(const a of o){const s=document.createElement("option");s.value=a.slug,s.textContent=`${a.title} (${a.slug})${G(a)==="core"&&r!=="core"?" \u2014 core":""}`,y.appendChild(s)}if(!o.length){const a=document.createElement("option");a.value="",a.textContent="No collections available in this project",y.appendChild(a)}t&&[...y.options].some(a=>a.value===t)&&(y.value=t)}z();const L=n("ep-auth");for(const[e,t]of[["public","Public \u2014 no credentials"],["token","Token \u2014 project API token"]]){const r=document.createElement("option");r.value=e,r.textContent=t,L.appendChild(r)}for(const e of ye){const t=document.createElement("option");t.value=e,t.textContent=`Role: ${e}`,L.appendChild(t)}let T=[];async function R(){const e=y.value;if(T=[],e)try{T=((await _.collections.get(e))?.fields||[]).map(r=>r.name).filter(Boolean)}catch{}ee();for(const t of K.querySelectorAll(".ep-filter-row"))U(t.querySelector(".ep-f-field"));te()}function F(){return[...T,...xe]}function ee(e=[]){const t=n("ep-fields-list");t.textContent="";for(const r of T){const o=document.createElement("label");o.style.cssText="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.9rem;";const a=document.createElement("input");a.type="checkbox",a.className="ep-field-cb",a.value=r,a.checked=e.includes(r),a.addEventListener("change",h),o.appendChild(a),o.appendChild(document.createTextNode(r)),t.appendChild(o)}if(!T.length){const r=document.createElement("p");r.className="text-muted",r.style.cssText="font-size:.85rem;margin:0;",r.textContent="No fields found on this collection.",t.appendChild(r)}}function te(e){const t=n("ep-sort"),r=e!==void 0?e:t.value;t.textContent="";for(const o of["createdAt",...F()]){const a=document.createElement("option");a.value=o==="createdAt"?"":o,a.textContent=o==="createdAt"?"createdAt (default)":o,t.appendChild(a)}t.value=r||""}function U(e,t){const r=t!==void 0?t:e.value;e.textContent="";for(const a of F()){const s=document.createElement("option");s.value=a,s.textContent=a,e.appendChild(s)}const o=document.createElement("option");if(o.value="__custom__",o.textContent="custom dot-path\u2026",e.appendChild(o),r&&[...e.options].some(a=>a.value===r))e.value=r;else if(r){const a=document.createElement("option");a.value=r,a.textContent=r,e.insertBefore(a,o),e.value=r}}const K=n("ep-filter-rows");function M(e="",t="_eq",r=""){const o=document.createElement("div");o.className="ep-filter-row",o.style.cssText="display:grid;grid-template-columns:2fr 1.2fr 2fr auto;gap:.5rem;margin-bottom:.5rem;align-items:center;";const a=document.createElement("select");a.className="form-input ep-f-field",U(a,e||void 0),a.addEventListener("change",()=>{if(a.value==="__custom__"){const i=prompt("Dot-path field (e.g. address.city):","");i&&i.trim()?U(a,i.trim()):a.value=F()[0]||""}h()});const s=document.createElement("select");s.className="form-input ep-f-op";for(const[i,j]of de){const w=document.createElement("option");w.value=i,w.textContent=j,s.appendChild(w)}s.value=t,s.addEventListener("change",h);const c=document.createElement("input");c.type="text",c.className="form-input ep-f-value",c.placeholder="e.g. {{params.date}} or {{query.team}} or a literal",c.value=r,c.addEventListener("input",h);const d=document.createElement("button");d.className="btn btn-sm btn-ghost",d.innerHTML='<span data-icon="trash"></span>',d.addEventListener("click",()=>{o.remove(),h()}),o.appendChild(a),o.appendChild(s),o.appendChild(c),o.appendChild(d),K.appendChild(o),Domma.icons.scan(o)}function he(){const e={};for(const t of K.querySelectorAll(".ep-filter-row")){const r=t.querySelector(".ep-f-field").value,o=t.querySelector(".ep-f-op").value,a=t.querySelector(".ep-f-value").value;!r||r==="__custom__"||(e[o==="_eq"?r:r+o]=a)}return e}function I(){return{name:n("ep-name").value.trim(),project:k.value,path:n("ep-path").value.trim(),collection:y.value,auth:L.value,mode:n("ep-mode").value,filter:he(),sort:n("ep-sort").value||null,order:n("ep-order").value,limit:parseInt(n("ep-limit").value,10)||0,fields:[...b.querySelectorAll(".ep-field-cb:checked")].map(e=>e.value),enabled:n("ep-enabled").checked}}let B=null,v=null;function D(){return!B||JSON.stringify(I())!==B}function h(){ne(),Z()}function ne(){n("ep-url-preview").textContent=`/api/x/${k.value||"<project>"}${n("ep-path").value||"/\u2026"}`}function ve(e){return(e.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g)||[]).map(t=>t.slice(1))}function Z(){const e=D();n("tryit-dirty-note").style.display=e?"":"none",n("tryit-send").disabled=e,n("tryit-token-group").style.display=v?.auth==="token"?"":"none";const t=n("tryit-params"),r={};for(const s of t.querySelectorAll("input"))r[s.dataset.param]=s.value;t.textContent="";for(const s of ve(v?.path||"")){const c=document.createElement("div");c.className="form-group";const d=document.createElement("label");d.className="form-label",d.textContent=`:${s}`;const i=document.createElement("input");i.type="text",i.className="form-input",i.dataset.param=s,i.value=r[s]||"",c.appendChild(d),c.appendChild(i),t.appendChild(c)}const o=n("tryit-query-params"),a={};for(const s of o.querySelectorAll("input"))a[s.dataset.queryParam]=s.value;o.textContent="";for(const s of Se(v)){const c=document.createElement("div");c.className="form-group";const d=document.createElement("label");d.className="form-label",d.textContent=`?${s}`;const i=document.createElement("input");i.type="text",i.className="form-input",i.dataset.queryParam=s,i.value=a[s]||"",i.placeholder=`optional \u2014 filters by ${s}`,c.appendChild(d),c.appendChild(i),o.appendChild(c)}}let J="";const W=`apitryit:hist:${q||"new"}`,ge=12;function oe(){const e=S.get(W);return Array.isArray(e)?e:[]}function ae(e){const t=oe().filter(o=>o.url!==e.url),r=[e,...t].slice(0,ge);S.set(W,r),Y()}function Ee(e){for(const t of n("tryit-params").querySelectorAll("input"))t.value=e.params?.[t.dataset.param]??"";for(const t of n("tryit-query-params").querySelectorAll("input"))t.value=e.queryParams?.[t.dataset.queryParam]??"";n("tryit-query").value=e.extra||"",Q()}function Y(){const e=n("tryit-history-list"),t=n("tryit-history-card");if(!e||!t)return;const r=oe();t.style.display=r.length?"":"none",e.textContent="";for(const o of r){const a=document.createElement("button");a.type="button",a.className="btn btn-ghost btn-sm",a.style.cssText="display:flex;align-items:center;gap:.5rem;width:100%;justify-content:flex-start;text-align:left;font-family:var(--font-mono,monospace);font-size:.78rem;padding:.35rem .5rem;";const s=document.createElement("span"),c=o.ok;s.className="badge "+(c?"badge-success":o.status>=500||o.status===0?"badge-danger":"badge-warning"),s.style.cssText="flex:0 0 auto;",s.textContent=o.status||"ERR";const d=document.createElement("span");d.style.cssText="flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",d.textContent=o.url.replace(`/api/x/${v?.project||""}`,"")||o.url,d.title=o.url,a.appendChild(s),a.appendChild(d),a.addEventListener("click",()=>Ee(o)),e.appendChild(a)}}const m={key:"#4098d7",index:"#9aa4b2",string:"#3f9d63",number:"#c0803a",boolean:"#9a6dce",nul:"#c05b52",punc:"rgba(127,127,127,.8)",preview:"#9aa4b2"};function f(e,t,r){const o=document.createElement("span");return o.style.color=e,r&&(o.style.cssText+=";"+r),o.textContent=t,o}function re(e,t){return t?f(m.index,String(e)):f(m.key,JSON.stringify(String(e)))}function Ce(e){return e===null?f(m.nul,"null"):typeof e=="string"?f(m.string,JSON.stringify(e)):typeof e=="number"?f(m.number,String(e)):typeof e=="boolean"?f(m.boolean,String(e)):f(m.string,JSON.stringify(e))}function se(e,t,r){const o=e&&typeof e=="object",a=o&&(Array.isArray(e)?e.length===0:Object.keys(e).length===0);if(!o||a){const p=document.createElement("div");return p.style.cssText="white-space:pre-wrap;word-break:break-word;padding-left:1.2em;",t!==void 0&&(p.appendChild(re(t,r)),p.appendChild(f(m.punc,": "))),p.appendChild(o?f(m.punc,Array.isArray(e)?"[]":"{}"):Ce(e)),p}const s=Array.isArray(e),c=s?e.map((p,x)=>[x,p,!0]):Object.entries(e).map(([p,x])=>[p,x,!1]),d=document.createElement("div"),i=document.createElement("div");i.style.cssText="white-space:pre-wrap;cursor:pointer;";const j=f(m.punc,"\u25BE","display:inline-block;width:1.2em;");i.appendChild(j),t!==void 0&&(i.appendChild(re(t,r)),i.appendChild(f(m.punc,": "))),i.appendChild(f(m.punc,s?"[":"{"));const w=f(m.preview,` ${c.length} ${s?"item":"key"}${c.length===1?"":"s"} `,"font-style:italic;"),P=f(m.punc,s?"]":"}");i.appendChild(w),i.appendChild(P),d.appendChild(i);const A=document.createElement("div");A.style.cssText="padding-left:1.2em;border-left:1px solid rgba(127,127,127,.18);";for(const[p,x,X]of c)A.appendChild(se(x,p,X));d.appendChild(A);const $=document.createElement("div");$.style.paddingLeft="1.2em",$.appendChild(f(m.punc,s?"]":"}")),d.appendChild($);let l=!1;const g=p=>{l=p,j.textContent=p?"\u25B8":"\u25BE",w.style.display=p?"":"none",P.style.display=p?"":"none",A.style.display=p?"none":"",$.style.display=p?"none":""};return g(!1),i.addEventListener("click",()=>g(!l)),d}let V="";function ie(e){n("tryit-json").style.display=e?"":"none",n("tryit-body").style.display=e?"none":"",n("tryit-json-toggle").textContent=e?"Raw":"Tree"}function le(e){V=e;const t=n("tryit-results-placeholder");t&&(t.style.display="none"),n("tryit-response-card").style.display="";let r,o=!0;try{r=JSON.parse(e)}catch{o=!1}n("tryit-body").textContent=o?JSON.stringify(r,null,2):e;const a=n("tryit-json");a.textContent="",o&&a.appendChild(se(r)),n("tryit-json-toggle").style.display=o?"":"none",ie(o)}async function Q(){if(!v)return;const e={},t={};let r=v.path;for(const l of n("tryit-params").querySelectorAll("input")){e[l.dataset.param]=l.value;const g=new RegExp(":"+l.dataset.param.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?=/|$)","g");r=r.replace(g,encodeURIComponent(l.value))}const o=[];for(const l of n("tryit-query-params").querySelectorAll("input")){const g=l.value.trim();t[l.dataset.queryParam]=l.value,g&&o.push(`${encodeURIComponent(l.dataset.queryParam)}=${encodeURIComponent(g)}`)}const a=n("tryit-query").value.trim();a&&o.push(a);const s=o.join("&"),c=`/api/x/${v.project}${r}${s?"?"+s:""}`,d={ts:Date.now(),url:c,params:e,queryParams:t,extra:a},i={},j=n("tryit-token").value.trim();v.auth==="token"&&j&&(i.Authorization=`Bearer ${j}`);const w=location.origin+c,P=l=>`'${String(l).replace(/'/g,"'\\''")}'`,A=[`curl ${P(w)}`];for(const[l,g]of Object.entries(i))A.push(` -H ${P(`${l}: ${g}`)}`);J=A.join(` \\
2
+ `),n("tryit-result").style.display="",n("tryit-curl").textContent=J;const $=n("tryit-send");$.disabled=!0;try{const l=await fetch(c,{headers:i}),g=await l.text(),p=n("tryit-status");p.textContent=`${l.status} ${l.statusText}`,p.className="badge "+(l.ok?"badge-success":l.status>=500?"badge-danger":"badge-warning");const x=[];l.headers.forEach((X,be)=>x.push(`${be}: ${X}`)),n("tryit-headers-group").style.display="",n("tryit-headers").textContent=x.length?x.join(`
3
+ `):"(none exposed)",le(g),ae({...d,status:l.status,statusText:l.statusText,ok:l.ok})}catch(l){n("tryit-status").textContent="Network error",n("tryit-status").className="badge badge-danger",n("tryit-headers-group").style.display="none",le(String(l.message||l)),ae({...d,status:0,statusText:"Network error",ok:!1})}finally{$.disabled=D()}}if(u){if(n("ep-name").value=u.name||"",k.value=u.project,k.disabled=!0,n("ep-path").value=u.path||"",z(u.collection),y.value!==u.collection){const e=document.createElement("option");e.value=u.collection,e.textContent=`${u.collection} (other project \u2014 no longer allowed)`,y.appendChild(e),y.value=u.collection}L.value=u.auth||"public",n("ep-mode").value=u.mode||"list",n("ep-order").value=u.order||"desc",n("ep-limit").value=u.limit??50,n("ep-enabled").checked=u.enabled!==!1,await R(),ee(u.fields||[]),te(u.sort||"");for(const[e,t]of Object.entries(u.filter||{})){const{field:r,op:o}=qe(e);M(r,o,String(t))}v=u,B=JSON.stringify(I())}else await R(),M();ne(),Z();for(const e of["ep-name","ep-path","ep-limit"])n(e).addEventListener("input",h);for(const e of["ep-mode","ep-order","ep-sort"])n(e).addEventListener("change",h);n("ep-enabled").addEventListener("change",h),k.addEventListener("change",async()=>{z(),await R(),h()}),L.addEventListener("change",h),y.addEventListener("change",async()=>{await R(),h()}),n("ep-add-filter").addEventListener("click",()=>{M(),h()}),n("tryit-send").addEventListener("click",Q),n("tryit-history-clear").addEventListener("click",()=>{S.set(W,[]),Y()}),n("tryit-json-toggle").addEventListener("click",()=>{ie(n("tryit-json").style.display==="none")}),n("tryit-body-copy").addEventListener("click",async()=>{if(!V){E.toast("Send a request first.",{type:"info"});return}try{await navigator.clipboard.writeText(V),E.toast("Response copied.",{type:"success"})}catch{E.toast("Copy failed.",{type:"error"})}}),n("tryit-copy-curl").addEventListener("click",async()=>{if(!J){E.toast("Send a request first.",{type:"info"});return}try{await navigator.clipboard.writeText(J),E.toast("curl command copied.",{type:"success"})}catch{E.toast("Copy failed.",{type:"error"})}});async function ce(){const e=I();if(!e.name){E.toast("A name is required.",{type:"error"});return}if(!e.path.startsWith("/")){E.toast('The path must start with "/".',{type:"error"});return}try{if(q)v=await _.apiEndpoints.update(q,e),B=JSON.stringify(I()),E.toast("Endpoint saved.",{type:"success"}),Z();else{const t=await _.apiEndpoints.create(e);E.toast("Endpoint created.",{type:"success"}),location.hash="#/api-endpoints/edit/"+encodeURIComponent(t.id)}}catch(t){E.toast(`Save failed: ${t.message||t}`,{type:"error"})}}n("ep-save").addEventListener("click",ce);function H(e){if(!document.getElementById("ep-save")){document.removeEventListener("keydown",H),O===H&&(O=null);return}if(!(e.metaKey||e.ctrlKey))return;const r=e.key.toLowerCase();r==="s"?(e.preventDefault(),ce()):r==="enter"&&(e.preventDefault(),v&&!D()?Q():D()&&E.toast("Save first \u2014 Send tests the saved definition.",{type:"info"}))}O&&document.removeEventListener("keydown",O),O=H,document.addEventListener("keydown",H),Y(),Domma.icons.scan(b)}};
@@ -1,44 +1,44 @@
1
- /**
2
- * Domma CMS — Config Merge Utility
3
- * Merges new keys from an upstream config into an existing user config,
4
- * without ever overwriting values the user already has.
5
- */
6
-
7
- /**
8
- * Deep-merge new keys from `upstream` into `existing`.
9
- * Existing values are never modified — only missing keys are added.
10
- *
11
- * @param {object} existing - The user's current config object
12
- * @param {object} upstream - The upstream (new version) config object
13
- * @param {string} [_prefix] - Internal: key path prefix for reporting
14
- * @returns {{ merged: object, added: string[] }} Merged object + list of added key paths
15
- */
16
- export function deepMergeNewKeys(existing, upstream, _prefix = '') {
17
- const merged = {...existing};
18
- const added = [];
19
-
20
- for (const [key, upstreamVal] of Object.entries(upstream)) {
21
- const fullKey = _prefix ? `${_prefix}.${key}` : key;
22
-
23
- if (!(key in existing)) {
24
- // Key is entirely missing — add it wholesale
25
- merged[key] = upstreamVal;
26
- added.push(fullKey);
27
- } else if (
28
- upstreamVal !== null &&
29
- typeof upstreamVal === 'object' &&
30
- !Array.isArray(upstreamVal) &&
31
- typeof existing[key] === 'object' &&
32
- existing[key] !== null &&
33
- !Array.isArray(existing[key])
34
- ) {
35
- // Both sides are plain objects — recurse
36
- const child = deepMergeNewKeys(existing[key], upstreamVal, fullKey);
37
- merged[key] = child.merged;
38
- added.push(...child.added);
39
- }
40
- // Otherwise: existing value wins — no action
41
- }
42
-
43
- return {merged, added};
44
- }
1
+ /**
2
+ * Domma CMS — Config Merge Utility
3
+ * Merges new keys from an upstream config into an existing user config,
4
+ * without ever overwriting values the user already has.
5
+ */
6
+
7
+ /**
8
+ * Deep-merge new keys from `upstream` into `existing`.
9
+ * Existing values are never modified — only missing keys are added.
10
+ *
11
+ * @param {object} existing - The user's current config object
12
+ * @param {object} upstream - The upstream (new version) config object
13
+ * @param {string} [_prefix] - Internal: key path prefix for reporting
14
+ * @returns {{ merged: object, added: string[] }} Merged object + list of added key paths
15
+ */
16
+ export function deepMergeNewKeys(existing, upstream, _prefix = '') {
17
+ const merged = {...existing};
18
+ const added = [];
19
+
20
+ for (const [key, upstreamVal] of Object.entries(upstream)) {
21
+ const fullKey = _prefix ? `${_prefix}.${key}` : key;
22
+
23
+ if (!(key in existing)) {
24
+ // Key is entirely missing — add it wholesale
25
+ merged[key] = upstreamVal;
26
+ added.push(fullKey);
27
+ } else if (
28
+ upstreamVal !== null &&
29
+ typeof upstreamVal === 'object' &&
30
+ !Array.isArray(upstreamVal) &&
31
+ typeof existing[key] === 'object' &&
32
+ existing[key] !== null &&
33
+ !Array.isArray(existing[key])
34
+ ) {
35
+ // Both sides are plain objects — recurse
36
+ const child = deepMergeNewKeys(existing[key], upstreamVal, fullKey);
37
+ merged[key] = child.merged;
38
+ added.push(...child.added);
39
+ }
40
+ // Otherwise: existing value wins — no action
41
+ }
42
+
43
+ return {merged, added};
44
+ }
@@ -20,14 +20,11 @@
20
20
  "settings": {}
21
21
  },
22
22
  "blog": {
23
- "enabled": true,
23
+ "enabled": false,
24
24
  "bundled": false,
25
25
  "settings": {}
26
26
  },
27
27
  "analytics": {
28
28
  "enabled": true
29
- },
30
- "surveys": {
31
- "enabled": true
32
29
  }
33
30
  }
package/config/site.json CHANGED
@@ -41,7 +41,7 @@
41
41
  "dayStart": "07:00",
42
42
  "nightStart": "19:00"
43
43
  },
44
- "adminTheme": "admin-sharp-steel",
44
+ "adminTheme": "silver-dark",
45
45
  "layoutOptions": {
46
46
  "spacerSize": 40
47
47
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.32.4",
3
+ "version": "0.33.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",