domma-cms 0.75.0 → 0.77.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 (150) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/api-tokens-arrange.js +3 -0
  7. package/admin/js/lib/block-editor-arrange.js +2 -0
  8. package/admin/js/lib/collection-editor-arrange.js +3 -0
  9. package/admin/js/lib/collection-entries-arrange.js +1 -0
  10. package/admin/js/lib/component-editor-arrange.js +27 -0
  11. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  12. package/admin/js/lib/dashboard-arrange.js +1 -0
  13. package/admin/js/lib/effects-arrange.js +3 -0
  14. package/admin/js/lib/form-editor-arrange.js +1 -0
  15. package/admin/js/lib/form-submissions-arrange.js +2 -0
  16. package/admin/js/lib/layouts-arrange.js +1 -0
  17. package/admin/js/lib/menu-editor-arrange.js +2 -0
  18. package/admin/js/lib/my-profile-arrange.js +1 -0
  19. package/admin/js/lib/notifications-arrange.js +1 -0
  20. package/admin/js/lib/page-editor-arrange.js +1 -0
  21. package/admin/js/lib/plugin-code-arrange.js +1 -0
  22. package/admin/js/lib/plugins-arrange.js +2 -0
  23. package/admin/js/lib/project-page-arrange.js +1 -0
  24. package/admin/js/lib/role-editor-arrange.js +3 -0
  25. package/admin/js/lib/roles-arrange.js +1 -0
  26. package/admin/js/lib/search-settings-arrange.js +1 -0
  27. package/admin/js/lib/sidebar-renderer.js +2 -2
  28. package/admin/js/lib/site-settings-arrange.js +1 -0
  29. package/admin/js/lib/user-editor-arrange.js +1 -0
  30. package/admin/js/lib/users-arrange.js +1 -0
  31. package/admin/js/lib/view-editor-arrange.js +14 -0
  32. package/admin/js/templates/action-editor.html +223 -147
  33. package/admin/js/templates/api-endpoint-editor.html +172 -117
  34. package/admin/js/templates/api-tokens.html +83 -10
  35. package/admin/js/templates/block-editor.html +194 -203
  36. package/admin/js/templates/collection-editor.html +191 -113
  37. package/admin/js/templates/collection-entries.html +99 -36
  38. package/admin/js/templates/component-editor.html +128 -126
  39. package/admin/js/templates/context-menu-editor.html +208 -148
  40. package/admin/js/templates/dashboard.html +184 -22
  41. package/admin/js/templates/effects.html +155 -72
  42. package/admin/js/templates/form-editor.html +41 -26
  43. package/admin/js/templates/form-submissions.html +78 -26
  44. package/admin/js/templates/layouts.html +126 -43
  45. package/admin/js/templates/menu-editor.html +57 -17
  46. package/admin/js/templates/menu-locations.html +54 -11
  47. package/admin/js/templates/my-profile.html +100 -12
  48. package/admin/js/templates/notifications.html +70 -10
  49. package/admin/js/templates/page-editor.html +47 -26
  50. package/admin/js/templates/plugin-code.html +91 -73
  51. package/admin/js/templates/plugins.html +100 -28
  52. package/admin/js/templates/project-detail.html +83 -145
  53. package/admin/js/templates/project-settings.html +88 -53
  54. package/admin/js/templates/role-editor.html +158 -83
  55. package/admin/js/templates/roles.html +78 -7
  56. package/admin/js/templates/search.html +193 -216
  57. package/admin/js/templates/settings.html +404 -472
  58. package/admin/js/templates/user-editor.html +150 -14
  59. package/admin/js/templates/users.html +69 -9
  60. package/admin/js/templates/view-editor.html +203 -180
  61. package/admin/js/templates/view-preview.html +45 -45
  62. package/admin/js/views/action-editor.js +7 -1
  63. package/admin/js/views/api-endpoint-editor.js +2 -3
  64. package/admin/js/views/api-tokens.js +62 -8
  65. package/admin/js/views/block-editor.js +14 -8
  66. package/admin/js/views/collection-editor.js +64 -5
  67. package/admin/js/views/collection-entries.js +50 -1
  68. package/admin/js/views/component-editor.js +2 -28
  69. package/admin/js/views/context-menu-editor.js +54 -55
  70. package/admin/js/views/dashboard.js +7 -1
  71. package/admin/js/views/effects.js +31 -1
  72. package/admin/js/views/form-editor.js +7 -7
  73. package/admin/js/views/form-submissions.js +11 -1
  74. package/admin/js/views/index.js +1 -1
  75. package/admin/js/views/layouts.js +81 -1
  76. package/admin/js/views/login.js +2 -2
  77. package/admin/js/views/menu-editor.js +21 -43
  78. package/admin/js/views/menu-locations.js +16 -1
  79. package/admin/js/views/my-profile.js +1 -1
  80. package/admin/js/views/notifications.js +18 -1
  81. package/admin/js/views/page-editor.js +26 -26
  82. package/admin/js/views/plugin-code.js +23 -4
  83. package/admin/js/views/plugins.js +97 -39
  84. package/admin/js/views/project-detail.js +9 -1
  85. package/admin/js/views/project-settings.js +1 -1
  86. package/admin/js/views/role-editor.js +11 -3
  87. package/admin/js/views/roles.js +49 -4
  88. package/admin/js/views/search.js +7 -1
  89. package/admin/js/views/settings.js +13 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +13 -4
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/admin/js/views/view-preview.js +1 -1
  94. package/package.json +1 -1
  95. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  96. package/plugins/_lib/admin/mail/contacts.js +4 -1
  97. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  98. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  99. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  100. package/plugins/_lib/admin/mail/mail.css +1 -1
  101. package/plugins/_lib/admin/mail/mail.html +1 -1
  102. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  103. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  104. package/plugins/_lib/mail/readRoutes.js +43 -1
  105. package/plugins/_lib/mail/unreadBadge.js +42 -0
  106. package/plugins/free-tier.lock.json +5 -5
  107. package/plugins/mail-reader/CLAUDE.md +1 -0
  108. package/plugins/mail-reader/plugin.js +3 -1
  109. package/plugins/mail-reader/plugin.json +5 -4
  110. package/server/routes/api/api-tokens.js +56 -0
  111. package/server/routes/api/auth.js +7 -1
  112. package/server/routes/api/collections.js +13 -1
  113. package/server/routes/api/dashboard.js +32 -4
  114. package/server/routes/api/effects.js +68 -13
  115. package/server/routes/api/forms.js +104 -13
  116. package/server/routes/api/layouts.js +93 -2
  117. package/server/routes/api/notifications.js +140 -14
  118. package/server/routes/api/plugins.js +135 -4
  119. package/server/routes/api/roles.js +111 -0
  120. package/server/routes/api/search.js +38 -4
  121. package/server/routes/api/settings.js +52 -0
  122. package/server/routes/api/users.js +65 -3
  123. package/server/routes/api/views.js +55 -1
  124. package/server/server.js +12 -0
  125. package/server/services/components.js +9 -0
  126. package/server/services/plugins.js +7 -0
  127. package/server/services/roles.js +3 -0
  128. package/server/services/search.js +58 -0
  129. package/server/services/users.js +16 -2
  130. package/server/services/views.js +14 -0
  131. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  132. package/admin/js/templates/dashboard/cache.html +0 -32
  133. package/admin/js/templates/dashboard/health-detail.html +0 -2
  134. package/admin/js/templates/dashboard/journeys.html +0 -17
  135. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  136. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  137. package/admin/js/templates/dashboard/top-pages.html +0 -3
  138. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  139. package/admin/js/templates/plugin-marketplace.html +0 -29
  140. package/admin/js/views/block-editor-enhance.js +0 -1
  141. package/admin/js/views/dashboard/lib/escape.js +0 -1
  142. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  143. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  144. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  145. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  146. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  147. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  148. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  149. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
  150. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -1 +1,7 @@
1
- import{api as _}from"../api.js";let N=null,L={};async function J(e){const t=e.find("#action-project").get(0);if(t)try{(await _.projects.list()).forEach(l=>{const n=document.createElement("option");n.value=l.slug,n.textContent=l.name||l.slug,t.appendChild(n)})}catch{}}const z={deleteEntry:[],moveToCollection:[{name:"targetCollection",label:"Target Collection Slug",placeholder:"e.g. archived-applications"}],webhook:[{name:"url",label:"URL",placeholder:"https://hooks.example.com/notify"},{name:"method",label:"Method",placeholder:"POST"},{name:"body",label:"Body (JSON)",placeholder:'{"email": "{{entry.data.email}}"}',multiline:!0}],email:[{name:"to",label:"To",placeholder:"{{entry.data.email}}"},{name:"subject",label:"Subject",placeholder:"Your application update"},{name:"template",label:"Body",placeholder:"Your application has been approved.",multiline:!0}]};export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(e){N=null,L={};const t=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);t&&(N=t[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await P(e),await U(e),await J(e),N&&(e.find("#action-editor-title").text("Edit Action"),await B(e,N)),e.find("#add-step-btn").off("click").on("click",()=>{const a=e.find("#add-step-type").val()||"updateField";M(e,{type:a,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await Q(e)}),K(e),Domma.icons.scan()}};async function P(e){const t=e.find("#action-collection").get(0);if(t)try{(await _.collections.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=`${a.title} (${a.slug})`,t.appendChild(l)})}catch{}}async function U(e){const t=e.find("#action-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(a=>{const l=document.createElement("label");l.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const n=document.createElement("input");n.type="checkbox",n.value=a,n.dataset.role=a,n.className="action-role-cb",n.checked=a==="admin",l.appendChild(n),l.appendChild(document.createTextNode(a)),t.appendChild(l)})}async function B(e,t){try{const a=await _.actions.get(t);if(!a){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}Y(e,a)}catch(a){E.toast(a.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function K(e){const t=e.find("#action-rowlevel-enabled").get(0),a=e.find("#action-rowlevel-config").get(0),l=e.find("#action-rowlevel-mode").get(0),n=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{a&&(a.style.display=t.checked?"flex":"none")}),l&&l.addEventListener("change",()=>{n&&(n.style.display=l.value==="field"?"":"none")}))}function Y(e,t){e.find("#action-title").val(t.title||""),e.find("#action-slug").val(t.slug||""),e.find("#action-description").val(t.description||""),e.find("#action-collection").val(t.collection||""),L=t.meta||{},e.find("#action-project").val(t.meta?.project||""),e.find("#action-bundled").prop("checked",!!t.bundled),e.find("#action-trigger-type").val(t.trigger?.type||"manual"),e.find("#action-trigger-label").val(t.trigger?.label||"Run"),e.find("#action-trigger-icon").val(t.trigger?.icon||"zap"),e.find("#action-trigger-confirm").val(t.trigger?.confirmMessage||"");const a=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=a.includes(this.value)});const l=t.access?.rowLevel;l&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(l.mode||"owner"),e.find("#action-rowlevel-userkey").val(l.userKey||"id"),l.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(l.field||"")));const n=e.find("#action-steps-list").get(0);if(n){const s=n.querySelector(".steps-empty-placeholder");for(s&&s.remove();n.firstChild;)n.removeChild(n.firstChild);const i=document.createElement("p");i.className="text-muted steps-empty-placeholder",i.textContent="No steps yet. Add a step to define what this action does.",i.style.cssText="text-align:center;padding:2rem 0;",n.appendChild(i)}(t.steps||[]).forEach(s=>M(e,s))}function M(e,t){const a=e.find("#action-steps-list").get(0);if(!a)return;const l=a.querySelector(".steps-empty-placeholder");l&&l.remove();const n=z[t.type]||[],s=document.createElement("div");s.className="card mb-2 step-card",s.dataset.stepType=t.type;const i=document.createElement("div");i.className="card-header",i.style.cssText="display:flex;align-items:center;gap:.5rem;";const p=document.createElement("code");p.textContent=t.type,p.style.cssText="flex:1;font-size:.85rem;";const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost";const f=document.createElement("span");f.setAttribute("data-icon","trash"),r.appendChild(f),r.addEventListener("click",()=>{if(s.remove(),!a.querySelector(".step-card")){const o=document.createElement("p");o.className="text-muted steps-empty-placeholder",o.textContent="No steps yet. Add a step to define what this action does.",o.style.cssText="text-align:center;padding:2rem 0;",a.appendChild(o)}}),i.appendChild(p),i.appendChild(r);const h=document.createElement("div");if(h.className="card-body",n.length===0){const o=document.createElement("p");o.className="text-muted",o.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",o.style.margin="0",h.appendChild(o)}t.type==="updateField"?G(h,t,e):n.forEach(o=>{const g=document.createElement("div");g.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=o.label,g.appendChild(b);let m;o.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[o.name]=="object"?JSON.stringify(t.config[o.name],null,2):t.config?.[o.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[o.name]??""),m.className=`form-input step-field-${o.name}`,m.placeholder=o.placeholder||"",m.dataset.field=o.name,g.appendChild(m),h.appendChild(g)}),s.appendChild(i),s.appendChild(h),a.appendChild(s),Domma.icons.scan(s)}async function G(e,t,a){const l=a.find("#action-collection").val(),n=document.createElement("div");n.style.cssText="margin-bottom:.75rem;";const s=document.createElement("label");s.className="form-label",s.textContent="Field",n.appendChild(s);const i=document.createElement("select");i.className="form-input step-field-field",i.dataset.field="field";const p=document.createElement("input");p.type="text",p.className="form-input mt-2 step-field-field-custom",p.placeholder="Field name, e.g. status",p.style.display="none";const r=document.createElement("div");r.style.cssText="margin-bottom:.75rem;";const f=document.createElement("label");f.className="form-label",f.textContent="New Value",r.appendChild(f);const h=document.createElement("div");r.appendChild(h);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm mt-2",o.style.cssText="font-size:.75rem;padding:.2rem .5rem;",o.textContent="Template Variables";const g=document.createElement("div");g.style.cssText="display:none;background:var(--dm-surface-raised,#1e1e2e);border-radius:.4rem;padding:.75rem;margin-top:.5rem;font-size:.8rem;line-height:1.7;";const b=document.createElement("strong");b.textContent="Available template variables:",g.appendChild(b);const m=document.createElement("table");m.style.cssText="width:100%;border-collapse:collapse;margin-top:.4rem;",[["{{now}}","Current ISO timestamp"],["{{user.id}}","Executing user's ID"],["{{user.name}}","Executing user's name"],["{{user.email}}","Executing user's email"],["{{entry.id}}","Entry's unique ID"],["{{entry.data.fieldName}}","Any field value from the entry"],["{{env.CMS_PUBLIC_*}}","Public environment variables"]].forEach(([d,u])=>{const x=document.createElement("tr"),w=document.createElement("td");w.style.cssText="padding:.2rem .5rem .2rem 0;opacity:.7;white-space:nowrap;";const T=document.createElement("code");T.textContent=d,w.appendChild(T);const c=document.createElement("td");c.textContent=u,x.appendChild(w),x.appendChild(c),m.appendChild(x)}),g.appendChild(m),o.addEventListener("click",()=>{const d=g.style.display!=="none";g.style.display=d?"none":"",o.textContent=d?"Template Variables":"Hide Variables"}),r.appendChild(o),r.appendChild(g),e.appendChild(n),e.appendChild(r);let S=[];if(l)try{S=(await _.collections.get(l)).fields||[]}catch{}const F=document.createElement("option");F.value="",F.textContent=S.length?"- select a field -":"- no fields available -",i.appendChild(F),S.forEach(d=>{const u=document.createElement("option");u.value=d.name,u.textContent=`${d.label} (${d.name})`,u.dataset.fieldType=d.type,u.dataset.fieldOptions=d.type==="select"?JSON.stringify(d.options||[]):"",i.appendChild(u)});const j=document.createElement("option");j.value="__custom__",j.textContent="- enter manually -",i.appendChild(j);const C=t.config?.field||"",V=C&&[...i.options].find(d=>d.value===C);V?i.value=C:C&&(i.value="__custom__",p.value=C,p.style.display=""),n.appendChild(i),n.appendChild(p);function I(d,u){h.textContent="";const x=d?[...i.options].find(c=>c.value===d):null,w=x?.dataset.fieldType==="select",T=w?JSON.parse(x.dataset.fieldOptions||"[]"):[];if(w&&T.length){const c=document.createElement("select");c.className="form-input step-field-value",c.dataset.field="value";const O=document.createElement("option");O.value="",O.textContent="- select a value -",c.appendChild(O),T.forEach(y=>{const k=typeof y=="string"?y:y.value??"",D=typeof y=="string"?y:y.label||y.value||k;if(!k||k==="undefined")return;const q=document.createElement("option");q.value=k,q.textContent=D,c.appendChild(q)});const A=document.createElement("option");A.value="__custom__",A.textContent="- enter manually -",c.appendChild(A);const v=document.createElement("input");v.type="text",v.className="form-input mt-2",v.placeholder="e.g. approved or {{now}}",v.style.display="none",u&&[...c.options].find(y=>y.value===u&&y.value!=="__custom__")?c.value=u:u&&(c.value="__custom__",v.value=u,v.style.display=""),c.addEventListener("change",()=>{const y=c.value==="__custom__";v.style.display=y?"":"none",y||(v.value="")}),h.appendChild(c),h.appendChild(v)}else{const c=document.createElement("input");c.type="text",c.className="form-input step-field-value",c.dataset.field="value",c.placeholder="e.g. approved or {{now}}",c.value=u||"",h.appendChild(c)}}I(V?C:null,t.config?.value||""),i.addEventListener("change",()=>{const d=i.value==="__custom__";p.style.display=d?"":"none",d||(p.value=""),I(d?null:i.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const a=this.dataset.stepType,l={};if(a==="updateField"){let n=this.querySelector(".step-field-field")?.value?.trim()||"";n==="__custom__"&&(n=this.querySelector(".step-field-field-custom")?.value?.trim()||""),l.field=n;const s=this.querySelector(".step-field-value");let i=s?.value?.trim()||"";i==="__custom__"&&(i=s?.nextElementSibling?.value?.trim()||""),l.value=i}else(z[a]||[]).forEach(n=>{const s=this.querySelector(`.step-field-${n.name}`);if(!s)return;const i=s.value.trim();if(n.multiline&&i)try{l[n.name]=JSON.parse(i)}catch{l[n.name]=i}else l[n.name]=i});t.push({type:a,config:l})}),t}async function Q(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const a=e.find("#action-collection").val();if(!a){E.toast("Target collection is required (General tab).",{type:"warning"});return}const l=[];e.find(".action-role-cb:checked").each(function(){l.push(this.value)});const n=e.find("#action-rowlevel-enabled").is(":checked");let s=null;if(n){const f=e.find("#action-rowlevel-mode").val()||"owner",h=e.find("#action-rowlevel-userkey").val()||"id";if(s={mode:f,userKey:h},f==="field"){const o=e.find("#action-rowlevel-field").val().trim();if(!o){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}s.field=o}}const i=e.find("#action-project").val()||"",p={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:a,...e.find("#action-bundled").is(":checked")?{bundled:!0}:{},trigger:{type:e.find("#action-trigger-type").val()||"manual",label:e.find("#action-trigger-label").val().trim()||"Run",icon:e.find("#action-trigger-icon").val().trim()||"zap",confirmMessage:e.find("#action-trigger-confirm").val().trim()||null},steps:H(e),access:{roles:l,rowLevel:s},meta:{...L||{},project:i||null}},r=e.find("#save-action-btn").get(0);r&&(r.disabled=!0);try{if(N)await _.actions.update(N,p),E.toast("Action updated.",{type:"success"});else{const f=await _.actions.create(p);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${f.slug}`)}}catch(f){E.toast(f.message||"Failed to save action.",{type:"error"})}finally{r&&(r.disabled=!1)}}
1
+ import{apiRequest as L}from"../api.js";import{addBannerAction as ge}from"../lib/plugin-chrome.js";import{helpPopovers as Ke}from"../lib/help-popover.js";import{openMenuAt as Ve,sidebarCount as Se}from"../lib/tool-kit.js";import{isMongoMissing as We}from"../lib/actions-arrange.js";import{STEP_TYPES as Ye,checkAction as gt,draftKey as St,draftOf as Ee,duplicateStep as Et,formFromAction as X,makeStep as xt,moveStep as ze,payloadFromForm as kt,problemsText as Je,removeStep as wt,restoreDraft as Ct,rolePicker as Mt,slugify as Ge,snapshot as N,stepSummary as Qe}from"../lib/action-editor-arrange.js";const _t=/^\/actions\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,re=h=>String(h||"").split("?")[0].replace(/\/$/,""),xe="#/actions",$t=`
2
+ <label class="aex-role" data-role="{{name}}">
3
+ <input type="checkbox" class="aex-role-cb" data-bind-checked="on">
4
+ <span class="aex-step-main"><strong>{{label}}</strong><small>{{name}}</small></span>
5
+ <span class="aex-chip" data-bind-hidden="!levelText">{{levelText}}</span>
6
+ <span class="aex-chip is-warn" data-bind-hidden="!missing" title="The action names it, but System \u203A Roles has no such role">No longer exists</span>
7
+ </label>`,Lt=[[["entry.data.fieldName"],"Any field of the entry"],[["entry.id"],"The entry's id"],[["now"],"The current time (ISO)"],[["user.id","user.name","user.email"],"Who ran it"],[["env.CMS_PUBLIC_*"],"Public environment variables"]],Nt=h=>String(h??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ke(h,y){const l=h.querySelector(".dm-plugin-banner-name");if(!l)return;const T=l.querySelector(".dm-plugin-banner-sep");if(T){for(;T.nextSibling;)T.nextSibling.remove();T.after(document.createTextNode(y))}}let A=null,Xe=!1;function At(){Xe||!window.R?.use||(Xe=!0,R.use((h,y,l)=>{if(!(_t.test(y?.path||"")&&re(h?.path)!==re(y?.path))||!A?.dirty())return l();const m=`#${y.path}`;return E.confirm("This action has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(Z=>{Z?l():history.replaceState(null,"",m)})}),M.subscribe("router:afterChange",({to:h}={})=>{if(A&&re(h?.path)!==A.key){const y=A;A=null,y.dispose()}}))}export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(h){if(At(),A){const e=A;A=null,e.dispose()}const y=h.get(0),l=y.querySelector(".aex")||y,T=[],m=e=>(T.push(e),e);let Z=!1;const C=()=>!Z&&l.isConnected,ie=location.hash.replace(/^#/,""),we=ie.match(/^\/actions\/edit\/([^/?#]+)/),x=we?decodeURIComponent(we[1]):null,_=!!x,ue=new URLSearchParams(ie.split("?")[1]||"").get("project")||"",Ze=re(ie),w=St(x),F=M.observable(!0),H=M.observable(!1),ee=M.observable(!1),j=M.observable(!1),K=X(null,{project:ue}),i={};for(const[e,t]of Object.entries(K))e!=="steps"&&e!=="roles"&&(i[e]=M.observable(t));const U=M.observable([...K.roles]);let v=K.steps;const te=M.observable(0),de=()=>{te.value=te.peek()+1},Ce=M.observable("updateField");let ne={},p=null;const Me=()=>p?X(p):K,_e=()=>{te.value;const e={steps:v,roles:U.value};for(const t of Object.keys(i))e[t]=i[t].value;return e},O=()=>{const e={steps:v,roles:U.peek()};for(const t of Object.keys(i))e[t]=i[t].peek();return e},V=M.observable(N(K)),oe=M.computed(()=>!F.value&&!H.value&&N(_e())!==V.value),ae=M.computed(()=>gt(_e())),et=M.computed(()=>{const e=ae.value;if(j.value)return e;const t={};return i.title.value&&e.title&&(t.title=e.title),e.slug&&(t.slug=e.slug),e.roles&&(t.roles=e.roles),t}),tt=M.computed(()=>j.value?Je(ae.value):""),nt=M.computed(()=>(te.value,v.length)),b=l.querySelector("#action-steps-list"),pe=M.computed(()=>ae.value.steps||{});let me=null,$e=!1;const P=l.querySelector("#action-collection"),z=l.querySelector("#action-project"),se=l.querySelector("#action-transition-field"),ot=l.querySelector("#action-collection-fields"),at=l.querySelector("#action-transition-values"),f=(e,t)=>{const n=document.createElement("option");return n.value=e,n.textContent=t,n},Le=M.observable([]),Ne=M.observable([]);m(M.effect(()=>{Ne.value=Mt(Le.value,U.value)}));const fe=new Map,J=M.observable([]);let Ae=0;async function st(e){const t=++Ae;if(!e){J.value=[];return}fe.has(e)||fe.set(e,L(`/collections/${encodeURIComponent(e)}`).then(o=>o?.fields||[]).catch(()=>[]));const n=await fe.get(e);t===Ae&&C()&&(J.value=n)}m(M.effect(()=>{st(i.collection.value)})),m(M.effect(()=>{const e=J.value,t=i.transitionField.peek();se.replaceChildren(f("",e.length?"- choose a field -":"- no fields -"));for(const n of e)se.appendChild(f(n.name,`${n.label||n.name} (${n.name})`));t&&!e.some(n=>n.name===t)&&se.appendChild(f(t,`${t} (not in the collection)`)),se.value=t,ot.replaceChildren(...e.map(n=>f(n.name,n.label||n.name))),$e&&he()}));const ve=M.computed(()=>{const e=J.value.find(t=>t.name===i.transitionField.value);return e?.type==="select"?(e.options||[]).map(t=>typeof t=="string"?t:t?.value).filter(Boolean):[]});m(M.effect(()=>{at.replaceChildren(...ve.value.map(e=>f(e,e)))}));function lt(e,t,n){const o=document.createElement("div"),r=document.createElement("label");r.className="form-label",r.textContent=t.label,o.appendChild(r);const a=document.createElement(t.multiline?"textarea":"input");return t.multiline?a.rows=3:a.type="text",a.className=`form-input step-field-${t.name}`,a.placeholder=t.placeholder||"",a.value=e.config[t.name]??"",a.addEventListener("input",()=>{e.config[t.name]=a.value,n()}),o.appendChild(a),o}function ct(e,t,n){const o=J.peek(),r=document.createElement("div"),a=document.createElement("label");a.className="form-label",a.textContent="Field",r.appendChild(a);const s=document.createElement("select");s.className="form-input step-field-field",s.appendChild(f("",o.length?"- choose a field -":"- no fields - enter one below -"));for(const g of o)s.appendChild(f(g.name,`${g.label||g.name} (${g.name})`));s.appendChild(f("__custom__","- enter a field name -"));const d=document.createElement("input");d.type="text",d.className="form-input step-field-field-custom",d.placeholder="Field name, e.g. status",o.some(g=>g.name===e.config.field)?s.value=e.config.field:e.config.field&&(s.value="__custom__",d.value=e.config.field),d.hidden=s.value!=="__custom__",r.append(s,d);const q=document.createElement("div"),W=document.createElement("label");W.className="form-label",W.textContent="New value",q.appendChild(W);const k=document.createElement("div");q.appendChild(k);const $=()=>{k.replaceChildren();const g=o.find(c=>c.name===e.config.field),Y=g?.type==="select"?(g.options||[]).map(c=>typeof c=="string"?{value:c,label:c}:{value:c?.value,label:c?.label||c?.value}).filter(c=>c.value):[],u=document.createElement("input");if(u.type="text",u.className="form-input step-field-value",u.placeholder="e.g. approved or {{now}}",Y.length){const c=document.createElement("select");c.className="form-input",c.appendChild(f("","- choose a value -"));for(const ce of Y)c.appendChild(f(ce.value,ce.label));c.appendChild(f("__custom__","- enter a value -"));const He=Y.some(ce=>ce.value===e.config.value);c.value=He?e.config.value:e.config.value?"__custom__":"",u.value=He?"":e.config.value,u.hidden=c.value!=="__custom__",c.addEventListener("change",()=>{u.hidden=c.value!=="__custom__",e.config.value=c.value==="__custom__"?u.value:c.value,n()}),u.addEventListener("input",()=>{e.config.value=u.value,n()}),k.append(c,u)}else u.value=e.config.value,u.addEventListener("input",()=>{e.config.value=u.value,n()}),k.append(u)};s.addEventListener("change",()=>{d.hidden=s.value!=="__custom__",e.config.field=s.value==="__custom__"?d.value.trim():s.value,e.config.value="",$(),n()}),d.addEventListener("input",()=>{e.config.field=d.value.trim(),n()}),$(),t.append(r,q)}function he(){if(!b)return;const e=pe.peek();b.replaceChildren(),v.forEach((t,n)=>{const o=Ye[t.type],r=document.createElement("div");r.className=`aex-step${j.peek()&&e[n]?" is-bad":""}`,r.dataset.index=String(n);const a=document.createElement("div");a.className="aex-step-head",a.tabIndex=0;const s=document.createElement("span");s.className="aex-step-n",s.textContent=String(n+1);const d=document.createElement("span");d.className="aex-step-main";const be=document.createElement("strong");be.textContent=o?.label||t.type;const q=document.createElement("small");q.className="aex-step-summary",q.textContent=Qe(t),d.append(be,q);const W=document.createElement("span");W.setAttribute("data-help",o?.help||"A step type this editor does not know - it is kept exactly as stored.");const k=document.createElement("button");k.type="button",k.className="aex-kebab",k.textContent="\u22EE",k.title="Actions",k.setAttribute("aria-label",`Actions for step ${n+1}`),a.append(s,d,W,k);const $=document.createElement("div");$.className="aex-step-body";const g=document.createElement("small");g.className="aex-err";const Y=()=>{q.textContent=Qe(t),de()};if(t.type==="updateField")ct(t,$,Y);else if(o&&o.fields.length)o.fields.forEach(u=>$.appendChild(lt(t,u,Y)));else{const u=document.createElement("p");u.className="aex-note",u.textContent=t.type==="deleteEntry"?"Deletes the entry. Nothing to set.":"Nothing to set.",$.appendChild(u)}$.appendChild(g),r.append(a,$),b.appendChild(r)}),I.scan(b),me?.(),me=Ke(b,{position:"bottom"}),Re()}m(()=>me?.());function Re(){const e=pe.peek();for(const t of b?.querySelectorAll(".aex-step")||[]){const n=Number(t.dataset.index),o=j.peek()&&e[n]||"";t.classList.toggle("is-bad",!!o);const r=t.querySelector(":scope > .aex-step-body > .aex-err");r&&(r.textContent=o,r.hidden=!o)}}m(M.effect(()=>{pe.value,j.value,Re()}));const G=e=>{v=e,he(),de()};function rt(e){const t=v[e];return t?[{header:`Step ${e+1} - ${Ye[t.type]?.label||t.type}`},{label:"Move up",icon:"arrow-up",disabled:e===0,action:()=>{G(ze(v,e,-1)),Q(e-1)}},{label:"Move down",icon:"arrow-down",disabled:e===v.length-1,action:()=>{G(ze(v,e,1)),Q(e+1)}},{label:"Duplicate",icon:"copy",action:()=>{G(Et(v,e)),Q(e+1)}},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{G(wt(v,e)),Q(Math.min(e,v.length-1))}}]:[]}const Q=e=>requestAnimationFrame(()=>b?.querySelector(`.aex-step[data-index="${e}"] .aex-step-head`)?.focus()),it=E.contextMenu(b,{cursor:"glyph",match:".aex-step",inherit:!1,density:"compact",items:e=>rt(Number(e.dataset.index))});m(()=>it?.destroy?.());const Te=e=>{const t=e.target.closest(".aex-kebab");t&&Ve(t)},je=e=>{const t=e.target.closest(".aex-step-head");!t||e.target!==t||(e.key==="F10"&&e.shiftKey||e.key==="ContextMenu"?(e.preventDefault(),Ve(t)):e.key==="Enter"&&(e.preventDefault(),t.parentElement.querySelector(".aex-step-body input, .aex-step-body select, .aex-step-body textarea")?.focus()))};b.addEventListener("click",Te),b.addEventListener("keydown",je),m(()=>{b.removeEventListener("click",Te),b.removeEventListener("keydown",je)});function le(e){$e=!0;for(const[t,n]of Object.entries(e))t!=="steps"&&(t==="roles"?U.value=[...n]:i[t]&&(i[t].value=n));v=e.steps.map(t=>({type:t.type,config:{...t.config}})),he(),de()}async function ye(){if(ee.peek()||F.peek()||H.peek())return;j.value=!0;const e=ae.peek();if(Object.keys(e).length){E.toast(Je(e),{type:"warning",duration:6e3});return}ee.value=!0;const t=kt(O(),{meta:ne,editing:_});try{if(_){const n=await L(`/actions/${encodeURIComponent(x)}`,{method:"PUT",body:JSON.stringify(t)});if(!C())return;p=n||{...p,...t},ne=p.meta||ne,V.value=N(O()),j.value=!1;try{S.remove?S.remove(w):S.set(w,null)}catch{}ke(y,p.title||x),Se(xe),E.toast("Action saved.",{type:"success"})}else{const n=await L("/actions",{method:"POST",body:JSON.stringify(t)});V.value=N(O());try{S.remove?S.remove(w):S.set(w,null)}catch{}Se(xe),E.toast(`Action "${n.title||t.title}" created.`,{type:"success"}),C()&&(location.hash=`#/actions/edit/${encodeURIComponent(n.slug)}`)}}catch(n){if(We(n)){H.value=!0;return}E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{ee.value=!1}}async function ut(){if(!_)return;const e=p?.title||x;if(await E.confirm(`Delete the action "${Nt(e)}"? Its button disappears from every entry. This cannot be undone.`))try{await L(`/actions/${encodeURIComponent(x)}`,{method:"DELETE"}),V.value=N(O());try{S.remove?S.remove(w):S.set(w,null)}catch{}Se(xe),E.toast(`Deleted "${e}".`,{type:"success"}),location.hash="#/actions"}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}function dt(){E.confirm("Discard the changes you have not saved?").then(e=>{!e||!C()||(le(X(p,{project:ue})),j.value=!1)})}l.querySelector(".aex-roles").innerHTML=$t;const pt={...i,roles:U,loading:F,needsMongo:H,saving:ee,dirty:oe,err:et,problems:tt,stepCount:nt,addType:Ce,roleRows:Ne,editing:_,dirtyText:M.computed(()=>"Unsaved changes - Save (Ctrl+S) to keep them. Leave and they are kept as a draft for next time."),slugHint:M.computed(()=>_?"":Ge(i.title.value)?`Made from the title: ${Ge(i.title.value)}`:"Made from the title"),buttonText:M.computed(()=>String(i.triggerLabel.value||"").trim()||"Run"),stateValuesText:M.computed(()=>ve.value.length?`This field's values: ${ve.value.join(", ")}`:"")},mt=E.tabs(l.querySelector(".aex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),ft=M.applyBindings(pt,l,{methods:{save:()=>ye(),discard:()=>dt(),addStep:()=>{G([...v,xt(Ce.peek())]),Q(v.length-1)}}});m(()=>ft.dispose()),m(Ke(l,{position:"bottom"}));const vt=l.querySelector(".aex-vars");for(const[e,t]of Lt){const n=document.createElement("tr"),o=document.createElement("td");e.forEach((a,s)=>{s&&o.append(", ");const d=document.createElement("code");d.textContent=`{{${a}}}`,o.appendChild(d)});const r=document.createElement("td");r.textContent=t,n.append(o,r),vt.appendChild(n)}const qe=l.querySelector(".aex-roles"),Ie=e=>{if(!e.target.matches(".aex-role-cb"))return;const t=e.target.closest("[data-role]")?.dataset.role;if(!t)return;const n=U.peek();U.value=e.target.checked?[...new Set([...n,t])]:n.filter(o=>o!==t)};qe.addEventListener("change",Ie),m(()=>qe.removeEventListener("change",Ie));const De=l.querySelector(".aex-preview-icon");m(M.effect(()=>{const e=String(i.triggerIcon.value||"").trim()||"zap",t=document.createElement("span");t.setAttribute("data-icon",e),t.setAttribute("data-icon-size","14"),De.replaceChildren(t),I.scan(De)})),ge(h,{id:"action-editor-back",icon:"arrow-left",title:"All actions",onClick:()=>{location.hash="#/actions"}});const ht=()=>{_&&ge(h,{id:"action-editor-delete",icon:"trash",title:"Delete this action",onClick:()=>ut()}),ge(h,{id:"action-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>ye()})};ke(y,_?x:"New action");const Fe=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),ye())};document.addEventListener("keydown",Fe,!0),m(()=>document.removeEventListener("keydown",Fe,!0));const Ue=()=>{if(!oe.peek())return;const e=Me();try{S.set(w,{form:Ee(O(),e),base:N(Ee(e,e)),at:new Date().toISOString()})}catch{}},Oe=e=>{oe.peek()&&(Ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Oe),m(()=>window.removeEventListener("beforeunload",Oe)),A={key:Ze,dirty:()=>oe.peek(),dispose(){Z=!0,Ue(),T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(l);const yt=await L("/actions/_count").catch(()=>null);if(!C())return;if(yt?.available===!1){H.value=!0,F.value=!1,I.scan(l);return}ht();const[Pe,Be,bt]=await Promise.all([L("/collections").catch(()=>[]),L("/projects").catch(()=>[]),L("/collections/roles/entries?limit=1000").catch(()=>({entries:[]}))]);if(!C())return;for(const e of Array.isArray(Pe)?Pe:[])P.appendChild(f(e.slug,`${e.title||e.slug} (${e.slug})`));for(const e of Array.isArray(Be)?Be:[])z.appendChild(f(e.slug,e.name||e.slug));if(Le.value=(bt?.entries||[]).map(e=>e.data||{}).filter(e=>e.name),P.value=i.collection.peek(),z.value=i.project.peek(),_){try{p=await L(`/actions/${encodeURIComponent(x)}`)}catch(t){if(!C())return;if(We(t)){H.value=!0,F.value=!1;return}E.toast(t.status===404||/not found/i.test(t.message||"")?`There is no action "${x}".`:`Could not load the action: ${t.message||t}`,{type:"error"}),F.value=!1,location.hash="#/actions";return}if(!C())return;if(!p){E.toast(`There is no action "${x}".`,{type:"error"}),location.hash="#/actions";return}ne=p.meta||{},p.collection&&![...P.options].some(t=>t.value===p.collection)&&P.appendChild(f(p.collection,`${p.collection} (not found)`));const e=X(p);e.project&&![...z.options].some(t=>t.value===e.project)&&z.appendChild(f(e.project,e.project)),le(e),P.value=i.collection.peek(),z.value=i.project.peek(),V.value=N(O()),ke(y,p.title||x)}else le(K),V.value=N(O());F.value=!1,I.scan(l);let B=null;try{B=S.get(w)}catch{}if(B?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=Me(),t=B.base&&B.base!==N(Ee(e,e)),n=B.at?` (${D(B.at).format("D MMM HH:mm")})`:"",o=_?"this action":"a new action",r=t?`You left unsaved changes to ${o}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${o}${n}. Restore them?`;if(await E.confirm(r)&&C()){const{form:a,lost:s}=Ct(B.form,X(p,{project:ue}));s&&E.toast(`${s===1?"A webhook URL":`${s} webhook URLs`} typed before you left ${s===1?"was":"were"} not kept in the draft - enter ${s===1?"it":"them"} again.`,{type:"warning",duration:8e3}),a.collection&&![...P.options].some(d=>d.value===a.collection)&&P.appendChild(f(a.collection,a.collection)),le(a)}}}};
@@ -1,3 +1,2 @@
1
- import{api as _}from"../api.js";const pe=[["_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"]],we=["id","createdBy"],Se=/\{\{\s*(params|query)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;function qe(x){const v=new Set;for(const n of Object.values(x?.filter||{}))if(typeof n=="string")for(const N of n.matchAll(Se))N[1]==="query"&&v.add(N[2]);return[...v]}function Ae(x){return String(x??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let O=null;function ke(x){for(const[v]of pe)if(x.endsWith(v))return{field:x.slice(0,-v.length),op:v};return{field:x,op:"_eq"}}export const apiEndpointEditorView={templateUrl:"/admin/js/templates/api-endpoint-editor.html",async onMount(x){const v=x.get(0),n=e=>v.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 G=E.loader(v,{type:"dots"}),[ue,me,fe,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){G.destroy(),E.toast("Endpoint not found.",{type:"error"}),location.hash="#/api-endpoints";return}const ye=me.filter(e=>!e.systemManaged&&!e.preset),he=(fe.entries||[]).map(e=>e.data?.name).filter(Boolean),k=n("endpoint-project");for(const e of ue){const t=document.createElement("option");t.value=e.slug,t.textContent=`${e.name} (${e.slug})`,k.appendChild(t)}const y=n("ep-collection");function ee(e){return e.meta&&e.meta.project||"core"}function z(e){const t=e!==void 0?e:y.value,r=k.value,o=ye.filter(a=>{const s=ee(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})${ee(a)==="core"&&r!=="core"?" - 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 - no credentials"],["token","Token - project API token"]]){const r=document.createElement("option");r.value=e,r.textContent=t,L.appendChild(r)}for(const e of he){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{}te();for(const t of K.querySelectorAll(".ep-filter-row"))U(t.querySelector(".ep-f-field"));ne()}function F(){return[...T,...we]}function te(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 ne(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 pe){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 ve(){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:ve(),sort:n("ep-sort").value||null,order:n("ep-order").value,limit:parseInt(n("ep-limit").value,10)||0,fields:[...v.querySelectorAll(".ep-field-cb:checked")].map(e=>e.value),enabled:n("ep-enabled").checked}}let B=null,g=null;function D(){return!B||JSON.stringify(I())!==B}function h(){oe(),Z()}function oe(){n("ep-url-preview").textContent=`/api/x/${k.value||"<project>"}${n("ep-path").value||"/\u2026"}`}function ge(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=g?.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 ge(g?.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 qe(g)){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 - filters by ${s}`,c.appendChild(d),c.appendChild(i),o.appendChild(c)}}let J="";const W=`apitryit:hist:${q||"new"}`,Ee=12;function ae(){const e=S.get(W);return Array.isArray(e)?e:[]}function re(e){const t=ae().filter(o=>o.url!==e.url),r=[e,...t].slice(0,Ee);S.set(W,r),Y()}function Ce(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=ae();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(--dm-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/${g?.project||""}`,"")||o.url,d.title=o.url,a.appendChild(s),a.appendChild(d),a.addEventListener("click",()=>Ce(o)),e.appendChild(a)}}const m={key:"var(--dm-info)",index:"var(--dm-text-muted)",string:"var(--dm-success)",number:"var(--dm-warning-dark)",boolean:"var(--dm-secondary)",nul:"var(--dm-danger)",punc:"var(--dm-text-muted)",preview:"var(--dm-text-muted)"};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 se(e,t){return t?f(m.index,String(e)):f(m.key,JSON.stringify(String(e)))}function xe(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 ie(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(se(t,r)),p.appendChild(f(m.punc,": "))),p.appendChild(o?f(m.punc,Array.isArray(e)?"[]":"{}"):xe(e)),p}const s=Array.isArray(e),c=s?e.map((p,b)=>[b,p,!0]):Object.entries(e).map(([p,b])=>[p,b,!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(se(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 var(--dm-border-light);";for(const[p,b,X]of c)A.appendChild(ie(b,p,X));d.appendChild(A);const $=document.createElement("div");$.style.paddingLeft="1.2em",$.appendChild(f(m.punc,s?"]":"}")),d.appendChild($);let l=!1;const C=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 C(!1),i.addEventListener("click",()=>C(!l)),d}let V="";function le(e){n("tryit-json").style.display=e?"":"none",n("tryit-body").style.display=e?"none":"",n("tryit-json-toggle").textContent=e?"Raw":"Tree"}function ce(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(ie(r)),n("tryit-json-toggle").style.display=o?"":"none",le(o)}async function Q(){if(!g)return;const e={},t={};let r=g.path;for(const l of n("tryit-params").querySelectorAll("input")){e[l.dataset.param]=l.value;const C=new RegExp(":"+l.dataset.param.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?=/|$)","g");r=r.replace(C,encodeURIComponent(l.value))}const o=[];for(const l of n("tryit-query-params").querySelectorAll("input")){const C=l.value.trim();t[l.dataset.queryParam]=l.value,C&&o.push(`${encodeURIComponent(l.dataset.queryParam)}=${encodeURIComponent(C)}`)}const a=n("tryit-query").value.trim();a&&o.push(a);const s=o.join("&"),c=`/api/x/${g.project}${r}${s?"?"+s:""}`,d={ts:Date.now(),url:c,params:e,queryParams:t,extra:a},i={},j=n("tryit-token").value.trim();g.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,C]of Object.entries(i))A.push(` -H ${P(`${l}: ${C}`)}`);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}),C=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 b=[];l.headers.forEach((X,be)=>b.push(`${be}: ${X}`)),n("tryit-headers-group").style.display="",n("tryit-headers").textContent=b.length?b.join(`
3
- `):"(none exposed)",ce(C),re({...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",ce(String(l.message||l)),re({...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 - 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(),te(u.fields||[]),ne(u.sort||"");for(const[e,t]of Object.entries(u.filter||{})){const{field:r,op:o}=ke(e);M(r,o,String(t))}g=u,B=JSON.stringify(I())}else await R(),M();oe(),Z(),G.destroy();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",()=>{le(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 de(){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)g=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",de);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(),de()):r==="enter"&&(e.preventDefault(),g&&!D()?Q():D()&&E.toast("Save first - Send tests the saved definition.",{type:"info"}))}O&&document.removeEventListener("keydown",O),O=H,document.addEventListener("keydown",H),Y(),Domma.icons.scan(v)}};
1
+ import{api as _}from"../api.js";import{addBannerAction as Ue}from"../lib/plugin-chrome.js";import{helpPopovers as Et}from"../lib/help-popover.js";import{openMenuAt as Ye,sidebarCount as Ve}from"../lib/tool-kit.js";import{OPERATORS as xt,PSEUDO_FIELDS as kt,checkEndpoint as We,filterFromRows as Ct,normaliseDraft as wt,pathParams as Xe,placeholders as St,publicPath as jt,snapshot as G,splitFilterKey as qt,statusTone as ze,tryRequest as At}from"../lib/api-endpoint-editor-arrange.js";const Rt=/^\/api-endpoints\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,Q=d=>String(d||"").split("?")[0].replace(/\/$/,""),$t="api-endpoint-editor.draft.",Lt=12;function Nt(d){return String(d??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const j={get(d){try{return S.get(d)}catch{return null}},set(d,p){try{S.set(d,p)}catch{}},remove(d){try{S.remove?S.remove(d):S.set(d,null)}catch{}}};function Mt(d,p){const n=d.querySelector(".dm-plugin-banner-name");if(!n||!n.querySelector(".dm-plugin-banner-sep"))return;const T=n.lastChild;T&&T.nodeType===3?T.textContent=p:n.appendChild(document.createTextNode(p))}let L=null,Ge=!1;function Tt(){Ge||!window.R?.use||(Ge=!0,R.use((d,p,n)=>!(Rt.test(p?.path||"")&&Q(d?.path)!==Q(p?.path))||!L?.dirty()?n():E.confirm("This endpoint has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(x=>{x?n():history.replaceState(null,"","#"+p.path)})),M.subscribe("router:afterChange",({to:d}={})=>{if(L&&Q(d?.path)!==L.path){const p=L;L=null,p.dispose()}}))}export const apiEndpointEditorView={templateUrl:"/admin/js/templates/api-endpoint-editor.html",async onMount(d){if(Tt(),L){const e=L;L=null,e.dispose()}const p=d.get(0),n=e=>p.querySelector("#"+e)||document.getElementById(e),T=[],x=e=>(T.push(e),e);let F=!1;const ke=Q(location.hash.slice(1)),Z=()=>!F&&p.isConnected&&Q(location.hash.slice(1))===ke,Ce=location.hash.split("?")[0].match(/\/api-endpoints\/edit\/([^/?#]+)/),w=Ce?decodeURIComponent(Ce[1]):null,B=$t+(w||"new"),ee=`apitryit:hist:${w||"new"}`,Qe=E.tabs(n("ep-editor-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Ze=E.loader(p,{type:"dots"}),[et,tt,nt,y]=await Promise.all([_.projects.list().catch(()=>[]),_.collections.list().catch(()=>[]),_.get("/collections/roles/entries?limit=1000").catch(()=>({entries:[]})),w?_.apiEndpoints.get(w).catch(()=>null):Promise.resolve(null)]);if(Ze.destroy(),!Z())return;if(w&&!y){E.toast("Endpoint not found.",{type:"error"}),location.hash="#/api-endpoints";return}const ot=tt.filter(e=>!e.systemManaged&&!e.preset),at=(nt.entries||[]).map(e=>e.data?.name).filter(Boolean),N=n("endpoint-project");for(const e of et){const t=document.createElement("option");t.value=e.slug,t.textContent=`${e.name} (${e.slug})`,N.appendChild(t)}const k=n("ep-collection"),we=e=>e.meta&&e.meta.project||"core";function pe(e){const t=e!==void 0?e:k.value,o=N.value,a=ot.filter(r=>{const s=we(r);return s===o||s==="core"});k.textContent="";for(const r of a){const s=document.createElement("option");s.value=r.slug,s.textContent=`${r.title} (${r.slug})${we(r)==="core"&&o!=="core"?" - core":""}`,k.appendChild(s)}if(!a.length){const r=document.createElement("option");r.value="",r.textContent="No collections available in this project",k.appendChild(r)}t&&[...k.options].some(r=>r.value===t)&&(k.value=t)}pe();const K=n("ep-auth");for(const[e,t]of[["public","Public - no credentials"],["token","Token - project API token"]]){const o=document.createElement("option");o.value=e,o.textContent=t,K.appendChild(o)}for(const e of at){const t=document.createElement("option");t.value=e,t.textContent=`Role: ${e}`,K.appendChild(t)}const de=M.observable(0),f=()=>{de.value=de.peek()+1},Y=M.computed(()=>(de.value,H()));let ue=null;const P=M.observable(""),q=M.observable(y||null),V=M.observable(!1),te=M.observable(!1),h=M.computed(()=>G(Y.value)!==P.value),ne=M.computed(()=>We(Y.value)),rt=M.computed(()=>V.value?ne.value:{}),st=M.computed(()=>(V.value||Y.value.path)&&ne.value.path||""),it=M.computed(()=>{const e=Object.keys(ne.value).length;return e?`${e} thing${e===1?"":"s"} to fix before it can be saved.`:"Save to put these changes live."});let O=[];async function oe(){const e=k.value;if(O=[],e)try{O=((await _.collections.get(e))?.fields||[]).map(o=>o.name).filter(Boolean)}catch{}Se(me());for(const t of A.querySelectorAll(".epx-frow"))he(t.querySelector(".ep-f-field"));je()}const fe=()=>[...O,...kt],me=()=>[...p.querySelectorAll(".ep-field-cb:checked")].map(e=>e.value);function Se(e=[]){const t=n("ep-fields-list");t.textContent="";for(const o of O){const a=document.createElement("label"),r=document.createElement("input");r.type="checkbox",r.className="ep-field-cb",r.value=o,r.checked=e.includes(o),r.addEventListener("change",()=>{f(),ye()}),a.appendChild(r),a.appendChild(document.createTextNode(o)),t.appendChild(a)}ye()}function ye(){const e=me().length;n("ep-fields-note").textContent=O.length?e?`Returns ${e} of ${O.length} fields.`:"None ticked - every field is returned.":"No fields found on this collection."}function je(e){const t=n("ep-sort"),o=e!==void 0?e:t.value;t.textContent="";for(const a of["createdAt",...fe()]){const r=document.createElement("option");r.value=a==="createdAt"?"":a,r.textContent=a==="createdAt"?"createdAt (default)":a,t.appendChild(r)}t.value=o||""}function he(e,t){const o=t!==void 0?t:e.value;e.textContent="";for(const r of fe()){const s=document.createElement("option");s.value=r,s.textContent=r,e.appendChild(s)}const a=document.createElement("option");if(a.value="__custom__",a.textContent="custom dot-path\u2026",e.appendChild(a),o&&o!=="__custom__"&&[...e.options].some(r=>r.value===o))e.value=o;else if(o&&o!=="__custom__"){const r=document.createElement("option");r.value=o,r.textContent=o,e.insertBefore(r,a),e.value=o}}const A=n("ep-filter-rows");function ae(e="",t="_eq",o="",a=null){const r=document.createElement("div");r.className="epx-frow",r.tabIndex=0;const s=document.createElement("select");s.className="form-input ep-f-field",s.setAttribute("aria-label","Field"),he(s,e||void 0);let i=s.value;s.addEventListener("change",async()=>{if(s.value==="__custom__"){const c=await E.prompt("Dot-path field (e.g. address.city):",{inputPlaceholder:"address.city"});c&&String(c).trim()?he(s,String(c).trim()):s.value=i||fe()[0]||""}i=s.value,f()});const l=document.createElement("select");l.className="form-input ep-f-op",l.setAttribute("aria-label","Operator");for(const[c,J]of xt){const C=document.createElement("option");C.value=c,C.textContent=J,l.appendChild(C)}l.value=t,l.addEventListener("change",f);const u=document.createElement("input");u.type="text",u.className="form-input font-mono ep-f-value",u.setAttribute("aria-label","Value"),u.placeholder="a literal, {{params.date}} or {{query.team}}",u.value=o,u.addEventListener("input",f);const g=document.createElement("button");return g.type="button",g.className="epx-kebab",g.title="Actions",g.setAttribute("aria-label","Filter actions"),g.textContent="\u22EE",r.append(s,l,u,g),A.insertBefore(r,a),ve(),r}const ve=()=>{n("ep-filter-empty").hidden=!!A.querySelector(".epx-frow")};function be(){return[...A.querySelectorAll(".epx-frow")].map(e=>({field:e.querySelector(".ep-f-field").value,op:e.querySelector(".ep-f-op").value,value:e.querySelector(".ep-f-value").value}))}function H(){return{name:n("ep-name").value.trim(),project:N.value,path:n("ep-path").value.trim(),collection:k.value,auth:K.value,mode:n("ep-mode").value,filter:Ct(be()),sort:n("ep-sort").value||null,order:n("ep-order").value,limit:Number(n("ep-limit").value===""?0:n("ep-limit").value),fields:me(),enabled:n("ep-enabled").checked}}async function ge(e){if(n("ep-name").value=e.name||"",!w&&e.project&&[...N.options].some(t=>t.value===e.project)&&(N.value=e.project),n("ep-path").value=e.path||"",pe(e.collection),e.collection&&k.value!==e.collection){const t=document.createElement("option");t.value=e.collection,t.textContent=`${e.collection} (other project - no longer allowed)`,k.appendChild(t),k.value=e.collection}[...K.options].some(t=>t.value===e.auth)&&(K.value=e.auth),n("ep-mode").value=e.mode||"list",n("ep-order").value=e.order||"desc",n("ep-limit").value=e.limit??50,n("ep-enabled").checked=e.enabled!==!1,await oe(),Se(e.fields||[]),je(e.sort||""),A.textContent="";for(const[t,o]of Object.entries(e.filter||{})){const{field:a,op:r}=qt(t);ae(a,r,String(o))}ve(),f()}x(M.effect(()=>{const e=Y.value;ue=e,n("ep-url-preview").textContent=jt(e.project,e.path||"");const t=ne.value,o=V.value;n("ep-def-dot").hidden=!(o&&(t.name||t.path||t.project||t.collection)),n("ep-query-dot").hidden=!(o&&(t.filter||t.limit))&&!(e.path&&t.filter);const a=n("ep-filter-err");a.textContent=t.filter||"",a.hidden=!t.filter;const r=n("ep-limit-err");r.textContent=t.limit||"",r.hidden=!t.limit}));const lt=e=>Mt(p,e||(w?"Edit":"New endpoint"));x(M.effect(()=>{const e=Y.value.name;lt(e||q.value?.name||"")}));function ct(){const e=q.peek(),t=h.peek()||!e;n("tryit-dirty-note").style.display=h.peek()?"":"none",n("tryit-send").disabled=t,n("tryit-token-group").style.display=e?.auth==="token"?"":"none";const o=n("tryit-params"),a={};for(const i of o.querySelectorAll("input"))a[i.dataset.param]=i.value;o.textContent="";for(const i of Xe(e?.path||""))o.appendChild(qe(`:${i}`,"param",i,a[i]));const r=n("tryit-query-params"),s={};for(const i of r.querySelectorAll("input"))s[i.dataset.queryParam]=i.value;r.textContent="";for(const i of St(e?.filter,"query"))r.appendChild(qe(`?${i}`,"queryParam",i,s[i],`optional - filters by ${i}`))}function qe(e,t,o,a="",r=""){const s=document.createElement("div");s.className="form-group";const i=document.createElement("label");i.className="form-label",i.textContent=e;const l=document.createElement("input");return l.type="text",l.className="form-input",l.dataset[t]=o,l.value=a,r&&(l.placeholder=r),s.append(i,l),s}x(M.effect(()=>{h.value,q.value,ct()}));let Ae="";const W=()=>{const e=j.get(ee);return Array.isArray(e)?e:[]};function Re(e){const t=W().filter(o=>o.url!==e.url);j.set(ee,[e,...t].slice(0,Lt)),re()}function $e(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||"",se()}function re(){const e=n("tryit-history-list"),t=W();n("tryit-history-card").style.display=t.length?"":"none",e.textContent="",t.forEach((o,a)=>{const r=document.createElement("div");r.className="epx-hrow",r.tabIndex=0,r.dataset.index=String(a);const s=document.createElement("span");s.className=`epx-chip ${ze(o.status,o.ok)}`,s.textContent=o.status||"ERR";const i=document.createElement("span");i.className="epx-url",i.textContent=o.url.replace(`/api/x/${q.peek()?.project||""}`,"")||o.url,i.title=o.url;const l=document.createElement("button");l.type="button",l.className="epx-kebab",l.title="Actions",l.setAttribute("aria-label","Request actions"),l.textContent="\u22EE",r.append(s,i,l),e.appendChild(r)})}const v={key:"var(--dm-info)",index:"var(--dm-text-muted)",string:"var(--dm-success)",number:"var(--dm-warning)",boolean:"var(--dm-secondary)",nul:"var(--dm-danger)",punc:"var(--dm-text-muted)",preview:"var(--dm-text-muted)"};function b(e,t,o){const a=document.createElement("span");return a.style.color=e,o&&(a.style.cssText+=";"+o),a.textContent=t,a}const Le=(e,t)=>t?b(v.index,String(e)):b(v.key,JSON.stringify(String(e)));function pt(e){return e===null?b(v.nul,"null"):typeof e=="number"?b(v.number,String(e)):typeof e=="boolean"?b(v.boolean,String(e)):b(v.string,JSON.stringify(e))}function Ne(e,t,o){const a=e&&typeof e=="object",r=a&&(Array.isArray(e)?e.length===0:Object.keys(e).length===0);if(!a||r){const m=document.createElement("div");return m.style.cssText="white-space:pre-wrap;word-break:break-word;padding-left:1.2em;",t!==void 0&&(m.appendChild(Le(t,o)),m.appendChild(b(v.punc,": "))),m.appendChild(a?b(v.punc,Array.isArray(e)?"[]":"{}"):pt(e)),m}const s=Array.isArray(e),i=s?e.map((m,z)=>[z,m,!0]):Object.entries(e).map(([m,z])=>[m,z,!1]),l=document.createElement("div"),u=document.createElement("div");u.style.cssText="white-space:pre-wrap;cursor:pointer;";const g=b(v.punc,"\u25BE","display:inline-block;width:1.2em;");u.appendChild(g),t!==void 0&&(u.appendChild(Le(t,o)),u.appendChild(b(v.punc,": "))),u.appendChild(b(v.punc,s?"[":"{"));const c=b(v.preview,` ${i.length} ${s?"item":"key"}${i.length===1?"":"s"} `,"font-style:italic;"),J=b(v.punc,s?"]":"}");u.append(c,J),l.appendChild(u);const C=document.createElement("div");C.style.cssText="padding-left:1.2em;border-left:1px solid var(--dm-border);";for(const[m,z,gt]of i)C.appendChild(Ne(z,m,gt));l.appendChild(C);const U=document.createElement("div");U.style.paddingLeft="1.2em",U.appendChild(b(v.punc,s?"]":"}")),l.appendChild(U);let ce=!1;const Je=m=>{ce=m,g.textContent=m?"\u25B8":"\u25BE",c.style.display=m?"":"none",J.style.display=m?"":"none",C.style.display=m?"none":"",U.style.display=m?"none":""};return Je(!1),u.addEventListener("click",()=>Je(!ce)),l}let Me="";function Te(e){n("tryit-json").style.display=e?"":"none",n("tryit-body").style.display=e?"none":"",n("tryit-json-toggle").textContent=e?"Raw":"Tree"}function Pe(e){Me=e,n("tryit-results-placeholder").style.display="none",n("tryit-response-card").style.display="";let t,o=!0;try{t=JSON.parse(e)}catch{o=!1}n("tryit-body").textContent=o?JSON.stringify(t,null,2):e;const a=n("tryit-json");a.textContent="",o&&a.appendChild(Ne(t)),n("tryit-json-toggle").style.display=o?"":"none",Te(o)}async function se(){const e=q.peek();if(!e||h.peek())return;const t={},o={};for(const c of n("tryit-params").querySelectorAll("input"))t[c.dataset.param]=c.value;for(const c of n("tryit-query-params").querySelectorAll("input"))o[c.dataset.queryParam]=c.value;const a=n("tryit-query").value.trim(),{url:r,headers:s,curl:i}=At(e,{params:t,queryParams:o,extra:a,token:n("tryit-token").value.trim(),origin:location.origin});Ae=i;const l={ts:Date.now(),url:r,params:t,queryParams:o,extra:a};n("tryit-result").style.display="",n("tryit-curl").textContent=i;const u=n("tryit-send");u.disabled=!0;const g=n("tryit-status");try{const c=await fetch(r,{headers:s}),J=await c.text();if(F)return;g.textContent=`${c.status} ${c.statusText}`,g.className=`epx-chip ${ze(c.status,c.ok)}`;const C=[];c.headers.forEach((U,ce)=>C.push(`${ce}: ${U}`)),n("tryit-headers-group").style.display="",n("tryit-headers").textContent=C.length?C.join(`
2
+ `):"(none exposed)",Pe(J),Re({...l,status:c.status,statusText:c.statusText,ok:c.ok})}catch(c){if(F)return;g.textContent="Network error",g.className="epx-chip is-danger",n("tryit-headers-group").style.display="none",Pe(String(c.message||c)),Re({...l,status:0,statusText:"Network error",ok:!1})}finally{F||(u.disabled=h.peek()||!q.peek())}}async function ie(e,t){if(!e){E.toast("Send a request first.",{type:"info"});return}try{await navigator.clipboard.writeText(e),E.toast(`${t} copied.`,{type:"success"})}catch{E.toast("Copy failed.",{type:"error"})}}async function le({thenSend:e=!1}={}){if(te.peek())return!1;V.value=!0;const t=H(),o=We(t);if(Object.keys(o).length){const a=Object.keys(o)[0];return Qe.activate?.(["filter","limit"].includes(a)?1:0),E.toast(o[a],{type:"error"}),!1}te.value=!0;try{if(w){const a=await _.apiEndpoints.update(w,t);if(F)return!0;q.value=a,$=H(),P.value=G($),j.remove(B),E.toast("Endpoint saved.",{type:"success"}),Ve("#/api-endpoints"),e&&se()}else{const a=await _.apiEndpoints.create(t);$=H(),P.value=G($),j.remove(B),E.toast("Endpoint created.",{type:"success"}),Ve("#/api-endpoints"),location.hash="#/api-endpoints/edit/"+encodeURIComponent(a.id)}return!0}catch(a){return E.toast(`Save failed: ${a.message||a}`,{type:"error"}),!1}finally{te.value=!1}}let $=null;async function dt(){!h.peek()||!await E.confirm("Discard the changes you have not saved?")||Z()&&(await ge($),V.value=!1,j.remove(B))}function ut(e){if(!e)return[];const t=[...A.querySelectorAll(".epx-frow")],o=t.indexOf(e),a=Xe(n("ep-path").value),r=e.querySelector(".ep-f-value");return[{header:`Filter ${o+1}`},{label:"Move up",icon:"arrow-up",disabled:o<=0,action:()=>{A.insertBefore(e,t[o-1]),f()}},{label:"Move down",icon:"arrow-down",disabled:o>=t.length-1,action:()=>{A.insertBefore(t[o+1],e),f()}},{label:"Duplicate",icon:"copy",action:()=>{const s=be()[o];ae(s.field,s.op,s.value,e.nextSibling),f()}},...a.length?[{type:"divider"},...a.map(s=>({label:`Use :${s}`,icon:"link",action:()=>{r.value=`{{params.${s}}}`,f()}}))]:[],{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{e.remove(),ve(),f()}}]}const ft=E.contextMenu(A,{cursor:"glyph",match:".epx-frow",inherit:!1,density:"compact",exclude:"input, select, textarea",items:e=>ut(e)});x(()=>ft?.destroy?.());const Pt=n("ep-fields-list"),Ee=e=>{for(const t of p.querySelectorAll(".ep-field-cb"))t.checked=e(t.checked);f(),ye()},mt=E.contextMenu(n("ep-fields-more").parentElement.parentElement,{cursor:"glyph",match:"#ep-fields-list, #ep-fields-more",inherit:!1,density:"compact",items:()=>O.length?[{header:"Response fields"},{label:"Tick all",icon:"check",action:()=>Ee(()=>!0)},{label:"Tick none (return everything)",icon:"x",action:()=>Ee(()=>!1)},{label:"Invert",icon:"refresh-cw",action:()=>Ee(e=>!e)}]:[]});x(()=>mt?.destroy?.());const Ie=n("tryit-history-list"),yt=e=>{const t=W()[Number(e?.dataset.index)];if(!t)return[];const o=q.peek();return[{header:`${t.status||"ERR"} ${t.statusText||""}`.trim()},{label:"Run again",icon:"play",disabled:h.peek()||!o,action:()=>$e(t)},{label:"Copy address",icon:"link",action:()=>ie(location.origin+t.url,"Address")},{label:"Copy as curl",icon:"copy",action:()=>ie(`curl '${(location.origin+t.url).replace(/'/g,"'\\''")}'`,"curl command")},{type:"divider"},{label:"Remove from history",icon:"trash",danger:!0,action:()=>{j.set(ee,W().filter(a=>a.url!==t.url)),re()}}]},ht=E.contextMenu(Ie,{cursor:"glyph",match:".epx-hrow",inherit:!1,density:"compact",items:yt});x(()=>ht?.destroy?.());const Oe=e=>{const t=e.target.closest(".epx-kebab");if(t&&p.contains(t)){Ye(t);return}const o=e.target.closest(".epx-hrow");if(o&&Ie.contains(o)){const a=W()[Number(o.dataset.index)];if(!a)return;if(h.peek()){E.toast("Save first - Send tests the saved definition.",{type:"info"});return}$e(a)}},_e=e=>{if(e.key==="F10"&&e.shiftKey){const t=e.target.closest(".epx-frow, .epx-hrow, #ep-fields-list");t&&p.contains(t)&&(e.preventDefault(),Ye(t))}else e.key==="Enter"&&e.target.classList?.contains("epx-hrow")&&(e.preventDefault(),e.target.click())};if(p.addEventListener("click",Oe),p.addEventListener("keydown",_e),x(()=>{p.removeEventListener("click",Oe),p.removeEventListener("keydown",_e)}),y?(N.value=y.project,N.disabled=!0,await ge({name:y.name||"",project:y.project,path:y.path||"",collection:y.collection,auth:y.auth||"public",mode:y.mode||"list",filter:y.filter||{},sort:y.sort||null,order:y.order||"desc",limit:y.limit??50,fields:y.fields||[],enabled:y.enabled!==!1})):(await oe(),ae(),f()),!Z())return;$=H(),P.value=G($);for(const e of["ep-name","ep-path","ep-limit"])n(e).addEventListener("input",f);for(const e of["ep-mode","ep-order","ep-sort","ep-enabled"])n(e).addEventListener("change",f);K.addEventListener("change",f),N.addEventListener("change",async()=>{pe(),await oe(),f(),!w&&!n("ep-name").value.trim()&&!n("ep-path").value.trim()&&!be().some(e=>e.value.trim())&&($=H(),P.value=G($))}),k.addEventListener("change",async()=>{await oe(),f()}),n("ep-add-filter").addEventListener("click",()=>{ae().querySelector(".ep-f-value").focus(),f()}),n("tryit-send").addEventListener("click",se),n("tryit-save-send").addEventListener("click",()=>le({thenSend:!0})),n("tryit-history-clear").addEventListener("click",async()=>{await E.confirm("Clear this endpoint's test history?")&&(j.set(ee,[]),re())}),n("tryit-json-toggle").addEventListener("click",()=>Te(n("tryit-json").style.display==="none")),n("tryit-body-copy").addEventListener("click",()=>ie(Me,"Response")),n("tryit-copy-curl").addEventListener("click",()=>ie(Ae,"curl command"));const De={dirty:h,saving:te,barText:it,err:rt,pathErr:st},Fe={save:()=>le(),discard:()=>dt()},vt=M.applyBindings(De,n("ep-bar"),{methods:Fe}),bt=M.applyBindings(De,n("ep-def-panel"),{methods:Fe});x(()=>{vt.dispose(),bt.dispose()}),x(Et(p,{position:"bottom"})),Ue(d,{id:"ep-back",icon:"arrow-left",title:"All endpoints",onClick:()=>{location.hash="#/api-endpoints"}}),Ue(d,{id:"ep-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>le()});const Be=e=>{if(!(e.metaKey||e.ctrlKey))return;const o=e.key.toLowerCase();o==="s"?(e.preventDefault(),e.stopImmediatePropagation(),le()):o==="enter"&&(e.preventDefault(),q.peek()&&!h.peek()?se():h.peek()&&E.toast("Save first - Send tests the saved definition.",{type:"info"}))};document.addEventListener("keydown",Be,!0),x(()=>document.removeEventListener("keydown",Be,!0));const Ke=()=>{!h.peek()||!ue||j.set(B,{state:ue,base:P.peek(),at:new Date().toISOString()})},He=e=>{h.peek()&&(Ke(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",He),x(()=>window.removeEventListener("beforeunload",He)),L={path:ke,dirty:()=>h.peek(),dispose(){Ke(),F=!0,T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},re(),I.scan(p);const X=j.get(B),xe=wt(X);if(xe){j.remove(B);const e=X.base&&X.base!==P.peek(),t=X.at?` (${D(X.at).format("D MMM HH:mm")})`:"";await E.confirm(`You left unsaved changes to this endpoint${Nt(t)}.${e?" It has been saved since - restoring puts your older changes over it.":""} Restore them?`)&&Z()&&(w&&(xe.project=y.project),await ge(xe))}}};
@@ -1,8 +1,62 @@
1
- import{api as m}from"../api.js";import{keepTableIconsAlive as g}from"../lib/table-icons.js";function d(a){return String(a??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function f(a){return!Array.isArray(a)||a.length===0?'<span class="text-muted">All collections in project</span>':d(a.map(l=>{const r=Array.isArray(l.verbs)&&l.verbs.length?l.verbs.join(","):"all";return`${l.collection}: ${r}`}).join("; "))}function h(a){const l=[];for(const r of(a||"").split(`
2
- `)){const n=r.trim();if(!n)continue;const[o,e]=n.split(":").map(s=>s.trim());if(!o)return{error:`Invalid scope line: "${n}"`};const t={collection:o};if(e){const s=e.split(",").map(i=>i.trim()).filter(Boolean),c=s.find(i=>!["create","read","update","delete"].includes(i));if(c)return{error:`Unknown verb "${c}" in scope "${n}"`};t.verbs=s}l.push(t)}return{scopes:l}}function b(a,l,r){const n=document.createElement("div");n.style.marginBottom=".75rem";const o=document.createElement("label");if(o.className="form-label",o.textContent=a,n.appendChild(o),n.appendChild(l),r){const e=document.createElement("p");e.className="text-muted",e.style.cssText="font-size:.8rem;margin:.25rem 0 0;",e.textContent=r,n.appendChild(e)}return n}function v(a,l){const r=E.slideover({title:"Token created",size:"sm",position:"right"}),n=document.createElement("div");n.style.padding="1rem";const o=document.createElement("p");o.innerHTML="<strong>Copy this token now.</strong> It is shown once and cannot be recovered - only a hash is stored.",o.style.marginBottom=".75rem",n.appendChild(o);const e=document.createElement("code");e.textContent=a,e.style.cssText="display:block;padding:.6rem;word-break:break-all;user-select:all;margin-bottom:.75rem;",n.appendChild(e);const t=document.createElement("button");t.className="btn btn-primary",t.textContent="Copy to clipboard",t.style.marginRight=".5rem",t.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(a),t.textContent="Copied!"}catch{E.toast("Copy failed - select the token text manually.",{type:"error"})}}),n.appendChild(t);const s=document.createElement("button");s.className="btn btn-secondary",s.textContent="Done",s.addEventListener("click",()=>{r.close(),l()}),n.appendChild(s),r.element.appendChild(n),r.open()}function x(a,l){const r=E.slideover({title:"New API Token",size:"md",position:"right"}),n=document.createElement("div");n.style.padding=".5rem 1rem 1rem";const o=document.createElement("input");o.type="text",o.className="form-input",o.placeholder="e.g. mobile-app, ci-pipeline";const e=document.createElement("select");e.className="form-input";for(const i of a){const p=document.createElement("option");p.value=i.slug,p.textContent=`${i.name} (${i.slug})`,e.appendChild(p)}const t=document.createElement("input");t.type="datetime-local",t.className="form-input";const s=document.createElement("textarea");s.className="form-input",s.rows=3,s.placeholder=`jobs: read
3
- enquiries: create, read`,n.appendChild(b("Name",o)),n.appendChild(b("Project",e,"The token only works on collections in this project.")),n.appendChild(b("Expires (optional)",t,"Leave empty for a non-expiring token.")),n.appendChild(b("Scopes (optional)",s,'One per line: "collection: verb, verb". Leave empty to allow every collection in the project.'));const c=document.createElement("button");c.className="btn btn-primary",c.textContent="Create token",c.addEventListener("click",async()=>{const i=o.value.trim();if(!i){E.toast("A token name is required.",{type:"error"});return}const{scopes:p,error:y}=h(s.value);if(y){E.toast(y,{type:"error"});return}const k={name:i,project:e.value,scopes:p,expiresAt:t.value?new Date(t.value).toISOString():null};try{c.disabled=!0;const{plaintext:u}=await m.apiTokens.create(k);r.close(),v(u,l)}catch(u){c.disabled=!1,E.toast(`Create failed: ${u.message||u}`,{type:"error"})}}),n.appendChild(c),r.element.appendChild(n),r.open(),setTimeout(()=>o.focus(),100)}export const apiTokensView={templateUrl:"/admin/js/templates/api-tokens.html",async onMount(a){const l=E.loader(a.get(0),{type:"dots"}),[r,n]=await Promise.all([m.apiTokens.list().catch(()=>[]),m.projects.list().catch(()=>[])]);l.destroy(),T.create("#api-tokens-table",{pageSize:25,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">-</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"}" data-icon-size="14"></span></button>
6
- <button class="btn btn-sm btn-ghost btn-revoke-token" data-id="${d(t.id)}" data-name="${d(t.name)}" data-tooltip="Revoke"><span data-icon="trash" data-icon-size="14"></span></button>
7
- </span>
8
- `}]}),Domma.icons.scan(a.get(0)),g(a.get(0)),document.querySelectorAll("#api-tokens-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})});const o=()=>location.reload();a.off("click","#btn-new-token").on("click","#btn-new-token",()=>{if(!n.length){E.toast("No projects available - create a project first.",{type:"error"});return}x(n,o)}),a.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"}),o()}catch(s){E.toast(`Update failed: ${s.message||s}`,{type:"error"})}}),a.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"}),o()}catch(s){E.toast(`Revoke failed: ${s.message||s}`,{type:"error"})}})}};
1
+ import{api as g}from"../api.js";import{addBannerAction as Ce}from"../lib/plugin-chrome.js";import{helpPopovers as ie}from"../lib/help-popover.js";import{onceClosed as le,openMenuAt as re,sidebarCount as Me,viewLifetime as Le}from"../lib/tool-kit.js";import{FILTERS as Ae,arrangeTokens as $e,checkToken as Ne,countStates as Oe,scopesToText as Ie,toLocalInput as qe,tokenRow as Pe,tokenState as ce}from"../lib/api-tokens-arrange.js";const de=/^#\/api-tokens\/?(\?.*)?$/,Re="#/api-tokens",R="api-tokens.draft",H="api-tokens.filter",De=`
2
+ <div class="atx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
3
+ <span class="atx-icon" aria-hidden="true"><span data-icon="key" data-icon-size="16"></span></span>
4
+ <span class="atx-main"><strong>{{name}}</strong><small><code>{{hint}}</code> \xB7 {{meta}}</small></span>
5
+ <span class="atx-chips">
6
+ <span class="atx-chip" title="The project it works in">{{project}}</span>
7
+ <span class="atx-chip" data-bind-class="stateCls" data-bind-text="stateLabel"></span>
8
+ </span>
9
+ <span class="atx-used" data-bind-text="used"></span>
10
+ <button type="button" class="atx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
11
+ </div>`,A=c=>String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function pe(c){const d=document.createElement("template");return d.innerHTML=c.trim(),d.content.firstElementChild}async function ue(c){try{return await navigator.clipboard.writeText(c),!0}catch{return!1}}const l=Le(de);export const apiTokensView={templateUrl:"/admin/js/templates/api-tokens.html",async onMount(c){l.reset();const d=c.get(0).querySelector(".atx")||c.get(0),$=()=>de.test(location.hash)&&d.isConnected,w=M.observable([]),j=M.observable([]),D=M.observable(!0),_=M.observable("");let U="all";try{U=Ae.includes(S.get(H))?S.get(H):"all"}catch{}const r=M.observable(U),b=M.observable(""),Y=M.observable("");l.add(M.effect(()=>{const e=r.value;try{S.set(H,e)}catch{}}));const F=M.observable([]);l.add(M.effect(()=>{const e=Date.now();F.value=$e(w.value,{filter:r.value,project:b.value,query:Y.value},e).map(t=>Pe(t,e))}));const V=M.computed(()=>Oe(w.value)),fe=M.computed(()=>w.value.length?"No tokens match.":"No API tokens yet - New token in the banner makes one."),T=e=>w.peek().find(t=>t.id===e);async function J(){try{const[e,t]=await Promise.all([g.apiTokens.list(),g.projects.list().catch(()=>[])]);if(!$())return;w.value=Array.isArray(e)?e:[],j.value=Array.isArray(t)?t:[],_.value="",ke()}catch(e){_.value=`Could not load the tokens: ${e.message||e}`}finally{D.value=!1}}const N=async()=>{Me(Re),$()&&await J()};async function W(e,t){if(!(!t&&!await E.confirm(`Switch off "${A(e.name)}"? Callers using it are refused until it is switched back on.`)))try{await g.apiTokens.update(e.id,{enabled:t}),E.toast(`"${e.name}" ${t?"switched on":"switched off"}.`,{type:"success"}),await N()}catch(a){E.toast(`Not changed: ${a.message||a}`,{type:"error"})}}async function me(e){if(await E.confirm(`Revoke "${A(e.name)}"? Anything using it loses access at once. This cannot be undone - a new token would have a different value.`))try{await g.apiTokens.revoke(e.id),E.toast(`Revoked "${e.name}".`,{type:"success"}),await N()}catch(t){E.toast(`Not revoked: ${t.message||t}`,{type:"error"})}}let h=null,y=null;const C=new Set;let ve=!1;function G(e,t,a){const o=e.close.bind(e);let s=!1,u=!1;return e.close=async v=>{if(!s&&v!==!0&&t()){if(u)return;u=!0;const O=await E.confirm(a);if(u=!1,!O)return}return s=!0,o()},()=>{s=!0}}function L(e=null,t=null){const a=e?T(e):null;if(e&&!a){E.toast("That token is no longer there.",{type:"warning"});return}const o=!a,s=j.peek();if(o&&!s.length){E.toast("Make a project first - a token works inside one.",{type:"warning"});return}const u={name:a?.name??"",project:a?.project??(b.peek()||s[0]?.slug||""),expires:qe(a?.expiresAt),scopes:Ie(a?.scopes),enabled:a?a.enabled!==!1:!0,...t||{}},v=Object.fromEntries(Object.entries(u).map(([n,i])=>[n,M.observable(i)])),O=n=>JSON.stringify(Object.fromEntries(Object.entries(n).map(([i,z])=>[i,typeof z=="boolean"?z:String(z??"")]))),ee=()=>Object.fromEntries(Object.keys(v).map(n=>[n,v[n].peek()])),ge=O(u),te=()=>O(ee())!==ge||!!t,q=M.observable(!1),K=M.computed(()=>Ne(Object.fromEntries(Object.keys(v).map(n=>[n,v[n].value])),{isNew:o,projects:j.value.map(n=>n.slug)})),P=n=>M.computed(()=>K.value.errors[n]||""),Ee=a?ce(a):null,ae={...v,saving:q,isNew:o,notNew:!o,nameErr:P("name"),projectErr:P("project"),scopesErr:P("scopes"),expiresErr:P("expires"),cannot:M.computed(()=>q.value||Object.keys(K.value.errors).length>0),saveText:o?"Create token":"Save",fixedProject:a?`${a.project} - fixed for the token's life (revoke and make a new one to move it)`:"",hint:a?`dcms_\u2026${a.tokenHint||"????"}${Ee?.state==="expired"?" - expired: set a later date to bring it back":""}`:""},f=E.slideover({title:o?"New API token":`Edit ${A(a.name)}`,size:"md",position:"right"}),x=pe(`
12
+ <div class="atx-form">
13
+ <div>
14
+ <label class="form-label" for="atx-f-name">Name <span class="text-danger">*</span></label>
15
+ <input id="atx-f-name" type="text" class="form-input" maxlength="80" placeholder="mobile-app, ci-pipeline\u2026" data-model="name.value">
16
+ <small class="atx-err" data-bind-hidden="!nameErr.value" data-bind-text="nameErr.value"></small>
17
+ </div>
18
+ <div data-bind-hidden="notNew">
19
+ <label class="form-label" for="atx-f-project">Project
20
+ <span data-help="The token works only on collections in this project (and API Builder endpoints under it). It cannot be moved later."></span></label>
21
+ <select id="atx-f-project" class="form-input" data-model="project.value"></select>
22
+ <small class="atx-err" data-bind-hidden="!projectErr.value" data-bind-text="projectErr.value"></small>
23
+ </div>
24
+ <div data-bind-hidden="isNew">
25
+ <label class="form-label">Token</label>
26
+ <div class="atx-fixed"><code data-bind-text="hint"></code><br><span data-bind-text="fixedProject"></span></div>
27
+ </div>
28
+ <div>
29
+ <label class="form-label" for="atx-f-scopes">Scopes
30
+ <span data-help-title="Scopes" data-help="Leave empty and the token may use every collection in its project that allows token access. Otherwise one line per collection: the collection's slug, then a colon and the verbs it may use - create, read, update, delete. No verbs means all four. For example: jobs: read"></span></label>
31
+ <textarea id="atx-f-scopes" class="form-input font-mono" rows="4" placeholder="jobs: read&#10;enquiries: create, read" data-model="scopes.value"></textarea>
32
+ <small class="atx-err" data-bind-hidden="!scopesErr.value" data-bind-text="scopesErr.value"></small>
33
+ </div>
34
+ <div>
35
+ <label class="form-label" for="atx-f-expires">Expires
36
+ <span data-help="Leave empty for a token that never expires. From this moment on, calls with it are refused - the sidebar badge warns two weeks ahead."></span></label>
37
+ <input id="atx-f-expires" type="datetime-local" class="form-input" data-model="expires.value">
38
+ <small class="atx-err" data-bind-hidden="!expiresErr.value" data-bind-text="expiresErr.value"></small>
39
+ </div>
40
+ <div class="atx-setting" data-bind-hidden="isNew">
41
+ <label class="form-label" for="atx-f-enabled">Switched on
42
+ <span data-help="Off refuses every call with the token without losing it - switch it back on to carry on. Revoke (from the list) removes it for good."></span></label>
43
+ <input id="atx-f-enabled" type="checkbox" class="form-switch" data-model="enabled.value">
44
+ </div>
45
+ <div class="atx-actions">
46
+ <small class="atx-hint">Ctrl+Enter saves.</small>
47
+ <span class="atx-spacer"></span>
48
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
49
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
50
+ <span data-icon="check" data-icon-size="13"></span> <span data-bind-text="saveText"></span></button>
51
+ </div>
52
+ </div>`),ne=x.querySelector("#atx-f-project");for(const n of s){const i=document.createElement("option");i.value=n.slug,i.textContent=`${n.name||n.slug} (${n.slug})`,ne.appendChild(i)}let B=()=>{};async function oe(){if(ae.cannot.peek())return;q.value=!0;const{payload:n}=K.peek();try{if(o){const i=await g.apiTokens.create(n);B(),f.close(),await N(),be(i?.plaintext,n.name)}else await g.apiTokens.update(a.id,n),E.toast("Saved.",{type:"success"}),B(),f.close(),await N()}catch(i){E.toast(`Not saved: ${i.message||i}`,{type:"error"})}finally{q.value=!1}}f.element.appendChild(x);const je=ie(x),Se=M.applyBindings(ae,x,{methods:{save:oe,cancel:()=>f.close()}});ne.value=v.project.peek();const Te=n=>{(n.ctrlKey||n.metaKey)&&n.key==="Enter"&&(n.preventDefault(),oe())};x.addEventListener("keydown",Te);const se={id:a?.id??null,dirty:te,values:ee};h=se,C.add(f),le(f,()=>{Se.dispose(),je(),C.delete(f),h===se&&(h=null)}),B=G(f,te,"Discard the changes you have not saved?"),I.scan(x),f.open(),x.querySelector("#atx-f-name")?.focus()}function be(e,t){if(!e){E.toast("The token was made, but its value did not come back - revoke it and make another.",{type:"error"});return}let a=!1;const o=E.slideover({title:"Token created",size:"sm",position:"right"}),s=pe(`
53
+ <div class="atx-secret">
54
+ <p class="atx-warn"><strong>Copy it now.</strong> This is the only time it is shown - only a fingerprint is kept. If it is lost, revoke it and make another.</p>
55
+ <code class="atx-token"></code>
56
+ <div class="atx-actions">
57
+ <small class="atx-hint copied-note" hidden>Copied.</small>
58
+ <span class="atx-spacer"></span>
59
+ <button type="button" class="btn btn-sm btn-primary atx-copy"><span data-icon="copy" data-icon-size="13"></span> Copy</button>
60
+ <button type="button" class="btn btn-sm btn-ghost atx-done">Done</button>
61
+ </div>
62
+ </div>`);s.querySelector(".atx-token").textContent=e,s.querySelector(".atx-copy").addEventListener("click",async()=>{await ue(e)?(a=!0,s.querySelector(".copied-note").hidden=!1):E.toast("Copy failed - select the token and copy it by hand.",{type:"error"})}),s.querySelector(".atx-done").addEventListener("click",()=>o.close()),o.element.appendChild(s);const u={copied:()=>a};o.__dmSecret=!0,y=u,C.add(o),le(o,()=>{C.delete(o),y===u&&(y=null),e=null}),G(o,()=>!a,`You have not copied the token for "${A(t)}". Once this closes it can never be shown again. Close anyway?`),I.scan(s),o.open()}function he(e){const t=T(e);if(!t)return[];const{state:a}=ce(t);return[{header:t.name||"Token"},{label:"Edit\u2026",icon:"edit",action:()=>L(t.id)},t.enabled===!1?{label:"Switch on",icon:"play",action:()=>W(t,!0)}:{label:"Switch off\u2026",icon:"pause",disabled:a==="expired",action:()=>W(t,!1)},{type:"divider"},{label:"Copy its ending",icon:"copy",action:async()=>{E.toast(await ue(`dcms_\u2026${t.tokenHint||""}`)?"Copied the ending - the token itself is never shown again.":"Copy failed.",{type:"info"})}},{label:`Show only ${t.project}`,icon:"filter",disabled:b.peek()===t.project,action:()=>{b.value=t.project,ye()}},{type:"divider"},{label:"Revoke\u2026",icon:"trash",danger:!0,action:()=>me(t)}]}const m=d.querySelector(".atx-list");m.innerHTML=De;const k=d.querySelector(".atx-project"),ye=()=>{k&&(k.value=b.peek())};function ke(){if(!k)return;const e=b.peek();for(;k.options.length>1;)k.remove(1);for(const t of j.peek()){const a=document.createElement("option");a.value=t.slug,a.textContent=t.name||t.slug,k.appendChild(a)}k.value=e}const xe=M.applyBindings({tokens:w,projects:j,loading:D,loadError:_,filter:r,project:b,query:Y,rows:F,counts:V,emptyText:fe},d,{methods:{showAll:()=>{r.value="all"},showActive:()=>{r.value="active"},showExpiring:()=>{r.value="expiring"},showExpired:()=>{r.value="expired"},showDisabled:()=>{r.value="disabled"}}});l.add(()=>xe.dispose()),l.add(ie(d,{position:"bottom"})),l.add(M.effect(()=>{F.value,requestAnimationFrame(()=>{m.isConnected&&I.scan(m)})})),l.add(M.effect(()=>{const e=V.value,t=r.peek();t!=="all"&&!D.value&&!e[t]&&(r.value="all")}));const we=E.contextMenu(m,{cursor:"glyph",match:".atx-row",inherit:!1,density:"compact",items:e=>he(e.dataset.id)});l.add(()=>we?.destroy?.());const Q=e=>{const t=e.target.closest(".atx-row");if(!t)return;const a=e.target.closest(".atx-kebab");if(a){re(a);return}L(t.dataset.id)},X=e=>{const t=e.target.closest(".atx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),re(t)):e.key==="Enter"&&e.target===t&&L(t.dataset.id))};m.addEventListener("click",Q),m.addEventListener("keydown",X),l.add(()=>{m.removeEventListener("click",Q),m.removeEventListener("keydown",X)}),Ce(c,{id:"api-tokens-new",icon:"plus",title:"New token",onClick:()=>L()});const Z=e=>{(h?.dirty()||y&&!y.copied())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Z),l.add(()=>window.removeEventListener("beforeunload",Z)),l.add(()=>{if(h?.dirty())try{S.set(R,{id:h.id,values:h.values(),at:new Date().toISOString()})}catch{}ve=!0,[...C].forEach(e=>{if(!(y&&e.__dmSecret&&!y.copied()))try{e.close(!0)}catch{}})}),I.scan(d),await J(),!$())return;let p=null;try{p=S.get(R)}catch{}if(p?.values){try{S.remove?S.remove(R):S.set(R,null)}catch{}const e=p.id&&T(p.id)?`your changes to "${A(T(p.id).name)}"`:"the new token you started";await E.confirm(`You left ${e} unsaved. Pick up where you left off?`)&&$()&&L(p.id&&T(p.id)?p.id:null,p.values)}}};
@@ -1,8 +1,14 @@
1
- import{api as f}from"../api.js";let u=null,h=!1,v={};function g(){h=!0;const e=document.getElementById("block-quick-switch");e&&(e.disabled=!0)}function x(){h=!1;const e=document.getElementById("block-quick-switch");e&&(e.disabled=!1)}function C(e,t){if(e==null||e==="")return null;const n=e instanceof Date?e:new Date(e);if(isNaN(n.getTime()))return null;const c=(s,o=2)=>String(s).padStart(o,"0"),a=["January","February","March","April","May","June","July","August","September","October","November","December"],l=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],i={YYYY:()=>n.getFullYear(),YY:()=>c(n.getFullYear()%100),MMMM:()=>a[n.getMonth()],MMM:()=>a[n.getMonth()].slice(0,3),MM:()=>c(n.getMonth()+1),M:()=>n.getMonth()+1,DD:()=>c(n.getDate()),D:()=>n.getDate(),dddd:()=>l[n.getDay()],ddd:()=>l[n.getDay()].slice(0,3),HH:()=>c(n.getHours()),H:()=>n.getHours(),hh:()=>c((n.getHours()+11)%12+1),h:()=>(n.getHours()+11)%12+1,mm:()=>c(n.getMinutes()),m:()=>n.getMinutes(),ss:()=>c(n.getSeconds()),s:()=>n.getSeconds(),A:()=>n.getHours()<12?"AM":"PM",a:()=>n.getHours()<12?"am":"pm"};return t.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g,s=>String(i[s]()))}export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(e){I.register("edit-2",{viewBox:"0 0 24 24",path:"M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z",stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),requestAnimationFrame(()=>{document.querySelectorAll('[data-icon="edit-2"]').forEach(l=>{if(l.tagName.toLowerCase()==="svg")return;const i=I.render("edit-2",{size:parseInt(l.dataset.iconSize,10)||24});i&&l.replaceWith(i)})}),u=null,h=!1,v={};const t=e.find("#block-project").get(0);if(t)try{(await f.projects.list()).forEach(i=>{const s=document.createElement("option");s.value=i.slug,s.textContent=i.name||i.slug,t.appendChild(s)})}catch{}const n=window.location.hash.match(/\/blocks\/edit\/([^/?#]+)/);n&&(u=decodeURIComponent(n[1]));const c=e.find("#block-name").get(0),a=e.find("#block-content").get(0);if(u){e.find("#block-editor-title").text("Edit Block"),c&&(c.value=u,c.disabled=!0);try{const l=await f.blocks.get(u);a&&(a.value=l.content||""),e.find("#block-bundled").prop("checked",!!l.bundled),e.find("#block-css").val(l.css||""),v=l.meta||{},e.find("#block-project").val(l.meta?.project||"");try{const i=await f.blocks.list(),s=e.find("#block-quick-switch").get(0);s&&Array.isArray(i)&&i.length&&(i.slice().sort((o,r)=>o.name.localeCompare(r.name)).forEach(o=>{const r=document.createElement("option");r.value=o.name,r.textContent=o.name,o.name===u&&(r.selected=!0),s.appendChild(r)}),s.style.display="",s.addEventListener("change",function(){const o=this.value;o&&o!==u&&!h&&R.navigate(`/blocks/edit/${encodeURIComponent(o)}`)}))}catch{}}catch(l){E.toast(l.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}}a&&(L(a,e),$(a,e)),e.find("#save-block-btn").off("click").on("click",async()=>{await q(e)}),Domma.icons.scan()}};function L(e,t){const n=t.find("#block-line-numbers").get(0),c=t.find("#block-cursor-pos").get(0);function a(){const s=e.value.split(`
2
- `).length;n.textContent=Array.from({length:s},(o,r)=>r+1).join(`
3
- `),n.scrollTop=e.scrollTop}a(),e.addEventListener("input",a),e.addEventListener("scroll",()=>{n.scrollTop=e.scrollTop});function l(){if(!c)return;const s=e.value.slice(0,e.selectionStart).split(`
4
- `);c.textContent=`Ln ${s.length}, Col ${s[s.length-1].length+1}`}e.addEventListener("keyup",l),e.addEventListener("click",l),e.addEventListener("keydown",s=>{if(s.key==="Tab"){s.preventDefault();const o=e.selectionStart,r=e.selectionEnd;if(!s.shiftKey)e.value=e.value.slice(0,o)+" "+e.value.slice(r),e.selectionStart=e.selectionEnd=o+2;else{const d=e.value.lastIndexOf(`
5
- `,o-1)+1,m=e.value.slice(d,d+2),p=m===" "?2:m[0]===" "?1:0;p>0&&(e.value=e.value.slice(0,d)+e.value.slice(d+p),e.selectionStart=e.selectionEnd=Math.max(d,o-p))}e.dispatchEvent(new Event("input"))}if(s.key==="Enter"){s.preventDefault();const o=e.selectionStart,r=e.value.lastIndexOf(`
6
- `,o-1)+1,d=e.value.slice(r,o).match(/^(\s*)/)[1];e.value=e.value.slice(0,o)+`
7
- `+d+e.value.slice(e.selectionEnd),e.selectionStart=e.selectionEnd=o+1+d.length,e.dispatchEvent(new Event("input"))}});const i=t.find("#block-editor-toolbar").get(0);i&&i.addEventListener("click",s=>{const o=s.target.closest("[data-action]");o&&(D(o.dataset.action,e),e.focus())})}function D(e,t){switch(e){case"undo":document.execCommand("undo");break;case"redo":document.execCommand("redo");break;case"cut":t.selectionStart!==t.selectionEnd&&(navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{}),document.execCommand("cut"));break;case"copy":t.selectionStart!==t.selectionEnd&&navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{});break;case"paste":navigator.clipboard.readText().then(n=>{const c=t.selectionStart;t.value=t.value.slice(0,c)+n+t.value.slice(t.selectionEnd),t.selectionStart=t.selectionEnd=c+n.length,t.dispatchEvent(new Event("input"))}).catch(()=>E.toast("Use Ctrl+V to paste.",{type:"info"}));break;case"select-all":t.select();break;case"format":T(t);break}}function T(e){const t=e.value.match(/(<[^>]+>|[^<]+)/g)||[],n=[];let c=0;const a=" ";for(const s of t){const o=s.trim();if(o)if(o.startsWith("</"))c=Math.max(0,c-1),n.push(a.repeat(c)+o);else if(o.startsWith("<")&&!o.startsWith("<!")&&!o.endsWith("/>")){n.push(a.repeat(c)+o);const r=(o.match(/^<(\w+)/)||[])[1]||"";j.has(r.toLowerCase())||c++}else n.push(a.repeat(c)+o)}const l=n.join(`
8
- `),i=e.selectionStart;e.value=l,e.selectionStart=e.selectionEnd=Math.min(i,l.length),e.dispatchEvent(new Event("input"))}const j=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function $(e,t){const n=t.find("#block-editor-body").get(0),c=t.find("#block-sample-data").get(0),a=t.find("#block-preview-output");t.find("[data-mode]").each(function(){this.addEventListener("click",()=>{t.find("[data-mode]").each(function(){this.classList.remove("active")}),this.classList.add("active"),n.className=`editor-body editor-mode-${this.dataset.mode}`,this.dataset.mode!=="write"&&l()})}),e.addEventListener("input",()=>{M(c,e),l(),g()});{const i=t.find("#block-css").get(0);i&&i.addEventListener("input",()=>{l(),g()});const s=t.find("#block-name").get(0);s&&s.addEventListener("input",()=>{l(),g()});const o=t.find("#block-bundled").get(0);o&&o.addEventListener("change",g)}e.value.trim()&&(M(c,e),l());function l(){if(n.classList.contains("editor-mode-write"))return;const i=e.value.replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g,(p,k,y)=>{const w=c.querySelector(`[data-placeholder="${CSS.escape(k)}"]`)?.value??`[${k}]`;if(y){const S=C(w,y);if(S!==null)return b(S)}return b(w)}),s=(t.find("#block-name").val()||"preview").trim()||"preview",o=b(s),r=t.find("#block-css").val()||"",d=t.find("#block-css-scope-hint").get(0);d&&(d.textContent=`[data-block="${s}"]`);const m=r.trim()?`<style>[data-block="${o}"] { ${String(r).replace(/<\/style/gi,"<\\/style")} }</style>`:"";a.html(m+`<div data-block="${o}">${i}</div>`,{safe:!1}),Domma.icons.scan(a.get(0))}c._renderPreview=l}function M(e,t){const n=Y(t.value),c=new Set([...e.querySelectorAll("[data-placeholder]")].map(a=>a.dataset.placeholder));if(n.size>0&&e.querySelector("p.text-muted")?.remove(),n.forEach(a=>{c.has(a)||A(e,a)}),e.querySelectorAll("[data-placeholder]").forEach(a=>{n.has(a.dataset.placeholder)||a.closest(".block-sample-row")?.remove()}),n.size===0&&!e.querySelector("p.text-muted")){const a=document.createElement("p");a.className="text-muted",a.style.cssText="font-size:.8rem;margin:0;",a.textContent="No {{placeholders}} detected in template.",e.appendChild(a)}}function A(e,t){const n=document.createElement("div");n.className="block-sample-row",n.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem;";const c=document.createElement("label");c.style.cssText="font-size:.75rem;color:var(--dm-text-muted,#888);white-space:nowrap;min-width:90px;text-align:right;flex-shrink:0;",c.textContent=`{{${t}}}`;const a=document.createElement("input");a.type="text",a.className="form-input form-input--sm",a.style.flex="1",a.dataset.placeholder=t,a.placeholder=`Sample ${t}`,a.value=H(t),a.addEventListener("input",()=>{e._renderPreview&&e._renderPreview()}),n.appendChild(c),n.appendChild(a),e.appendChild(n)}function H(e){const t=e.toLowerCase();return t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"?new Date().toISOString():t==="_updatedat"?new Date().toISOString():t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":t.includes("message")||t.includes("content")||t.includes("description")?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("date")?new Date().toLocaleDateString():t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("category")?"general":`Sample ${e}`}function Y(e){const t=new Set;for(const[,n]of e.matchAll(/\{\{([\w_]+)(?::[^{}]+)?\}\}/g))t.add(n);return t}function b(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function q(e){const t=e.find("#block-name").get(0),n=e.find("#block-content").get(0),c=(t?.value||"").trim(),a=n?.value??"";if(!c){E.toast("Block name is required.",{type:"warning"});return}if(!/^[a-z0-9][a-z0-9-]*$/.test(c)){E.toast("Name must start with a letter or digit and contain only lowercase letters, digits, and hyphens.",{type:"warning"});return}const l=!!e.find("#block-bundled").is(":checked"),i=e.find("#save-block-btn").get(0);i&&(i.disabled=!0);try{await f.blocks.put(c,{content:a,bundled:l,css:e.find("#block-css").val()||"",meta:{...v||{},project:e.find("#block-project").val()||""||null}}),x(),E.toast(u?"Block updated.":"Block created.",{type:"success"}),u||(u=c,R.navigate(`/blocks/edit/${encodeURIComponent(c)}`))}catch(s){E.toast(s.message||"Failed to save block.",{type:"error"})}finally{i&&(i.disabled=!1)}}
1
+ import{api as T}from"../api.js";import{addBannerAction as G}from"../lib/plugin-chrome.js";import{helpPopovers as Te}from"../lib/help-popover.js";import{openMenuAt as ye,sidebarCount as De}from"../lib/tool-kit.js";import{createSimpleEditor as ke}from"../lib/simple-editor.js";import{checkForm as Ie,draftKey as g,draftOffer as Fe,escapeHtml as Se,extractPlaceholders as Ne,formFromBlock as Pe,formatHtml as He,payloadFromForm as Ve,renderPreview as Be,sampleValue as ge,scopeName as Ke,shortcodeFor as Ye,snapshot as Q}from"../lib/block-editor-arrange.js";const Ue=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=c=>String(c||"").split("?")[0],X="block-editor.mode",We=["write","split","preview"],_e=`
2
+ <div class="bex-sample" data-key="{{key}}" tabindex="0">
3
+ <label class="bex-sample-label" for="bex-sample-{{key}}">{{label}}</label>
4
+ <input id="bex-sample-{{key}}" type="text" class="form-input form-input--sm" data-bind-value="value" data-sample="{{key}}" placeholder="Sample {{key}}">
5
+ <button type="button" class="bex-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
6
+ </div>`;let p=null,we=!1;function ze(){we||!window.R?.use||(we=!0,R.use((c,i,s)=>{if(!(Ue.test(i?.path||"")&&F(c?.path)!==F(i?.path))||!p?.dirty())return s();const r=p;return E.confirm(`You have unsaved changes to ${Se(r.label())}. Leave anyway? They are kept as a draft for your next visit.`).then(A=>{A?s():history.replaceState(null,"",r.hash)})}),M.subscribe("router:afterChange",({to:c}={})=>{if(p&&F(c?.path)!==p.path){const i=p;p=null,i.dispose()}}))}function Je(c,i){const s=c.querySelector(".dm-plugin-banner-name"),f=s?.lastChild;f&&f.nodeType===3&&s.querySelector(".dm-plugin-banner-sep")&&(f.textContent=i)}function N(c){return navigator.clipboard?.writeText(c).then(()=>!0,()=>!1)??Promise.resolve(!1)}export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(c){if(ze(),p){const e=p;p=null,e.dispose()}const i=c.get(0),s=i.querySelector(".bex")||i,f=[],r=e=>(f.push(e),e);let A=!1;const P=()=>!A&&s.isConnected,Z=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),a=Z?decodeURIComponent(Z[1]):null,l=!a,Ee=location.hash,Me=F(location.hash.slice(1)),Ce=l&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",ee=E.loader(i,{type:"dots"});let b=null,H=[],V=[];try{[b,H,V]=await Promise.all([a?T.blocks.get(a):Promise.resolve(null),T.blocks.list().catch(()=>[]),T.projects.list().catch(()=>[])])}catch(e){ee.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(ee.destroy(),!s.isConnected)return;const te=(Array.isArray(H)?H:[]).map(e=>e.name).sort((e,t)=>e.localeCompare(t));let oe=b?.meta||{};const u=Pe(b?{...b,name:a}:null,{project:Ce}),n={};for(const[e,t]of Object.entries(u))n[e]=M.observable(t);const B=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),ae=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),w=M.observable(Q(u)),C=M.computed(()=>Q(ae())!==w.value),x=M.observable(!1),O=M.observable(!1),q=M.computed(()=>Ie(ae(),{isNew:l,taken:te}));let ne="split";try{ne=We.includes(S.get(X))?S.get(X):"split"}catch{}const h=M.observable(ne);r(M.effect(()=>{const e=h.value;try{S.set(X,e)}catch{}}));const se=M.observable("Ln 1, Col 1"),y={},K=M.observable(0),Y=M.observable([]);r(M.effect(()=>{const e=Ne(n.content.value);for(const t of e)t in y||(y[t]=ge(t));Y.value=e.map(t=>({key:t,label:`{{${t}}}`,value:y[t]}))}));const xe={...n,dirty:C,saving:x,mode:h,cursor:se,sampleRows:Y,isNew:l,notNew:!l,titleText:l?"New block":a,metaText:M.computed(()=>{if(l)return"Not saved yet";const e=[];return(b?.updatedAt||b?.meta?.updatedAt)&&e.push(`changed ${D(b.updatedAt||b.meta.updatedAt).format("D MMM YYYY HH:mm")}`),e.push(`${n.content.value.split(`
7
+ `).length} lines`),n.css.value.trim()&&e.push(`${Math.ceil(n.css.value.length/1024)} KB CSS`),e.join(" \xB7 ")}),nameErr:M.computed(()=>(O.value||n.name.value)&&q.value.errors.name||""),cssErr:M.computed(()=>q.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${Ke(n.name.value)}"]`),noSamples:M.computed(()=>Y.value.length===0),noSamplesText:"No {{placeholders}} in the template yet.",modeCls:M.computed(()=>`bex-body is-${h.value}`),cannotSave:M.computed(()=>x.value||O.value&&Object.keys(q.value.errors).length>0)},U=s.querySelector("#block-project");for(const e of Array.isArray(V)?V:[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,U.appendChild(t)}if(u.project&&![...U.options].some(e=>e.value===u.project)){const e=document.createElement("option");e.value=u.project,e.textContent=`${u.project} (no longer a project)`,U.appendChild(e)}const $=s.querySelector("#block-quick-switch");for(const e of te){const t=document.createElement("option");t.value=e,t.textContent=e,e===a&&(t.selected=!0),$.appendChild(t)}const m=s.querySelector(".bex-samples");m.innerHTML=_e;let k=null,j=null,o=null;function re(e){for(const[t,v]of Object.entries(e))n[t]&&(n[t].value=v);k.setValue(String(e.content??"")),j.setValue(String(e.css??""))}const ce=s.querySelector("#block-preview-output");let W=0;r(M.effect(()=>{const e={content:n.content.value,css:n.css.value,name:n.name.value};K.value,h.value!=="write"&&(cancelAnimationFrame(W),W=requestAnimationFrame(()=>{P()&&(ce.innerHTML=Be(e,y),I.scan(ce))}))})),r(()=>cancelAnimationFrame(W));const le=e=>{const t=e.target?.dataset?.sample;t&&(y[t]=e.target.value,K.value++)};m.addEventListener("input",le),r(()=>m.removeEventListener("input",le));function _(e){o.focus(),o.setRangeText(e,o.selectionStart,o.selectionEnd,"end"),o.dispatchEvent(new Event("input",{bubbles:!0}))}async function ie(e){switch(e){case"undo":o.focus(),document.execCommand("undo");break;case"redo":o.focus(),document.execCommand("redo");break;case"cut":o.selectionStart!==o.selectionEnd&&(await N(o.value.slice(o.selectionStart,o.selectionEnd)),_(""));break;case"copy":o.selectionStart!==o.selectionEnd&&await N(o.value.slice(o.selectionStart,o.selectionEnd));break;case"paste":try{_(await navigator.clipboard.readText())}catch{E.toast("Use Ctrl+V to paste.",{type:"info"})}break;case"select-all":o.focus(),o.select();break;case"format":{const t=He(o.value);k.setValue(t),n.content.value=t;break}default:break}}async function z(){if(x.peek())return;O.value=!0;const{errors:e,name:t}=q.peek();if(Object.keys(e).length){E.toast(e.name||e.css,{type:"warning"});return}const v=l?t:a;x.value=!0;try{const d=Ve(B(),oe);if(await T.blocks.put(v,d),!P())return;oe=d.meta,w.value=Q(B());try{S.remove?S.remove(g(a)):S.set(g(a),null)}catch{}De("#/blocks"),E.toast(l?`Created "${v}".`:"Block saved.",{type:"success"}),l&&(location.hash=`#/blocks/edit/${encodeURIComponent(v)}`)}catch(d){E.toast(`Not saved: ${d.message||d}`,{type:"error"})}finally{x.value=!1}}async function $e(){C.peek()&&await E.confirm("Discard the changes you have not saved?")&&(O.value=!1,re(je(w.peek())))}function je(e){try{const[t,v,d,Re,qe]=JSON.parse(e);return{name:t,project:v,bundled:d,content:Re,css:qe}}catch{return u}}const Le=M.applyBindings(xe,s,{methods:{save:()=>z(),discard:()=>$e(),setWrite:()=>{h.value="write"},setSplit:()=>{h.value="split"},setPreview:()=>{h.value="preview"},act:e=>ie(e)}});r(()=>Le.dispose()),r(Te(s,{position:"bottom"})),k=ke(s.querySelector(".bex-write"),{initialValue:u.content,language:"html",autoIndent:!0,onChange:e=>{n.content.value=e},onCursor:({line:e,col:t})=>{se.value=`Ln ${e}, Col ${t}`}}),j=ke(s.querySelector(".bex-css"),{initialValue:u.css,language:"css",autoIndent:!0,onChange:e=>{n.css.value=e}}),k.textarea.setAttribute("aria-label","Template HTML"),k.textarea.placeholder=`<div class="card mb-3">
8
+ <div class="card-body">
9
+ <h3>{{title}}</h3>
10
+ <p>{{message}}</p>
11
+ </div>
12
+ </div>`,j.textarea.setAttribute("aria-label","Custom CSS"),j.textarea.placeholder=`/* Scoped to this block - e.g.
13
+ color: var(--dm-primary);
14
+ h3 { font-size: 1.25rem; } */`,r(()=>{k.destroy(),j.destroy()}),o=k.textarea;const ue=s.querySelector(".bex-toolbar"),de=e=>{const t=e.target.closest("[data-action]");t&&ie(t.dataset.action)};ue.addEventListener("click",de),r(()=>ue.removeEventListener("click",de));const pe=()=>{const e=$.value;e&&e!==a?location.hash=`#/blocks/edit/${encodeURIComponent(e)}`:$.value=a||""};$.addEventListener("change",pe),r(()=>$.removeEventListener("change",pe));const Ae=e=>[{header:`{{${e}}}`},{label:"Insert into the template",icon:"corner-down-right",action:()=>_(`{{${e}}}`)},{label:"Copy placeholder",icon:"copy",action:async()=>{E.toast(await N(`{{${e}}}`)?"Copied.":`{{${e}}}`,{type:"info"})}},{label:"Reset sample value",icon:"rotate-ccw",action:()=>{y[e]=ge(e);const t=m.querySelector(`[data-sample="${CSS.escape(e)}"]`);t&&(t.value=y[e]),K.value++}}],Oe=E.contextMenu(m,{cursor:"glyph",match:".bex-sample",inherit:!1,density:"compact",items:e=>Ae(e.dataset.key)});r(()=>Oe?.destroy?.());const me=e=>{const t=e.target.closest(".bex-kebab");t&&ye(t)},ve=e=>{const t=e.target.closest(".bex-sample");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ye(t))};m.addEventListener("click",me),m.addEventListener("keydown",ve),r(()=>{m.removeEventListener("click",me),m.removeEventListener("keydown",ve)}),Je(i,l?"New block":a),G(c,{id:"block-editor-list",icon:"arrow-left",title:"All blocks",onClick:()=>{location.hash="#/blocks"}}),l?i.querySelector('[data-banner-action="block-editor-shortcode"]')?.remove():G(c,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=Ye(a);E.toast(await N(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),G(c,{id:"block-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>z()});const be=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),z())};document.addEventListener("keydown",be,!0),r(()=>document.removeEventListener("keydown",be,!0));const fe=()=>{if(C.peek())try{S.set(g(a),{form:B(),base:w.peek(),at:new Date().toISOString()})}catch{}},he=e=>{C.peek()&&(fe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",he),r(()=>window.removeEventListener("beforeunload",he)),p={hash:Ee,path:Me,dirty:()=>C.peek(),label:()=>l?"the new block":`"${a}"`,dispose(){A=!0,fe(),f.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let J=null;try{J=S.get(g(a))}catch{}const L=Fe(J,w.peek());if(J)try{S.remove?S.remove(g(a)):S.set(g(a),null)}catch{}if(L){const e=L.at?` (${D(L.at).format("D MMM HH:mm")})`:"",t=l?"a new block":`"${Se(a)}"`,v=L.stale?`You left unsaved changes to ${t}${e}, but the block has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${e}. Restore them?`;if(await E.confirm(v)&&P()){const d={...u,...L.form};l||(d.name=a),re(d)}}}};