domma-cms 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CLAUDE.md +9 -0
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/crud-tutorial.js +1 -1
  5. package/admin/js/lib/project-context.js +1 -1
  6. package/admin/js/templates/api-tokens.html +13 -0
  7. package/admin/js/templates/effects.html +752 -752
  8. package/admin/js/templates/form-submissions.html +30 -30
  9. package/admin/js/templates/forms.html +17 -17
  10. package/admin/js/templates/my-profile.html +17 -17
  11. package/admin/js/templates/role-editor.html +70 -70
  12. package/admin/js/templates/roles.html +10 -10
  13. package/admin/js/views/api-tokens.js +8 -0
  14. package/admin/js/views/collection-editor.js +4 -4
  15. package/admin/js/views/index.js +1 -1
  16. package/admin/js/views/roles.js +1 -1
  17. package/bin/lib/config-merge.js +44 -44
  18. package/bin/update.js +547 -547
  19. package/config/menus/admin-sidebar.json +7 -1
  20. package/package.json +1 -1
  21. package/server/middleware/auth.js +253 -253
  22. package/server/routes/api/api-tokens.js +83 -0
  23. package/server/routes/api/auth.js +309 -309
  24. package/server/routes/api/collections.js +113 -16
  25. package/server/routes/api/navigation.js +42 -42
  26. package/server/routes/api/settings.js +141 -141
  27. package/server/routes/public.js +202 -202
  28. package/server/server.js +8 -1
  29. package/server/services/apiTokens.js +259 -0
  30. package/server/services/email.js +167 -167
  31. package/server/services/permissionRegistry.js +13 -0
  32. package/server/services/presetCollections.js +25 -0
  33. package/server/services/roles.js +16 -0
  34. package/server/services/scaffolder.js +31 -1
  35. package/server/services/sidebar-migration.js +44 -0
  36. package/server/services/userProfiles.js +199 -199
  37. package/server/services/users.js +302 -302
  38. package/config/connections.json.bak +0 -9
@@ -1,30 +1,30 @@
1
- <div class="view-header">
2
- <h1><span data-icon="inbox"></span> <span id="submissions-title">Submissions</span></h1>
3
- <div style="display:flex;gap:.5rem;align-items:center;">
4
- <a href="#/forms" class="btn btn-ghost btn-sm">
5
- <span data-icon="arrow-left"></span> All Forms
6
- </a>
7
- <button id="export-btn" class="btn btn-ghost btn-sm">
8
- <span data-icon="download"></span> Export
9
- </button>
10
- <button id="clear-all-btn" class="btn btn-danger btn-sm">
11
- <span data-icon="trash-2"></span> Clear All
12
- </button>
13
- </div>
14
- </div>
15
-
16
- <div class="card mb-3">
17
- <div class="card-body" style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;">
18
- <input id="sub-search" type="text" class="form-input" placeholder="Search submissions…" style="flex:1;min-width:200px;">
19
- <input id="sub-date-from" type="date" class="form-input" style="width:auto;">
20
- <span style="color:var(--text-muted,#888);">to</span>
21
- <input id="sub-date-to" type="date" class="form-input" style="width:auto;">
22
- <span id="sub-count" class="text-muted" style="white-space:nowrap;font-size:.85rem;"></span>
23
- </div>
24
- </div>
25
-
26
- <div class="card">
27
- <div class="card-body">
28
- <div id="submissions-table"></div>
29
- </div>
30
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="inbox"></span> <span id="submissions-title">Submissions</span></h1>
3
+ <div style="display:flex;gap:.5rem;align-items:center;">
4
+ <a href="#/forms" class="btn btn-ghost btn-sm">
5
+ <span data-icon="arrow-left"></span> All Forms
6
+ </a>
7
+ <button id="export-btn" class="btn btn-ghost btn-sm">
8
+ <span data-icon="download"></span> Export
9
+ </button>
10
+ <button id="clear-all-btn" class="btn btn-danger btn-sm">
11
+ <span data-icon="trash-2"></span> Clear All
12
+ </button>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="card mb-3">
17
+ <div class="card-body" style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;">
18
+ <input id="sub-search" type="text" class="form-input" placeholder="Search submissions…" style="flex:1;min-width:200px;">
19
+ <input id="sub-date-from" type="date" class="form-input" style="width:auto;">
20
+ <span style="color:var(--text-muted,#888);">to</span>
21
+ <input id="sub-date-to" type="date" class="form-input" style="width:auto;">
22
+ <span id="sub-count" class="text-muted" style="white-space:nowrap;font-size:.85rem;"></span>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="card">
27
+ <div class="card-body">
28
+ <div id="submissions-table"></div>
29
+ </div>
30
+ </div>
@@ -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>
@@ -0,0 +1,8 @@
1
+ import{api as m}from"../api.js";function d(o){return String(o??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function f(o){return!Array.isArray(o)||o.length===0?'<span class="text-muted">All collections in project</span>':d(o.map(r=>{const s=Array.isArray(r.verbs)&&r.verbs.length?r.verbs.join(","):"all";return`${r.collection}: ${s}`}).join("; "))}function g(o){const r=[];for(const s of(o||"").split(`
2
+ `)){const n=s.trim();if(!n)continue;const[e,t]=n.split(":").map(l=>l.trim());if(!e)return{error:`Invalid scope line: "${n}"`};const a={collection:e};if(t){const l=t.split(",").map(c=>c.trim()).filter(Boolean),i=l.find(c=>!["create","read","update","delete"].includes(c));if(i)return{error:`Unknown verb "${i}" in scope "${n}"`};a.verbs=l}r.push(a)}return{scopes:r}}function b(o,r,s){const n=document.createElement("div");n.style.marginBottom=".75rem";const e=document.createElement("label");if(e.className="form-label",e.textContent=o,n.appendChild(e),n.appendChild(r),s){const t=document.createElement("p");t.className="text-muted",t.style.cssText="font-size:.8rem;margin:.25rem 0 0;",t.textContent=s,n.appendChild(t)}return n}function h(o,r){const s=E.modal({title:"Token created",size:"sm"}),n=document.createElement("div");n.style.padding="1rem";const e=document.createElement("p");e.innerHTML="<strong>Copy this token now.</strong> It is shown once and cannot be recovered \u2014 only a hash is stored.",e.style.marginBottom=".75rem",n.appendChild(e);const t=document.createElement("code");t.textContent=o,t.style.cssText="display:block;padding:.6rem;word-break:break-all;user-select:all;margin-bottom:.75rem;",n.appendChild(t);const a=document.createElement("button");a.className="btn btn-primary",a.textContent="Copy to clipboard",a.style.marginRight=".5rem",a.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(o),a.textContent="Copied!"}catch{E.toast("Copy failed \u2014 select the token text manually.",{type:"error"})}}),n.appendChild(a);const l=document.createElement("button");l.className="btn btn-secondary",l.textContent="Done",l.addEventListener("click",()=>{s.close(),r()}),n.appendChild(l),s.element.appendChild(n),s.open()}function v(o,r){const s=E.modal({title:"New API Token",size:"md"}),n=document.createElement("div");n.style.padding=".5rem 1rem 1rem";const e=document.createElement("input");e.type="text",e.className="form-input",e.placeholder="e.g. mobile-app, ci-pipeline";const t=document.createElement("select");t.className="form-input";for(const c of o){const p=document.createElement("option");p.value=c.slug,p.textContent=`${c.name} (${c.slug})`,t.appendChild(p)}const a=document.createElement("input");a.type="datetime-local",a.className="form-input";const l=document.createElement("textarea");l.className="form-input",l.rows=3,l.placeholder=`jobs: read
3
+ enquiries: create, read`,n.appendChild(b("Name",e)),n.appendChild(b("Project",t,"The token only works on collections in this project.")),n.appendChild(b("Expires (optional)",a,"Leave empty for a non-expiring token.")),n.appendChild(b("Scopes (optional)",l,'One per line: "collection: verb, verb". Leave empty to allow every collection in the project.'));const i=document.createElement("button");i.className="btn btn-primary",i.textContent="Create token",i.addEventListener("click",async()=>{const c=e.value.trim();if(!c){E.toast("A token name is required.",{type:"error"});return}const{scopes:p,error:y}=g(l.value);if(y){E.toast(y,{type:"error"});return}const k={name:c,project:t.value,scopes:p,expiresAt:a.value?new Date(a.value).toISOString():null};try{i.disabled=!0;const{plaintext:u}=await m.apiTokens.create(k);s.close(),h(u,r)}catch(u){i.disabled=!1,E.toast(`Create failed: ${u.message||u}`,{type:"error"})}}),n.appendChild(i),s.element.appendChild(n),s.open(),setTimeout(()=>e.focus(),100)}export const apiTokensView={templateUrl:"/admin/js/templates/api-tokens.html",async onMount(o){const[r,s]=await Promise.all([m.apiTokens.list().catch(()=>[]),m.projects.list().catch(()=>[])]);T.create("#api-tokens-table",{data:r,emptyMessage:'No API tokens yet. Click "New token" to create one.',columns:[{key:"name",title:"Name",sortable:!0},{key:"project",title:"Project",sortable:!0,render:e=>`<code>${d(e)}</code>`},{key:"tokenHint",title:"Token",render:e=>`<code>dcms_\u2026${d(e||"????")}</code>`},{key:"scopes",title:"Scopes",render:e=>f(e)},{key:"enabled",title:"Status",render:(e,t)=>t.expiresAt&&Date.parse(t.expiresAt)<Date.now()?'<span class="badge badge-warning">Expired</span>':e?'<span class="badge badge-success">Enabled</span>':'<span class="badge badge-secondary">Disabled</span>'},{key:"expiresAt",title:"Expires",render:e=>e?D(e).format("D MMM YYYY, HH:mm"):'<span class="text-muted">\u2014</span>'},{key:"lastUsedAt",title:"Last used",render:e=>e?D(e).format("D MMM YYYY, HH:mm"):'<span class="text-muted">Never</span>'},{key:"_actions",title:"Actions",render:(e,t)=>`
4
+ <span style="display:inline-flex;gap:0.25rem;">
5
+ <button class="btn btn-sm btn-ghost btn-toggle-token" data-id="${d(t.id)}" data-enabled="${t.enabled?"1":""}" data-tooltip="${t.enabled?"Disable":"Enable"}"><span data-icon="${t.enabled?"pause":"play"}"></span></button>
6
+ <button class="btn btn-sm btn-danger btn-revoke-token" data-id="${d(t.id)}" data-name="${d(t.name)}" data-tooltip="Revoke"><span data-icon="trash"></span></button>
7
+ </span>
8
+ `}]}),Domma.icons.scan(o.get(0)),document.querySelectorAll("#api-tokens-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})});const n=()=>location.reload();o.off("click","#btn-new-token").on("click","#btn-new-token",()=>{if(!s.length){E.toast("No projects available \u2014 create a project first.",{type:"error"});return}v(s,n)}),o.off("click",".btn-toggle-token").on("click",".btn-toggle-token",async function(){const e=$(this).data("id"),t=!!$(this).data("enabled");try{await m.apiTokens.update(e,{enabled:!t}),E.toast(`Token ${t?"disabled":"enabled"}.`,{type:"success"}),n()}catch(a){E.toast(`Update failed: ${a.message||a}`,{type:"error"})}}),o.off("click",".btn-revoke-token").on("click",".btn-revoke-token",async function(){const e=$(this).data("id"),t=$(this).data("name");if(await E.confirm(`Revoke token "${t}"? External callers using it will immediately lose access. This cannot be undone.`))try{await m.apiTokens.revoke(e),E.toast("Token revoked.",{type:"success"}),n()}catch(a){E.toast(`Revoke failed: ${a.message||a}`,{type:"error"})}})}};
@@ -1,5 +1,5 @@
1
- import{api as w}from"../api.js";const ee=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],Q=new Set(["select","radio","checkbox-group"]),ae=["public","subscriber","editor","manager","admin"],te=["create","read","update","delete"];let f=[],h=null,S=!0,z=null,L="file",X={};function ne(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function le(e){return ee.find(t=>t.value===e)?.label||e}function oe(e){const t={...f[e]},l=document.getElementById(`fb-label-${e}`),i=document.getElementById(`fb-name-${e}`),s=document.getElementById(`fb-type-${e}`),m=document.getElementById(`fb-required-${e}`),p=document.getElementById(`fb-placeholder-${e}`),n=document.getElementById(`fb-helper-${e}`);if(l&&(t.label=l.value.trim()||t.label),i&&(t.name=i.value.trim()||t.name),s&&(t.type=s.value||t.type),m&&(t.required=m.checked),p&&(t.placeholder=p.value.trim()),n&&(t.helper=n.value.trim()),Q.has(t.type)){const o=document.getElementById(`fb-options-${e}`);o&&(t.options=o.value.split(`
2
- `).filter(c=>c.trim()).map(c=>{const[b,...y]=c.split(":");return{value:b.trim(),label:y.join(":").trim()||b.trim()}}))}const a=document.getElementById(`fb-span-${e}`);if(document.getElementById(`fb-fullwidth-${e}`)?.checked)t.fullWidth=!0,delete t.span;else{delete t.fullWidth;const o=parseInt(a?.value,10);o>1?t.span=o:delete t.span}return t}function Z(){return f.map((e,t)=>oe(t))}function de(e,t){const l=document.createElement("div");l.className="fb-field-card",l.dataset.index=t,l.style.cssText="border:1px solid var(--border-color,#333);border-radius:8px;margin-bottom:.75rem;overflow:hidden;";const i=document.createElement("div");i.className="fb-field-header",i.style.cssText="display:flex;align-items:center;gap:.5rem;padding:.6rem .75rem;background:var(--card-header-bg,rgba(255,255,255,.03));cursor:pointer;user-select:none;";const s=document.createElement("span");s.textContent="\u283F",s.style.cssText="cursor:grab;opacity:.4;font-size:1.1rem;flex-shrink:0;",l.draggable=!0,s.addEventListener("mousedown",()=>{l.draggable=!0}),l.addEventListener("dragstart",d=>{z=t,d.dataTransfer.effectAllowed="move",l.style.opacity="0.4"}),l.addEventListener("dragend",()=>{l.style.opacity="",document.querySelectorAll(".fb-field-card").forEach(d=>d.classList.remove("fb-drag-over"))}),l.addEventListener("dragover",d=>{d.preventDefault(),d.dataTransfer.dropEffect="move",document.querySelectorAll(".fb-field-card").forEach(C=>C.classList.remove("fb-drag-over")),l.classList.add("fb-drag-over")}),l.addEventListener("dragleave",()=>{l.classList.remove("fb-drag-over")}),l.addEventListener("drop",d=>{if(d.preventDefault(),l.classList.remove("fb-drag-over"),z===null||z===t)return;f=Z();const[C]=f.splice(z,1);f.splice(t,0,C),z=null,P(document.getElementById("fields-list"))});const m=document.createElement("span");m.className="fb-field-summary",m.style.cssText="flex:1;font-weight:500;font-size:.9rem;",m.textContent=e.label||"(Untitled field)";const p=document.createElement("span");p.style.cssText="font-size:.75rem;opacity:.5;",p.textContent=le(e.type);const n=document.createElement("span");n.className="fb-field-chevron",n.textContent="\u25BE",n.style.cssText="opacity:.5;transition:transform .2s;";const a=document.createElement("button");a.type="button",a.textContent="\xD7",a.className="btn btn-sm",a.style.cssText="padding:.15rem .45rem;line-height:1;font-size:1rem;opacity:.6;",a.title="Remove field",a.addEventListener("click",d=>{d.stopPropagation(),f.splice(t,1),P(document.getElementById("fields-list"))}),i.appendChild(s),i.appendChild(m),i.appendChild(p),i.appendChild(n),i.appendChild(a);const o=document.createElement("div");o.className="fb-field-body",o.style.cssText="padding:.75rem;display:none;";const c=document.createElement("div");c.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem;";const b=document.createElement("div"),y=document.createElement("label");y.className="form-label",y.textContent="Label";const g=document.createElement("input");g.id=`fb-label-${t}`,g.type="text",g.className="form-input",g.value=e.label||"",g.addEventListener("input",()=>{m.textContent=g.value.trim()||"(Untitled field)";const d=document.getElementById(`fb-name-${t}`);d&&!d.dataset.manual&&(d.value=ne(g.value).replace(/-/g,"_"))}),b.appendChild(y),b.appendChild(g);const u=document.createElement("div"),$=document.createElement("label");$.className="form-label",$.textContent="Name (key)";const v=document.createElement("input");v.id=`fb-name-${t}`,v.type="text",v.className="form-input",v.value=e.name||"",v.addEventListener("input",()=>{v.dataset.manual="1"}),u.appendChild($),u.appendChild(v);const N=document.createElement("div"),B=document.createElement("label");B.className="form-label",B.textContent="Type";const r=document.createElement("select");r.id=`fb-type-${t}`,r.className="form-input",ee.forEach(d=>{const C=document.createElement("option");C.value=d.value,C.textContent=d.label,d.value===e.type&&(C.selected=!0),r.appendChild(C)}),r.addEventListener("change",()=>{p.textContent=le(r.value);const d=o.querySelector(".fb-options-wrap");d&&(d.style.display=Q.has(r.value)?"":"none")}),N.appendChild(B),N.appendChild(r),c.appendChild(b),c.appendChild(u),c.appendChild(N);const T=document.createElement("div");T.style.cssText="display:grid;grid-template-columns:1fr 1fr auto;gap:.6rem;align-items:end;margin-bottom:.6rem;";const I=document.createElement("div"),H=document.createElement("label");H.className="form-label",H.textContent="Placeholder";const j=document.createElement("input");j.id=`fb-placeholder-${t}`,j.type="text",j.className="form-input",j.value=e.placeholder||"",I.appendChild(H),I.appendChild(j);const O=document.createElement("div"),V=document.createElement("label");V.className="form-label",V.textContent="Helper text";const q=document.createElement("input");q.id=`fb-helper-${t}`,q.type="text",q.className="form-input",q.value=e.helper||"",O.appendChild(V),O.appendChild(q);const Y=document.createElement("div");Y.style.cssText="padding-bottom:.35rem;";const F=document.createElement("label");F.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const M=document.createElement("input");M.id=`fb-required-${t}`,M.type="checkbox",M.checked=!!e.required,F.appendChild(M),F.appendChild(document.createTextNode("Required")),Y.appendChild(F),T.appendChild(I),T.appendChild(O),T.appendChild(Y);const D=document.createElement("div");D.className="fb-options-wrap",D.style.display=Q.has(e.type)?"":"none";const _=document.createElement("label");_.className="form-label",_.textContent="Options (one per line: value: Label)";const A=document.createElement("textarea");A.id=`fb-options-${t}`,A.className="form-input",A.rows=4,A.value=(e.options||[]).map(d=>typeof d=="string"?`${d}: ${d}`:`${d.value??""}: ${d.label??d.value??""}`).join(`
3
- `),D.appendChild(_),D.appendChild(A);const x=document.createElement("div");x.className="fb-grid-row",x.style.gridTemplateColumns="1fr auto",x.style.gap=".6rem",x.style.alignItems="end",x.style.marginBottom=".6rem",x.style.display=document.getElementById("collection-layout")?.value==="grid"?"grid":"none";const G=document.createElement("div"),J=document.createElement("label");J.className="form-label",J.textContent="Column Span";const k=document.createElement("input");k.id=`fb-span-${t}`,k.type="number",k.className="form-input",k.min="1",k.max="6",k.value=e.span>1?String(e.span):"1",G.appendChild(J),G.appendChild(k);const K=document.createElement("div");K.style.cssText="padding-bottom:.35rem;";const U=document.createElement("label");U.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const W=document.createElement("input");return W.id=`fb-fullwidth-${t}`,W.type="checkbox",W.checked=!!e.fullWidth,U.appendChild(W),U.appendChild(document.createTextNode("Full Width")),K.appendChild(U),x.appendChild(G),x.appendChild(K),o.appendChild(c),o.appendChild(T),o.appendChild(D),o.appendChild(x),i.addEventListener("click",()=>{const d=o.style.display!=="none";o.style.display=d?"none":"",n.style.transform=d?"":"rotate(180deg)"}),l.appendChild(i),l.appendChild(o),l}function P(e){if(e){if(e.textContent="",f.length===0){const t=document.createElement("p");t.className="text-muted",t.id="fields-empty-msg",t.style.cssText="text-align:center;padding:2rem 0;",t.textContent='No fields yet. Click "Add Field" to get started.',e.appendChild(t);return}f.forEach((t,l)=>{e.appendChild(de(t,l))})}}function ie(e,t){t.textContent="",te.forEach(l=>{const i=e?.[l]||{enabled:!1,access:"admin"},s=document.createElement("div");s.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);";const m=document.createElement("strong");m.textContent=l.charAt(0).toUpperCase()+l.slice(1),m.style.cssText="font-size:.9rem;";const p=document.createElement("label");p.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const n=document.createElement("input");n.type="checkbox",n.id=`api-${l}-enabled`,n.checked=!!i.enabled,p.appendChild(n),p.appendChild(document.createTextNode("Enable public access"));const a=document.createElement("select");a.id=`api-${l}-access`,a.className="form-input",ae.forEach(o=>{const c=document.createElement("option");c.value=o,c.textContent=o.charAt(0).toUpperCase()+o.slice(1),o===i.access&&(c.selected=!0),a.appendChild(c)}),s.appendChild(m),s.appendChild(p),s.appendChild(a),t.appendChild(s)})}function ce(e,t){E.dropdown("#storage-adapter-trigger",{items:[{label:"File (default)",value:"file"},{label:"MongoDB",value:"mongodb"}],onSelect:({item:i})=>{e.find("#storage-adapter").val(i.value),e.find("#storage-adapter-label").text(i.label);const s=i.value==="mongodb";e.find("#storage-connection-group").toggle(s),e.find("#storage-migration-warning").toggle(s&&!S)}});const l=t.map(i=>({label:i,value:i}));E.dropdown("#storage-connection-trigger",{items:l.length?l:[{label:"default",value:"default"}],onSelect:({item:i})=>{e.find("#storage-connection").val(i.value),e.find("#storage-connection-label").text(i.label)}})}function se(){return(document.getElementById("storage-adapter")?.value||"file")==="mongodb"?{adapter:"mongodb",connection:document.getElementById("storage-connection")?.value||"default"}:{adapter:"file"}}function re(){const e={};return te.forEach(t=>{const l=document.getElementById(`api-${t}-enabled`)?.checked??!1,i=document.getElementById(`api-${t}-access`)?.value||"admin";e[t]={enabled:l,access:i}}),e}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(e){f=[],h=null,S=!0;const t=window.location.hash.match(/\/collections\/edit\/([^/?#]+)/);t&&(h=t[1],S=!1),E.tabs(e.find("#collection-tabs").get(0)),e.find("#collection-layout").get(0)?.addEventListener("change",function(){const n=this.value==="grid";e.find("#collection-columns-group").get(0).style.display=n?"":"none",document.querySelectorAll(".fb-grid-row").forEach(a=>{a.style.display=n?"grid":"none"})});const l=e.find("#fields-list").get(0),i=e.find("#api-access-rows").get(0);X={};const s=e.find("#collection-project").get(0);if(s)try{(await w.projects.list()).forEach(a=>{const o=document.createElement("option");o.value=a.slug,o.textContent=a.name||a.slug,s.appendChild(o)})}catch{}const m=await w.collections.proStatus();m?.pro&&h!=="roles"&&(e.find("#storage-tab-btn").show(),ce(e,m.connections));let p={create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}};if(S){const n=e.find("#field-title").get(0),a=e.find("#field-slug").get(0);n&&a&&(n.addEventListener("input",()=>{a.dataset.manual||(a.value=ne(n.value))}),a.addEventListener("input",()=>{a.dataset.manual="1"}))}else try{const n=await w.collections.get(h);if(!n){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const a=e.find("#editor-title-text").get(0);a&&(a.textContent=n.title),e.find("#field-title").val(n.title||""),e.find("#field-slug").val(n.slug||""),e.find("#field-slug").prop("readonly",!0),e.find("#slug-hint").get(0).textContent="Slug cannot be changed after creation.",e.find("#field-description").val(n.description||""),e.find("#collection-layout").val(n.layout||"stacked"),e.find("#collection-columns").val(n.columns||2),e.find("#collection-bundled").prop("checked",!!n.bundled),X=n.meta||{},e.find("#collection-project").val(n.meta?.project||""),e.find("#collection-columns-group").get(0).style.display=n.layout==="grid"?"":"none",f=n.fields||[],p=n.api||p,L=n.storage?.adapter||"file",n.storage&&(e.find("#storage-adapter").val(n.storage.adapter||"file"),e.find("#storage-adapter-label").text(n.storage.adapter==="mongodb"?"MongoDB":"File (default)"),n.storage.adapter==="mongodb"&&(e.find("#storage-connection-group").show(),e.find("#storage-connection").val(n.storage.connection||"default"),e.find("#storage-connection-label").text(n.storage.connection||"default"))),h==="roles"&&e.find("#storage-tab-btn").hide()}catch{E.toast("Failed to load collection.",{type:"error"}),R.navigate("/collections");return}P(l),ie(p,i),e.find("#add-field-btn").off("click").on("click",()=>{f=Z(),f.push({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null}),P(l);const n=l.querySelectorAll(".fb-field-card");if(n.length){const a=n[n.length-1],o=a.querySelector(".fb-field-body"),c=a.querySelector(".fb-field-chevron");o&&(o.style.display=""),c&&(c.style.transform="rotate(180deg)"),a.querySelector(`#fb-label-${f.length-1}`)?.focus()}}),e.find("#save-collection-btn").off("click").on("click",async()=>{const n=e.find("#field-title").val().trim(),a=e.find("#field-slug").val().trim(),o=e.find("#field-description").val().trim();if(!n){E.toast("Title is required.",{type:"warning"});return}const c=Z(),b=re(),y=e.find("#collection-layout").val()||"stacked",g=parseInt(e.find("#collection-columns").val(),10)||2,u=se(),$=e.find("#collection-bundled").is(":checked"),v=e.find("#collection-project").val()||"",N={...X||{},project:v||null},B=e.find("#save-collection-btn");B.prop("disabled",!0);try{if(S){const r=await w.collections.create({title:n,slug:a,description:o,layout:y,columns:g,fields:c,api:b,storage:u,meta:N,...$?{bundled:!0}:{}});h=r.slug,L=u.adapter||"file",S=!1,E.toast("Collection created.",{type:"success"}),R.navigate(`/collections/edit/${r.slug}`)}else if((u.adapter||"file")!==L){let r=0;try{r=(await w.collections.listEntries(h,{limit:1}))?.total??0}catch{}const T=L==="file"?`file \u2192 ${u.adapter}`:`${L} \u2192 ${u.adapter||"file"}`;if(r>0&&await E.confirm(`You changed the storage adapter (${T}).
1
+ import{api as w}from"../api.js";const ee=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],Q=new Set(["select","radio","checkbox-group"]),ae=["public","token","subscriber","editor","manager","admin"],te=["create","read","update","delete"];let g=[],h=null,S=!0,z=null,B="file",X={};function le(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function ne(e){return ee.find(t=>t.value===e)?.label||e}function oe(e){const t={...g[e]},n=document.getElementById(`fb-label-${e}`),d=document.getElementById(`fb-name-${e}`),s=document.getElementById(`fb-type-${e}`),r=document.getElementById(`fb-required-${e}`),u=document.getElementById(`fb-placeholder-${e}`),l=document.getElementById(`fb-helper-${e}`);if(n&&(t.label=n.value.trim()||t.label),d&&(t.name=d.value.trim()||t.name),s&&(t.type=s.value||t.type),r&&(t.required=r.checked),u&&(t.placeholder=u.value.trim()),l&&(t.helper=l.value.trim()),Q.has(t.type)){const a=document.getElementById(`fb-options-${e}`);a&&(t.options=a.value.split(`
2
+ `).filter(c=>c.trim()).map(c=>{const[p,...y]=c.split(":");return{value:p.trim(),label:y.join(":").trim()||p.trim()}}))}const o=document.getElementById(`fb-span-${e}`);if(document.getElementById(`fb-fullwidth-${e}`)?.checked)t.fullWidth=!0,delete t.span;else{delete t.fullWidth;const a=parseInt(o?.value,10);a>1?t.span=a:delete t.span}return t}function Z(){return g.map((e,t)=>oe(t))}function de(e,t){const n=document.createElement("div");n.className="fb-field-card",n.dataset.index=t,n.style.cssText="border:1px solid var(--border-color,#333);border-radius:8px;margin-bottom:.75rem;overflow:hidden;";const d=document.createElement("div");d.className="fb-field-header",d.style.cssText="display:flex;align-items:center;gap:.5rem;padding:.6rem .75rem;background:var(--card-header-bg,rgba(255,255,255,.03));cursor:pointer;user-select:none;";const s=document.createElement("span");s.textContent="\u283F",s.style.cssText="cursor:grab;opacity:.4;font-size:1.1rem;flex-shrink:0;",n.draggable=!0,s.addEventListener("mousedown",()=>{n.draggable=!0}),n.addEventListener("dragstart",i=>{z=t,i.dataTransfer.effectAllowed="move",n.style.opacity="0.4"}),n.addEventListener("dragend",()=>{n.style.opacity="",document.querySelectorAll(".fb-field-card").forEach(i=>i.classList.remove("fb-drag-over"))}),n.addEventListener("dragover",i=>{i.preventDefault(),i.dataTransfer.dropEffect="move",document.querySelectorAll(".fb-field-card").forEach(C=>C.classList.remove("fb-drag-over")),n.classList.add("fb-drag-over")}),n.addEventListener("dragleave",()=>{n.classList.remove("fb-drag-over")}),n.addEventListener("drop",i=>{if(i.preventDefault(),n.classList.remove("fb-drag-over"),z===null||z===t)return;g=Z();const[C]=g.splice(z,1);g.splice(t,0,C),z=null,P(document.getElementById("fields-list"))});const r=document.createElement("span");r.className="fb-field-summary",r.style.cssText="flex:1;font-weight:500;font-size:.9rem;",r.textContent=e.label||"(Untitled field)";const u=document.createElement("span");u.style.cssText="font-size:.75rem;opacity:.5;",u.textContent=ne(e.type);const l=document.createElement("span");l.className="fb-field-chevron",l.textContent="\u25BE",l.style.cssText="opacity:.5;transition:transform .2s;";const o=document.createElement("button");o.type="button",o.textContent="\xD7",o.className="btn btn-sm",o.style.cssText="padding:.15rem .45rem;line-height:1;font-size:1rem;opacity:.6;",o.title="Remove field",o.addEventListener("click",i=>{i.stopPropagation(),g.splice(t,1),P(document.getElementById("fields-list"))}),d.appendChild(s),d.appendChild(r),d.appendChild(u),d.appendChild(l),d.appendChild(o);const a=document.createElement("div");a.className="fb-field-body",a.style.cssText="padding:.75rem;display:none;";const c=document.createElement("div");c.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem;";const p=document.createElement("div"),y=document.createElement("label");y.className="form-label",y.textContent="Label";const b=document.createElement("input");b.id=`fb-label-${t}`,b.type="text",b.className="form-input",b.value=e.label||"",b.addEventListener("input",()=>{r.textContent=b.value.trim()||"(Untitled field)";const i=document.getElementById(`fb-name-${t}`);i&&!i.dataset.manual&&(i.value=le(b.value).replace(/-/g,"_"))}),p.appendChild(y),p.appendChild(b);const f=document.createElement("div"),$=document.createElement("label");$.className="form-label",$.textContent="Name (key)";const v=document.createElement("input");v.id=`fb-name-${t}`,v.type="text",v.className="form-input",v.value=e.name||"",v.addEventListener("input",()=>{v.dataset.manual="1"}),f.appendChild($),f.appendChild(v);const N=document.createElement("div"),L=document.createElement("label");L.className="form-label",L.textContent="Type";const m=document.createElement("select");m.id=`fb-type-${t}`,m.className="form-input",ee.forEach(i=>{const C=document.createElement("option");C.value=i.value,C.textContent=i.label,i.value===e.type&&(C.selected=!0),m.appendChild(C)}),m.addEventListener("change",()=>{u.textContent=ne(m.value);const i=a.querySelector(".fb-options-wrap");i&&(i.style.display=Q.has(m.value)?"":"none")}),N.appendChild(L),N.appendChild(m),c.appendChild(p),c.appendChild(f),c.appendChild(N);const T=document.createElement("div");T.style.cssText="display:grid;grid-template-columns:1fr 1fr auto;gap:.6rem;align-items:end;margin-bottom:.6rem;";const I=document.createElement("div"),H=document.createElement("label");H.className="form-label",H.textContent="Placeholder";const j=document.createElement("input");j.id=`fb-placeholder-${t}`,j.type="text",j.className="form-input",j.value=e.placeholder||"",I.appendChild(H),I.appendChild(j);const O=document.createElement("div"),V=document.createElement("label");V.className="form-label",V.textContent="Helper text";const q=document.createElement("input");q.id=`fb-helper-${t}`,q.type="text",q.className="form-input",q.value=e.helper||"",O.appendChild(V),O.appendChild(q);const Y=document.createElement("div");Y.style.cssText="padding-bottom:.35rem;";const F=document.createElement("label");F.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const M=document.createElement("input");M.id=`fb-required-${t}`,M.type="checkbox",M.checked=!!e.required,F.appendChild(M),F.appendChild(document.createTextNode("Required")),Y.appendChild(F),T.appendChild(I),T.appendChild(O),T.appendChild(Y);const A=document.createElement("div");A.className="fb-options-wrap",A.style.display=Q.has(e.type)?"":"none";const _=document.createElement("label");_.className="form-label",_.textContent="Options (one per line: value: Label)";const D=document.createElement("textarea");D.id=`fb-options-${t}`,D.className="form-input",D.rows=4,D.value=(e.options||[]).map(i=>typeof i=="string"?`${i}: ${i}`:`${i.value??""}: ${i.label??i.value??""}`).join(`
3
+ `),A.appendChild(_),A.appendChild(D);const x=document.createElement("div");x.className="fb-grid-row",x.style.gridTemplateColumns="1fr auto",x.style.gap=".6rem",x.style.alignItems="end",x.style.marginBottom=".6rem",x.style.display=document.getElementById("collection-layout")?.value==="grid"?"grid":"none";const G=document.createElement("div"),J=document.createElement("label");J.className="form-label",J.textContent="Column Span";const k=document.createElement("input");k.id=`fb-span-${t}`,k.type="number",k.className="form-input",k.min="1",k.max="6",k.value=e.span>1?String(e.span):"1",G.appendChild(J),G.appendChild(k);const K=document.createElement("div");K.style.cssText="padding-bottom:.35rem;";const U=document.createElement("label");U.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const W=document.createElement("input");return W.id=`fb-fullwidth-${t}`,W.type="checkbox",W.checked=!!e.fullWidth,U.appendChild(W),U.appendChild(document.createTextNode("Full Width")),K.appendChild(U),x.appendChild(G),x.appendChild(K),a.appendChild(c),a.appendChild(T),a.appendChild(A),a.appendChild(x),d.addEventListener("click",()=>{const i=a.style.display!=="none";a.style.display=i?"none":"",l.style.transform=i?"":"rotate(180deg)"}),n.appendChild(d),n.appendChild(a),n}function P(e){if(e){if(e.textContent="",g.length===0){const t=document.createElement("p");t.className="text-muted",t.id="fields-empty-msg",t.style.cssText="text-align:center;padding:2rem 0;",t.textContent='No fields yet. Click "Add Field" to get started.',e.appendChild(t);return}g.forEach((t,n)=>{e.appendChild(de(t,n))})}}function ie(e,t){t.textContent="",te.forEach(n=>{const d=e?.[n]||{enabled:!1,access:"admin"},s=document.createElement("div");s.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);";const r=document.createElement("strong");r.textContent=n.charAt(0).toUpperCase()+n.slice(1),r.style.cssText="font-size:.9rem;";const u=document.createElement("label");u.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const l=document.createElement("input");l.type="checkbox",l.id=`api-${n}-enabled`,l.checked=!!d.enabled,u.appendChild(l),u.appendChild(document.createTextNode("Enable public access"));const o=document.createElement("select");if(o.id=`api-${n}-access`,o.className="form-input",ae.forEach(a=>{const c=document.createElement("option");c.value=a,c.textContent=a.charAt(0).toUpperCase()+a.slice(1),a===d.access&&(c.selected=!0),o.appendChild(c)}),s.appendChild(r),s.appendChild(u),s.appendChild(o),t.appendChild(s),n==="read"){const a=document.createElement("div");a.style.cssText="display:grid;grid-template-columns:140px 1fr;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);";const c=document.createElement("label");c.htmlFor="api-read-fields",c.style.cssText="font-size:.85rem;color:var(--dm-text-muted,#aaa);",c.textContent="Read fields";const p=document.createElement("input");p.type="text",p.id="api-read-fields",p.className="form-input",p.placeholder="Comma-separated allowlist \u2014 empty = all fields",p.value=Array.isArray(d.fields)?d.fields.join(", "):"",a.appendChild(c),a.appendChild(p),t.appendChild(a)}})}function ce(e,t){E.dropdown("#storage-adapter-trigger",{items:[{label:"File (default)",value:"file"},{label:"MongoDB",value:"mongodb"}],onSelect:({item:d})=>{e.find("#storage-adapter").val(d.value),e.find("#storage-adapter-label").text(d.label);const s=d.value==="mongodb";e.find("#storage-connection-group").toggle(s),e.find("#storage-migration-warning").toggle(s&&!S)}});const n=t.map(d=>({label:d,value:d}));E.dropdown("#storage-connection-trigger",{items:n.length?n:[{label:"default",value:"default"}],onSelect:({item:d})=>{e.find("#storage-connection").val(d.value),e.find("#storage-connection-label").text(d.label)}})}function se(){return(document.getElementById("storage-adapter")?.value||"file")==="mongodb"?{adapter:"mongodb",connection:document.getElementById("storage-connection")?.value||"default"}:{adapter:"file"}}function re(){const e={};return te.forEach(t=>{const n=document.getElementById(`api-${t}-enabled`)?.checked??!1,d=document.getElementById(`api-${t}-access`)?.value||"admin";if(e[t]={enabled:n,access:d},t==="read"){const s=(document.getElementById("api-read-fields")?.value||"").split(",").map(r=>r.trim()).filter(Boolean);s.length&&(e[t].fields=s)}}),e}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(e){g=[],h=null,S=!0;const t=window.location.hash.match(/\/collections\/edit\/([^/?#]+)/);t&&(h=t[1],S=!1),E.tabs(e.find("#collection-tabs").get(0)),e.find("#collection-layout").get(0)?.addEventListener("change",function(){const l=this.value==="grid";e.find("#collection-columns-group").get(0).style.display=l?"":"none",document.querySelectorAll(".fb-grid-row").forEach(o=>{o.style.display=l?"grid":"none"})});const n=e.find("#fields-list").get(0),d=e.find("#api-access-rows").get(0);X={};const s=e.find("#collection-project").get(0);if(s)try{(await w.projects.list()).forEach(o=>{const a=document.createElement("option");a.value=o.slug,a.textContent=o.name||o.slug,s.appendChild(a)})}catch{}const r=await w.collections.proStatus();r?.pro&&h!=="roles"&&(e.find("#storage-tab-btn").show(),ce(e,r.connections));let u={create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}};if(S){const l=e.find("#field-title").get(0),o=e.find("#field-slug").get(0);l&&o&&(l.addEventListener("input",()=>{o.dataset.manual||(o.value=le(l.value))}),o.addEventListener("input",()=>{o.dataset.manual="1"}))}else try{const l=await w.collections.get(h);if(!l){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const o=e.find("#editor-title-text").get(0);o&&(o.textContent=l.title),e.find("#field-title").val(l.title||""),e.find("#field-slug").val(l.slug||""),e.find("#field-slug").prop("readonly",!0),e.find("#slug-hint").get(0).textContent="Slug cannot be changed after creation.",e.find("#field-description").val(l.description||""),e.find("#collection-layout").val(l.layout||"stacked"),e.find("#collection-columns").val(l.columns||2),e.find("#collection-bundled").prop("checked",!!l.bundled),X=l.meta||{},e.find("#collection-project").val(l.meta?.project||""),e.find("#collection-columns-group").get(0).style.display=l.layout==="grid"?"":"none",g=l.fields||[],u=l.api||u,B=l.storage?.adapter||"file",l.storage&&(e.find("#storage-adapter").val(l.storage.adapter||"file"),e.find("#storage-adapter-label").text(l.storage.adapter==="mongodb"?"MongoDB":"File (default)"),l.storage.adapter==="mongodb"&&(e.find("#storage-connection-group").show(),e.find("#storage-connection").val(l.storage.connection||"default"),e.find("#storage-connection-label").text(l.storage.connection||"default"))),h==="roles"&&e.find("#storage-tab-btn").hide()}catch{E.toast("Failed to load collection.",{type:"error"}),R.navigate("/collections");return}P(n),ie(u,d),e.find("#add-field-btn").off("click").on("click",()=>{g=Z(),g.push({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null}),P(n);const l=n.querySelectorAll(".fb-field-card");if(l.length){const o=l[l.length-1],a=o.querySelector(".fb-field-body"),c=o.querySelector(".fb-field-chevron");a&&(a.style.display=""),c&&(c.style.transform="rotate(180deg)"),o.querySelector(`#fb-label-${g.length-1}`)?.focus()}}),e.find("#save-collection-btn").off("click").on("click",async()=>{const l=e.find("#field-title").val().trim(),o=e.find("#field-slug").val().trim(),a=e.find("#field-description").val().trim();if(!l){E.toast("Title is required.",{type:"warning"});return}const c=Z(),p=re(),y=e.find("#collection-layout").val()||"stacked",b=parseInt(e.find("#collection-columns").val(),10)||2,f=se(),$=e.find("#collection-bundled").is(":checked"),v=e.find("#collection-project").val()||"",N={...X||{},project:v||null},L=e.find("#save-collection-btn");L.prop("disabled",!0);try{if(S){const m=await w.collections.create({title:l,slug:o,description:a,layout:y,columns:b,fields:c,api:p,storage:f,meta:N,...$?{bundled:!0}:{}});h=m.slug,B=f.adapter||"file",S=!1,E.toast("Collection created.",{type:"success"}),R.navigate(`/collections/edit/${m.slug}`)}else if((f.adapter||"file")!==B){let m=0;try{m=(await w.collections.listEntries(h,{limit:1}))?.total??0}catch{}const T=B==="file"?`file \u2192 ${f.adapter}`:`${B} \u2192 ${f.adapter||"file"}`;if(m>0&&await E.confirm(`You changed the storage adapter (${T}).
4
4
 
5
- Migrate ${r} existing ${r===1?"entry":"entries"} to the new storage?`)){const I=await w.collections.migrateStorage(h,u);L=u.adapter||"file",E.toast(`Migrated ${I.migrated} of ${I.total} entries.`,{type:"success"})}else await w.collections.update(h,{title:n,description:o,layout:y,columns:g,fields:c,api:b,storage:u,meta:N,...$?{bundled:!0}:{bundled:!1}}),L=u.adapter||"file",E.toast("Collection saved.",{type:"success"})}else await w.collections.update(h,{title:n,description:o,layout:y,columns:g,fields:c,api:b,storage:u,meta:N,...$?{bundled:!0}:{bundled:!1}}),E.toast("Collection saved.",{type:"success"})}catch(r){E.toast(r.message||"Failed to save.",{type:"error"})}finally{B.prop("disabled",!1)}}),Domma.icons.scan()}};
5
+ Migrate ${m} existing ${m===1?"entry":"entries"} to the new storage?`)){const I=await w.collections.migrateStorage(h,f);B=f.adapter||"file",E.toast(`Migrated ${I.migrated} of ${I.total} entries.`,{type:"success"})}else await w.collections.update(h,{title:l,description:a,layout:y,columns:b,fields:c,api:p,storage:f,meta:N,...$?{bundled:!0}:{bundled:!1}}),B=f.adapter||"file",E.toast("Collection saved.",{type:"success"})}else await w.collections.update(h,{title:l,description:a,layout:y,columns:b,fields:c,api:p,storage:f,meta:N,...$?{bundled:!0}:{bundled:!1}}),E.toast("Collection saved.",{type:"success"})}catch(m){E.toast(m.message||"Failed to save.",{type:"error"})}finally{L.prop("disabled",!1)}}),Domma.icons.scan()}};
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as s}from"./layouts.js";import{mediaView as p}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as a}from"./tutorials.js";import{apiReferenceView as l}from"./api-reference.js";import{collectionsView as d}from"./collections.js";import{collectionEditorView as u}from"./collection-editor.js";import{collectionEntriesView as E}from"./collection-entries.js";import{formsView as g}from"./forms.js";import{formEditorView as v}from"./form-editor.js";import{formSubmissionsView as b}from"./form-submissions.js";import{viewsListView as j}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as y}from"./view-preview.js";import{actionsListView as h}from"./actions-list.js";import{actionEditorView as k}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as P}from"./blocks.js";import{blockEditorView as R}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as S}from"./components.js";import{componentEditorView as x}from"./component-editor.js";import{myProfileView as M}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{menusView as A}from"./menus.js";import{menuEditorView as B}from"./menu-editor.js";import{menuLocationsView as C}from"./menu-locations.js";import{projectsView as F}from"./projects.js";import{projectEditorView as G}from"./project-editor.js";import{projectDetailView as H}from"./project-detail.js";import{projectSettingsView as I}from"./project-settings.js";const J={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:F,projectEditor:G,projectDetail:H,projectSettings:I,dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:s,media:p,login:n,users:f,userEditor:w,plugins:c,documentation:V,tutorials:a,apiReference:l,collections:d,collectionEditor:u,collectionEntries:E,forms:g,formEditor:v,formSubmissions:b,viewsList:j,viewEditor:L,viewPreview:y,actionsList:h,actionEditor:k,proDocs:D,blocks:P,blockEditor:R,components:S,componentEditor:x,myProfile:M,roles:U,roleEditor:q,effects:z,notifications:m,menus:A,menuEditor:B,menuLocations:C,menusRedirect:J};
1
+ import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as a}from"./tutorials.js";import{apiReferenceView as l}from"./api-reference.js";import{collectionsView as d}from"./collections.js";import{collectionEditorView as u}from"./collection-editor.js";import{collectionEntriesView as E}from"./collection-entries.js";import{formsView as g}from"./forms.js";import{formEditorView as v}from"./form-editor.js";import{formSubmissionsView as b}from"./form-submissions.js";import{viewsListView as j}from"./views-list.js";import{viewEditorView as k}from"./view-editor.js";import{viewPreviewView as L}from"./view-preview.js";import{actionsListView as y}from"./actions-list.js";import{actionEditorView as h}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as P}from"./blocks.js";import{blockEditorView as R}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as S}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as x}from"./my-profile.js";import{rolesView as M}from"./roles.js";import{roleEditorView as U}from"./role-editor.js";import{effectsView as q}from"./effects.js";import{menusView as z}from"./menus.js";import{menuEditorView as A}from"./menu-editor.js";import{menuLocationsView as B}from"./menu-locations.js";import{projectsView as C}from"./projects.js";import{projectEditorView as F}from"./project-editor.js";import{projectDetailView as G}from"./project-detail.js";import{projectSettingsView as H}from"./project-settings.js";import{apiTokensView as I}from"./api-tokens.js";const J={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:C,projectEditor:F,projectDetail:G,projectSettings:H,apiTokens:I,dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:p,media:s,login:n,users:f,userEditor:w,plugins:c,documentation:V,tutorials:a,apiReference:l,collections:d,collectionEditor:u,collectionEntries:E,forms:g,formEditor:v,formSubmissions:b,viewsList:j,viewEditor:k,viewPreview:L,actionsList:y,actionEditor:h,proDocs:D,blocks:P,blockEditor:R,components:S,componentEditor:T,myProfile:x,roles:M,roleEditor:U,effects:q,notifications:m,menus:z,menuEditor:A,menuLocations:B,menusRedirect:J};
@@ -1,4 +1,4 @@
1
- import{api as y,getUser as C}from"../api.js";import{getProjectFromHash as N}from"../lib/project-context.js";const $=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];function k(a){if(!a||!a.length)return"<em>None</em>";const f=new Set(a.map(c=>c.split(".")[0])),b=a.every(c=>!c.includes(".")),l=f.size;return`${l} resource${l!==1?"s":""} ${b?"(full)":"(partial)"}`}function w(a){return String(a).replace(/"/g,"&quot;")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(a){if(!C()){R.navigate("/");return}const b=E.loader(a.get(0),{type:"dots"});let l=[];const c=N(),v=async()=>{l=(await y.get("/collections/roles/entries?limit=100")).entries||[],c&&(l=l.filter(t=>t?.data?.meta?.project===c))},h=()=>{T.create("#roles-table",{data:l,columns:[{key:"data",title:"Label",render:e=>`<span class="badge ${e.badgeClass||"badge-secondary"}">${e.label||""}</span>`},{key:"data",title:"Name",render:e=>`<code>${e.name||""}</code>`},{key:"data",title:"Level",render:e=>e.level??""},{key:"data",title:"Permissions",render:e=>k(e.permissions)},{key:"id",title:"Actions",render:(e,t)=>{const n=t.data?.level===0;return`
1
+ import{api as y,getUser as C}from"../api.js";import{getProjectFromHash as N}from"../lib/project-context.js";const $=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];function k(a){if(!a||!a.length)return"<em>None</em>";const f=new Set(a.map(c=>c.split(".")[0])),b=a.every(c=>!c.includes(".")),l=f.size;return`${l} resource${l!==1?"s":""} ${b?"(full)":"(partial)"}`}function w(a){return String(a).replace(/"/g,"&quot;")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(a){if(!C()){R.navigate("/");return}const b=E.loader(a.get(0),{type:"dots"});let l=[];const c=N(),v=async()=>{l=(await y.get("/collections/roles/entries?limit=100")).entries||[],c&&(l=l.filter(t=>(t?.data?.meta?.project||"core")===c))},h=()=>{T.create("#roles-table",{data:l,columns:[{key:"data",title:"Label",render:e=>`<span class="badge ${e.badgeClass||"badge-secondary"}">${e.label||""}</span>`},{key:"data",title:"Name",render:e=>`<code>${e.name||""}</code>`},{key:"data",title:"Level",render:e=>e.level??""},{key:"data",title:"Permissions",render:e=>k(e.permissions)},{key:"id",title:"Actions",render:(e,t)=>{const n=t.data?.level===0;return`
2
2
  <a href="#/roles/edit/${e}" class="btn btn-sm btn-primary">Edit</a>
3
3
  ${n?"":`<button class="btn btn-sm btn-danger btn-delete-role" data-id="${e}" data-name="${w(t.data?.label)}">Delete</button>`}
4
4
  `}}],emptyMessage:"No roles found."}),Domma.icons.scan()};try{await v()}finally{b.destroy()}h(),document.addEventListener("click",async function e(t){if(!document.contains(a.get(0))){document.removeEventListener("click",e);return}const n=t.target.closest(".btn-delete-role");if(!n)return;const m=n.dataset.id,u=n.dataset.name;if(await E.confirm(`Delete role <strong>${u}</strong>? This cannot be undone.`))try{await y.delete(`/collections/roles/entries/${m}`),E.toast("Role deleted.",{type:"success"}),await v(),h()}catch(p){E.toast(`Failed: ${p.message}`,{type:"error"})}}),a.find("#btn-add-role").on("click",()=>{const e=document.createElement("div");e.style.cssText="display:flex;flex-direction:column;gap:1rem;padding:1rem;";const t=(r,i)=>{const s=document.createElement("div"),o=document.createElement("label");return o.textContent=r,o.style.cssText="display:block;font-size:.85rem;margin-bottom:.25rem;font-weight:600;",s.appendChild(o),s.appendChild(i),s},n=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. moderator"}),m=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. Moderator"}),u=Object.assign(document.createElement("input"),{type:"number",className:"form-input",value:"5",min:"1"}),d=document.createElement("select");d.className="form-input",$.forEach(r=>{const i=Object.assign(document.createElement("option"),{value:r.value,textContent:r.label});d.appendChild(i)});const p=Object.assign(document.createElement("button"),{className:"btn btn-primary",textContent:"Create Role"});e.appendChild(t("Name (slug)",n)),e.appendChild(t("Display Label",m)),e.appendChild(t("Level",u)),e.appendChild(t("Badge Colour",d)),e.appendChild(p);const g=E.modal({title:"Add Role",size:"sm"});g.element.appendChild(e),g.open(),p.addEventListener("click",async()=>{const r=n.value.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),i=m.value.trim(),s=parseInt(u.value,10);if(!r||!i||isNaN(s)||s<1){E.toast("Name, label and level (\u2265 1) are required.",{type:"error"});return}try{const o=await y.post("/collections/roles/entries",{data:{name:r,label:i,level:s,permissions:[],badgeClass:d.value}});g.close(),E.toast("Role created. Set permissions in the editor.",{type:"success"}),R.navigate(`/roles/edit/${o.id}`)}catch(o){E.toast(`Error: ${o.message}`,{type:"error"})}})})}};
@@ -1,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
+ }