domma-cms 0.92.1 → 0.94.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 (177) hide show
  1. package/CLAUDE.md +5 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/action-editor-arrange.js +1 -1
  5. package/admin/js/lib/api-tokens-arrange.js +2 -2
  6. package/admin/js/lib/block-editor-arrange.js +1 -1
  7. package/admin/js/lib/blocks-arrange.js +1 -1
  8. package/admin/js/lib/collection-entries-arrange.js +1 -1
  9. package/admin/js/lib/components-arrange.js +1 -1
  10. package/admin/js/lib/dashboard-arrange.js +1 -1
  11. package/admin/js/lib/dates.js +1 -0
  12. package/admin/js/lib/forms-arrange.js +1 -1
  13. package/admin/js/lib/media-arrange.js +1 -1
  14. package/admin/js/lib/notifications-arrange.js +1 -1
  15. package/admin/js/lib/pages-arrange.js +1 -1
  16. package/admin/js/lib/related.js +1 -1
  17. package/admin/js/lib/timeline-builder.js +2 -2
  18. package/admin/js/templates/action-editor.html +6 -5
  19. package/admin/js/templates/actions-list.html +1 -1
  20. package/admin/js/templates/contacts.html +1 -1
  21. package/admin/js/templates/docs/api-actions.html +86 -60
  22. package/admin/js/templates/docs/api-authentication.html +159 -123
  23. package/admin/js/templates/docs/api-builder.html +197 -0
  24. package/admin/js/templates/docs/api-collections.html +199 -259
  25. package/admin/js/templates/docs/api-external.html +225 -0
  26. package/admin/js/templates/docs/api-forms.html +268 -0
  27. package/admin/js/templates/docs/api-layouts.html +70 -45
  28. package/admin/js/templates/docs/api-media.html +57 -80
  29. package/admin/js/templates/docs/api-navigation.html +66 -22
  30. package/admin/js/templates/docs/api-pages.html +109 -129
  31. package/admin/js/templates/docs/api-plugins.html +123 -61
  32. package/admin/js/templates/docs/api-scaffold.html +185 -0
  33. package/admin/js/templates/docs/api-settings.html +72 -64
  34. package/admin/js/templates/docs/api-users.html +74 -107
  35. package/admin/js/templates/docs/api-views.html +68 -54
  36. package/admin/js/templates/docs/components-howto.html +20 -17
  37. package/admin/js/templates/docs/components-reference.html +13 -16
  38. package/admin/js/templates/docs/components-rules.html +7 -6
  39. package/admin/js/templates/docs/components-walkthrough.html +19 -19
  40. package/admin/js/templates/docs/tutorial-crud.html +71 -40
  41. package/admin/js/templates/docs/tutorial-forms.html +51 -35
  42. package/admin/js/templates/docs/tutorial-plugin.html +132 -56
  43. package/admin/js/templates/docs/usage-actions.html +61 -15
  44. package/admin/js/templates/docs/usage-collections.html +108 -0
  45. package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
  46. package/admin/js/templates/docs/usage-dconfig.html +0 -3
  47. package/admin/js/templates/docs/usage-editions.html +213 -0
  48. package/admin/js/templates/docs/usage-media.html +22 -6
  49. package/admin/js/templates/docs/usage-navigation.html +74 -18
  50. package/admin/js/templates/docs/usage-pages.html +60 -20
  51. package/admin/js/templates/docs/usage-plugins.html +89 -17
  52. package/admin/js/templates/docs/usage-shortcodes.html +123 -70
  53. package/admin/js/templates/docs/usage-site-settings.html +50 -18
  54. package/admin/js/templates/docs/usage-tools.html +73 -0
  55. package/admin/js/templates/docs/usage-users-roles.html +99 -20
  56. package/admin/js/templates/docs/usage-views.html +36 -19
  57. package/admin/js/templates/documentation.html +153 -32
  58. package/admin/js/templates/page-editor.html +0 -5
  59. package/admin/js/templates/plugin-guide.html +15 -0
  60. package/admin/js/templates/plugin-guides.html +21 -0
  61. package/admin/js/templates/pro-docs.html +53 -234
  62. package/admin/js/templates/tutorials.html +5 -4
  63. package/admin/js/views/actions-list.js +3 -3
  64. package/admin/js/views/analytics.js +5 -5
  65. package/admin/js/views/api-endpoint-editor.js +2 -2
  66. package/admin/js/views/block-editor.js +4 -4
  67. package/admin/js/views/blocks.js +4 -4
  68. package/admin/js/views/collection-editor.js +4 -4
  69. package/admin/js/views/collection-entries.js +7 -7
  70. package/admin/js/views/component-editor.js +2 -2
  71. package/admin/js/views/contacts.js +22 -20
  72. package/admin/js/views/context-menu-editor.js +5 -5
  73. package/admin/js/views/doc-pages.js +1 -1
  74. package/admin/js/views/form-editor.js +4 -4
  75. package/admin/js/views/form-submissions.js +2 -2
  76. package/admin/js/views/index.js +1 -1
  77. package/admin/js/views/media.js +3 -3
  78. package/admin/js/views/menu-editor.js +13 -13
  79. package/admin/js/views/menu-locations.js +2 -2
  80. package/admin/js/views/my-profile.js +1 -1
  81. package/admin/js/views/page-editor.js +8 -8
  82. package/admin/js/views/plugin-guides.js +5 -0
  83. package/admin/js/views/project-detail.js +2 -2
  84. package/admin/js/views/project-settings.js +1 -1
  85. package/admin/js/views/role-editor.js +4 -4
  86. package/admin/js/views/search.js +2 -2
  87. package/admin/js/views/seo.js +17 -17
  88. package/admin/js/views/settings.js +3 -3
  89. package/admin/js/views/theme.js +3 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +2 -2
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/bin/cli.js +13 -13
  94. package/bin/lib/node-version.js +29 -0
  95. package/package.json +1 -1
  96. package/plugins/_lib/admin/mail/compose-window.js +3 -2
  97. package/plugins/_lib/admin/mail/reader-view.js +7 -6
  98. package/plugins/_lib/admin/mail/scheduling.js +4 -2
  99. package/plugins/_lib/admin/mail/templates.js +4 -4
  100. package/plugins/_lib/admin/ui/dates.js +85 -0
  101. package/plugins/blog/CLAUDE.md +31 -22
  102. package/plugins/blog/admin/views/blog.js +3 -2
  103. package/plugins/blog/admin/views/comments.js +2 -1
  104. package/plugins/blog/admin/views/post-editor.js +4 -4
  105. package/plugins/blog/blocks/blog-card-row.html +1 -1
  106. package/plugins/blog/blocks/blog-card.html +2 -2
  107. package/plugins/blog/blocks/blog-post-classic.html +2 -2
  108. package/plugins/blog/blocks/blog-post-essay.html +2 -2
  109. package/plugins/blog/blocks/blog-post-feature.html +2 -2
  110. package/plugins/blog/blocks/blog-post-minimal.html +2 -2
  111. package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
  112. package/plugins/blog/blocks/blog-post-split.html +2 -2
  113. package/plugins/blog/docs/guide.md +205 -0
  114. package/plugins/blog/lib/layouts.js +3 -3
  115. package/plugins/blog/lib/page.js +2 -1
  116. package/plugins/blog/plugin.js +3 -3
  117. package/plugins/blog/plugin.json +4 -4
  118. package/plugins/blog/tests/layouts.test.js +6 -0
  119. package/plugins/feedback/CLAUDE.md +22 -3
  120. package/plugins/feedback/admin/lib/kit.js +6 -7
  121. package/plugins/feedback/admin/views/feedback.js +79 -10
  122. package/plugins/feedback/admin/views/send.js +28 -6
  123. package/plugins/feedback/docs/guide.md +95 -0
  124. package/plugins/feedback/lib/receiver.js +9 -2
  125. package/plugins/feedback/lib/sender.js +3 -2
  126. package/plugins/feedback/plugin.js +54 -6
  127. package/plugins/feedback/plugin.json +4 -4
  128. package/plugins/feedback/tests/api.test.js +74 -2
  129. package/plugins/free-tier.lock.json +49 -44
  130. package/plugins/mail-reader/CLAUDE.md +33 -18
  131. package/plugins/mail-reader/docs/guide.md +147 -0
  132. package/plugins/mail-reader/plugin.json +1 -1
  133. package/plugins/security/CLAUDE.md +4 -1
  134. package/plugins/security/admin/views/security.js +5 -5
  135. package/plugins/security/docs/guide.md +170 -0
  136. package/plugins/security/plugin.js +2 -1
  137. package/plugins/security/plugin.json +2 -1
  138. package/plugins/shopping-cart/CLAUDE.md +7 -1
  139. package/plugins/shopping-cart/admin/lib/kit.js +5 -2
  140. package/plugins/shopping-cart/admin/views/orders.js +4 -4
  141. package/plugins/shopping-cart/admin/views/overview.js +2 -2
  142. package/plugins/shopping-cart/docs/guide.md +191 -0
  143. package/plugins/shopping-cart/lib/render.js +2 -1
  144. package/plugins/shopping-cart/plugin.json +3 -3
  145. package/public/js/collection-browser.js +2 -2
  146. package/public/js/site.js +1 -1
  147. package/scripts/gen-instance-secret.js +3 -1
  148. package/scripts/setup.js +3 -1
  149. package/server/middleware/auth.js +2 -1
  150. package/server/routes/api/actions.js +47 -27
  151. package/server/routes/api/blocks.js +2 -1
  152. package/server/routes/api/collections.js +16 -52
  153. package/server/routes/api/contacts.js +66 -3
  154. package/server/routes/api/documentation.js +42 -0
  155. package/server/routes/api/notifications.js +3 -2
  156. package/server/routes/api/users.js +10 -6
  157. package/server/server.js +16 -1
  158. package/server/services/actions.js +110 -34
  159. package/server/services/adapterRegistry.js +169 -16
  160. package/server/services/adapters/FileAdapter.js +25 -0
  161. package/server/services/adapters/MongoAdapter.js +23 -0
  162. package/server/services/collections.js +104 -1
  163. package/server/services/connectionManager.js +12 -0
  164. package/server/services/dates.js +81 -0
  165. package/server/services/docs.js +13 -2
  166. package/server/services/markdown.js +75 -26
  167. package/server/services/notification-sources.js +6 -5
  168. package/server/services/passwordReset.js +2 -1
  169. package/server/services/permissionRegistry.js +3 -2
  170. package/server/services/pluginGuides.js +255 -0
  171. package/server/services/pluginInstaller.js +54 -13
  172. package/server/services/plugins.js +29 -1
  173. package/server/services/presetCollections.js +31 -5
  174. package/server/services/renderer.js +2 -2
  175. package/server/services/sidebarBadges.js +3 -1
  176. package/server/services/tools.js +4 -2
  177. package/server/templates/page.html +2 -2
@@ -1,2 +1,2 @@
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
+ import{api as _}from"../api.js";import{addBannerAction as Je}from"../lib/plugin-chrome.js";import{helpPopovers as gt}from"../lib/help-popover.js";import{openMenuAt as Ue,sidebarCount as Ye}from"../lib/tool-kit.js";import{OPERATORS as Et,PSEUDO_FIELDS as xt,checkEndpoint as Ve,filterFromRows as kt,normaliseDraft as Ct,pathParams as We,placeholders as wt,publicPath as St,snapshot as z,splitFilterKey as jt,statusTone as Xe,tryRequest as qt}from"../lib/api-endpoint-editor-arrange.js";import{fmtDateTime as At}from"../lib/dates.js";const Rt=/^\/api-endpoints\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,G=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 Tt(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,ze=!1;function Mt(){ze||!window.R?.use||(ze=!0,R.use((d,p,n)=>!(Rt.test(p?.path||"")&&G(d?.path)!==G(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&&G(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(Mt(),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 D=!1;const xe=G(location.hash.slice(1)),Q=()=>!D&&p.isConnected&&G(location.hash.slice(1))===xe,ke=location.hash.split("?")[0].match(/\/api-endpoints\/edit\/([^/?#]+)/),w=ke?decodeURIComponent(ke[1]):null,F=$t+(w||"new"),Z=`apitryit:hist:${w||"new"}`,Ge=E.tabs(n("ep-editor-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=E.loader(p,{type:"dots"}),[Ze,et,tt,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(Qe.destroy(),!Q())return;if(w&&!y){E.toast("Endpoint not found.",{type:"error"}),location.hash="#/api-endpoints";return}const nt=et.filter(e=>!e.systemManaged&&!e.preset),ot=(tt.entries||[]).map(e=>e.data?.name).filter(Boolean),N=n("endpoint-project");for(const e of Ze){const t=document.createElement("option");t.value=e.slug,t.textContent=`${e.name} (${e.slug})`,N.appendChild(t)}const k=n("ep-collection"),Ce=e=>e.meta&&e.meta.project||"core";function ce(e){const t=e!==void 0?e:k.value,o=N.value,a=nt.filter(r=>{const s=Ce(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})${Ce(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)}ce();const B=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,B.appendChild(o)}for(const e of ot){const t=document.createElement("option");t.value=e,t.textContent=`Role: ${e}`,B.appendChild(t)}const pe=M.observable(0),f=()=>{pe.value=pe.peek()+1},U=M.computed(()=>(pe.value,K()));let de=null;const P=M.observable(""),q=M.observable(y||null),Y=M.observable(!1),ee=M.observable(!1),h=M.computed(()=>z(U.value)!==P.value),te=M.computed(()=>Ve(U.value)),at=M.computed(()=>Y.value?te.value:{}),rt=M.computed(()=>(Y.value||U.value.path)&&te.value.path||""),st=M.computed(()=>{const e=Object.keys(te.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 ne(){const e=k.value;if(O=[],e)try{O=((await _.collections.get(e))?.fields||[]).map(o=>o.name).filter(Boolean)}catch{}we(fe());for(const t of A.querySelectorAll(".epx-frow"))ye(t.querySelector(".ep-f-field"));Se()}const ue=()=>[...O,...xt],fe=()=>[...p.querySelectorAll(".ep-field-cb:checked")].map(e=>e.value);function we(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(),me()}),a.appendChild(r),a.appendChild(document.createTextNode(o)),t.appendChild(a)}me()}function me(){const e=fe().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 Se(e){const t=n("ep-sort"),o=e!==void 0?e:t.value;t.textContent="";for(const a of["createdAt",...ue()]){const r=document.createElement("option");r.value=a==="createdAt"?"":a,r.textContent=a==="createdAt"?"createdAt (default)":a,t.appendChild(r)}t.value=o||""}function ye(e,t){const o=t!==void 0?t:e.value;e.textContent="";for(const r of ue()){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 oe(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"),ye(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()?ye(s,String(c).trim()):s.value=i||ue()[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,H]of Et){const C=document.createElement("option");C.value=c,C.textContent=H,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),he(),r}const he=()=>{n("ep-filter-empty").hidden=!!A.querySelector(".epx-frow")};function ve(){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 K(){return{name:n("ep-name").value.trim(),project:N.value,path:n("ep-path").value.trim(),collection:k.value,auth:B.value,mode:n("ep-mode").value,filter:kt(ve()),sort:n("ep-sort").value||null,order:n("ep-order").value,limit:Number(n("ep-limit").value===""?0:n("ep-limit").value),fields:fe(),enabled:n("ep-enabled").checked}}async function be(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||"",ce(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}[...B.options].some(t=>t.value===e.auth)&&(B.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 ne(),we(e.fields||[]),Se(e.sort||""),A.textContent="";for(const[t,o]of Object.entries(e.filter||{})){const{field:a,op:r}=jt(t);oe(a,r,String(o))}he(),f()}x(M.effect(()=>{const e=U.value;de=e,n("ep-url-preview").textContent=St(e.project,e.path||"");const t=te.value,o=Y.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 it=e=>Tt(p,e||(w?"Edit":"New endpoint"));x(M.effect(()=>{const e=U.value.name;it(e||q.value?.name||"")}));function lt(){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 We(e?.path||""))o.appendChild(je(`:${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 wt(e?.filter,"query"))r.appendChild(je(`?${i}`,"queryParam",i,s[i],`optional - filters by ${i}`))}function je(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,lt()}));let qe="";const V=()=>{const e=j.get(Z);return Array.isArray(e)?e:[]};function Ae(e){const t=V().filter(o=>o.url!==e.url);j.set(Z,[e,...t].slice(0,Lt)),ae()}function Re(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||"",re()}function ae(){const e=n("tryit-history-list"),t=V();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 ${Xe(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 $e=(e,t)=>t?b(v.index,String(e)):b(v.key,JSON.stringify(String(e)));function ct(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 Le(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($e(t,o)),m.appendChild(b(v.punc,": "))),m.appendChild(a?b(v.punc,Array.isArray(e)?"[]":"{}"):ct(e)),m}const s=Array.isArray(e),i=s?e.map((m,X)=>[X,m,!0]):Object.entries(e).map(([m,X])=>[m,X,!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($e(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;"),H=b(v.punc,s?"]":"}");u.append(c,H),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,X,bt]of i)C.appendChild(Le(X,m,bt));l.appendChild(C);const J=document.createElement("div");J.style.paddingLeft="1.2em",J.appendChild(b(v.punc,s?"]":"}")),l.appendChild(J);let le=!1;const He=m=>{le=m,g.textContent=m?"\u25B8":"\u25BE",c.style.display=m?"":"none",H.style.display=m?"":"none",C.style.display=m?"none":"",J.style.display=m?"none":""};return He(!1),u.addEventListener("click",()=>He(!le)),l}let Ne="";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 Me(e){Ne=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(Le(t)),n("tryit-json-toggle").style.display=o?"":"none",Te(o)}async function re(){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}=qt(e,{params:t,queryParams:o,extra:a,token:n("tryit-token").value.trim(),origin:location.origin});qe=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}),H=await c.text();if(D)return;g.textContent=`${c.status} ${c.statusText}`,g.className=`epx-chip ${Xe(c.status,c.ok)}`;const C=[];c.headers.forEach((J,le)=>C.push(`${le}: ${J}`)),n("tryit-headers-group").style.display="",n("tryit-headers").textContent=C.length?C.join(`
2
+ `):"(none exposed)",Me(H),Ae({...l,status:c.status,statusText:c.statusText,ok:c.ok})}catch(c){if(D)return;g.textContent="Network error",g.className="epx-chip is-danger",n("tryit-headers-group").style.display="none",Me(String(c.message||c)),Ae({...l,status:0,statusText:"Network error",ok:!1})}finally{D||(u.disabled=h.peek()||!q.peek())}}async function se(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 ie({thenSend:e=!1}={}){if(ee.peek())return!1;Y.value=!0;const t=K(),o=Ve(t);if(Object.keys(o).length){const a=Object.keys(o)[0];return Ge.activate?.(["filter","limit"].includes(a)?1:0),E.toast(o[a],{type:"error"}),!1}ee.value=!0;try{if(w){const a=await _.apiEndpoints.update(w,t);if(D)return!0;q.value=a,$=K(),P.value=z($),j.remove(F),E.toast("Endpoint saved.",{type:"success"}),Ye("#/api-endpoints"),e&&re()}else{const a=await _.apiEndpoints.create(t);$=K(),P.value=z($),j.remove(F),E.toast("Endpoint created.",{type:"success"}),Ye("#/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{ee.value=!1}}let $=null;async function pt(){!h.peek()||!await E.confirm("Discard the changes you have not saved?")||Q()&&(await be($),Y.value=!1,j.remove(F))}function dt(e){if(!e)return[];const t=[...A.querySelectorAll(".epx-frow")],o=t.indexOf(e),a=We(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=ve()[o];oe(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(),he(),f()}}]}const ut=E.contextMenu(A,{cursor:"glyph",match:".epx-frow",inherit:!1,density:"compact",exclude:"input, select, textarea",items:e=>dt(e)});x(()=>ut?.destroy?.());const Pt=n("ep-fields-list"),ge=e=>{for(const t of p.querySelectorAll(".ep-field-cb"))t.checked=e(t.checked);f(),me()},ft=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:()=>ge(()=>!0)},{label:"Tick none (return everything)",icon:"x",action:()=>ge(()=>!1)},{label:"Invert",icon:"refresh-cw",action:()=>ge(e=>!e)}]:[]});x(()=>ft?.destroy?.());const Pe=n("tryit-history-list"),mt=e=>{const t=V()[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:()=>Re(t)},{label:"Copy address",icon:"link",action:()=>se(location.origin+t.url,"Address")},{label:"Copy as curl",icon:"copy",action:()=>se(`curl '${(location.origin+t.url).replace(/'/g,"'\\''")}'`,"curl command")},{type:"divider"},{label:"Remove from history",icon:"trash",danger:!0,action:()=>{j.set(Z,V().filter(a=>a.url!==t.url)),ae()}}]},yt=E.contextMenu(Pe,{cursor:"glyph",match:".epx-hrow",inherit:!1,density:"compact",items:mt});x(()=>yt?.destroy?.());const Ie=e=>{const t=e.target.closest(".epx-kebab");if(t&&p.contains(t)){Ue(t);return}const o=e.target.closest(".epx-hrow");if(o&&Pe.contains(o)){const a=V()[Number(o.dataset.index)];if(!a)return;if(h.peek()){E.toast("Save first - Send tests the saved definition.",{type:"info"});return}Re(a)}},Oe=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(),Ue(t))}else e.key==="Enter"&&e.target.classList?.contains("epx-hrow")&&(e.preventDefault(),e.target.click())};if(p.addEventListener("click",Ie),p.addEventListener("keydown",Oe),x(()=>{p.removeEventListener("click",Ie),p.removeEventListener("keydown",Oe)}),y?(N.value=y.project,N.disabled=!0,await be({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 ne(),oe(),f()),!Q())return;$=K(),P.value=z($);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);B.addEventListener("change",f),N.addEventListener("change",async()=>{ce(),await ne(),f(),!w&&!n("ep-name").value.trim()&&!n("ep-path").value.trim()&&!ve().some(e=>e.value.trim())&&($=K(),P.value=z($))}),k.addEventListener("change",async()=>{await ne(),f()}),n("ep-add-filter").addEventListener("click",()=>{oe().querySelector(".ep-f-value").focus(),f()}),n("tryit-send").addEventListener("click",re),n("tryit-save-send").addEventListener("click",()=>ie({thenSend:!0})),n("tryit-history-clear").addEventListener("click",async()=>{await E.confirm("Clear this endpoint's test history?")&&(j.set(Z,[]),ae())}),n("tryit-json-toggle").addEventListener("click",()=>Te(n("tryit-json").style.display==="none")),n("tryit-body-copy").addEventListener("click",()=>se(Ne,"Response")),n("tryit-copy-curl").addEventListener("click",()=>se(qe,"curl command"));const _e={dirty:h,saving:ee,barText:st,err:at,pathErr:rt},De={save:()=>ie(),discard:()=>pt()},ht=M.applyBindings(_e,n("ep-bar"),{methods:De}),vt=M.applyBindings(_e,n("ep-def-panel"),{methods:De});x(()=>{ht.dispose(),vt.dispose()}),x(gt(p,{position:"bottom"})),Je(d,{id:"ep-back",icon:"arrow-left",title:"All endpoints",onClick:()=>{location.hash="#/api-endpoints"}}),Je(d,{id:"ep-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>ie()});const Fe=e=>{if(!(e.metaKey||e.ctrlKey))return;const o=e.key.toLowerCase();o==="s"?(e.preventDefault(),e.stopImmediatePropagation(),ie()):o==="enter"&&(e.preventDefault(),q.peek()&&!h.peek()?re():h.peek()&&E.toast("Save first - Send tests the saved definition.",{type:"info"}))};document.addEventListener("keydown",Fe,!0),x(()=>document.removeEventListener("keydown",Fe,!0));const Be=()=>{!h.peek()||!de||j.set(F,{state:de,base:P.peek(),at:new Date().toISOString()})},Ke=e=>{h.peek()&&(Be(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ke),x(()=>window.removeEventListener("beforeunload",Ke)),L={path:xe,dirty:()=>h.peek(),dispose(){Be(),D=!0,T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},ae(),I.scan(p);const W=j.get(F),Ee=Ct(W);if(Ee){j.remove(F);const e=W.base&&W.base!==P.peek(),t=W.at?` (${At(W.at)})`:"";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?`)&&Q()&&(w&&(Ee.project=y.project),await be(Ee))}}};
@@ -1,14 +1,14 @@
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=`
1
+ import{api as q}from"../api.js";import{addBannerAction as J}from"../lib/plugin-chrome.js";import{helpPopovers as qe}from"../lib/help-popover.js";import{openMenuAt as he,sidebarCount as Ie}from"../lib/tool-kit.js";import{createSimpleEditor as ye}from"../lib/simple-editor.js";import{checkForm as Fe,draftKey as g,draftOffer as Ne,escapeHtml as ke,extractPlaceholders as Pe,formFromBlock as De,formatHtml as Ve,payloadFromForm as Be,renderPreview as Ke,sampleValue as Se,scopeName as Ue,shortcodeFor as He,snapshot as G}from"../lib/block-editor-arrange.js";import{fmtDateTime as ge}from"../lib/dates.js";const Ye=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=c=>String(c||"").split("?")[0],Q="block-editor.mode",We=["write","split","preview"],_e=`
2
2
  <div class="bex-sample" data-key="{{key}}" tabindex="0">
3
3
  <label class="bex-sample-label" for="bex-sample-{{key}}">{{label}}</label>
4
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
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">
6
+ </div>`;let p=null,we=!1;function ze(){we||!window.R?.use||(we=!0,R.use((c,i,s)=>{if(!(Ye.test(i?.path||"")&&F(c?.path)!==F(i?.path))||!p?.dirty())return s();const r=p;return E.confirm(`You have unsaved changes to ${ke(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,X=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),a=X?decodeURIComponent(X[1]):null,l=!a,Ee=location.hash,Ce=F(location.hash.slice(1)),xe=l&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",Z=E.loader(i,{type:"dots"});let b=null,D=[],V=[];try{[b,D,V]=await Promise.all([a?q.blocks.get(a):Promise.resolve(null),q.blocks.list().catch(()=>[]),q.projects.list().catch(()=>[])])}catch(e){Z.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(Z.destroy(),!s.isConnected)return;const ee=(Array.isArray(D)?D:[]).map(e=>e.name).sort((e,t)=>e.localeCompare(t));let te=b?.meta||{};const u=De(b?{...b,name:a}:null,{project:xe}),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()])),oe=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),w=M.observable(G(u)),C=M.computed(()=>G(oe())!==w.value),x=M.observable(!1),O=M.observable(!1),T=M.computed(()=>Fe(oe(),{isNew:l,taken:ee}));let ae="split";try{ae=We.includes(S.get(Q))?S.get(Q):"split"}catch{}const h=M.observable(ae);r(M.effect(()=>{const e=h.value;try{S.set(Q,e)}catch{}}));const ne=M.observable("Ln 1, Col 1"),y={},K=M.observable(0),U=M.observable([]);r(M.effect(()=>{const e=Pe(n.content.value);for(const t of e)t in y||(y[t]=Se(t));U.value=e.map(t=>({key:t,label:`{{${t}}}`,value:y[t]}))}));const Me={...n,dirty:C,saving:x,mode:h,cursor:ne,sampleRows:U,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 ${ge(b.updatedAt||b.meta.updatedAt)}`),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)&&T.value.errors.name||""),cssErr:M.computed(()=>T.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${Ue(n.name.value)}"]`),noSamples:M.computed(()=>U.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(T.value.errors).length>0)},H=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,H.appendChild(t)}if(u.project&&![...H.options].some(e=>e.value===u.project)){const e=document.createElement("option");e.value=u.project,e.textContent=`${u.project} (no longer a project)`,H.appendChild(e)}const $=s.querySelector("#block-quick-switch");for(const e of ee){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 se(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 re=s.querySelector("#block-preview-output");let Y=0;r(M.effect(()=>{const e={content:n.content.value,css:n.css.value,name:n.name.value};K.value,h.value!=="write"&&(cancelAnimationFrame(Y),Y=requestAnimationFrame(()=>{P()&&(re.innerHTML=Ke(e,y),I.scan(re))}))})),r(()=>cancelAnimationFrame(Y));const ce=e=>{const t=e.target?.dataset?.sample;t&&(y[t]=e.target.value,K.value++)};m.addEventListener("input",ce),r(()=>m.removeEventListener("input",ce));function W(e){o.focus(),o.setRangeText(e,o.selectionStart,o.selectionEnd,"end"),o.dispatchEvent(new Event("input",{bubbles:!0}))}async function le(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)),W(""));break;case"copy":o.selectionStart!==o.selectionEnd&&await N(o.value.slice(o.selectionStart,o.selectionEnd));break;case"paste":try{W(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=Ve(o.value);k.setValue(t),n.content.value=t;break}default:break}}async function _(){if(x.peek())return;O.value=!0;const{errors:e,name:t}=T.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=Be(B(),te);if(await q.blocks.put(v,d),!P())return;te=d.meta,w.value=G(B());try{S.remove?S.remove(g(a)):S.set(g(a),null)}catch{}Ie("#/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,se(je(w.peek())))}function je(e){try{const[t,v,d,Re,Te]=JSON.parse(e);return{name:t,project:v,bundled:d,content:Re,css:Te}}catch{return u}}const Le=M.applyBindings(Me,s,{methods:{save:()=>_(),discard:()=>$e(),setWrite:()=>{h.value="write"},setSplit:()=>{h.value="split"},setPreview:()=>{h.value="preview"},act:e=>le(e)}});r(()=>Le.dispose()),r(qe(s,{position:"bottom"})),k=ye(s.querySelector(".bex-write"),{initialValue:u.content,language:"html",autoIndent:!0,onChange:e=>{n.content.value=e},onCursor:({line:e,col:t})=>{ne.value=`Ln ${e}, Col ${t}`}}),j=ye(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
8
  <div class="card-body">
9
9
  <h3>{{title}}</h3>
10
10
  <p>{{message}}</p>
11
11
  </div>
12
12
  </div>`,j.textarea.setAttribute("aria-label","Custom CSS"),j.textarea.placeholder=`/* Scoped to this block - e.g.
13
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)}}}};
14
+ h3 { font-size: 1.25rem; } */`,r(()=>{k.destroy(),j.destroy()}),o=k.textarea;const ie=s.querySelector(".bex-toolbar"),ue=e=>{const t=e.target.closest("[data-action]");t&&le(t.dataset.action)};ie.addEventListener("click",ue),r(()=>ie.removeEventListener("click",ue));const de=()=>{const e=$.value;e&&e!==a?location.hash=`#/blocks/edit/${encodeURIComponent(e)}`:$.value=a||""};$.addEventListener("change",de),r(()=>$.removeEventListener("change",de));const Ae=e=>[{header:`{{${e}}}`},{label:"Insert into the template",icon:"corner-down-right",action:()=>W(`{{${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]=Se(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 pe=e=>{const t=e.target.closest(".bex-kebab");t&&he(t)},me=e=>{const t=e.target.closest(".bex-sample");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),he(t))};m.addEventListener("click",pe),m.addEventListener("keydown",me),r(()=>{m.removeEventListener("click",pe),m.removeEventListener("keydown",me)}),Je(i,l?"New block":a),J(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():J(c,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=He(a);E.toast(await N(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),J(c,{id:"block-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>_()});const ve=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),_())};document.addEventListener("keydown",ve,!0),r(()=>document.removeEventListener("keydown",ve,!0));const be=()=>{if(C.peek())try{S.set(g(a),{form:B(),base:w.peek(),at:new Date().toISOString()})}catch{}},fe=e=>{C.peek()&&(be(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",fe),r(()=>window.removeEventListener("beforeunload",fe)),p={hash:Ee,path:Ce,dirty:()=>C.peek(),label:()=>l?"the new block":`"${a}"`,dispose(){A=!0,be(),f.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let z=null;try{z=S.get(g(a))}catch{}const L=Ne(z,w.peek());if(z)try{S.remove?S.remove(g(a)):S.set(g(a),null)}catch{}if(L){const e=L.at?` (${ge(L.at)})`:"",t=l?"a new block":`"${ke(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),se(d)}}}};
@@ -1,4 +1,4 @@
1
- import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as te}from"../lib/plugin-chrome.js";import{helpPopovers as ne}from"../lib/help-popover.js";import{onceClosed as ae,openMenuAt as se,sidebarCount as Me,viewLifetime as Le}from"../lib/tool-kit.js";import{filterByProject as Ie,getProjectFromHash as Te,renderProjectFilterNotice as je}from"../lib/project-context.js";import{FILTERS as $e,arrangeBlocks as De,blockRow as Ne,checkNewName as Be,copyName as oe,shortcodeFor as le,usageText as ce}from"../lib/blocks-arrange.js";const re=/^#\/(blocks|projects\/[^/?#]+\/blocks)\/?(\?.*)?$/,Fe="#/blocks",N="blocks.draft",U="blocks.filter",Re=`
1
+ import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as ee}from"../lib/plugin-chrome.js";import{helpPopovers as te}from"../lib/help-popover.js";import{onceClosed as ne,openMenuAt as ae,sidebarCount as Se,viewLifetime as Me}from"../lib/tool-kit.js";import{filterByProject as Le,getProjectFromHash as Ie,renderProjectFilterNotice as Te}from"../lib/project-context.js";import{FILTERS as je,arrangeBlocks as $e,blockRow as Ne,checkNewName as De,copyName as se,shortcodeFor as oe,usageText as le}from"../lib/blocks-arrange.js";import{fmtDate as Be}from"../lib/dates.js";const ce=/^#\/(blocks|projects\/[^/?#]+\/blocks)\/?(\?.*)?$/,Fe="#/blocks",N="blocks.draft",A="blocks.filter",Re=`
2
2
  <div class="blx-row" data-name="{{name}}" tabindex="0" data-bind-class="unused && 'is-unused'">
3
3
  <span class="blx-icon" aria-hidden="true"><span data-icon="box" data-icon-size="16"></span></span>
4
4
  <span class="blx-main"><strong>{{name}}</strong><span data-bind-hidden="!description">{{description}}</span><small>{{meta}}</small></span>
@@ -10,13 +10,13 @@ import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as te}fr
10
10
  </span>
11
11
  <span class="blx-uses" title="Pages and views that use it">{{uses}}</span>
12
12
  <button type="button" class="blx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
13
- </div>`,g=c=>String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ie(c){const a=document.createElement("template");return a.innerHTML=c.trim(),a.content.firstElementChild}const r=Le(re);async function qe(c){try{return await navigator.clipboard.writeText(c),!0}catch{const a=document.createElement("textarea");a.value=c,a.style.cssText="position:fixed;opacity:0;",document.body.appendChild(a),a.select();let b=!1;try{b=document.execCommand("copy")}catch{}return a.remove(),b}}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(c){r.reset();const a=c.get(0).querySelector(".blx")||c.get(0),b=()=>re.test(location.hash)&&a.isConnected,w=Te(),p=M.observable([]),H=M.observable(!0),B=M.observable("");let O="all";try{O=$e.includes(S.get(U))?S.get(U):"all"}catch{}const y=M.observable(O),P=M.observable("");r.add(M.effect(()=>{const e=y.value;try{S.set(U,e)}catch{}}));const de=e=>{try{return D(e).format("D MMM YYYY")}catch{return String(e).slice(0,10)}},F=M.observable([]);r.add(M.effect(()=>{F.value=De(p.value,{filter:y.value,query:P.value}).map(e=>Ne(e,de))}));const ue=M.computed(()=>{const e=p.value,t=e.filter(n=>(n.usage?.pages?.length||0)+(n.usage?.views?.length||0)>0).length;return{all:e.length,used:t,unused:e.length-t}}),pe=M.computed(()=>p.value.length?"No blocks match.":"No blocks yet - New block in the banner makes one, or Import brings one in."),R=e=>p.peek().find(t=>t.name===e),me=w?`#/blocks/new?project=${encodeURIComponent(w)}`:"#/blocks/new",Y=e=>`#/blocks/edit/${encodeURIComponent(e)}`;async function K(){try{let e=await Ce("/blocks?usage=1",{method:"GET"});if(!b())return;if(e=Array.isArray(e)?e:[],w){const t=e.length;e=Ie(e,w),je(a,{total:t,shown:e.length,slug:w,allHref:"#/blocks",noun:"blocks"})}p.value=e,B.value=""}catch(e){B.value=`Could not load the blocks: ${e.message||e}`}finally{H.value=!1}}const T=async()=>{Me(Fe),b()&&await K()};async function he(e){const n=(e.usage?.pages?.length||0)+(e.usage?.views?.length||0)?` It is ${g(ce(e).toLowerCase())} - they will show nothing where it was.`:"";if(await E.confirm(`Delete the block "${g(e.name)}"?${n} This cannot be undone.`))try{await v.blocks.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),await T()}catch(i){E.toast(i.message||"Failed to delete block.",{type:"error"})}}async function be(e){try{await v.blocks.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}async function fe(e){const t=le(e),n=await qe(t);E.toast(n?`Copied ${t}`:`Could not copy - your browser refused. The shortcode is ${t}`,{type:n?"success":"error"})}const k=a.querySelector(".blx-import-file");async function W(){const e=k.files?.[0];if(!e)return;k.value="";let t;try{t=JSON.parse(await e.text())}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!t||typeof t!="object"||!t.name||typeof t.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const n=await v.blocks.importBundle(t);E.toast(`Imported "${n.name}".`,{type:"success"}),await T()}catch(n){if(n.code==="CONFLICT"){if(!await E.confirm(`A block named "${g(n.name)}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const i=await v.blocks.importBundle(t,{overwrite:!0});E.toast(`Overwrote "${i.name}".`,{type:"success"}),await T()}catch(i){E.toast(i.message||"Import failed.",{type:"error"})}return}E.toast(n.message||"Import failed.",{type:"error"})}}k.addEventListener("change",W),r.add(()=>k.removeEventListener("change",W));const x=new Set;let f=null,z=!1;function ye(e){const t=E.slideover({title:`Where ${g(e.name)} is used`,size:"sm",position:"right"}),n=ie(`
13
+ </div>`,g=c=>String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ie(c){const a=document.createElement("template");return a.innerHTML=c.trim(),a.content.firstElementChild}const i=Me(ce);async function qe(c){try{return await navigator.clipboard.writeText(c),!0}catch{const a=document.createElement("textarea");a.value=c,a.style.cssText="position:fixed;opacity:0;",document.body.appendChild(a),a.select();let h=!1;try{h=document.execCommand("copy")}catch{}return a.remove(),h}}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(c){i.reset();const a=c.get(0).querySelector(".blx")||c.get(0),h=()=>ce.test(location.hash)&&a.isConnected,w=Ie(),p=M.observable([]),U=M.observable(!0),D=M.observable("");let H="all";try{H=je.includes(S.get(A))?S.get(A):"all"}catch{}const y=M.observable(H),O=M.observable("");i.add(M.effect(()=>{const e=y.value;try{S.set(A,e)}catch{}}));const re=Be,B=M.observable([]);i.add(M.effect(()=>{B.value=$e(p.value,{filter:y.value,query:O.value}).map(e=>Ne(e,re))}));const de=M.computed(()=>{const e=p.value,t=e.filter(n=>(n.usage?.pages?.length||0)+(n.usage?.views?.length||0)>0).length;return{all:e.length,used:t,unused:e.length-t}}),ue=M.computed(()=>p.value.length?"No blocks match.":"No blocks yet - New block in the banner makes one, or Import brings one in."),F=e=>p.peek().find(t=>t.name===e),pe=w?`#/blocks/new?project=${encodeURIComponent(w)}`:"#/blocks/new",P=e=>`#/blocks/edit/${encodeURIComponent(e)}`;async function K(){try{let e=await Ce("/blocks?usage=1",{method:"GET"});if(!h())return;if(e=Array.isArray(e)?e:[],w){const t=e.length;e=Le(e,w),Te(a,{total:t,shown:e.length,slug:w,allHref:"#/blocks",noun:"blocks"})}p.value=e,D.value=""}catch(e){D.value=`Could not load the blocks: ${e.message||e}`}finally{U.value=!1}}const T=async()=>{Se(Fe),h()&&await K()};async function me(e){const n=(e.usage?.pages?.length||0)+(e.usage?.views?.length||0)?` It is ${g(le(e).toLowerCase())} - they will show nothing where it was.`:"";if(await E.confirm(`Delete the block "${g(e.name)}"?${n} This cannot be undone.`))try{await v.blocks.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),await T()}catch(r){E.toast(r.message||"Failed to delete block.",{type:"error"})}}async function be(e){try{await v.blocks.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}async function he(e){const t=oe(e),n=await qe(t);E.toast(n?`Copied ${t}`:`Could not copy - your browser refused. The shortcode is ${t}`,{type:n?"success":"error"})}const k=a.querySelector(".blx-import-file");async function W(){const e=k.files?.[0];if(!e)return;k.value="";let t;try{t=JSON.parse(await e.text())}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!t||typeof t!="object"||!t.name||typeof t.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const n=await v.blocks.importBundle(t);E.toast(`Imported "${n.name}".`,{type:"success"}),await T()}catch(n){if(n.code==="CONFLICT"){if(!await E.confirm(`A block named "${g(n.name)}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const r=await v.blocks.importBundle(t,{overwrite:!0});E.toast(`Overwrote "${r.name}".`,{type:"success"}),await T()}catch(r){E.toast(r.message||"Import failed.",{type:"error"})}return}E.toast(n.message||"Import failed.",{type:"error"})}}k.addEventListener("change",W),i.add(()=>k.removeEventListener("change",W));const x=new Set;let f=null,z=!1;function fe(e){const t=E.slideover({title:`Where ${g(e.name)} is used`,size:"sm",position:"right"}),n=ie(`
14
14
  <div class="blx-form">
15
15
  <p class="blx-note blx-where-sum"></p>
16
16
  <ul class="blx-where"></ul>
17
17
  <div><div class="form-label">Shortcode</div><div class="blx-code"></div>
18
18
  <small class="blx-hint">Right-click the block in the list and Copy shortcode to paste it into a page.</small></div>
19
- </div>`);n.querySelector(".blx-where-sum").textContent=ce(e)+".",n.querySelector(".blx-code").textContent=le(e);const i=n.querySelector(".blx-where"),h=(o,j,$)=>{const L=document.createElement("li"),s=document.createElement("a");s.href=o,s.textContent=j,s.addEventListener("click",()=>t.close());const d=document.createElement("small");d.textContent=$,L.append(s,d),i.appendChild(L)};for(const o of e.usage?.pages||[])h(`#/pages/edit${o}`,o,"page");for(const o of e.usage?.views||[])h(`#/views/edit/${encodeURIComponent(o)}`,o,"view");t.element.appendChild(n),x.add(t),ae(t,()=>x.delete(t)),t.open()}function _(e,t=null){const n=()=>p.peek().map(l=>l.name),i=t?.name??oe(e.name,n()),h=M.observable(i),o=M.observable(!1),j=M.computed(()=>Be(h.value,p.value.map(l=>l.name))),$={name:h,saving:o,err:j,cannot:M.computed(()=>o.value||!!j.value)},L=()=>h.peek()!==oe(e.name,n())||!!t,s=E.slideover({title:`Duplicate ${g(e.name)}`,size:"sm",position:"right"}),d=ie(`
19
+ </div>`);n.querySelector(".blx-where-sum").textContent=le(e)+".",n.querySelector(".blx-code").textContent=oe(e);const r=n.querySelector(".blx-where"),b=(o,j,$)=>{const L=document.createElement("li"),s=document.createElement("a");s.href=o,s.textContent=j,s.addEventListener("click",()=>t.close());const d=document.createElement("small");d.textContent=$,L.append(s,d),r.appendChild(L)};for(const o of e.usage?.pages||[])b(`#/pages/edit${o}`,o,"page");for(const o of e.usage?.views||[])b(`#/views/edit/${encodeURIComponent(o)}`,o,"view");t.element.appendChild(n),x.add(t),ne(t,()=>x.delete(t)),t.open()}function _(e,t=null){const n=()=>p.peek().map(l=>l.name),r=t?.name??se(e.name,n()),b=M.observable(r),o=M.observable(!1),j=M.computed(()=>De(b.value,p.value.map(l=>l.name))),$={name:b,saving:o,err:j,cannot:M.computed(()=>o.value||!!j.value)},L=()=>b.peek()!==se(e.name,n())||!!t,s=E.slideover({title:`Duplicate ${g(e.name)}`,size:"sm",position:"right"}),d=ie(`
20
20
  <div class="blx-form">
21
21
  <div>
22
22
  <label class="form-label" for="blx-dup-name">Name of the copy
@@ -31,4 +31,4 @@ import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as te}fr
31
31
  <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
32
32
  <span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
33
33
  </div>
34
- </div>`);let q=!1;async function X(){if($.cannot.peek())return;o.value=!0;const l=h.peek().trim();try{const u=await v.blocks.get(e.name);await v.blocks.put(l,{content:u.content,css:u.css||"",...u.meta?.project&&{meta:{project:u.meta.project}}}),E.toast(`Made "${l}".`,{type:"success"}),q=!0,s.close(),await T()}catch(u){E.toast(`Not duplicated: ${u.message||u}`,{type:"error"})}finally{o.value=!1}}s.element.appendChild(d);const ke=ne(d),xe=M.applyBindings($,d,{methods:{save:X,cancel:()=>s.close()}}),Ee=l=>{l.key==="Enter"&&l.target.matches("input")&&(l.preventDefault(),X())};d.addEventListener("keydown",Ee);const Z={dirty:L,values:()=>({name:h.peek()}),source:e.name};f=Z,x.add(s),ae(s,()=>{xe.dispose(),ke(),x.delete(s),f===Z&&(f=null)});const Se=s.close.bind(s);let A=!1;s.close=async l=>{if(!q&&!z&&l!==!0&&L()){if(A)return;A=!0;const u=await E.confirm("Discard the name you typed?");if(A=!1,!u)return}return q=!0,Se()},I.scan(d),s.open();const ee=d.querySelector("#blx-dup-name");ee?.focus(),ee?.select()}function ve(e){const t=R(e);if(!t)return[];const n=(t.usage?.pages?.length||0)+(t.usage?.views?.length||0);return[{header:t.name},{label:"Edit",icon:"edit",action:()=>{location.hash=Y(t.name)}},{label:"Copy shortcode",icon:"copy",action:()=>fe(t)},{label:n?`Where it is used (${n})\u2026`:"Where it is used\u2026",icon:"link",action:()=>ye(t)},{type:"divider"},{label:"Duplicate\u2026",icon:"layers",action:()=>_(t)},{label:"Export (.dmblock.json)",icon:"download",action:()=>be(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>he(t)}]}const m=a.querySelector(".blx-list");m.innerHTML=Re;const ge=M.applyBindings({blocks:p,loading:H,loadError:B,filter:y,query:P,rows:F,counts:ue,emptyText:pe},a,{methods:{showAll:()=>{y.value="all"},showUsed:()=>{y.value="used"},showUnused:()=>{y.value="unused"}}});r.add(()=>ge.dispose()),r.add(ne(a,{position:"bottom"})),r.add(M.effect(()=>{F.value,requestAnimationFrame(()=>{m.isConnected&&I.scan(m)})}));const we=E.contextMenu(m,{cursor:"glyph",match:".blx-row",inherit:!1,density:"compact",items:e=>ve(e.dataset.name)});r.add(()=>we?.destroy?.());const J=e=>{R(e)&&(location.hash=Y(e))},V=e=>{const t=e.target.closest(".blx-row");if(!t)return;const n=e.target.closest(".blx-kebab");if(n){se(n);return}J(t.dataset.name)},G=e=>{const t=e.target.closest(".blx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),se(t)):e.key==="Enter"&&e.target===t&&J(t.dataset.name))};m.addEventListener("click",V),m.addEventListener("keydown",G),r.add(()=>{m.removeEventListener("click",V),m.removeEventListener("keydown",G)}),te(c,{id:"blocks-import",icon:"upload",title:"Import a .dmblock.json bundle",onClick:()=>k.click()}),te(c,{id:"blocks-new",icon:"plus",title:"New block",onClick:()=>{location.hash=me}});const Q=e=>{f?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Q),r.add(()=>window.removeEventListener("beforeunload",Q)),r.add(()=>{if(f?.dirty())try{S.set(N,{source:f.source,values:f.values(),at:new Date().toISOString()})}catch{}z=!0,[...x].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(a),await K(),!b())return;let C=null;try{C=S.get(N)}catch{}if(C?.values&&C.source){try{S.remove?S.remove(N):S.set(N,null)}catch{}const e=R(C.source);e&&await E.confirm(`You were duplicating "${g(e.name)}" and left it unfinished. Pick up where you left off?`)&&b()&&_(e,C.values)}}};
34
+ </div>`);let R=!1;async function Q(){if($.cannot.peek())return;o.value=!0;const l=b.peek().trim();try{const u=await v.blocks.get(e.name);await v.blocks.put(l,{content:u.content,css:u.css||"",...u.meta?.project&&{meta:{project:u.meta.project}}}),E.toast(`Made "${l}".`,{type:"success"}),R=!0,s.close(),await T()}catch(u){E.toast(`Not duplicated: ${u.message||u}`,{type:"error"})}finally{o.value=!1}}s.element.appendChild(d);const we=te(d),ke=M.applyBindings($,d,{methods:{save:Q,cancel:()=>s.close()}}),xe=l=>{l.key==="Enter"&&l.target.matches("input")&&(l.preventDefault(),Q())};d.addEventListener("keydown",xe);const X={dirty:L,values:()=>({name:b.peek()}),source:e.name};f=X,x.add(s),ne(s,()=>{ke.dispose(),we(),x.delete(s),f===X&&(f=null)});const Ee=s.close.bind(s);let q=!1;s.close=async l=>{if(!R&&!z&&l!==!0&&L()){if(q)return;q=!0;const u=await E.confirm("Discard the name you typed?");if(q=!1,!u)return}return R=!0,Ee()},I.scan(d),s.open();const Z=d.querySelector("#blx-dup-name");Z?.focus(),Z?.select()}function ye(e){const t=F(e);if(!t)return[];const n=(t.usage?.pages?.length||0)+(t.usage?.views?.length||0);return[{header:t.name},{label:"Edit",icon:"edit",action:()=>{location.hash=P(t.name)}},{label:"Copy shortcode",icon:"copy",action:()=>he(t)},{label:n?`Where it is used (${n})\u2026`:"Where it is used\u2026",icon:"link",action:()=>fe(t)},{type:"divider"},{label:"Duplicate\u2026",icon:"layers",action:()=>_(t)},{label:"Export (.dmblock.json)",icon:"download",action:()=>be(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>me(t)}]}const m=a.querySelector(".blx-list");m.innerHTML=Re;const ve=M.applyBindings({blocks:p,loading:U,loadError:D,filter:y,query:O,rows:B,counts:de,emptyText:ue},a,{methods:{showAll:()=>{y.value="all"},showUsed:()=>{y.value="used"},showUnused:()=>{y.value="unused"}}});i.add(()=>ve.dispose()),i.add(te(a,{position:"bottom"})),i.add(M.effect(()=>{B.value,requestAnimationFrame(()=>{m.isConnected&&I.scan(m)})}));const ge=E.contextMenu(m,{cursor:"glyph",match:".blx-row",inherit:!1,density:"compact",items:e=>ye(e.dataset.name)});i.add(()=>ge?.destroy?.());const Y=e=>{F(e)&&(location.hash=P(e))},J=e=>{const t=e.target.closest(".blx-row");if(!t)return;const n=e.target.closest(".blx-kebab");if(n){ae(n);return}Y(t.dataset.name)},V=e=>{const t=e.target.closest(".blx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ae(t)):e.key==="Enter"&&e.target===t&&Y(t.dataset.name))};m.addEventListener("click",J),m.addEventListener("keydown",V),i.add(()=>{m.removeEventListener("click",J),m.removeEventListener("keydown",V)}),ee(c,{id:"blocks-import",icon:"upload",title:"Import a .dmblock.json bundle",onClick:()=>k.click()}),ee(c,{id:"blocks-new",icon:"plus",title:"New block",onClick:()=>{location.hash=pe}});const G=e=>{f?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",G),i.add(()=>window.removeEventListener("beforeunload",G)),i.add(()=>{if(f?.dirty())try{S.set(N,{source:f.source,values:f.values(),at:new Date().toISOString()})}catch{}z=!0,[...x].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(a),await K(),!h())return;let C=null;try{C=S.get(N)}catch{}if(C?.values&&C.source){try{S.remove?S.remove(N):S.set(N,null)}catch{}const e=F(C.source);e&&await E.confirm(`You were duplicating "${g(e.name)}" and left it unfinished. Pick up where you left off?`)&&h()&&_(e,C.values)}}};
@@ -1,11 +1,11 @@
1
- import{api as g}from"../api.js";import{addBannerAction as pe}from"../lib/plugin-chrome.js";import{helpPopovers as De}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Le}from"../lib/tool-kit.js";import{FIELD_TYPES as rt,OPERATIONS as ct,OPTION_TYPES as dt,accessOptions as ut,checkForm as pt,draftOf as ft,duplicateField as mt,errorCount as H,fieldFromPanel as Ne,fieldProblem as vt,fieldRow as ht,formFromCollection as qe,formFromDraft as bt,moveField as Te,nameFromLabel as gt,newField as yt,panelForm as wt,payloadFromForm as xt,roleLadder as kt,slugify as ee,snapshot as U,storageChange as Pe}from"../lib/collection-editor-arrange.js";const Et=/^\/collections\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,St=s=>`collection-editor.draft.${s||"new"}`,Mt=`
1
+ import{api as g}from"../api.js";import{addBannerAction as ue}from"../lib/plugin-chrome.js";import{helpPopovers as Fe}from"../lib/help-popover.js";import{onceClosed as st,openMenuAt as Le}from"../lib/tool-kit.js";import{FIELD_TYPES as it,OPERATIONS as rt,OPTION_TYPES as ct,accessOptions as dt,checkForm as ut,draftOf as pt,duplicateField as ft,errorCount as K,fieldFromPanel as De,fieldProblem as mt,fieldRow as vt,formFromCollection as Ne,formFromDraft as ht,moveField as Te,nameFromLabel as bt,newField as gt,panelForm as yt,payloadFromForm as wt,roleLadder as xt,slugify as Z,snapshot as U,storageChange as qe}from"../lib/collection-editor-arrange.js";import{fmtDateTime as kt}from"../lib/dates.js";const Et=/^\/collections\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,St=s=>`collection-editor.draft.${s||"new"}`,Mt=`
2
2
  <div class="cex-row" data-key="{{key}}" tabindex="0" draggable="true" data-bind-class="rowCls">
3
3
  <span class="cex-grip" title="Drag to reorder" aria-hidden="true">\u283F</span>
4
4
  <span class="cex-main"><strong data-bind-text="label"></strong><small data-bind-text="meta"></small><small class="cex-problem" data-bind-hidden="!problem" data-bind-text="problem"></small></span>
5
5
  <span class="cex-chip is-ok" data-bind-hidden="!required">Required</span>
6
6
  <span class="cex-chip" data-bind-hidden="!hidden">Hidden</span>
7
7
  <button type="button" class="cex-kebab" aria-label="Actions for this field" title="Actions">\u22EE</button>
8
- </div>`,te=s=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function $t(s){const c=document.createElement("template");return c.innerHTML=s.trim(),c.content.firstElementChild}function Ie(s,c){const i=s.querySelector(".dm-plugin-banner-name");if(!i)return;const C=document.createElement("span");C.className="dm-plugin-banner-parent",C.textContent="Collections";const w=document.createElement("span");w.className="dm-plugin-banner-sep",w.setAttribute("aria-hidden","true"),w.textContent="\u203A",i.replaceChildren(C,w,document.createTextNode(c))}let y=null,Be=!1;const ne=s=>String(s||"").split("?")[0];function Ct(){Be||!window.R?.use||(Be=!0,R.use((s,c,i)=>{if(!(y&&Et.test(c?.path||"")&&ne(s?.path)!==ne(c?.path))||!y.dirty())return i();const w=`#${c.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(r=>{r?i():history.replaceState(null,"",w)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(y&&ne(s?.path)!==y.path){const c=y;y=null,c.dispose()}}))}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(s){if(Ct(),y){const e=y;y=null,e.dispose()}const c=s.get(0),i=c.querySelector(".cex")||c,C=ne(location.hash.replace(/^#/,""))||"/collections/new",w=C.match(/^\/collections\/edit\/([^/?#]+)/),r=w?decodeURIComponent(w[1]):null,P=!r,fe=[],b=e=>(fe.push(e),e);let me=!1;const oe=()=>!me&&i.isConnected,ve=E.loader(c,{type:"dots"});let O=null,ae=[],B=null,he=[],u=0,_=[];try{[O,ae,B,he,_]=await Promise.all([r?g.collections.get(r):Promise.resolve(null),g.projects.list().catch(()=>[]),g.collections.proStatus().catch(()=>null),P?g.collections.list().then(e=>(e||[]).map(t=>t.slug)).catch(()=>[]):Promise.resolve([]),g.collections.listEntries("roles",{limit:1e3}).then(kt).catch(()=>[])]),r&&(u=(await g.collections.listEntries(r,{limit:1}).catch(()=>null))?.total??0)}catch(e){ve.destroy(),E.toast(e?.status===404?"Collection not found.":`Could not load the collection: ${e.message||e}`,{type:"error"}),R.navigate("/collections");return}if(ve.destroy(),!i.isConnected)return;if(r&&!O){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const be=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",f=qe(O,{project:be,defaultRole:_.at(-1)?.name||""}),x=Object.fromEntries(f.fields.map(e=>[e._key,e.name])),_e=O?.meta||{};let W=f.storageAdapter;const Ke=new Set(["public","token",..._.map(e=>e.name)]),ge=(e,t)=>{if(!_.length)return"";const o=[...new Set(e.filter(a=>a&&!Ke.has(a)))];return o.length?`Not a role on this site: ${o.join(", ")}. Until you pick another, ${t}.`:""},n={};for(const[e,t]of Object.entries(f))n[e]=M.observable(t);const k=M.observable(P),Y=M.observable(!1),z=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),ye=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),L=M.observable(U(f)),K=M.computed(()=>U(ye())!==L.value);let G=!P;b(M.effect(()=>{const e=n.title.value;G||(n.slug.value=ee(e))}));const m=M.computed(()=>pt(ye(),{isNew:k.value,taken:he,original:x})),we=M.observable([]);b(M.effect(()=>{const e=m.value.fields||{},t=n.layout.value==="grid";we.value=n.fields.value.map(o=>ht(o,e,{grid:t}))}));const He=M.computed(()=>new Set(n.fields.value.map(e=>e.name))),Ue={...n,isNew:k,saving:Y,dirty:K,rows:we,errTitle:M.computed(()=>m.value.title||""),errSlug:M.computed(()=>m.value.slug||""),errColumns:M.computed(()=>m.value.columns||""),errExport:M.computed(()=>m.value.export||""),problemCount:M.computed(()=>H(m.value)),problemText:M.computed(()=>{const e=H(m.value);return e?`${e} thing${e===1?"":"s"} to fix before it can be saved`:""}),settingsBad:M.computed(()=>!!(m.value.title||m.value.slug||m.value.columns)),fieldsBad:M.computed(()=>!!m.value.fields),exportBad:M.computed(()=>!!m.value.export),fieldCount:M.computed(()=>n.fields.value.length),slugHint:M.computed(()=>k.value?`Will be created as "${ee(n.slug.value||n.title.value)||"\u2026"}".`:"Cannot be changed once the collection exists."),apiPath:M.computed(()=>`/api/collections/${ee(n.slug.value||n.title.value)||"<slug>"}/public`),unknownReadFields:M.computed(()=>{const e=String(n.readFields.value||"").split(",").map(t=>t.trim()).filter(t=>t&&!He.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),missingApiRoles:M.computed(()=>ge(ct.filter(e=>n[`${e}Enabled`].value).map(e=>n[`${e}Access`].value),"only the level-0 role passes")),missingExportRole:M.computed(()=>ge(n.exportEnabled.value?[n.exportAccess.value]:[],"only the level-0 role is offered export")),showStorage:M.observable(!!B?.pro&&r!=="roles"),storageNote:M.computed(()=>{const e=Pe(W,n.storageAdapter.value);return!e||k.value?"":u?`Switching ${e}: Save asks whether to move the ${u} existing ${u===1?"entry":"entries"} across. Export them first if they matter.`:`Switching ${e}. There are no entries to move.`})},xe=i.querySelector("#collection-project");for(const e of ae){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,xe.appendChild(t)}if(f.project&&!ae.some(e=>e.slug===f.project)){const e=document.createElement("option");e.value=f.project,e.textContent=`${f.project} (not found)`,xe.appendChild(e)}const We=[...i.querySelectorAll('.cex-op select[data-model$="Access.value"]')],ke=e=>{for(const t of We){const o=t.getAttribute("data-model").split(".")[0],a=e[o];t.replaceChildren(...ut(_,a,{token:t.id!=="export-access"}).map(d=>new Option(d.label,d.value))),t.value=a}};ke(f);const Ye=i.querySelector("#storage-connection"),ze=Array.isArray(B?.connections)&&B.connections.length?B.connections:["default"];for(const e of new Set([...ze,f.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,Ye.appendChild(t)}const N=e=>n.fields.peek().findIndex(t=>t._key===e),j=e=>{n.fields.value=e};function V(e,t){const o=N(e);o<0||(j(Te(n.fields.peek(),o,o+t)),requestAnimationFrame(()=>i.querySelector(`.cex-row[data-key="${e}"]`)?.focus()))}async function le(e){const t=N(e);if(t<0)return;const o=n.fields.peek()[t];!k.peek()&&u&&x[e]&&!await E.confirm(`Remove the field "${te(o.label||o.name)}"? The ${u} existing ${u===1?"entry keeps its":"entries keep their"} value for it, but it is no longer shown or edited. Nothing is lost until you Save - Discard puts it back.`)||j(n.fields.peek().filter(a=>a._key!==e))}function J(e,{isAdded:t=!1}={}){const o=N(e);if(o<0)return;const a=n.fields.peek()[o],d=wt(a),v={};for(const[l,$]of Object.entries(d))v[l]=M.observable($);const p=JSON.stringify(d),X=()=>Object.fromEntries(Object.keys(v).map(l=>[l,v[l].peek()])),Oe=()=>JSON.stringify(X())!==p;let je=!!a.name;const et=M.effect(()=>{const l=v.label.value;je||(v.name.value=gt(l))}),tt=new Set(n.fields.peek().filter(l=>l._key!==e).map(l=>l.name)),Ae=M.computed(()=>{const l=Ne(a,Object.fromEntries(Object.keys(v).map($=>[$,v[$].value])));return l.name&&tt.has(l.name)?`"${l.name}" is used by another field.`:vt(l,x[e])}),nt=M.computed(()=>!k.peek()&&u&&x[e]&&v.name.value.trim()!==x[e]?`Entries keep their value under "${x[e]}" - renaming the field does not move it.`:""),ot={...v,problem:Ae,renamed:nt,isOptions:M.computed(()=>dt.has(v.type.value)),isGrid:M.computed(()=>n.layout.value==="grid")},h=E.slideover({title:t?"New field":`Field: ${te(a.label||a.name||"Untitled")}`,size:"md",position:"right"}),F=$t(`
8
+ </div>`,ee=s=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function $t(s){const c=document.createElement("template");return c.innerHTML=s.trim(),c.content.firstElementChild}function Pe(s,c){const i=s.querySelector(".dm-plugin-banner-name");if(!i)return;const C=document.createElement("span");C.className="dm-plugin-banner-parent",C.textContent="Collections";const w=document.createElement("span");w.className="dm-plugin-banner-sep",w.setAttribute("aria-hidden","true"),w.textContent="\u203A",i.replaceChildren(C,w,document.createTextNode(c))}let y=null,Ie=!1;const te=s=>String(s||"").split("?")[0];function Ct(){Ie||!window.R?.use||(Ie=!0,R.use((s,c,i)=>{if(!(y&&Et.test(c?.path||"")&&te(s?.path)!==te(c?.path))||!y.dirty())return i();const w=`#${c.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(r=>{r?i():history.replaceState(null,"",w)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(y&&te(s?.path)!==y.path){const c=y;y=null,c.dispose()}}))}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(s){if(Ct(),y){const e=y;y=null,e.dispose()}const c=s.get(0),i=c.querySelector(".cex")||c,C=te(location.hash.replace(/^#/,""))||"/collections/new",w=C.match(/^\/collections\/edit\/([^/?#]+)/),r=w?decodeURIComponent(w[1]):null,q=!r,pe=[],b=e=>(pe.push(e),e);let fe=!1;const ne=()=>!fe&&i.isConnected,me=E.loader(c,{type:"dots"});let O=null,oe=[],P=null,ve=[],u=0,B=[];try{[O,oe,P,ve,B]=await Promise.all([r?g.collections.get(r):Promise.resolve(null),g.projects.list().catch(()=>[]),g.collections.proStatus().catch(()=>null),q?g.collections.list().then(e=>(e||[]).map(t=>t.slug)).catch(()=>[]):Promise.resolve([]),g.collections.listEntries("roles",{limit:1e3}).then(xt).catch(()=>[])]),r&&(u=(await g.collections.listEntries(r,{limit:1}).catch(()=>null))?.total??0)}catch(e){me.destroy(),E.toast(e?.status===404?"Collection not found.":`Could not load the collection: ${e.message||e}`,{type:"error"}),R.navigate("/collections");return}if(me.destroy(),!i.isConnected)return;if(r&&!O){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const he=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",f=Ne(O,{project:he,defaultRole:B.at(-1)?.name||""}),x=Object.fromEntries(f.fields.map(e=>[e._key,e.name])),Be=O?.meta||{};let H=f.storageAdapter;const _e=new Set(["public","token",...B.map(e=>e.name)]),be=(e,t)=>{if(!B.length)return"";const o=[...new Set(e.filter(a=>a&&!_e.has(a)))];return o.length?`Not a role on this site: ${o.join(", ")}. Until you pick another, ${t}.`:""},n={};for(const[e,t]of Object.entries(f))n[e]=M.observable(t);const k=M.observable(q),W=M.observable(!1),Y=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),ge=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),L=M.observable(U(f)),_=M.computed(()=>U(ge())!==L.value);let z=!q;b(M.effect(()=>{const e=n.title.value;z||(n.slug.value=Z(e))}));const m=M.computed(()=>ut(ge(),{isNew:k.value,taken:ve,original:x})),ye=M.observable([]);b(M.effect(()=>{const e=m.value.fields||{},t=n.layout.value==="grid";ye.value=n.fields.value.map(o=>vt(o,e,{grid:t}))}));const Ke=M.computed(()=>new Set(n.fields.value.map(e=>e.name))),Ue={...n,isNew:k,saving:W,dirty:_,rows:ye,errTitle:M.computed(()=>m.value.title||""),errSlug:M.computed(()=>m.value.slug||""),errColumns:M.computed(()=>m.value.columns||""),errExport:M.computed(()=>m.value.export||""),problemCount:M.computed(()=>K(m.value)),problemText:M.computed(()=>{const e=K(m.value);return e?`${e} thing${e===1?"":"s"} to fix before it can be saved`:""}),settingsBad:M.computed(()=>!!(m.value.title||m.value.slug||m.value.columns)),fieldsBad:M.computed(()=>!!m.value.fields),exportBad:M.computed(()=>!!m.value.export),fieldCount:M.computed(()=>n.fields.value.length),slugHint:M.computed(()=>k.value?`Will be created as "${Z(n.slug.value||n.title.value)||"\u2026"}".`:"Cannot be changed once the collection exists."),apiPath:M.computed(()=>`/api/collections/${Z(n.slug.value||n.title.value)||"<slug>"}/public`),unknownReadFields:M.computed(()=>{const e=String(n.readFields.value||"").split(",").map(t=>t.trim()).filter(t=>t&&!Ke.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),missingApiRoles:M.computed(()=>be(rt.filter(e=>n[`${e}Enabled`].value).map(e=>n[`${e}Access`].value),"only the level-0 role passes")),missingExportRole:M.computed(()=>be(n.exportEnabled.value?[n.exportAccess.value]:[],"only the level-0 role is offered export")),showStorage:M.observable(!!P?.pro&&r!=="roles"),storageNote:M.computed(()=>{const e=qe(H,n.storageAdapter.value);return!e||k.value?"":u?`Switching ${e}: Save asks whether to move the ${u} existing ${u===1?"entry":"entries"} across. Export them first if they matter.`:`Switching ${e}. There are no entries to move.`})},we=i.querySelector("#collection-project");for(const e of oe){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,we.appendChild(t)}if(f.project&&!oe.some(e=>e.slug===f.project)){const e=document.createElement("option");e.value=f.project,e.textContent=`${f.project} (not found)`,we.appendChild(e)}const He=[...i.querySelectorAll('.cex-op select[data-model$="Access.value"]')],xe=e=>{for(const t of He){const o=t.getAttribute("data-model").split(".")[0],a=e[o];t.replaceChildren(...dt(B,a,{token:t.id!=="export-access"}).map(d=>new Option(d.label,d.value))),t.value=a}};xe(f);const We=i.querySelector("#storage-connection"),Ye=Array.isArray(P?.connections)&&P.connections.length?P.connections:["default"];for(const e of new Set([...Ye,f.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,We.appendChild(t)}const D=e=>n.fields.peek().findIndex(t=>t._key===e),j=e=>{n.fields.value=e};function G(e,t){const o=D(e);o<0||(j(Te(n.fields.peek(),o,o+t)),requestAnimationFrame(()=>i.querySelector(`.cex-row[data-key="${e}"]`)?.focus()))}async function ae(e){const t=D(e);if(t<0)return;const o=n.fields.peek()[t];!k.peek()&&u&&x[e]&&!await E.confirm(`Remove the field "${ee(o.label||o.name)}"? The ${u} existing ${u===1?"entry keeps its":"entries keep their"} value for it, but it is no longer shown or edited. Nothing is lost until you Save - Discard puts it back.`)||j(n.fields.peek().filter(a=>a._key!==e))}function V(e,{isAdded:t=!1}={}){const o=D(e);if(o<0)return;const a=n.fields.peek()[o],d=yt(a),v={};for(const[l,$]of Object.entries(d))v[l]=M.observable($);const p=JSON.stringify(d),Q=()=>Object.fromEntries(Object.keys(v).map(l=>[l,v[l].peek()])),Ce=()=>JSON.stringify(Q())!==p;let Oe=!!a.name;const Ze=M.effect(()=>{const l=v.label.value;Oe||(v.name.value=bt(l))}),et=new Set(n.fields.peek().filter(l=>l._key!==e).map(l=>l.name)),je=M.computed(()=>{const l=De(a,Object.fromEntries(Object.keys(v).map($=>[$,v[$].value])));return l.name&&et.has(l.name)?`"${l.name}" is used by another field.`:mt(l,x[e])}),tt=M.computed(()=>!k.peek()&&u&&x[e]&&v.name.value.trim()!==x[e]?`Entries keep their value under "${x[e]}" - renaming the field does not move it.`:""),nt={...v,problem:je,renamed:tt,isOptions:M.computed(()=>ct.has(v.type.value)),isGrid:M.computed(()=>n.layout.value==="grid")},h=E.slideover({title:t?"New field":`Field: ${ee(a.label||a.name||"Untitled")}`,size:"md",position:"right"}),F=$t(`
9
9
  <div class="cex-panel">
10
10
  <div class="cex-grid">
11
11
  <div>
@@ -20,7 +20,7 @@ import{api as g}from"../api.js";import{addBannerAction as pe}from"../lib/plugin-
20
20
  <div>
21
21
  <label class="form-label" for="cex-f-type">Type</label>
22
22
  <select id="cex-f-type" class="form-input" data-model="type.value">
23
- ${rt.map(l=>`<option value="${l.value}">${l.label}</option>`).join("")}
23
+ ${it.map(l=>`<option value="${l.value}">${l.label}</option>`).join("")}
24
24
  </select>
25
25
  </div>
26
26
  <div class="cex-setting">
@@ -61,4 +61,4 @@ import{api as g}from"../api.js";import{addBannerAction as pe}from"../lib/plugin-
61
61
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
62
62
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done" data-bind-disabled="problem.value"><span data-icon="check" data-icon-size="13"></span> Done</button>
63
63
  </div>
64
- </div>`);let Z=!1,de=!1;const at=h.close.bind(h);function Re(){if(Ae.peek())return;const l=Ne(a,X());j(n.fields.peek().map($=>$._key===e?l:$)),Z=!0,h.close()}h.element.appendChild(F);const lt=De(F),st=M.applyBindings(ot,F,{methods:{done:Re,cancel:()=>h.close(),remove:async()=>{Z=!0,h.close(),await le(e)}}}),Fe=F.querySelector("#cex-f-name");Fe.addEventListener("input",()=>{je=!!Fe.value}),F.addEventListener("keydown",l=>{(l.ctrlKey||l.metaKey)&&l.key==="Enter"&&(l.preventDefault(),Re())}),it(h,()=>{et(),st.dispose(),lt(),re.delete(h)}),h.close=async l=>{if(!Z&&l!==!0&&Oe()){if(de)return;de=!0;const ue=await E.confirm("Discard the changes to this field?");if(de=!1,!ue)return}Z=!0;const $=at();return t&&!Oe()&&!a.name&&j(n.fields.peek().filter(ue=>ue._key!==e)),$},re.add(h),I.scan(F),h.open(),F.querySelector("#cex-f-label")?.focus()}function Ge(){const e=yt();j([...n.fields.peek(),e]),Qe.activate?.(1),J(e._key,{isAdded:!0})}function Ve(e){const t=N(e);if(t<0)return[];const o=n.fields.peek()[t],a=n.fields.peek().length;return[{header:o.label||o.name||"Field"},{label:"Edit\u2026",icon:"edit",action:()=>J(e)},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>V(e,-1)},{label:"Move down",icon:"arrow-down",disabled:t===a-1,action:()=>V(e,1)},{label:"Duplicate",icon:"copy",action:()=>j(mt(n.fields.peek(),t))},{type:"divider"},{label:"Remove\u2026",icon:"trash",danger:!0,action:()=>le(e)}]}async function se(){if(Y.peek())return;const e=m.peek();if(H(e)){E.toast(`Not saved - ${H(e)} thing${H(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=xt(z(),{isNew:k.peek(),meta:_e});Y.value=!0;try{if(k.peek()){const a=await g.collections.create(t);L.value=U(z()),ce(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(a.slug)}`);return}const o=Pe(W,t.storage.adapter);if(o&&u){if(await E.confirm(`You changed the storage (${o}). Move the ${u} existing ${u===1?"entry":"entries"} to the new storage?`)){const d=await g.collections.migrateStorage(r,t.storage);E.toast(`Moved ${d.migrated} of ${d.total} entries.`,{type:"success"})}else if(!await E.confirm(`Switch without moving them? The ${u} ${u===1?"entry stays":"entries stay"} in ${te(W)} storage and this collection will show none until they are moved.`))return}if(await g.collections.update(r,t),!oe())return;W=t.storage.adapter,o&&(u=(await g.collections.listEntries(r,{limit:1}).catch(()=>null))?.total??u);for(const a of n.fields.peek())x[a._key]=a.name;L.value=U(z()),ce(),Ie(c,t.title),E.toast("Collection saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{Y.value=!1}}async function Je(){if(!K.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=qe(O,{project:be});for(const t of e.fields)x[t._key]=t.name;for(const[t,o]of Object.entries(e))n[t].value=o;L.value=U(e),G=!k.peek()}const q=i.querySelector(".cex-list");q.innerHTML=Mt;const Qe=E.tabs(i.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Xe=M.applyBindings(Ue,i,{methods:{save:()=>se(),discard:()=>Je(),addField:()=>Ge()}});b(()=>Xe.dispose()),b(De(i,{position:"bottom"}));const ie=i.querySelector("#field-slug"),Ee=()=>{G=!!ie.value};ie.addEventListener("input",Ee),b(()=>ie.removeEventListener("input",Ee));const re=new Set;b(()=>[...re].forEach(e=>{try{e.close(!0)}catch{}}));const Ze=E.contextMenu(q,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>Ve(e.dataset.key)});b(()=>Ze?.destroy?.());let T=null;const Se={click:e=>{const t=e.target.closest(".cex-row");if(!t)return;const o=e.target.closest(".cex-kebab");if(o){Le(o);return}J(t.dataset.key)},keydown:e=>{const t=e.target.closest(".cex-row");!t||e.target!==t||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Le(t)):e.key==="Enter"?J(t.dataset.key):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),V(t.dataset.key,-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),V(t.dataset.key,1)):e.key==="Delete"&&(e.preventDefault(),le(t.dataset.key)))},dragstart:e=>{const t=e.target.closest(".cex-row");t&&(T=t.dataset.key,t.classList.add("is-dragging"),e.dataTransfer.effectAllowed="move")},dragover:e=>{const t=e.target.closest(".cex-row");!t||!T||(e.preventDefault(),q.querySelectorAll(".is-drag-over").forEach(o=>o.classList.remove("is-drag-over")),t.classList.add("is-drag-over"))},drop:e=>{const t=e.target.closest(".cex-row");if(!t||!T)return;e.preventDefault();const o=N(T),a=N(t.dataset.key);T=null,j(Te(n.fields.peek(),o,a))},dragend:()=>{T=null,q.querySelectorAll(".is-drag-over, .is-dragging").forEach(e=>e.classList.remove("is-drag-over","is-dragging"))}};for(const[e,t]of Object.entries(Se))q.addEventListener(e,t);b(()=>{for(const[e,t]of Object.entries(Se))q.removeEventListener(e,t)}),Ie(c,O?.title||"New collection"),pe(s,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),r&&pe(s,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(r)}/entries`)}),pe(s,{id:"collection-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>se()});const Me=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!oe()||(e.preventDefault(),e.stopImmediatePropagation(),se())};document.addEventListener("keydown",Me,!0),b(()=>document.removeEventListener("keydown",Me,!0));const Q=St(r);function ce(){try{S.remove?S.remove(Q):S.set(Q,null)}catch{}}const $e=()=>{if(K.peek())try{S.set(Q,{form:ft(z()),base:L.peek(),at:new Date().toISOString()})}catch{}},Ce=e=>{K.peek()&&($e(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ce),b(()=>window.removeEventListener("beforeunload",Ce)),y={path:C,dirty:()=>K.peek(),dispose(){me=!0,$e(),fe.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let A=null;try{A=S.get(Q)}catch{}if(A?.form){ce();const e=A.base&&A.base!==L.peek(),t=A.at?` (${D(A.at).format("D MMM HH:mm")})`:"",o=r?`changes to "${te(O?.title||r)}"`:"a new collection",a=e?`You left unsaved ${o}${t}, but the saved collection has changed since. Restore your version anyway?`:`You left unsaved ${o}${t}. Restore them?`;if(await E.confirm(a)&&oe()){const d=bt(A.form,f);P||(d.slug=f.slug);const v=Object.fromEntries(f.fields.filter(p=>p.id).map(p=>[p.id,p.name]));for(const p of d.fields)p.id&&v[p.id]!==void 0&&(x[p._key]=v[p.id]);ke(d);for(const[p,X]of Object.entries(d))n[p]&&(n[p].value=X);P&&(G=!!d.slug&&d.slug!==ee(d.title))}}}};
64
+ </div>`);let X=!1,ce=!1;const ot=h.close.bind(h);function Ae(){if(je.peek())return;const l=De(a,Q());j(n.fields.peek().map($=>$._key===e?l:$)),X=!0,h.close()}h.element.appendChild(F);const at=Fe(F),lt=M.applyBindings(nt,F,{methods:{done:Ae,cancel:()=>h.close(),remove:async()=>{X=!0,h.close(),await ae(e)}}}),Re=F.querySelector("#cex-f-name");Re.addEventListener("input",()=>{Oe=!!Re.value}),F.addEventListener("keydown",l=>{(l.ctrlKey||l.metaKey)&&l.key==="Enter"&&(l.preventDefault(),Ae())}),st(h,()=>{Ze(),lt.dispose(),at(),ie.delete(h)}),h.close=async l=>{if(!X&&l!==!0&&Ce()){if(ce)return;ce=!0;const de=await E.confirm("Discard the changes to this field?");if(ce=!1,!de)return}X=!0;const $=ot();return t&&!Ce()&&!a.name&&j(n.fields.peek().filter(de=>de._key!==e)),$},ie.add(h),I.scan(F),h.open(),F.querySelector("#cex-f-label")?.focus()}function ze(){const e=gt();j([...n.fields.peek(),e]),Je.activate?.(1),V(e._key,{isAdded:!0})}function Ge(e){const t=D(e);if(t<0)return[];const o=n.fields.peek()[t],a=n.fields.peek().length;return[{header:o.label||o.name||"Field"},{label:"Edit\u2026",icon:"edit",action:()=>V(e)},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>G(e,-1)},{label:"Move down",icon:"arrow-down",disabled:t===a-1,action:()=>G(e,1)},{label:"Duplicate",icon:"copy",action:()=>j(ft(n.fields.peek(),t))},{type:"divider"},{label:"Remove\u2026",icon:"trash",danger:!0,action:()=>ae(e)}]}async function le(){if(W.peek())return;const e=m.peek();if(K(e)){E.toast(`Not saved - ${K(e)} thing${K(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=wt(Y(),{isNew:k.peek(),meta:Be});W.value=!0;try{if(k.peek()){const a=await g.collections.create(t);L.value=U(Y()),re(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(a.slug)}`);return}const o=qe(H,t.storage.adapter);if(o&&u){if(await E.confirm(`You changed the storage (${o}). Move the ${u} existing ${u===1?"entry":"entries"} to the new storage?`)){const d=await g.collections.migrateStorage(r,t.storage);E.toast(`Moved ${d.migrated} of ${d.total} entries.`,{type:"success"})}else if(!await E.confirm(`Switch without moving them? The ${u} ${u===1?"entry stays":"entries stay"} in ${ee(H)} storage and this collection will show none until they are moved.`))return}if(await g.collections.update(r,t),!ne())return;H=t.storage.adapter,o&&(u=(await g.collections.listEntries(r,{limit:1}).catch(()=>null))?.total??u);for(const a of n.fields.peek())x[a._key]=a.name;L.value=U(Y()),re(),Pe(c,t.title),E.toast("Collection saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{W.value=!1}}async function Ve(){if(!_.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=Ne(O,{project:he});for(const t of e.fields)x[t._key]=t.name;for(const[t,o]of Object.entries(e))n[t].value=o;L.value=U(e),z=!k.peek()}const N=i.querySelector(".cex-list");N.innerHTML=Mt;const Je=E.tabs(i.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(Ue,i,{methods:{save:()=>le(),discard:()=>Ve(),addField:()=>ze()}});b(()=>Qe.dispose()),b(Fe(i,{position:"bottom"}));const se=i.querySelector("#field-slug"),ke=()=>{z=!!se.value};se.addEventListener("input",ke),b(()=>se.removeEventListener("input",ke));const ie=new Set;b(()=>[...ie].forEach(e=>{try{e.close(!0)}catch{}}));const Xe=E.contextMenu(N,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>Ge(e.dataset.key)});b(()=>Xe?.destroy?.());let T=null;const Ee={click:e=>{const t=e.target.closest(".cex-row");if(!t)return;const o=e.target.closest(".cex-kebab");if(o){Le(o);return}V(t.dataset.key)},keydown:e=>{const t=e.target.closest(".cex-row");!t||e.target!==t||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Le(t)):e.key==="Enter"?V(t.dataset.key):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),G(t.dataset.key,-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),G(t.dataset.key,1)):e.key==="Delete"&&(e.preventDefault(),ae(t.dataset.key)))},dragstart:e=>{const t=e.target.closest(".cex-row");t&&(T=t.dataset.key,t.classList.add("is-dragging"),e.dataTransfer.effectAllowed="move")},dragover:e=>{const t=e.target.closest(".cex-row");!t||!T||(e.preventDefault(),N.querySelectorAll(".is-drag-over").forEach(o=>o.classList.remove("is-drag-over")),t.classList.add("is-drag-over"))},drop:e=>{const t=e.target.closest(".cex-row");if(!t||!T)return;e.preventDefault();const o=D(T),a=D(t.dataset.key);T=null,j(Te(n.fields.peek(),o,a))},dragend:()=>{T=null,N.querySelectorAll(".is-drag-over, .is-dragging").forEach(e=>e.classList.remove("is-drag-over","is-dragging"))}};for(const[e,t]of Object.entries(Ee))N.addEventListener(e,t);b(()=>{for(const[e,t]of Object.entries(Ee))N.removeEventListener(e,t)}),Pe(c,O?.title||"New collection"),ue(s,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),r&&ue(s,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(r)}/entries`)}),ue(s,{id:"collection-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>le()});const Se=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!ne()||(e.preventDefault(),e.stopImmediatePropagation(),le())};document.addEventListener("keydown",Se,!0),b(()=>document.removeEventListener("keydown",Se,!0));const J=St(r);function re(){try{S.remove?S.remove(J):S.set(J,null)}catch{}}const Me=()=>{if(_.peek())try{S.set(J,{form:pt(Y()),base:L.peek(),at:new Date().toISOString()})}catch{}},$e=e=>{_.peek()&&(Me(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",$e),b(()=>window.removeEventListener("beforeunload",$e)),y={path:C,dirty:()=>_.peek(),dispose(){fe=!0,Me(),pe.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let A=null;try{A=S.get(J)}catch{}if(A?.form){re();const e=A.base&&A.base!==L.peek(),t=A.at?` (${kt(A.at)})`:"",o=r?`changes to "${ee(O?.title||r)}"`:"a new collection",a=e?`You left unsaved ${o}${t}, but the saved collection has changed since. Restore your version anyway?`:`You left unsaved ${o}${t}. Restore them?`;if(await E.confirm(a)&&ne()){const d=ht(A.form,f);q||(d.slug=f.slug);const v=Object.fromEntries(f.fields.filter(p=>p.id).map(p=>[p.id,p.name]));for(const p of d.fields)p.id&&v[p.id]!==void 0&&(x[p._key]=v[p.id]);xe(d);for(const[p,Q]of Object.entries(d))n[p]&&(n[p].value=Q);q&&(z=!!d.slug&&d.slug!==Z(d.title))}}}};