domma-cms 1.6.55 → 1.6.57

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 (113) hide show
  1. package/admin/css/calculator.css +1 -1
  2. package/admin/js/lib/analytics-shape.js +1 -1
  3. package/admin/js/lib/calculator-engine.js +1 -1
  4. package/admin/js/lib/calculator.js +22 -10
  5. package/admin/js/lib/form-editor-arrange.js +1 -1
  6. package/admin/js/lib/forms-arrange.js +1 -1
  7. package/admin/js/lib/notes-arrange.js +1 -1
  8. package/admin/js/lib/search-settings-arrange.js +1 -1
  9. package/admin/js/lib/seo-arrange.js +1 -0
  10. package/admin/js/lib/todo-arrange.js +1 -1
  11. package/admin/js/lib/tool-kit.js +1 -1
  12. package/admin/js/lib/tool-recovery.js +4 -4
  13. package/admin/js/templates/analytics.html +26 -14
  14. package/admin/js/templates/contacts.html +26 -5
  15. package/admin/js/templates/form-editor.html +2 -0
  16. package/admin/js/templates/notes.html +11 -8
  17. package/admin/js/templates/search.html +9 -3
  18. package/admin/js/templates/seo.html +23 -17
  19. package/admin/js/templates/todo.html +18 -14
  20. package/admin/js/views/analytics.js +10 -10
  21. package/admin/js/views/contacts.js +47 -47
  22. package/admin/js/views/form-editor.js +7 -7
  23. package/admin/js/views/form-submissions.js +2 -2
  24. package/admin/js/views/forms.js +5 -5
  25. package/admin/js/views/notes.js +6 -6
  26. package/admin/js/views/search.js +2 -2
  27. package/admin/js/views/seo.js +43 -43
  28. package/admin/js/views/todo.js +8 -8
  29. package/package.json +1 -1
  30. package/plugins/_lib/admin/mail/compose-window.js +4 -1
  31. package/plugins/_lib/admin/mail/mail.css +1 -1
  32. package/plugins/_lib/admin/mail/reader-kit.js +364 -0
  33. package/plugins/_lib/admin/mail/reader-view.js +1009 -161
  34. package/plugins/_lib/admin/mail/when-closed.js +33 -3
  35. package/plugins/_lib/mail/readRoutes.js +48 -10
  36. package/plugins/_lib/mail/unreadBadge.js +4 -1
  37. package/plugins/blog/CLAUDE.md +14 -0
  38. package/plugins/blog/admin/css/index.css +5 -0
  39. package/plugins/blog/admin/lib/drafts.js +95 -0
  40. package/plugins/blog/admin/templates/blog.html +1 -1
  41. package/plugins/blog/admin/templates/categories.html +1 -1
  42. package/plugins/blog/admin/templates/comments.html +1 -1
  43. package/plugins/blog/admin/templates/post-editor.html +5 -1
  44. package/plugins/blog/admin/views/blog.js +44 -19
  45. package/plugins/blog/admin/views/categories.js +138 -20
  46. package/plugins/blog/admin/views/comments.js +32 -23
  47. package/plugins/blog/admin/views/kit.js +232 -22
  48. package/plugins/blog/admin/views/post-editor.js +348 -178
  49. package/plugins/blog/admin/views/settings.js +17 -3
  50. package/plugins/blog/plugin.js +1 -1
  51. package/plugins/blog/plugin.json +7 -7
  52. package/plugins/blog/plugin.public.js +1 -1
  53. package/plugins/blog/tests/drafts.test.js +50 -0
  54. package/plugins/booking/CLAUDE.md +13 -0
  55. package/plugins/booking/admin/lib/drafts.js +139 -0
  56. package/plugins/booking/admin/lib/kit.js +246 -25
  57. package/plugins/booking/admin/views/booking.js +11 -2
  58. package/plugins/booking/admin/views/bookings.js +7 -2
  59. package/plugins/booking/admin/views/diary.js +13 -6
  60. package/plugins/booking/admin/views/panels.js +85 -12
  61. package/plugins/booking/admin/views/people.js +41 -2
  62. package/plugins/booking/admin/views/services.js +45 -3
  63. package/plugins/booking/admin/views/settings.js +22 -2
  64. package/plugins/booking/plugin.json +2 -2
  65. package/plugins/booking/tests/admin-drafts.test.js +67 -0
  66. package/plugins/feedback/CLAUDE.md +12 -0
  67. package/plugins/feedback/admin/css/index.css +5 -0
  68. package/plugins/feedback/admin/lib/drafts.js +145 -0
  69. package/plugins/feedback/admin/lib/kit.js +161 -5
  70. package/plugins/feedback/admin/lib/settings.js +46 -28
  71. package/plugins/feedback/admin/views/feedback.js +1 -1
  72. package/plugins/feedback/admin/views/inbox.js +110 -46
  73. package/plugins/feedback/admin/views/send.js +174 -83
  74. package/plugins/feedback/plugin.json +3 -3
  75. package/plugins/feedback/tests/drafts.test.js +93 -0
  76. package/plugins/free-tier.lock.json +75 -63
  77. package/plugins/security/CLAUDE.md +9 -0
  78. package/plugins/security/admin/lib/screen-state.js +120 -0
  79. package/plugins/security/admin/templates/security.html +15 -10
  80. package/plugins/security/admin/views/security.js +270 -132
  81. package/plugins/security/plugin.json +2 -2
  82. package/plugins/security/tests/screen-state.test.js +69 -0
  83. package/plugins/shopping-cart/CLAUDE.md +13 -0
  84. package/plugins/shopping-cart/admin/css/index.css +21 -5
  85. package/plugins/shopping-cart/admin/lib/drafts.js +174 -0
  86. package/plugins/shopping-cart/admin/lib/kit.js +136 -28
  87. package/plugins/shopping-cart/admin/views/orders.js +117 -36
  88. package/plugins/shopping-cart/admin/views/overview.js +38 -8
  89. package/plugins/shopping-cart/admin/views/products.js +317 -68
  90. package/plugins/shopping-cart/admin/views/sales.js +21 -3
  91. package/plugins/shopping-cart/admin/views/settings.js +62 -3
  92. package/plugins/shopping-cart/admin/views/shop.js +18 -3
  93. package/plugins/shopping-cart/plugin.json +3 -3
  94. package/plugins/shopping-cart/tests/accounts.test.js +11 -0
  95. package/plugins/shopping-cart/tests/drafts.test.js +118 -0
  96. package/plugins/support/CLAUDE.md +14 -0
  97. package/plugins/support/admin/css/index.css +10 -0
  98. package/plugins/support/admin/lib/drafts.js +162 -0
  99. package/plugins/support/admin/lib/kit.js +156 -16
  100. package/plugins/support/admin/lib/settings.js +52 -29
  101. package/plugins/support/admin/views/desk.js +114 -33
  102. package/plugins/support/admin/views/inbox.js +85 -26
  103. package/plugins/support/admin/views/support.js +1 -1
  104. package/plugins/support/plugin.json +3 -3
  105. package/plugins/support/tests/drafts.test.js +92 -0
  106. package/public/js/site.js +1 -1
  107. package/server/routes/api/analytics.js +5 -1
  108. package/server/routes/api/contacts.js +42 -0
  109. package/server/routes/api/forms.js +8 -0
  110. package/server/routes/api/seo.js +4 -1
  111. package/server/routes/api/todo.js +50 -10
  112. package/server/server.js +1 -1
  113. package/server/templates/page.html +1 -1
@@ -1,4 +1,4 @@
1
- import{apiRequest as A}from"/admin/js/api.js";import{addBannerAction as z}from"/admin/js/lib/plugin-chrome.js";import{SHAPES as ke,whileLoading as Se}from"/admin/js/lib/loading.js";import{helpPopovers as re}from"/admin/js/lib/help-popover.js";import{onceClosed as $e,openMenuAt as ce,sidebarCount as G,viewLifetime as Ne}from"/admin/js/lib/tool-kit.js";import{FILTERS as Te,answerFields as Me,answerText as O,arrangeSubmissions as Ae,submissionCounts as Le,submissionRow as L,submissionText as De}from"/admin/js/lib/form-submissions-arrange.js";import{fmtDateTime as Re}from"../lib/dates.js";const le=/^#\/forms\/[^/?#]+\/submissions\/?(\?.*)?$/,X="form-submissions.filter",Ie=`
1
+ import{apiRequest as A}from"/admin/js/api.js";import{addBannerAction as Y}from"/admin/js/lib/plugin-chrome.js";import{SHAPES as De,whileLoading as Le}from"/admin/js/lib/loading.js";import{helpPopovers as me}from"/admin/js/lib/help-popover.js";import{onceClosed as Re,openMenuAt as pe,sidebarCount as Q,viewLifetime as Ie}from"/admin/js/lib/tool-kit.js";import{FILTERS as Oe,answerFields as Ue,answerText as U,arrangeSubmissions as qe,submissionCounts as je,submissionRow as L,submissionText as Fe}from"/admin/js/lib/form-submissions-arrange.js";import{fmtDateTime as Pe}from"../lib/dates.js";const ue=/^#\/forms\/[^/?#]+\/submissions\/?(\?.*)?$/,Z="form-submissions.filter",Be=`
2
2
  <div class="fsx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
3
3
  <span class="fsx-dot" aria-hidden="true"></span>
4
4
  <span class="fsx-main"><strong>{{title}}</strong><small>{{meta}}</small></span>
@@ -8,4 +8,4 @@ import{apiRequest as A}from"/admin/js/api.js";import{addBannerAction as z}from"/
8
8
  </span>
9
9
  <span class="fsx-when" title="{{whenFull}}">{{when}}</span>
10
10
  <button type="button" class="fsx-kebab" aria-label="Actions for this submission" title="Actions">\u22EE</button>
11
- </div>`,Y=i=>String(i??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;"),U=(i,r,b=`${r}s`)=>`${i} ${i===1?r:b}`,Q=i=>i?Re(i):"";async function me(i){try{return await navigator.clipboard.writeText(i),!0}catch{}try{const r=document.createElement("textarea");r.value=i,r.style.cssText="position:fixed;opacity:0;",document.body.appendChild(r),r.select();const b=document.execCommand("copy");return r.remove(),b}catch{return!1}}const h=Ne(le);export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(i){h.reset();const r=i.get(0).querySelector(".fsx")||i.get(0),b=()=>le.test(location.hash)&&r.isConnected,p=decodeURIComponent((location.hash.match(/^#\/forms\/([^/?#]+)\/submissions/)||[])[1]||"");if(!p){E.toast("No form selected.",{type:"error"});return}const C=encodeURIComponent(p),y=M.observable(null),c=M.observable([]),Z=M.observable(!0),q=M.observable("");let ee="inbox";try{ee=Te.includes(S.get(X))?S.get(X):"inbox"}catch{}const w=M.observable(ee),te=M.observable(""),j=M.observable(""),F=M.observable("");h.add(M.effect(()=>{const e=w.value;try{S.set(X,e)}catch{}}));const k=M.computed(()=>Me(y.value||{})),P=M.observable([]);h.add(M.effect(()=>{const e=Date.now(),t=Ae(c.value,{filter:w.value,query:te.value,from:j.value,to:F.value,now:e}),s=k.value;P.value=t.map(n=>{const a=L(n,s,e);return{...a,when:a.at?D(a.at).fromNow():"",whenFull:Q(a.at)}})}));const de=M.computed(()=>Le(c.value)),pe=M.computed(()=>{const e=P.value.length,t=c.value.length;return t?e===t?U(t,"submission"):`${e} of ${U(t,"submission")}`:""}),ue=M.computed(()=>c.value.length?w.value==="spam"?"Nothing marked as spam.":"No submissions match.":"No submissions yet - they appear here as people send the form."),fe=M.computed(()=>y.value?`What people sent through "${y.value.title||p}".`:"What people sent through this form."),R=e=>c.peek().find(t=>String(t.id)===String(e));async function he(){try{const[e,t]=await Se(r.querySelector(".dm-loading-slot"),Promise.all([A(`/forms/${C}`),A(`/forms/${C}/submissions`)]),ke.rows);if(!b())return;y.value=e||null,c.value=Array.isArray(t)?t:[],q.value="",be(e?.title||p)}catch(e){if(!b())return;q.value=`Could not load the submissions: ${e.message||e}`}finally{Z.value=!1}}function be(e){const t=i.get(0).querySelector(".dm-plugin-banner-name"),s=t?.lastChild;s&&s.nodeType===Node.TEXT_NODE&&t.querySelector(".dm-plugin-banner-sep")&&(s.textContent=`${e} submissions`),document.title=`${e} - Submissions - Domma CMS`}async function B(e,t){try{const s=await A(`/forms/${C}/submissions/${encodeURIComponent(e.id)}/spam`,{method:"POST",body:JSON.stringify({spam:t})});return c.value=c.peek().map(n=>String(n.id)===String(e.id)?{...n,data:s?.entry?.data||{...n.data,spam:t}}:n),E.toast(t?"Marked as spam - it is under Spam now.":"Back in the inbox.",{type:"success"}),G("#/forms"),!0}catch(s){return E.toast(`Not changed: ${s.message||s}`,{type:"error"}),!1}}async function W(e){const t=L(e,k.peek()).title;if(!await E.confirm(`Delete the submission from "${Y(t)}"? This cannot be undone.`))return!1;try{return await A(`/forms/${C}/submissions/${encodeURIComponent(e.id)}`,{method:"DELETE"}),c.value=c.peek().filter(s=>String(s.id)!==String(e.id)),E.toast("Submission deleted.",{type:"success"}),G("#/forms"),!0}catch(s){return E.toast(`Not deleted: ${s.message||s}`,{type:"error"}),!1}}async function ye(){const e=c.peek().length;if(!e){E.toast("There are no submissions to clear.",{type:"info"});return}const t=y.peek()?.title||p;if(await E.confirm(`Delete all ${U(e,"submission")} to "${Y(t)}", spam included? This cannot be undone - Export them first if you may need them.`))try{await A(`/forms/${C}/submissions`,{method:"DELETE"}),c.value=[],E.toast("All submissions cleared.",{type:"success"}),G("#/forms")}catch(s){E.toast(`Not cleared: ${s.message||s}`,{type:"error"})}}const se=async e=>{const t=De(e,k.peek(),{formTitle:y.peek()?.title||p,when:Q(e.meta?.createdAt)});E.toast(await me(t)?"Copied.":"Copy failed.",{type:"info"})},we=async e=>{E.toast(await me(e)?`Copied ${e}.`:"Copy failed.",{type:"info"})},ne=e=>{const t=L(e,k.peek()).email;if(!t)return;const s=`Re: ${y.peek()?.title||p}`;window.location.href=`mailto:${encodeURIComponent(t)}?subject=${encodeURIComponent(s)}`};async function Ee(e,t){try{const s=await fetch(`/api/forms/${C}/submissions/${e}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!s.ok)throw new Error(`${s.status} ${s.statusText}`);const n=await s.blob(),a=document.createElement("a");return a.href=URL.createObjectURL(n),a.download=t,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3),!0}catch(s){return E.toast(`Export failed: ${s.message||s}`,{type:"error"}),!1}}const _=new Set;function oe(e,t,s="md"){const n=E.slideover({title:Y(e),size:s,position:"right"});n.element.appendChild(t);const a=re(t);return _.add(n),$e(n,()=>{_.delete(n),a()}),I.scan(t),n.open(),n}function ve(){const e=c.peek().length,t=document.createElement("div");t.className="fsx-export";const s=document.createElement("p");s.className="fsx-note",s.textContent=e?`All ${U(e,"submission")}, spam included.`:"There are no submissions yet - the file will be empty.",t.appendChild(s);let n;const a=(N,v,J,K,$)=>{const l=document.createElement("button");l.type="button",l.className="btn btn-ghost";const x=document.createElement("span");x.setAttribute("data-icon",v),l.append(x,document.createTextNode(` ${N}`));const T=document.createElement("span");T.setAttribute("data-help",$),l.addEventListener("click",async()=>{await Ee(J,`${p}-submissions.${K}`)&&n?.close()});const o=document.createElement("div");o.append(l,T),t.appendChild(o)};a("Export as CSV","file-text","export","csv","One row per submission, one column per question - opens in Excel, Numbers or Google Sheets."),a("Export as JSON","code","export/json","json","Every submission exactly as stored, with its id and dates - for importing elsewhere or keeping a full copy."),n=oe("Export submissions",t,"sm")}function H(e){if(!e)return;const t=k.peek(),s=L(e,t),n=document.createElement("div");n.className="fsx-detail";const a=document.createElement("div");a.className="fsx-facts";const N=o=>{const m=document.createElement("span");m.textContent=o,a.appendChild(m)};e.meta?.createdAt&&N(`Sent ${Q(e.meta.createdAt)} (${D(e.meta.createdAt).fromNow()})`),e.meta?.ip&&N(`IP ${e.meta.ip}`),s.spam&&N("Marked as spam"),n.appendChild(a);const v=document.createElement("dl");v.className="fsx-answers";const J=t.filter(o=>O(e.data?.[o.name])!=="");for(const o of J){const m=document.createElement("div");m.className="fsx-answer";const f=document.createElement("dt");f.textContent=o.label;const g=document.createElement("dd");g.textContent=O(e.data[o.name]),m.append(f,g),v.appendChild(m)}const K=new Set(t.map(o=>o.name));for(const[o,m]of Object.entries(e.data||{})){if(K.has(o)||o==="spam"||O(m)==="")continue;const f=document.createElement("div");f.className="fsx-answer";const g=document.createElement("dt");g.textContent=`${o} (no longer on the form)`;const d=document.createElement("dd");d.textContent=O(m),f.append(g,d),v.appendChild(f)}if(v.children.length)n.appendChild(v);else{const o=document.createElement("p");o.className="fsx-note",o.textContent="Nothing was filled in.",n.appendChild(o)}const $=document.createElement("div");$.className="fsx-actions";const l=(o,m,f,g)=>{const d=document.createElement("button");d.type="button",d.className=`btn btn-sm ${f}`;const V=document.createElement("span");return V.setAttribute("data-icon",m),V.setAttribute("data-icon-size","13"),d.append(V,document.createTextNode(` ${o}`)),d.addEventListener("click",g),$.appendChild(d),d};let x;l("Copy","copy","btn-ghost",()=>se(e)),s.email&&l("Email them","mail","btn-ghost",()=>ne(e)),l(s.spam?"Not spam":"Mark as spam",s.spam?"inbox":"alert-triangle","btn-ghost",async()=>{await B(e,!s.spam)&&x.close()});const T=document.createElement("span");T.className="fsx-spacer",$.appendChild(T),l("Delete","trash","btn-ghost text-danger",async()=>{await W(e)&&x.close()}),l("Close","x","btn-ghost",()=>x.close()),n.appendChild($),x=oe(s.title,n)}function xe(e){const t=R(e);if(!t)return[];const s=L(t,k.peek());return[{header:s.title},{label:"Open\u2026",icon:"eye",action:()=>H(t)},{label:"Copy submission",icon:"copy",action:()=>se(t)},{type:"divider"},{label:s.email?`Copy ${s.email}`:"Copy email",icon:"at-sign",disabled:!s.email,action:()=>we(s.email)},{label:"Email them\u2026",icon:"mail",disabled:!s.email,action:()=>ne(t)},{type:"divider"},s.spam?{label:"Not spam - back to the inbox",icon:"inbox",action:()=>B(t,!1)}:{label:"Mark as spam",icon:"alert-triangle",action:()=>B(t,!0)},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>W(t)}]}const u=r.querySelector(".fsx-list");u.innerHTML=Ie;const ge=M.applyBindings({loading:Z,loadError:q,filter:w,query:te,from:j,to:F,rows:P,counts:de,summary:pe,emptyText:ue,intro:fe},r,{methods:{showInbox:()=>{w.value="inbox"},showNew:()=>{w.value="new"},showSpam:()=>{w.value="spam"},clearDates:()=>{j.value="",F.value=""}}});h.add(()=>ge.dispose()),h.add(re(r,{position:"bottom"}));const Ce=E.contextMenu(u,{cursor:"glyph",match:".fsx-row",inherit:!1,density:"compact",items:e=>xe(e.dataset.id)});h.add(()=>Ce?.destroy?.());const ae=e=>{const t=e.target.closest(".fsx-row");if(!t)return;const s=e.target.closest(".fsx-kebab");if(s){ce(s);return}H(R(t.dataset.id))},ie=e=>{const t=e.target.closest(".fsx-row");if(!(!t||e.target!==t)){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),ce(t);else if(e.key==="Enter")H(R(t.dataset.id));else if(e.key==="Delete"){e.preventDefault();const s=R(t.dataset.id);s&&W(s)}}};u.addEventListener("click",ae),u.addEventListener("keydown",ie),h.add(()=>{u.removeEventListener("click",ae),u.removeEventListener("keydown",ie)}),z(i,{id:"form-submissions-back",icon:"arrow-left",title:"All forms",onClick:()=>{location.hash="#/forms"}}),z(i,{id:"form-submissions-export",icon:"download",title:"Export (CSV or JSON)",onClick:ve}),z(i,{id:"form-submissions-clear",icon:"trash",title:"Clear all submissions",onClick:ye}),h.add(()=>{[..._].forEach(e=>{try{e.close()}catch{}})}),I.scan(r),await he(),b()&&requestAnimationFrame(()=>{u.isConnected&&I.scan(u)})}};
11
+ </div>`,ee=i=>String(i??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;"),q=(i,r,y=`${r}s`)=>`${i} ${i===1?r:y}`,te=i=>i?Pe(i):"";async function fe(i){try{return await navigator.clipboard.writeText(i),!0}catch{}try{const r=document.createElement("textarea");r.value=i,r.style.cssText="position:fixed;opacity:0;",document.body.appendChild(r),r.select();const y=document.execCommand("copy");return r.remove(),y}catch{return!1}}const b=Ie(ue);export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(i){b.reset();const r=i.get(0).querySelector(".fsx")||i.get(0),y=()=>ue.test(location.hash)&&r.isConnected,u=decodeURIComponent((location.hash.match(/^#\/forms\/([^/?#]+)\/submissions/)||[])[1]||"");if(!u){E.toast("No form selected.",{type:"error"});return}const k=encodeURIComponent(u),w=M.observable(null),l=M.observable([]),ne=M.observable(!0),j=M.observable("");let se="inbox";try{se=Oe.includes(S.get(Z))?S.get(Z):"inbox"}catch{}const v=M.observable(se),oe=M.observable(""),F=M.observable(""),P=M.observable("");b.add(M.effect(()=>{const e=v.value;try{S.set(Z,e)}catch{}}));const $=M.computed(()=>Ue(w.value||{})),B=M.observable([]);b.add(M.effect(()=>{const e=Date.now(),t=qe(l.value,{filter:v.value,query:oe.value,from:F.value,to:P.value,now:e}),n=$.value;B.value=t.map(a=>{const s=L(a,n,e);return{...s,when:s.at?D(s.at).fromNow():"",whenFull:te(s.at)}})}));const he=M.computed(()=>je(l.value)),be=M.computed(()=>{const e=B.value.length,t=l.value.length;return t?e===t?q(t,"submission"):`${e} of ${q(t,"submission")}`:""}),ye=M.computed(()=>l.value.length?v.value==="spam"?"Nothing marked as spam.":"No submissions match.":"No submissions yet - they appear here as people send the form."),we=M.computed(()=>w.value?`What people sent through "${w.value.title||u}".`:"What people sent through this form."),R=e=>l.peek().find(t=>String(t.id)===String(e));async function Ee(){try{const[e,t]=await Le(r.querySelector(".dm-loading-slot"),Promise.all([A(`/forms/${k}`),A(`/forms/${k}/submissions`)]),De.rows);if(!y())return;w.value=e||null,l.value=Array.isArray(t)?t:[],j.value="",ve(e?.title||u)}catch(e){if(!y())return;j.value=`Could not load the submissions: ${e.message||e}`}finally{ne.value=!1}}function ve(e){const t=i.get(0).querySelector(".dm-plugin-banner-name"),n=t?.lastChild;n&&n.nodeType===Node.TEXT_NODE&&t.querySelector(".dm-plugin-banner-sep")&&(n.textContent=`${e} submissions`),document.title=`${e} - Submissions - Domma CMS`}const K=(e,t)=>ce(e.id,()=>xe(e,t)),W=e=>ce(e.id,()=>ge(e));async function xe(e,t){try{const n=await A(`/forms/${k}/submissions/${encodeURIComponent(e.id)}/spam`,{method:"POST",body:JSON.stringify({spam:t})});return l.value=l.peek().map(a=>String(a.id)===String(e.id)?{...a,data:n?.entry?.data||{...a.data,spam:t}}:a),E.toast(t?"Marked as spam - it is under Spam now.":"Back in the inbox.",{type:"success"}),Q("#/forms"),!0}catch(n){return E.toast(`Not changed: ${n.message||n}`,{type:"error"}),!1}}async function ge(e){const t=L(e,$.peek()).title;if(!await E.confirm(`Delete the submission from "${ee(t)}"? This cannot be undone.`,{confirmText:"Delete"}))return!1;try{return await A(`/forms/${k}/submissions/${encodeURIComponent(e.id)}`,{method:"DELETE"}),l.value=l.peek().filter(n=>String(n.id)!==String(e.id)),E.toast("Submission deleted.",{type:"success"}),Q("#/forms"),!0}catch(n){return E.toast(`Not deleted: ${n.message||n}`,{type:"error"}),!1}}let _=!1;async function Ce(){if(!_){_=!0;try{await ke()}finally{_=!1}}}async function ke(){const e=l.peek().length;if(!e){E.toast("There are no submissions to clear.",{type:"info"});return}const t=w.peek()?.title||u;if(await E.confirm(`Delete all ${q(e,"submission")} to "${ee(t)}", spam included? This cannot be undone - Export them first if you may need them.`))try{await A(`/forms/${k}/submissions`,{method:"DELETE"}),l.value=[],E.toast("All submissions cleared.",{type:"success"}),Q("#/forms")}catch(n){E.toast(`Not cleared: ${n.message||n}`,{type:"error"})}}const ae=async e=>{const t=Fe(e,$.peek(),{formTitle:w.peek()?.title||u,when:te(e.meta?.createdAt)});E.toast(await fe(t)?"Copied.":"Copy failed.",{type:"info"})},Se=async e=>{E.toast(await fe(e)?`Copied ${e}.`:"Copy failed.",{type:"info"})},ie=e=>{const t=L(e,$.peek()).email;if(!t)return;const n=`Re: ${w.peek()?.title||u}`;window.location.href=`mailto:${encodeURIComponent(t)}?subject=${encodeURIComponent(n)}`};async function $e(e,t){try{const n=await fetch(`/api/forms/${k}/submissions/${e}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!n.ok)throw new Error(`${n.status} ${n.statusText}`);const a=await n.blob(),s=document.createElement("a");return s.href=URL.createObjectURL(a),s.download=t,document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>URL.revokeObjectURL(s.href),1e3),!0}catch(n){return E.toast(`Export failed: ${n.message||n}`,{type:"error"}),!1}}const H=new Set,O=new Set;function re(e,t,n,a="md"){if(O.has(e))return null;O.add(e);let s;try{s=E.slideover({title:ee(t),size:a,position:"right"}),s.element.appendChild(n);const m=me(n);return H.add(s),Re(s,()=>{O.delete(e),H.delete(s),m()}),I.scan(n),s.open(),s}catch(m){O.delete(e);try{s?.destroy?.(),s?.element?.remove()}catch{}return E.toast(`Could not open it: ${m.message||m}`,{type:"error"}),null}}const J=new Set;async function ce(e,t){const n=String(e);if(J.has(n))return!1;J.add(n);try{return await t()}finally{J.delete(n)}}function Ne(){const e=l.peek().length,t=document.createElement("div");t.className="fsx-export";const n=document.createElement("p");n.className="fsx-note",n.textContent=e?`All ${q(e,"submission")}, spam included.`:"There are no submissions yet - the file will be empty.",t.appendChild(n);let a;const s=(m,x,z,G,N)=>{const c=document.createElement("button");c.type="button",c.className="btn btn-ghost";const g=document.createElement("span");g.setAttribute("data-icon",x),c.append(g,document.createTextNode(` ${m}`));const T=document.createElement("span");T.setAttribute("data-help",N),c.addEventListener("click",async()=>{if(!c.disabled){c.disabled=!0;try{await $e(z,`${u}-submissions.${G}`)&&a?.close()}finally{c.disabled=!1}}});const o=document.createElement("div");o.append(c,T),t.appendChild(o)};s("Export as CSV","file-text","export","csv","One row per submission, one column per question - opens in Excel, Numbers or Google Sheets."),s("Export as JSON","code","export/json","json","Every submission exactly as stored, with its id and dates - for importing elsewhere or keeping a full copy."),a=re("export","Export submissions",t,"sm")}function V(e){if(!e)return;const t=$.peek(),n=L(e,t),a=document.createElement("div");a.className="fsx-detail";const s=document.createElement("div");s.className="fsx-facts";const m=o=>{const d=document.createElement("span");d.textContent=o,s.appendChild(d)};e.meta?.createdAt&&m(`Sent ${te(e.meta.createdAt)} (${D(e.meta.createdAt).fromNow()})`),e.meta?.ip&&m(`IP ${e.meta.ip}`),n.spam&&m("Marked as spam"),a.appendChild(s);const x=document.createElement("dl");x.className="fsx-answers";const z=t.filter(o=>U(e.data?.[o.name])!=="");for(const o of z){const d=document.createElement("div");d.className="fsx-answer";const h=document.createElement("dt");h.textContent=o.label;const C=document.createElement("dd");C.textContent=U(e.data[o.name]),d.append(h,C),x.appendChild(d)}const G=new Set(t.map(o=>o.name));for(const[o,d]of Object.entries(e.data||{})){if(G.has(o)||o==="spam"||U(d)==="")continue;const h=document.createElement("div");h.className="fsx-answer";const C=document.createElement("dt");C.textContent=`${o} (no longer on the form)`;const p=document.createElement("dd");p.textContent=U(d),h.append(C,p),x.appendChild(h)}if(x.children.length)a.appendChild(x);else{const o=document.createElement("p");o.className="fsx-note",o.textContent="Nothing was filled in.",a.appendChild(o)}const N=document.createElement("div");N.className="fsx-actions";const c=(o,d,h,C)=>{const p=document.createElement("button");p.type="button",p.className=`btn btn-sm ${h}`;const X=document.createElement("span");return X.setAttribute("data-icon",d),X.setAttribute("data-icon-size","13"),p.append(X,document.createTextNode(` ${o}`)),p.addEventListener("click",C),N.appendChild(p),p};let g;c("Copy","copy","btn-ghost",()=>ae(e)),n.email&&c("Email them","mail","btn-ghost",()=>ie(e)),c(n.spam?"Not spam":"Mark as spam",n.spam?"inbox":"alert-triangle","btn-ghost",async()=>{await K(e,!n.spam)&&g?.close()});const T=document.createElement("span");T.className="fsx-spacer",N.appendChild(T),c("Delete","trash","btn-ghost text-danger",async()=>{await W(e)&&g?.close()}),c("Close","x","btn-ghost",()=>g?.close()),a.appendChild(N),g=re(`sub:${e.id}`,n.title,a)}function Te(e){const t=R(e);if(!t)return[];const n=L(t,$.peek());return[{header:n.title},{label:"Open\u2026",icon:"eye",action:()=>V(t)},{label:"Copy submission",icon:"copy",action:()=>ae(t)},{type:"divider"},{label:n.email?`Copy ${n.email}`:"Copy email",icon:"at-sign",disabled:!n.email,action:()=>Se(n.email)},{label:"Email them\u2026",icon:"mail",disabled:!n.email,action:()=>ie(t)},{type:"divider"},n.spam?{label:"Not spam - back to the inbox",icon:"inbox",action:()=>K(t,!1)}:{label:"Mark as spam",icon:"alert-triangle",action:()=>K(t,!0)},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>W(t)}]}const f=r.querySelector(".fsx-list");f.innerHTML=Be;const Me=M.applyBindings({loading:ne,loadError:j,filter:v,query:oe,from:F,to:P,rows:B,counts:he,summary:be,emptyText:ye,intro:we},r,{methods:{showInbox:()=>{v.value="inbox"},showNew:()=>{v.value="new"},showSpam:()=>{v.value="spam"},clearDates:()=>{F.value="",P.value=""}}});b.add(()=>Me.dispose()),b.add(me(r,{position:"bottom"}));const Ae=E.contextMenu(f,{cursor:"glyph",match:".fsx-row",inherit:!1,density:"compact",items:e=>Te(e.dataset.id)});b.add(()=>Ae?.destroy?.());const le=e=>{const t=e.target.closest(".fsx-row");if(!t)return;const n=e.target.closest(".fsx-kebab");if(n){pe(n);return}V(R(t.dataset.id))},de=e=>{const t=e.target.closest(".fsx-row");if(!(!t||e.target!==t)){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),pe(t);else if(e.key==="Enter")V(R(t.dataset.id));else if(e.key==="Delete"){e.preventDefault();const n=R(t.dataset.id);n&&W(n)}}};f.addEventListener("click",le),f.addEventListener("keydown",de),b.add(()=>{f.removeEventListener("click",le),f.removeEventListener("keydown",de)}),Y(i,{id:"form-submissions-back",icon:"arrow-left",title:"All forms",onClick:()=>{location.hash="#/forms"}}),Y(i,{id:"form-submissions-export",icon:"download",title:"Export (CSV or JSON)",onClick:Ne}),Y(i,{id:"form-submissions-clear",icon:"trash",title:"Clear all submissions",onClick:Ce}),b.add(()=>{[...H].forEach(e=>{try{e.close()}catch{}})}),I.scan(r),await Ee(),y()&&requestAnimationFrame(()=>{f.isConnected&&I.scan(f)})}};
@@ -1,4 +1,4 @@
1
- import{apiRequest as v}from"/admin/js/api.js";import{addBannerAction as W}from"/admin/js/lib/plugin-chrome.js";import{SHAPES as me,whileLoading as fe}from"/admin/js/lib/loading.js";import{helpPopovers as J}from"/admin/js/lib/help-popover.js";import{onceClosed as pe,openMenuAt as Y,sidebarCount as P,viewLifetime as be}from"/admin/js/lib/tool-kit.js";import{filterByProject as ve,getProjectFromHash as ge,renderProjectFilterNotice as he}from"/admin/js/lib/project-context.js";import{FILTERS as ye,arrangeForms as we,duplicatePayload as xe,formRow as Ee,shortcodeFor as Se}from"/admin/js/lib/forms-arrange.js";const V=/^#\/(projects\/[^/?#]+\/)?forms\/?(\?.*)?$/,R="#/forms",w="forms.draft",A="forms.filter",ke=`
1
+ import{apiRequest as h}from"/admin/js/api.js";import{addBannerAction as Y}from"/admin/js/lib/plugin-chrome.js";import{SHAPES as ve,whileLoading as he}from"/admin/js/lib/loading.js";import{helpPopovers as Q}from"/admin/js/lib/help-popover.js";import{onceClosed as ge,openMenuAt as G,sidebarCount as P,viewLifetime as ye}from"/admin/js/lib/tool-kit.js";import{filterByProject as we,getProjectFromHash as xe,renderProjectFilterNotice as ke}from"/admin/js/lib/project-context.js";import{FILTERS as Ee,arrangeForms as Se,duplicatePayload as Te,formRow as Me,newFormDraftQuestion as Ce,shortcodeFor as $e,slugify as X}from"/admin/js/lib/forms-arrange.js";const Z=/^#\/(projects\/[^/?#]+\/)?forms\/?(\?.*)?$/,q="#/forms",x="forms.draft",j="forms.filter",De=`
2
2
  <div class="fmx-row" data-slug="{{slug}}" tabindex="0" data-bind-class="rowCls">
3
3
  <span class="fmx-icon" aria-hidden="true"><span data-icon="layout" data-icon-size="16"></span></span>
4
4
  <span class="fmx-main"><strong>{{title}}</strong><small>{{meta}}</small></span>
@@ -10,7 +10,7 @@ import{apiRequest as v}from"/admin/js/api.js";import{addBannerAction as W}from"/
10
10
  </span>
11
11
  <span class="fmx-count" title="Submissions">{{totalText}}</span>
12
12
  <button type="button" class="fmx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
13
- </div>`,x=l=>String(l??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function Me(l){const i=document.createElement("template");return i.innerHTML=l.trim(),i.content.firstElementChild}const G=l=>String(l??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""),r=be(V);export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(l){r.reset();const i=l.get(0).querySelector(".fmx")||l.get(0),h=()=>V.test(location.hash)&&i.isConnected,k=ge(),u=M.observable([]),N=M.observable(!0),T=M.observable("");let q="all";try{q=ye.includes(S.get(A))?S.get(A):"all"}catch{}const p=M.observable(q),j=M.observable("");r.add(M.effect(()=>{const t=p.value;try{S.set(A,t)}catch{}}));const C=M.observable([]);r.add(M.effect(()=>{const t=Date.now();C.value=we(u.value,{filter:p.value,query:j.value}).map(e=>Ee(e,t))}));const Q=M.computed(()=>{const t=u.value;return{all:t.length,recent:t.filter(e=>Number(e.submissionsThisWeek)>0).length,empty:t.filter(e=>!Number(e.submissionCount)).length}}),X=M.computed(()=>{const t=u.value.reduce((e,s)=>e+(Number(s.submissionCount)||0),0);return u.value.length?`${t} submission${t===1?"":"s"} in all`:""}),Z=M.computed(()=>u.value.length?"No forms match.":"No forms yet - New form in the banner makes one."),$=t=>u.peek().find(e=>e.slug===t);async function O(){try{let t=await fe(i.querySelector(".dm-loading-slot"),v("/forms"),me.rows);if(!h())return;if(t=Array.isArray(t)?t:[],k){const e=t.length;t=ve(t,k),he(i,{total:e,shown:t.length,slug:k,allHref:"#/forms",noun:"forms"})}u.value=t,T.value=""}catch(t){T.value=`Could not load the forms: ${t.message||t}`}finally{N.value=!1}}const g=t=>{location.hash=`#/forms/edit/${encodeURIComponent(t)}`},ee=t=>{location.hash=`#/forms/${encodeURIComponent(t)}/submissions`};async function te(t){const e=Se(t.slug);try{await navigator.clipboard.writeText(e),E.toast(`Copied ${e}`,{type:"success"})}catch{E.toast(`Copy this into a page: ${e}`,{type:"info",duration:8e3})}}async function se(t){const e=t.plugin?`This form is managed by the <strong>${x(t.plugin)}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete the form "${x(t.title||t.slug)}" and all its submissions? This cannot be undone.`;if(await E.confirm(e))try{await v(`/forms/${encodeURIComponent(t.slug)}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),P(R),h()&&await O()}catch(s){E.toast(`Failed to delete the form: ${s.message||s}`,{type:"error"})}}const L=new Set;let b=null,_=!1;function D({title:t,html:e,model:s,methods:c,dirty:d=()=>!1,cleanup:n=()=>{}}){const a=E.slideover({title:t,size:"sm",position:"right"}),o=Me(e);a.element.appendChild(o);const f=J(o),re=M.applyBindings(s,o,{methods:c});L.add(a),pe(a,()=>{n(),re.dispose(),f(),L.delete(a)});const ce=a.close.bind(a);let K=!1,F=!1;return a.close=async de=>{if(!K&&de!==!0&&!_&&d()){if(F)return;F=!0;const ue=await E.confirm("Discard what you have typed?");if(F=!1,!ue)return}return K=!0,ce()},I.scan(o),a.open(),{so:a,body:o}}function z(t=null){const e={title:M.observable(t?.title??""),saving:M.observable(!1)};e.slug=M.computed(()=>G(e.title.value)),e.err=M.computed(()=>e.title.value.trim()?e.slug.value?$(e.slug.value)?`There is already a form "${e.slug.value}" - choose another title.`:"":"The title needs at least one letter or number.":""),e.hint=M.computed(()=>e.slug.value&&!e.err.value?`Its address will be "${e.slug.value}" - [form name="${e.slug.value}" /].`:""),e.cannot=M.computed(()=>e.saving.value||!e.title.value.trim()||!!e.err.value);const s=()=>!!e.title.peek().trim(),c={kind:"create",dirty:s,values:()=>({title:e.title.peek()})};async function d(){if(!e.cannot.peek()){e.saving.value=!0;try{const f=await v("/forms",{method:"POST",body:JSON.stringify({title:e.title.peek().trim()})});P(R),n.close(!0),g(f.slug)}catch(f){E.toast(f.message||"Failed to create the form.",{type:"error"})}finally{e.saving.value=!1}}}const{so:n,body:a}=D({title:"New form",model:e,dirty:s,cleanup:()=>{b===c&&(b=null)},methods:{save:d,cancel:()=>n.close()},html:`
13
+ </div>`,k=m=>String(m??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function Fe(m){const i=document.createElement("template");return i.innerHTML=m.trim(),i.content.firstElementChild}const c=ye(Z);export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(m){c.reset();const i=m.get(0).querySelector(".fmx")||m.get(0),p=()=>Z.test(location.hash)&&i.isConnected,T=xe(),d=M.observable([]),B=M.observable(!0),C=M.observable("");let O="all";try{O=Ee.includes(S.get(j))?S.get(j):"all"}catch{}const b=M.observable(O),U=M.observable("");c.add(M.effect(()=>{const e=b.value;try{S.set(j,e)}catch{}}));const $=M.observable([]);c.add(M.effect(()=>{const e=Date.now();$.value=Se(d.value,{filter:b.value,query:U.value}).map(t=>Me(t,e))}));const ee=M.computed(()=>{const e=d.value;return{all:e.length,recent:e.filter(t=>Number(t.submissionsThisWeek)>0).length,empty:e.filter(t=>!Number(t.submissionCount)).length}}),te=M.computed(()=>{const e=d.value.reduce((t,s)=>t+(Number(s.submissionCount)||0),0);return d.value.length?`${e} submission${e===1?"":"s"} in all`:""}),se=M.computed(()=>d.value.length?"No forms match.":"No forms yet - New form in the banner makes one."),D=e=>d.peek().find(t=>t.slug===e);async function _(){try{let e=await he(i.querySelector(".dm-loading-slot"),h("/forms"),ve.rows);if(!p())return;if(e=Array.isArray(e)?e:[],T){const t=e.length;e=we(e,T),ke(i,{total:t,shown:e.length,slug:T,allHref:"#/forms",noun:"forms"})}d.value=e,C.value=""}catch(e){C.value=`Could not load the forms: ${e.message||e}`}finally{B.value=!1}}const g=e=>{location.hash=`#/forms/edit/${encodeURIComponent(e)}`},ae=e=>{location.hash=`#/forms/${encodeURIComponent(e)}/submissions`};async function ne(e){const t=$e(e.slug);try{await navigator.clipboard.writeText(t),E.toast(`Copied ${t}`,{type:"success"})}catch{E.toast(`Copy this into a page: ${t}`,{type:"info",duration:8e3})}}async function oe(e){const t=e.plugin?`This form is managed by the <strong>${k(e.plugin)}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete the form "${k(e.title||e.slug)}" and all its submissions? This cannot be undone.`;if(await E.confirm(t))try{await h(`/forms/${encodeURIComponent(e.slug)}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),P(q),p()&&await _()}catch(s){E.toast(`Failed to delete the form: ${s.message||s}`,{type:"error"})}}const F=new Set,y=new Set;let v=null,z=!1;function L({key:e,title:t,html:s,model:r,methods:n,dirty:o=()=>!1,cleanup:l=()=>{}}){if(y.has(e))return null;y.add(e);let a;try{a=E.slideover({title:t,size:"sm",position:"right"});const u=Fe(s);a.element.appendChild(u);const ue=Q(u),me=M.applyBindings(r,u,{methods:n});F.add(a),ge(a,()=>{y.delete(e),l(),me.dispose(),ue(),F.delete(a)});const fe=a.close.bind(a);let V=!1,R=!1;a.close=async N=>{if(!V&&N!==!0&&!z&&o()){if(R)return;R=!0;const be=await E.confirm("Throw away what you have typed?",{confirmText:"Throw away",cancelText:"Keep editing"});if(R=!1,!be)return}return V=!0,fe()};const pe=N=>{u.inert=!!N};return I.scan(u),a.open(),{so:a,body:u,lock:pe}}catch(u){y.delete(e);try{a?.destroy?.(),a?.element?.remove()}catch{}return E.toast(`Could not open the panel: ${u.message||u}`,{type:"error"}),null}}function K(e=null){const t={title:M.observable(e?.title??""),saving:M.observable(!1)};t.slug=M.computed(()=>X(t.title.value)),t.err=M.computed(()=>t.title.value.trim()?t.slug.value?D(t.slug.value)?`There is already a form "${t.slug.value}" - choose another title.`:"":"The title needs at least one letter or number.":""),t.hint=M.computed(()=>t.slug.value&&!t.err.value?`Its address will be "${t.slug.value}" - [form name="${t.slug.value}" /].`:""),t.cannot=M.computed(()=>t.saving.value||!t.title.value.trim()||!!t.err.value);const s=()=>!!t.title.peek().trim(),r={kind:"create",dirty:s,values:()=>({title:t.title.peek()})};async function n(){if(!t.cannot.peek()){t.saving.value=!0,o.lock(!0);try{const a=await h("/forms",{method:"POST",body:JSON.stringify({title:t.title.peek().trim()})});P(q),o.so.close(!0),A(),p()?g(a.slug):E.toast(`Form "${a.title||a.slug}" created.`,{type:"success"})}catch(a){o.lock(!1),E.toast(a.message||"Failed to create the form.",{type:"error"})}finally{t.saving.value=!1}}}const o=L({key:"create",title:"New form",model:t,dirty:s,cleanup:()=>{v===r&&(v=null)},methods:{save:n,cancel:()=>o.so.close()},html:`
14
14
  <div class="fmx-form">
15
15
  <div>
16
16
  <label class="form-label" for="fmx-new-title">Title
@@ -26,7 +26,7 @@ import{apiRequest as v}from"/admin/js/api.js";import{addBannerAction as W}from"/
26
26
  <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
27
27
  <span data-icon="plus" data-icon-size="13"></span> Create</button>
28
28
  </div>
29
- </div>`});b=c;const o=a.querySelector("#fmx-new-title");o.addEventListener("keydown",f=>{f.key==="Enter"&&(f.preventDefault(),d())}),setTimeout(()=>o?.focus(),50)}function ae(t){const e={title:M.observable(`${t.title||t.slug} (Copy)`),saving:M.observable(!1)};e.slug=M.computed(()=>G(e.title.value)),e.err=M.computed(()=>e.slug.value?$(e.slug.value)?`There is already a form "${e.slug.value}".`:"":"The title needs at least one letter or number."),e.cannot=M.computed(()=>e.saving.value||!!e.err.value);async function s(){if(!e.cannot.peek()){e.saving.value=!0;try{const a=await v(`/forms/${encodeURIComponent(t.slug)}`),o=await v("/forms",{method:"POST",body:JSON.stringify(xe(a,e.title.peek()))});E.toast(`Duplicated as "${o.title}".`,{type:"success"}),P(R),c.close(!0),g(o.slug)}catch(a){E.toast(a.message||"Failed to duplicate the form.",{type:"error"})}finally{e.saving.value=!1}}}const{so:c,body:d}=D({title:`Duplicate ${x(t.title||t.slug)}`,model:e,methods:{save:s,cancel:()=>c.close()},html:`
29
+ </div>`});if(!o)return!1;v=r;const l=o.body.querySelector("#fmx-new-title");return l.addEventListener("keydown",a=>{a.key==="Enter"&&(a.preventDefault(),n())}),setTimeout(()=>l?.focus(),50),!0}function le(e){const t=`${e.title||e.slug} (Copy)`,s={title:M.observable(t),saving:M.observable(!1)};s.slug=M.computed(()=>X(s.title.value)),s.err=M.computed(()=>s.slug.value?D(s.slug.value)?`There is already a form "${s.slug.value}".`:"":"The title needs at least one letter or number."),s.cannot=M.computed(()=>s.saving.value||!!s.err.value);async function r(){if(!s.cannot.peek()){s.saving.value=!0,n.lock(!0);try{const l=await h(`/forms/${encodeURIComponent(e.slug)}`),a=await h("/forms",{method:"POST",body:JSON.stringify(Te(l,s.title.peek()))});E.toast(`Duplicated as "${a.title}".`,{type:"success"}),P(q),n.so.close(!0),p()&&g(a.slug)}catch(l){n.lock(!1),E.toast(l.message||"Failed to duplicate the form.",{type:"error"})}finally{s.saving.value=!1}}}const n=L({key:`duplicate:${e.slug}`,title:`Duplicate ${k(e.title||e.slug)}`,model:s,dirty:()=>s.title.peek().trim()!==t,methods:{save:r,cancel:()=>n.so.close()},html:`
30
30
  <div class="fmx-form">
31
31
  <div>
32
32
  <label class="form-label" for="fmx-dup-title">Title of the copy</label>
@@ -40,7 +40,7 @@ import{apiRequest as v}from"/admin/js/api.js";import{addBannerAction as W}from"/
40
40
  <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
41
41
  <span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
42
42
  </div>
43
- </div>`}),n=d.querySelector("#fmx-dup-title");n.addEventListener("keydown",a=>{a.key==="Enter"&&(a.preventDefault(),s())}),setTimeout(()=>{n?.focus(),n?.select()},50)}function ne(){const t={to:M.observable(window.__CMS_SITE__?.smtp?.fromAddress||""),sending:M.observable(!1)};t.cannot=M.computed(()=>t.sending.value||!/^[^\s@]+@[^\s@]+$/.test(t.to.value.trim()));async function e(){if(!t.cannot.peek()){t.sending.value=!0;try{await v("/forms/test-email",{method:"POST",body:JSON.stringify({to:t.to.peek().trim()})}),s.close(!0),E.toast("Test email sent.",{type:"success"})}catch(n){E.toast(n.message||"Failed to send the test email.",{type:"error"})}finally{t.sending.value=!1}}}const{so:s,body:c}=D({title:"Send test email",model:t,methods:{send:e,cancel:()=>s.close()},html:`
43
+ </div>`});if(!n)return;const o=n.body.querySelector("#fmx-dup-title");o.addEventListener("keydown",l=>{l.key==="Enter"&&(l.preventDefault(),r())}),setTimeout(()=>{o?.focus(),o?.select()},50)}function ie(){const e={to:M.observable(window.__CMS_SITE__?.smtp?.fromAddress||""),sending:M.observable(!1)};e.cannot=M.computed(()=>e.sending.value||!/^[^\s@]+@[^\s@]+$/.test(e.to.value.trim()));async function t(){if(!e.cannot.peek()){e.sending.value=!0,s.lock(!0);try{await h("/forms/test-email",{method:"POST",body:JSON.stringify({to:e.to.peek().trim()})}),s.so.close(!0),E.toast("Test email sent.",{type:"success"})}catch(n){s.lock(!1),E.toast(n.message||"Failed to send the test email.",{type:"error"})}finally{e.sending.value=!1}}}const s=L({key:"test-email",title:"Send test email",model:e,methods:{send:t,cancel:()=>s.so.close()},html:`
44
44
  <div class="fmx-form">
45
45
  <div>
46
46
  <label class="form-label" for="fmx-test-to">Send to
@@ -54,4 +54,4 @@ import{apiRequest as v}from"/admin/js/api.js";import{addBannerAction as W}from"/
54
54
  <button type="button" class="btn btn-sm btn-primary" data-on-click="send" data-bind-disabled="cannot.value">
55
55
  <span data-icon="mail" data-icon-size="13"></span> Send</button>
56
56
  </div>
57
- </div>`}),d=c.querySelector("#fmx-test-to");d.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),e())}),setTimeout(()=>d?.focus(),50)}function oe(t){const e=$(t);if(!e)return[];const s=Number(e.submissionCount)||0;return[{header:e.title||e.slug},{label:"Edit",icon:"edit",action:()=>g(e.slug)},{label:`Submissions (${s})`,icon:"inbox",action:()=>ee(e.slug)},{type:"divider"},{label:"Copy shortcode",icon:"code",action:()=>te(e)},{label:"Duplicate\u2026",icon:"copy",action:()=>ae(e)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>se(e)}]}const m=i.querySelector(".fmx-list");m.innerHTML=ke;const le=M.applyBindings({forms:u,loading:N,loadError:T,filter:p,query:j,rows:C,counts:Q,summary:X,emptyText:Z},i,{methods:{showAll:()=>{p.value="all"},showRecent:()=>{p.value="recent"},showEmpty:()=>{p.value="empty"}}});r.add(()=>le.dispose()),r.add(J(i,{position:"bottom"})),r.add(M.effect(()=>{C.value,requestAnimationFrame(()=>{m.isConnected&&I.scan(m)})}));const ie=E.contextMenu(m,{cursor:"glyph",match:".fmx-row",inherit:!1,density:"compact",items:t=>oe(t.dataset.slug)});r.add(()=>ie?.destroy?.());const B=t=>{const e=t.target.closest(".fmx-row");if(!e)return;const s=t.target.closest(".fmx-kebab");if(s){Y(s);return}g(e.dataset.slug)},U=t=>{const e=t.target.closest(".fmx-row");e&&(t.key==="F10"&&t.shiftKey?(t.preventDefault(),Y(e)):t.key==="Enter"&&t.target===e&&g(e.dataset.slug))};m.addEventListener("click",B),m.addEventListener("keydown",U),r.add(()=>{m.removeEventListener("click",B),m.removeEventListener("keydown",U)}),W(l,{id:"forms-new",icon:"plus",title:"New form",onClick:()=>z()}),W(l,{id:"forms-test-email",icon:"mail",title:"Send a test email",onClick:()=>ne()});const H=t=>{b?.dirty()&&(t.preventDefault(),t.returnValue="")};if(window.addEventListener("beforeunload",H),r.add(()=>window.removeEventListener("beforeunload",H)),r.add(()=>{if(b?.dirty())try{S.set(w,{values:b.values(),at:new Date().toISOString()})}catch{}_=!0,[...L].forEach(t=>{try{t.close(!0)}catch{}})}),I.scan(i),await O(),!h())return;let y=null;try{y=S.get(w)}catch{}if(y?.values?.title){try{S.remove?S.remove(w):S.set(w,null)}catch{}await E.confirm(`You started a new form, "${x(y.values.title)}", and left it. Pick up where you left off?`)&&h()&&z(y.values)}}};
57
+ </div>`});if(!s)return;const r=s.body.querySelector("#fmx-test-to");r.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),t())}),setTimeout(()=>r?.focus(),50)}function re(e){const t=D(e);if(!t)return[];const s=Number(t.submissionCount)||0;return[{header:t.title||t.slug},{label:"Edit",icon:"edit",action:()=>g(t.slug)},{label:`Submissions (${s})`,icon:"inbox",action:()=>ae(t.slug)},{type:"divider"},{label:"Copy shortcode",icon:"code",action:()=>ne(t)},{label:"Duplicate\u2026",icon:"copy",action:()=>le(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>oe(t)}]}const f=i.querySelector(".fmx-list");f.innerHTML=De;const ce=M.applyBindings({forms:d,loading:B,loadError:C,filter:b,query:U,rows:$,counts:ee,summary:te,emptyText:se},i,{methods:{showAll:()=>{b.value="all"},showRecent:()=>{b.value="recent"},showEmpty:()=>{b.value="empty"}}});c.add(()=>ce.dispose()),c.add(Q(i,{position:"bottom"})),c.add(M.effect(()=>{$.value,requestAnimationFrame(()=>{f.isConnected&&I.scan(f)})}));const de=E.contextMenu(f,{cursor:"glyph",match:".fmx-row",inherit:!1,density:"compact",items:e=>re(e.dataset.slug)});c.add(()=>de?.destroy?.());const H=e=>{const t=e.target.closest(".fmx-row");if(!t)return;const s=e.target.closest(".fmx-kebab");if(s){G(s);return}g(t.dataset.slug)},W=e=>{const t=e.target.closest(".fmx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),G(t)):e.key==="Enter"&&e.target===t&&g(t.dataset.slug))};f.addEventListener("click",H),f.addEventListener("keydown",W),c.add(()=>{f.removeEventListener("click",H),f.removeEventListener("keydown",W)}),Y(m,{id:"forms-new",icon:"plus",title:"New form",onClick:()=>K()}),Y(m,{id:"forms-test-email",icon:"mail",title:"Send a test email",onClick:()=>ie()});const A=()=>{try{S.remove?S.remove(x):S.set(x,null)}catch{}},J=e=>{v?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",J),c.add(()=>window.removeEventListener("beforeunload",J)),c.add(()=>{if(v?.dirty())try{S.set(x,{values:v.values(),at:new Date().toISOString()})}catch{}z=!0,[...F].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(i),await _(),!p())return;let w=null;try{w=S.get(x)}catch{}if(w?.values?.title){const e=Ce(w,d.peek());if(await E.confirm(k(e),{title:"Unsaved new form",confirmText:"Throw away",cancelText:"Keep it"})){A();return}if(!p())return;K(w.values)&&A()}}};
@@ -1,4 +1,4 @@
1
- import{protectToolEditor as se,mountToolTrash as ne}from"../lib/tool-recovery.js";import{apiRequest as le}from"../api.js";import{addBannerAction as ie}from"../lib/plugin-chrome.js";import{helpPopovers as R}from"../lib/help-popover.js";import{mountRelated as re,relatedMenuItems as ce,relatedProviders as de}from"../lib/related.js";import{onceClosed as K,openMenuAt as ue,settingsStore as pe,sidebarCount as ve,viewLifetime as me}from"../lib/tool-kit.js";import{arrangeNotes as be,collectCategories as fe,DEFAULT_SETTINGS as ye,normaliseSettings as j,parseCategories as he,preview as ge,PREVIEW_MAX as we,PREVIEW_MIN as ke}from"../lib/notes-arrange.js";const z=pe("notes.settings",j),N=(m,r="GET",g)=>le(m,{method:r,...g!==void 0?{body:JSON.stringify(g)}:{}});function W(m){const r=document.createElement("template");return r.innerHTML=m.trim(),r.content.firstElementChild}const Ee=`
1
+ import{protectToolEditor as ye,mountToolTrash as he}from"../lib/tool-recovery.js";import{apiRequest as ge}from"../api.js";import{addBannerAction as we}from"../lib/plugin-chrome.js";import{helpPopovers as B}from"../lib/help-popover.js";import{mountRelated as Ee,relatedMenuItems as ke,relatedProviders as Me}from"../lib/related.js";import{onceClosed as Z,openMenuAt as Ce,settingsStore as xe,sidebarCount as Te,viewLifetime as Le}from"../lib/tool-kit.js";import{SHAPES as Q,whileLoading as Se}from"../lib/loading.js";import{arrangeNotes as Ne,categoryOptions as De,collectCategories as Ae,DEFAULT_SETTINGS as Oe,normaliseSettings as ee,parseCategories as Pe,preview as qe,PREVIEW_MAX as Re,PREVIEW_MIN as $e}from"../lib/notes-arrange.js";const te=xe("notes.settings",ee),P=(i,r="GET",w)=>ge(i,{method:r,...w!==void 0?{body:JSON.stringify(w)}:{}});function ae(i){if(i){try{i.close=Object.getPrototypeOf(i).close?.bind(i)??i.close,i.destroy?.()}catch{}try{i.element?.remove()}catch{}}}function oe(i){const r=document.createElement("template");return r.innerHTML=i.trim(),r.content.firstElementChild}const Fe=`
2
2
  <article class="note-card" data-id="{{id}}" tabindex="0">
3
3
  <h3 class="note-title">{{title}}</h3>
4
4
  <p class="note-preview">{{preview}}</p>
@@ -7,9 +7,9 @@ import{protectToolEditor as se,mountToolTrash as ne}from"../lib/tool-recovery.js
7
7
  <time class="note-time" title="{{updatedFull}}">{{updatedRel}}</time>
8
8
  </footer>
9
9
  <button type="button" class="note-kebab" aria-label="Note actions" title="Actions">\u22EE</button>
10
- </article>`,c=me(/^#\/notes\b/);export const notesView={templateUrl:"/admin/js/templates/notes.html",async onMount(m){c.reset();const r=m.get(0);let g=[];de("note").then(e=>{g=e});const d=M.observableArray([]),S=M.observable(""),p=M.observable(""),b=M.observable(!0),C=M.observable(""),$=M.observable(!1),s=M.observable(z.read()),x=M.computed(()=>fe(d.value));c.add(M.effect(()=>{d.value,b.peek()||ve("#/notes")}));const _=M.computed(()=>be(d.value,{query:S.value,category:p.value,sort:s.value.sort})),T=M.observable([]);c.add(M.effect(()=>{const e=s.value.previewLength;T.value=_.value.map(t=>({id:t.id,title:t.title||"Untitled",preview:ge(t.content,e),cats:(t.categories||[]).map(a=>({name:a})),updatedRel:t.updatedAt?D(t.updatedAt).fromNow():"",updatedFull:t.updatedAt?"Updated "+D(t.updatedAt).format("DD MMM YYYY HH:mm"):""}))}));const F=M.computed(()=>!!(S.value.trim()||p.value)),G={query:S,category:p,loading:b,loadError:C,searchFocus:$,categories:x,visible:T,isCards:M.computed(()=>s.value.layout==="cards"),listClass:M.computed(()=>(s.value.layout==="cards"?"is-cards":"is-list")+(s.value.showCategories?"":" hide-cats")),isEmpty:M.computed(()=>!b.value&&!C.value&&T.value.length===0),emptyText:M.computed(()=>F.value?"No notes match. Clear the search or pick another category.":"No notes yet. Click New to write your first."),countText:M.computed(()=>{if(b.value)return"";const e=d.value.length,t=T.value.length;return F.value?`${t} of ${e}`:`${e} note${e===1?"":"s"}`})};c.add(M.effect(()=>{const e=p.value.toLowerCase();e&&!x.value.some(t=>t.toLowerCase()===e)&&(p.value="")})),c.add(M.effect(()=>z.write(s.value)));const L=(e,t)=>{s.value=j({...s.peek(),[e]:t})};r.querySelector(".notes-items").innerHTML=Ee;const J=M.applyBindings(G,r,{methods:{create:()=>w(null),retry:A,useCards:()=>L("layout","cards"),useList:()=>L("layout","list")}});c.add(()=>J.dispose()),ne({root:r,toolbar:r.querySelector(".notes-bar-end"),tool:"notes",endpoint:"/notes",itemPath:"/items",label:"Notes",titleOf:e=>e.title||e.content,reload:A,lifetime:c}),c.add(R(r,{position:"bottom"}));const f=e=>d.peek().find(t=>t.id===e);function X(e){const t=d.peek(),a=t.findIndex(o=>o.id===e.id);d.value=a===-1?[e,...t]:t.map(o=>o.id===e.id?e:o)}async function A(){b.value=!0;try{const e=await N("/notes/items");d.value=Array.isArray(e)?e:e.data??[],C.value=""}catch{C.value="Could not load this list. Your items have not been removed.",E.toast("Failed to load notes",{type:"error"})}finally{b.value=!1}}async function U(e,t){const a=e?await N("/notes/items/"+e,"PUT",t):await N("/notes/items","POST",t);return X(a),a}async function Y(e){if(s.peek().confirmDelete&&!await E.confirm("Move this note to Trash? You can restore it later."))return!1;try{return await N("/notes/items/"+e,"DELETE"),d.value=d.peek().filter(t=>t.id!==e),E.toast("Note moved to Trash",{type:"success"}),!0}catch{return E.toast("Failed to delete note",{type:"error"}),!1}}async function Z(e){const t=f(e);if(t)try{await U(null,{title:`${t.title||"Untitled"} (copy)`,content:t.content||"",categories:t.categories||[]}),E.toast("Note duplicated",{type:"success"})}catch{E.toast("Failed to duplicate note",{type:"error"})}}async function Q(e){const t=f(e);if(t)try{await navigator.clipboard.writeText([t.title,t.content].filter(Boolean).join(`
10
+ </article>`,u=Le(/^#\/notes\b/);export const notesView={templateUrl:"/admin/js/templates/notes.html",async onMount(i){u.reset();const r=i.get(0);let w=!1;u.add(()=>{w=!0});const v=()=>!w&&r.isConnected;let K=[];Me("note").then(e=>{v()&&(K=e)}).catch(()=>{});const p=M.observableArray([]),q=M.observable(""),m=M.observable(""),k=M.observable(!0),R=M.observable(!1);let $=!1;const C=M.observable(""),j=M.observable(!1),s=M.observable(te.read()),N=M.computed(()=>Ae(p.value));u.add(M.effect(()=>{p.value,$&&Te("#/notes")}));const se=M.computed(()=>Ne(p.value,{query:q.value,category:m.value,sort:s.value.sort})),A=M.observable([]);u.add(M.effect(()=>{const e=s.value.previewLength;A.value=se.value.map(t=>({id:t.id,title:t.title||"Untitled",preview:qe(t.content,e),cats:(t.categories||[]).map(a=>({name:a})),updatedRel:t.updatedAt?D(t.updatedAt).fromNow():"",updatedFull:t.updatedAt?"Updated "+D(t.updatedAt).format("DD MMM YYYY HH:mm"):""}))}));const z=M.computed(()=>!!(q.value.trim()||m.value)),ne={query:q,category:m,loading:k,busy:R,loadError:C,searchFocus:j,categories:N,visible:A,categoryChoices:M.computed(()=>De(p.value)),isCards:M.computed(()=>s.value.layout==="cards"),listClass:M.computed(()=>(s.value.layout==="cards"?"is-cards":"is-list")+(s.value.showCategories?"":" hide-cats")),isEmpty:M.computed(()=>!k.value&&!C.value&&A.value.length===0),emptyText:M.computed(()=>z.value?"No notes match. Clear the search or pick another category.":"No notes yet. Click New to write your first."),countText:M.computed(()=>{if(k.value)return"";const e=p.value.length,t=A.value.length;return z.value?`${t} of ${e}`:`${e} note${e===1?"":"s"}`})};u.add(M.effect(()=>{const e=m.value.toLowerCase();e&&!N.value.some(t=>t.toLowerCase()===e)&&(m.value="")})),u.add(M.effect(()=>te.write(s.value)));const O=(e,t)=>{s.value=ee({...s.peek(),[e]:t})};r.querySelector(".notes-items").innerHTML=Fe;const le=M.applyBindings(ne,r,{methods:{create:()=>T(null),retry:H,useCards:()=>O("layout","cards"),useList:()=>O("layout","list")}});u.add(()=>le.dispose()),he({root:r,toolbar:r.querySelector(".notes-bar-end"),tool:"notes",endpoint:"/notes",itemPath:"/items",label:"Notes",titleOf:e=>e.title||e.content,reload:H,lifetime:u}),u.add(B(r,{position:"bottom"}));const h=e=>p.peek().find(t=>t.id===e);function ie(e){const t=p.peek(),a=t.findIndex(o=>o.id===e.id);p.value=a===-1?[e,...t]:t.map(o=>o.id===e.id?e:o)}let x=null,F=!1;function H(){return x?(F=!0,x):(R.value=!0,x=(async()=>{do F=!1,await re();while(F&&v())})().finally(()=>{x=null,v()&&(R.value=!1)}),x)}async function re(){const e=!$;e&&(k.value=!0,C.value="");try{const t=P("/notes/items"),a=s.peek().layout==="cards"?Q.tiles:Q.rows,o=e?await Se(r.querySelector(".notes-loading"),t,a):await t;if(!v())return;p.value=Array.isArray(o)?o:o.data??[],$=!0,C.value=""}catch{if(!v())return;C.value="Could not load this list. Your items have not been removed.",E.toast("Failed to load notes",{type:"error"})}finally{v()&&(k.value=!1)}}async function W(e,t){const a=e?await P("/notes/items/"+e,"PUT",t):await P("/notes/items","POST",t);return v()&&ie(a),a}async function _(e){if(s.peek().confirmDelete&&!await E.confirm("Move this note to Trash? You can restore it later.")||!v())return!1;try{return await P("/notes/items/"+e,"DELETE"),v()&&(p.value=p.peek().filter(t=>t.id!==e),c?.id===e&&(c.recovery?.discard(),c.so.close()),E.toast("Note moved to Trash",{type:"success"})),!0}catch{return E.toast("Failed to delete note",{type:"error"}),!1}}async function ce(e){const t=h(e);if(t)try{await W(null,{title:`${t.title||"Untitled"} (copy)`,content:t.content||"",categories:t.categories||[]}),E.toast("Note duplicated",{type:"success"})}catch{E.toast("Failed to duplicate note",{type:"error"})}}async function de(e){const t=h(e);if(t)try{await navigator.clipboard.writeText([t.title,t.content].filter(Boolean).join(`
11
11
 
12
- `)),E.toast("Copied to clipboard",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}let y=null;async function w(e){if(y&&!await y.close())return;const t=!e,a=E.slideover({title:t?"New note":"Edit note",size:"md",position:"right"});y=a;const o=W(`
12
+ `)),E.toast("Copied to clipboard",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}let c=null,U=!1;async function T(e){if(!(U||e&&c?.id===e.id)){U=!0;try{if(c&&!await c.so.close()||!v()||c)return;ue(e)}catch(t){ae(c?.so),c=null,console.error("[notes] editor failed to open",t),E.toast("Could not open the note",{type:"error"})}finally{U=!1}}}function ue(e){const t=!e,a=E.slideover({title:t?"New note":"Edit note",size:"md",position:"right"});c={so:a,id:e?.id??null,recovery:null};const o=oe(`
13
13
  <div class="notes-panel">
14
14
  <div>
15
15
  <label class="form-label" for="note-title-input">Title</label>
@@ -47,7 +47,7 @@ import{protectToolEditor as se,mountToolTrash as ne}from"../lib/tool-recovery.js
47
47
  <span data-icon="save" data-icon-size="13"></span> Save
48
48
  </button>
49
49
  </div>
50
- </div>`),l={title:e?.title||"",content:e?.content||"",cats:t?s.peek().defaultCategories:e.categories||[]},v=M.observable(l.title),u=M.observable(l.content),i=M.observable([...l.cats]),h=M.observable(!1),k={title:v,content:u,saving:h,canDelete:!t,stats:M.computed(()=>{const n=u.value.trim(),B=n?n.split(/\s+/).length:0;return`${B} word${B===1?"":"s"} \xB7 ${u.value.length} characters`}),dirty:M.computed(()=>v.value!==l.title||u.value!==l.content||i.value.join("\0")!==l.cats.join("\0"))};async function O(){if(!h.peek()){h.value=!0;try{await U(e?.id,{title:v.peek().trim(),content:u.peek(),categories:i.peek()}),E.toast(t?"Note created":"Note saved",{type:"success"}),V.saved(),a.close()}catch{E.toast("Failed to save note",{type:"error"})}finally{h.value=!1}}}a.element.appendChild(o),t||re(o.querySelector(".related-box"),"note",e.id,{onOpen:()=>a.close()});const ae=M.applyBindings(k,o,{methods:{submit:O,cancel:()=>a.close(),del:async()=>{await Y(e.id)&&(V.discard(),a.close())}}}),P=E.pillbox(o.querySelector(".notes-cats-input"),{data:x.peek().map(n=>({value:n,label:n})),value:i.peek(),creatable:!0,placeholder:"Add a category\u2026",onChange:()=>{i.value=he(P.getValue())}}),oe=R(o);o.addEventListener("keydown",n=>{(n.ctrlKey||n.metaKey)&&n.key==="Enter"&&(n.preventDefault(),O())}),K(a,()=>{ae.dispose(),oe(),P.destroy?.(),y===a&&(y=null)});const V=se({so:a,panel:o,tool:"notes",id:e?.id,lifetime:c,read:()=>({title:v.peek(),content:u.peek(),cats:[...i.peek()]}),write:n=>{v.value=n.title??"",u.value=n.content??"",i.value=Array.isArray(n.cats)?n.cats:[],P.setValue(i.peek())}});a.open(),setTimeout(()=>o.querySelector(t?"#note-title-input":"#note-content-input")?.focus(),60)}function ee(){const e=E.slideover({title:"Notes settings",size:"sm",position:"right"}),t=W(`
50
+ </div>`),n={title:e?.title||"",content:e?.content||"",cats:t?s.peek().defaultCategories:e.categories||[]},y=M.observable(n.title),f=M.observable(n.content),d=M.observable([...n.cats]),g=M.observable(!1),L={title:y,content:f,saving:g,canDelete:!t,stats:M.computed(()=>{const l=f.value.trim(),X=l?l.split(/\s+/).length:0;return`${X} word${X===1?"":"s"} \xB7 ${f.value.length} characters`}),dirty:M.computed(()=>y.value!==n.title||f.value!==n.content||d.value.join("\0")!==n.cats.join("\0"))};async function J(){if(!g.peek()){g.value=!0,o.inert=!0;try{await W(e?.id,{title:y.peek().trim(),content:f.peek(),categories:d.peek()}),E.toast(t?"Note created":"Note saved",{type:"success"}),V.saved(),a.close()}catch{E.toast("Failed to save note",{type:"error"}),o.inert=!1}finally{g.value=!1}}}a.element.appendChild(o),t||Ee(o.querySelector(".related-box"),"note",e.id,{onOpen:()=>a.close()});const me=M.applyBindings(L,o,{methods:{submit:J,cancel:()=>a.close(),del:async()=>{await _(e.id)&&(V.discard(),a.close())}}});let S=null;S=E.pillbox(o.querySelector(".notes-cats-input"),{data:N.peek().map(l=>({value:l,label:l})),value:d.peek(),creatable:!0,placeholder:"Add a category\u2026",onChange:()=>{S&&(d.value=Pe(S.getValue()))}});const be=B(o);o.addEventListener("keydown",l=>{(l.ctrlKey||l.metaKey)&&l.key==="Enter"&&(l.preventDefault(),J())}),Z(a,()=>{me.dispose(),be(),S?.destroy?.(),c?.so===a&&(c=null)});const V=ye({so:a,panel:o,tool:"notes",id:e?.id,lifetime:u,read:()=>({title:y.peek(),content:f.peek(),cats:[...d.peek()]}),write:l=>{y.value=l.title??"",f.value=l.content??"",d.value=Array.isArray(l.cats)?l.cats:[],S?.setValue(d.peek())}});c?.so===a&&(c.recovery=V),a.open(),setTimeout(()=>o.querySelector(t?"#note-title-input":"#note-content-input")?.focus(),60)}let b=null;function pe(){if(!b)try{ve()}catch(e){ae(b),b=null,console.error("[notes] settings failed to open",e)}}u.add(()=>b?.close());function ve(){const e=E.slideover({title:"Notes settings",size:"sm",position:"right"});b=e;const t=oe(`
51
51
  <div class="notes-panel">
52
52
  <div class="notes-setting">
53
53
  <label class="form-label" for="notes-set-layout">Layout
@@ -71,7 +71,7 @@ import{protectToolEditor as se,mountToolTrash as ne}from"../lib/tool-recovery.js
71
71
  <span data-help="How much of each note's content shows on its card. 0 shows titles only."></span>
72
72
  </label>
73
73
  <div class="notes-range">
74
- <input id="notes-set-preview" type="range" min="${ke}" max="${we}" step="20"
74
+ <input id="notes-set-preview" type="range" min="${$e}" max="${Re}" step="20"
75
75
  data-model="previewLength.value">
76
76
  <output data-bind-text="previewText.value"></output>
77
77
  </div>
@@ -102,4 +102,4 @@ import{protectToolEditor as se,mountToolTrash as ne}from"../lib/tool-recovery.js
102
102
  <span class="spacer"></span>
103
103
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
104
104
  </div>
105
- </div>`),a=(i,h=k=>k)=>M.computed({read:()=>s.value[i],write:k=>L(i,h(k))}),o={layout:a("layout"),sort:a("sort"),previewLength:a("previewLength",Number),showCategories:a("showCategories"),confirmDelete:a("confirmDelete"),previewText:M.computed(()=>s.value.previewLength?`${s.value.previewLength} ch`:"Off")};e.element.appendChild(t);let l;const v=M.applyBindings(o,t,{methods:{done:()=>e.close(),reset:()=>{s.value={...ye},l?.setValue([])}}});l=E.pillbox(t.querySelector(".notes-set-defaults"),{data:x.peek().map(i=>({value:i,label:i})),value:s.peek().defaultCategories,creatable:!0,placeholder:"None",onChange:()=>L("defaultCategories",l.getValue())});const u=R(t);K(e,()=>{v.dispose(),u(),l.destroy?.()}),e.open()}ie(m,{id:"notes-settings",icon:"settings",title:"Notes settings",onClick:ee});const q=r.querySelector(".notes-list"),te=E.contextMenu(q,{cursor:"glyph",match:".note-card",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,o=f(t)?.categories||[];return[{label:"Edit",icon:"edit",action:()=>w(f(t))},...ce(g,t),{label:"Duplicate",icon:"copy",action:()=>Z(t)},{label:"Copy text",icon:"clipboard",action:()=>Q(t)},{label:"Show only\u2026",icon:"filter",disabled:o.length===0,submenu:o.map(l=>({label:l,icon:"tag",action:()=>{p.value=l}}))},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>Y(t)}]}});c.add(()=>te?.destroy?.()),q.addEventListener("click",e=>{const t=e.target.closest(".note-card");if(!t)return;const a=e.target.closest(".note-cat");if(a){p.value=a.dataset.cat;return}const o=e.target.closest(".note-kebab");if(o){ue(o);return}w(f(t.dataset.id))}),q.addEventListener("keydown",e=>{const t=e.target.closest?.(".note-card");t&&e.key==="Enter"&&e.target===t&&w(f(t.dataset.id))});const H=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),$.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),w(null)))};document.addEventListener("keydown",H),c.add(()=>document.removeEventListener("keydown",H)),c.add(()=>y?.close()),I.scan(r),await A()}};
105
+ </div>`),a=(d,g=L=>L)=>M.computed({read:()=>s.value[d],write:L=>O(d,g(L))}),o={layout:a("layout"),sort:a("sort"),previewLength:a("previewLength",Number),showCategories:a("showCategories"),confirmDelete:a("confirmDelete"),previewText:M.computed(()=>s.value.previewLength?`${s.value.previewLength} ch`:"Off")};e.element.appendChild(t);let n;const y=M.applyBindings(o,t,{methods:{done:()=>e.close(),reset:()=>{s.value={...Oe},n?.setValue([])}}});n=E.pillbox(t.querySelector(".notes-set-defaults"),{data:N.peek().map(d=>({value:d,label:d})),value:s.peek().defaultCategories,creatable:!0,placeholder:"None",onChange:()=>{n&&O("defaultCategories",n.getValue())}});const f=B(t);Z(e,()=>{y.dispose(),f(),n?.destroy?.(),b===e&&(b=null)}),e.open()}we(i,{id:"notes-settings",icon:"settings",title:"Notes settings",onClick:pe});const Y=r.querySelector(".notes-list"),fe=E.contextMenu(Y,{cursor:"glyph",match:".note-card",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,o=h(t)?.categories||[];return[{label:"Edit",icon:"edit",action:()=>T(h(t))},...ke(K,t),{label:"Duplicate",icon:"copy",action:()=>ce(t)},{label:"Copy text",icon:"clipboard",action:()=>de(t)},{label:"Show only\u2026",icon:"filter",disabled:o.length===0,submenu:o.map(n=>({label:n,icon:"tag",action:()=>{m.value=n}}))},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>_(t)}]}});u.add(()=>fe?.destroy?.()),Y.addEventListener("click",e=>{const t=e.target.closest(".note-card");if(!t)return;const a=e.target.closest(".note-cat");if(a){m.value=a.dataset.cat;return}const o=e.target.closest(".note-kebab");if(o){Ce(o);return}T(h(t.dataset.id))}),Y.addEventListener("keydown",e=>{const t=e.target.closest?.(".note-card");t&&e.key==="Enter"&&e.target===t&&T(h(t.dataset.id))});const G=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),j.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),T(null)))};document.addEventListener("keydown",G),u.add(()=>document.removeEventListener("keydown",G)),I.scan(r),await H()}};
@@ -1,7 +1,7 @@
1
- import{apiRequest as N}from"../api.js";import{addBannerAction as Z}from"../lib/plugin-chrome.js";import{helpPopovers as ve}from"../lib/help-popover.js";import{openMenuAt as ee,sidebarCount as me}from"../lib/tool-kit.js";import{POSITION_LABEL as fe,clampNotes as be,formFromSettings as O,liveChips as he,maxRadius as ye,payloadFromForm as c,previewState as te,radiusForSize as ge,resultRow as ke,snapshot as F}from"../lib/search-settings-arrange.js";import{fmtDateTime as Se}from"../lib/dates.js";const U=/^\/search(\?.*)?$/,we="#/search",b="search.draft",xe='<span class="ssx-chip" data-bind-class="cls">{{text}}</span>',Me=`
1
+ import{apiRequest as O}from"../api.js";import{addBannerAction as ae}from"../lib/plugin-chrome.js";import{helpPopovers as xe}from"../lib/help-popover.js";import{openMenuAt as re,sidebarCount as Se}from"../lib/tool-kit.js";import{POSITION_LABEL as ke,clampNotes as Me,draftQuestion as Ee,formFromSettings as _,keptDraft as Te,searchDraftKey as Re,liveChips as Le,maxRadius as Ce,payloadFromForm as l,previewState as ie,radiusForSize as Pe,resultRow as ze,snapshot as v}from"../lib/search-settings-arrange.js";import{fmtDateTime as qe}from"../lib/dates.js";import{SHAPES as Ae,whileLoading as De}from"../lib/loading.js";const Y=/^\/search(\?.*)?$/,Ne="#/search",Oe='<span class="ssx-chip" data-bind-class="cls">{{text}}</span>',_e=`
2
2
  <div class="ssx-row" data-url="{{url}}" tabindex="0">
3
3
  <span class="ssx-rank">{{rank}}</span>
4
4
  <span class="ssx-main"><strong>{{title}}</strong><small>{{meta}}</small><span class="ssx-snippet">{{snippet}}</span></span>
5
5
  <span class="ssx-chip">{{score}}</span>
6
6
  <button type="button" class="ssx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
7
- </div>`;let u=null,se=!1;function Ee(){se||!window.R?.use||(se=!0,R.use((i,v,o)=>!(U.test(v?.path||"")&&!U.test(i?.path||""))||!u?.dirty()?o():E.confirm("You have unsaved search settings. Leave anyway? They are kept as a draft for your next visit.").then(r=>{r?o():history.replaceState(null,"","#/search")})),M.subscribe("router:afterChange",({to:i}={})=>{if(u&&!U.test(i?.path||"")){const v=u;u=null,v.dispose()}}))}export const searchView={templateUrl:"/admin/js/templates/search.html",async onMount(i){if(Ee(),u){const e=u;u=null,e.dispose()}const v=[],o=e=>(v.push(e),e);let P=!1;const r=i.get(0).querySelector(".ssx")||i.get(0),$=()=>!P&&r.isConnected,K=E.loader(i.get(0),{type:"dots"});let j,A=null;try{[j,A]=await Promise.all([N("/search/settings"),N("/search/_count").catch(()=>null)])}catch(e){K.destroy(),E.toast(`Could not load the search settings: ${e.message||e}`,{type:"error"});return}if(K.destroy(),!r.isConnected)return;const w=M.observable(j),B=O(j),s={};for(const[e,t]of Object.entries(B))s[e]=M.observable(t);const h=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),p=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),x=M.observable(F(B)),L=M.computed(()=>F(p())!==x.value),T=M.observable(!1),Y=M.observable(A?{searchable:A.count}:null),H=M.observable([]);o(M.effect(()=>{H.value=he(w.value,Y.value).map(e=>({text:e.text,cls:e.tone?`is-${e.tone}`:""}))}));let m=Number(s.iconSize.peek())||52;o(M.effect(()=>{const e=Number(s.iconSize.value)||52;if(e===m)return;const t=ge(s.iconRadius.peek(),m,e);m=e,String(t)!==String(s.iconRadius.peek())&&(s.iconRadius.value=String(t))}));const oe=M.computed(()=>ye(s.iconSize.value)),V=r.querySelector("#ssx-radius");o(M.effect(()=>{V&&(V.max=String(oe.value))}));const y=M.observable(""),g=M.observable([]),z=M.observable(""),C=M.observable(""),ne=M.computed(()=>te(y.value,s.minQueryLength.value).note);let _=0,q=null;async function ae(){const e=te(y.peek(),s.minQueryLength.peek()),t=++_;if(!e.run){g.value=[],C.value="",z.value="";return}const a=c(h()).maxResults;try{const n=await N(`/search/preview?q=${encodeURIComponent(y.peek().trim())}&max=${a}`);if(t!==_||!$())return;g.value=(n.results||[]).map(ke),z.value="";const l=g.peek().length;C.value=l?`${l} result${l===1?"":"s"} (showing up to ${n.maxResults}) from ${n.coverage?.searchable??"?"} searchable pages.`:`Nothing found in ${n.coverage?.searchable??"?"} searchable pages.`,n.coverage&&(Y.value=n.coverage)}catch(n){if(t!==_)return;g.value=[],C.value="",z.value=`Search failed: ${n.message||n}`}}o(M.effect(()=>{y.value,s.maxResults.value,clearTimeout(q),q=setTimeout(ae,c(h()).debounceMs)})),o(()=>clearTimeout(q));async function D(){if(!T.peek()){T.value=!0;try{const e=await N("/search/settings",{method:"PUT",body:JSON.stringify(c(h()))});if(!$())return;const t=e?.settings||c(h());w.value=t;const a=O(t);m=Number(a.iconSize)||52;for(const[n,l]of Object.entries(a))s[n].value=l;x.value=F(a);try{S.remove?S.remove(b):S.set(b,null)}catch{}me(we),E.toast(t.enabled?"Search settings saved.":"Saved - search is off on the public site.",{type:"success"})}catch(e){E.toast(`Not saved: ${e.message||e}`,{type:"error"})}finally{T.value=!1}}}function re(){const e=O(w.peek());m=Number(e.iconSize)||52;for(const[t,a]of Object.entries(e))s[t].value=a}r.querySelector(".ssx-chips").innerHTML=xe;const f=r.querySelector(".ssx-list");f.innerHTML=Me;const k=e=>`${e}px`,ie={...s,dirty:L,saving:T,chips:H,query:y,results:g,previewNote:ne,previewError:z,previewMeta:C,clampText:M.computed(()=>be(p()).join(" ")),sizeText:M.computed(()=>k(c(p()).iconSize)),radiusText:M.computed(()=>k(c(p()).iconRadius)),sizePx:M.computed(()=>k(c(p()).iconSize)),radiusPx:M.computed(()=>k(c(p()).iconRadius)),glyphPx:M.computed(()=>k(Math.round(c(p()).iconSize*20/52))),positionText:M.computed(()=>fe[s.position.value]||s.position.value)},ce=E.tabs(r.querySelector(".ssx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),le=M.applyBindings(ie,r,{methods:{save:()=>D(),discard:()=>re(),useNavbar:()=>{s.displayMode.value="navbar"},useFloating:()=>{s.displayMode.value="floating"},setPos:e=>{s.position.value=e}}});o(()=>le.dispose()),o(ve(r,{position:"bottom"}));const ue=e=>[{header:e},{label:"Open on the site",icon:"external-link",action:()=>window.open(e,"_blank","noopener")},{label:"Edit the page",icon:"edit",action:()=>{location.hash=`#/pages/edit${e}`}},{label:"Copy address",icon:"copy",action:()=>{const t=new URL(e,location.origin).href;navigator.clipboard?.writeText(t).then(()=>E.toast("Address copied.",{type:"success"}),()=>E.toast(t,{type:"info",duration:8e3}))}}],pe=E.contextMenu(f,{cursor:"glyph",match:".ssx-row",inherit:!1,density:"compact",items:e=>ue(e.dataset.url)});o(()=>pe?.destroy?.());const Q=e=>{const t=e.target.closest(".ssx-row");if(!t)return;const a=e.target.closest(".ssx-kebab");if(a){ee(a);return}window.open(t.dataset.url,"_blank","noopener")},J=e=>{const t=e.target.closest(".ssx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ee(t)):e.key==="Enter"&&e.target===t&&window.open(t.dataset.url,"_blank","noopener"))};f.addEventListener("click",Q),f.addEventListener("keydown",J),o(()=>{f.removeEventListener("click",Q),f.removeEventListener("keydown",J)}),Z(i,{id:"search-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),Z(i,{id:"search-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>D()});const W=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),D())};document.addEventListener("keydown",W,!0),o(()=>document.removeEventListener("keydown",W,!0));const G=()=>{if(L.peek())try{S.set(b,{form:h(),base:x.peek(),at:new Date().toISOString()})}catch{}},X=e=>{L.peek()&&(G(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",X),o(()=>window.removeEventListener("beforeunload",X)),u={dirty:()=>L.peek(),dispose(){P=!0,G(),v.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(r);let d=null;try{d=S.get(b)}catch{}if(d?.form){try{S.remove?S.remove(b):S.set(b,null)}catch{}const e=d.base&&d.base!==x.peek(),t=d.at?` (${Se(d.at)})`:"",a=e?`You left unsaved search settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved search settings${t}. Restore them?`;if(await E.confirm(a)&&$()){const n={...O(w.peek()),...d.form};m=Number(n.iconSize)||52;for(const[l,de]of Object.entries(n))s[l]&&(s[l].value=de)}}}};
7
+ </div>`;let d=null,ce=!1;function $e(){ce||!window.R?.use||(ce=!0,R.use((u,m,r)=>!(Y.test(m?.path||"")&&!Y.test(u?.path||""))||!d?.dirty()?r():E.confirm("You have unsaved search settings. Leave anyway? They are kept as a draft for your next visit.").then(o=>{o?r():history.replaceState(null,"","#/search")})),M.subscribe("router:afterChange",({to:u}={})=>{if(d&&!Y.test(u?.path||"")){const m=d;d=null,m.dispose()}}))}export const searchView={templateUrl:"/admin/js/templates/search.html",async onMount(u){if($e(),d){const e=d;d=null,e.dispose()}const m=[],r=e=>(m.push(e),e);let $=!1;const o=u.get(0).querySelector(".ssx")||u.get(0),j=()=>!$&&o.isConnected;let P="";try{P=S.get("auth_user")?.id||""}catch{}const c=Re(P),y=o.querySelector(".ssx-loading"),J=o.querySelector(".ssx-body");let F,K=null;try{[F,K]=await De(y,Promise.all([O("/search/settings"),O("/search/_count").catch(()=>null)]),Ae.rows)}catch(e){if(!o.isConnected)return;y&&(y.hidden=!0);const t=o.querySelector(".ssx-failed");t&&(t.textContent=`Could not load the search settings: ${e.message||e}. Reload the page to try again.`,t.hidden=!1);return}if(!o.isConnected)return;y&&(y.hidden=!0),J&&(J.hidden=!1);const z=M.observable(F),W=_(F),s={};for(const[e,t]of Object.entries(W))s[e]=M.observable(t);const g=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),f=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),w=M.observable(v(W)),x=M.computed(()=>v(f())!==w.value),q=M.observable(!1);let A=w.peek();const G=M.observable(K?{searchable:K.count}:null),X=M.observable([]);r(M.effect(()=>{X.value=Le(z.value,G.value).map(e=>({text:e.text,cls:e.tone?`is-${e.tone}`:""}))}));let h=Number(s.iconSize.peek())||52;r(M.effect(()=>{const e=Number(s.iconSize.value)||52;if(e===h)return;const t=Pe(s.iconRadius.peek(),h,e);h=e,String(t)!==String(s.iconRadius.peek())&&(s.iconRadius.value=String(t))}));const le=M.computed(()=>Ce(s.iconSize.value)),Z=o.querySelector("#ssx-radius");r(M.effect(()=>{Z&&(Z.max=String(le.value))}));const k=M.observable(""),T=M.observable([]),D=M.observable(""),N=M.observable(""),ue=M.computed(()=>ie(k.value,s.minQueryLength.value).note);let U=0,B=null;async function pe(){const e=ie(k.peek(),s.minQueryLength.peek()),t=++U;if(!e.run){T.value=[],N.value="",D.value="";return}const a=l(g()).maxResults;try{const n=await O(`/search/preview?q=${encodeURIComponent(k.peek().trim())}&max=${a}`);if(t!==U||!j())return;T.value=(n.results||[]).map(ze),D.value="";const i=T.peek().length;N.value=i?`${i} result${i===1?"":"s"} (showing up to ${n.maxResults}) from ${n.coverage?.searchable??"?"} searchable pages.`:`Nothing found in ${n.coverage?.searchable??"?"} searchable pages.`,n.coverage&&(G.value=n.coverage)}catch(n){if(t!==U)return;T.value=[],N.value="",D.value=`Search failed: ${n.message||n}`}}r(M.effect(()=>{k.value,s.maxResults.value,clearTimeout(B),B=setTimeout(pe,l(g()).debounceMs)})),r(()=>clearTimeout(B));async function H(){if(q.peek())return;q.value=!0;const e=g(),t=v(e);try{const n=(await O("/search/settings",{method:"PUT",body:JSON.stringify(l(e))}))?.settings||l(e),i=_(n);if(!j()){de(v(i),t);return}if(z.value=n,v(g())===t){h=Number(i.iconSize)||52;for(const[ge,we]of Object.entries(i))s[ge].value=we}w.value=v(i),A=w.peek(),x.peek()||L(),Se(Ne),E.toast(n.enabled?"Search settings saved.":"Saved - search is off on the public site.",{type:"success"})}catch(a){E.toast(`Not saved: ${a.message||a}`,{type:"error"})}finally{q.value=!1}}function L(){if(c)try{S.remove?S.remove(c):S.set(c,null)}catch{}}function de(...e){if(!c)return;let t=null;try{t=S.get(c)}catch{return}if(t?.form)try{e.includes(v(t.form))&&L()}catch{}}function fe(){const e=_(z.peek());h=Number(e.iconSize)||52;for(const[t,a]of Object.entries(e))s[t].value=a}o.querySelector(".ssx-chips").innerHTML=Oe;const b=o.querySelector(".ssx-list");b.innerHTML=_e;const C=e=>`${e}px`,ve={...s,dirty:x,saving:q,chips:X,query:k,results:T,previewNote:ue,previewError:D,previewMeta:N,clampText:M.computed(()=>Me(f()).join(" ")),sizeText:M.computed(()=>C(l(f()).iconSize)),radiusText:M.computed(()=>C(l(f()).iconRadius)),sizePx:M.computed(()=>C(l(f()).iconSize)),radiusPx:M.computed(()=>C(l(f()).iconRadius)),glyphPx:M.computed(()=>C(Math.round(l(f()).iconSize*20/52))),positionText:M.computed(()=>ke[s.position.value]||s.position.value)},me=E.tabs(o.querySelector(".ssx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),he=M.applyBindings(ve,o,{methods:{save:()=>H(),discard:()=>fe(),useNavbar:()=>{s.displayMode.value="navbar"},useFloating:()=>{s.displayMode.value="floating"},setPos:e=>{s.position.value=e}}});r(()=>he.dispose()),r(xe(o,{position:"bottom"}));const be=e=>[{header:e},{label:"Open on the site",icon:"external-link",action:()=>window.open(e,"_blank","noopener")},{label:"Edit the page",icon:"edit",action:()=>{location.hash=`#/pages/edit${e}`}},{label:"Copy address",icon:"copy",action:()=>{const t=new URL(e,location.origin).href;navigator.clipboard?.writeText(t).then(()=>E.toast("Address copied.",{type:"success"}),()=>E.toast(t,{type:"info",duration:8e3}))}}],ye=E.contextMenu(b,{cursor:"glyph",match:".ssx-row",inherit:!1,density:"compact",items:e=>be(e.dataset.url)});r(()=>ye?.destroy?.());const ee=e=>{const t=e.target.closest(".ssx-row");if(!t)return;const a=e.target.closest(".ssx-kebab");if(a){re(a);return}window.open(t.dataset.url,"_blank","noopener")},te=e=>{const t=e.target.closest(".ssx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),re(t)):e.key==="Enter"&&e.target===t&&(e.preventDefault(),e.stopPropagation(),window.open(t.dataset.url,"_blank","noopener")))};b.addEventListener("click",ee),b.addEventListener("keydown",te),r(()=>{b.removeEventListener("click",ee),b.removeEventListener("keydown",te)}),ae(u,{id:"search-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),ae(u,{id:"search-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>H()});const se=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),H())};document.addEventListener("keydown",se,!0),r(()=>document.removeEventListener("keydown",se,!0));let Q=null;try{Q=c?S.get(c):null}catch{}const p=Te(Q,w.peek(),P);let V=!p;const ne=()=>{if(c)if(x.peek())try{S.set(c,{owner:P,form:g(),was:A,at:new Date().toISOString()})}catch{}else V&&L()},oe=e=>{x.peek()&&(ne(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",oe),r(()=>window.removeEventListener("beforeunload",oe)),d={dirty:()=>x.peek(),dispose(){$=!0,ne(),m.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(o),!p&&Q&&L(),p){const e=Ee({when:p.at?qe(p.at):"",stale:p.stale});if(await new Promise(a=>setTimeout(a,0)),await E.confirm(e.text,{title:e.title,confirmText:e.confirmText,cancelText:e.cancelText}))L(),V=!0;else if(j()){V=!0;const a={..._(z.peek()),...p.form};h=Number(a.iconSize)||52;for(const[n,i]of Object.entries(a))s[n]&&(s[n].value=i);A=p.was||A}}}};