domma-cms 0.98.0 → 0.100.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.
@@ -1,11 +1,11 @@
1
- import{api as g}from"../api.js";import{addBannerAction as de}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{onceClosed as lt,openMenuAt as Fe}from"../lib/tool-kit.js";import{FIELD_TYPES as st,OPERATIONS as it,OPTION_TYPES as rt,accessOptions as ct,checkForm as dt,draftOf as ut,duplicateField as pt,errorCount as B,fieldFromPanel as Ne,fieldProblem as ft,fieldRow as mt,formFromCollection as De,formFromDraft as vt,moveField as Le,nameFromLabel as ht,newField as bt,panelForm as gt,payloadFromForm as yt,roleLadder as wt,slugify as X,snapshot as _,storageChange as Te}from"../lib/collection-editor-arrange.js";import{fmtDateTime as xt}from"../lib/dates.js";import{rowPlaceKeeper as kt}from"../lib/menu-drag.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 de}from"../lib/plugin-chrome.js";import{helpPopovers as Re}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Ie}from"../lib/tool-kit.js";import{ADDON_TYPES as rt,EXTRA_TYPES as pe,FIELD_TYPES as ct,OPERATIONS as dt,OPTION_TYPES as pt,accessOptions as ut,checkForm as ft,draftOf as mt,duplicateField as ht,errorCount as q,fieldFromPanel as Fe,fieldProblem as vt,fieldRow as bt,formFromCollection as Pe,formFromDraft as gt,moveField as De,nameFromLabel as yt,newField as xt,panelForm as wt,payloadFromForm as kt,roleLadder as Et,slugify as Q,snapshot as _,storageChange as Ne}from"../lib/collection-editor-arrange.js";import{fmtDateTime as St}from"../lib/dates.js";import{rowPlaceKeeper as Mt}from"../lib/menu-drag.js";import{openIconPicker as Be}from"../lib/card-builder.js";const Ct=/^\/collections\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,$t=i=>`collection-editor.draft.${i||"new"}`,At=`
2
2
  <div class="cex-row" data-key="{{key}}" tabindex="0" data-bind-class="rowCls">
3
3
  <span class="cex-grip" title="Drag to reorder" aria-hidden="true"></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>`,Z=s=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function $t(s){const d=document.createElement("template");return d.innerHTML=s.trim(),d.content.firstElementChild}function Pe(s,d){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(d))}let y=null,qe=!1;const ee=s=>String(s||"").split("?")[0];function Ct(){qe||!window.R?.use||(qe=!0,R.use((s,d,i)=>{if(!(y&&Et.test(d?.path||"")&&ee(s?.path)!==ee(d?.path))||!y.dirty())return i();const w=`#${d.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(c=>{c?i():history.replaceState(null,"",w)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(y&&ee(s?.path)!==y.path){const d=y;y=null,d.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 d=s.get(0),i=d.querySelector(".cex")||d,C=ee(location.hash.replace(/^#/,""))||"/collections/new",w=C.match(/^\/collections\/edit\/([^/?#]+)/),c=w?decodeURIComponent(w[1]):null,D=!c,ue=[],h=e=>(ue.push(e),e);let pe=!1;const te=()=>!pe&&i.isConnected,fe=E.loader(d,{type:"dots"});let O=null,ne=[],L=null,me=[],u=0,T=[];try{[O,ne,L,me,T]=await Promise.all([c?g.collections.get(c):Promise.resolve(null),g.projects.list().catch(()=>[]),g.collections.proStatus().catch(()=>null),D?g.collections.list().then(e=>(e||[]).map(t=>t.slug)).catch(()=>[]):Promise.resolve([]),g.collections.listEntries("roles",{limit:1e3}).then(wt).catch(()=>[])]),c&&(u=(await g.collections.listEntries(c,{limit:1}).catch(()=>null))?.total??0)}catch(e){fe.destroy(),E.toast(e?.status===404?"Collection not found.":`Could not load the collection: ${e.message||e}`,{type:"error"}),R.navigate("/collections");return}if(fe.destroy(),!i.isConnected)return;if(c&&!O){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const ve=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",f=De(O,{project:ve,defaultRole:T.at(-1)?.name||""}),x=Object.fromEntries(f.fields.map(e=>[e._key,e.name])),Ie=O?.meta||{};let K=f.storageAdapter;const Be=new Set(["public","token",...T.map(e=>e.name)]),he=(e,t)=>{if(!T.length)return"";const o=[...new Set(e.filter(a=>a&&!Be.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(D),U=M.observable(!1),H=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),be=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),N=M.observable(_(f)),P=M.computed(()=>_(be())!==N.value);let W=!D;h(M.effect(()=>{const e=n.title.value;W||(n.slug.value=X(e))}));const m=M.computed(()=>dt(be(),{isNew:k.value,taken:me,original:x})),ge=M.observable([]);h(M.effect(()=>{const e=m.value.fields||{},t=n.layout.value==="grid";ge.value=n.fields.value.map(o=>mt(o,e,{grid:t}))}));const _e=M.computed(()=>new Set(n.fields.value.map(e=>e.name))),Ke={...n,isNew:k,saving:U,dirty:P,rows:ge,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(()=>B(m.value)),problemText:M.computed(()=>{const e=B(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 "${X(n.slug.value||n.title.value)||"\u2026"}".`:"Cannot be changed once the collection exists."),apiPath:M.computed(()=>`/api/collections/${X(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&&!_e.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),missingApiRoles:M.computed(()=>he(it.filter(e=>n[`${e}Enabled`].value).map(e=>n[`${e}Access`].value),"only the level-0 role passes")),missingExportRole:M.computed(()=>he(n.exportEnabled.value?[n.exportAccess.value]:[],"only the level-0 role is offered export")),showStorage:M.observable(!!L?.pro&&c!=="roles"),storageNote:M.computed(()=>{const e=Te(K,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.`})},ye=i.querySelector("#collection-project");for(const e of ne){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,ye.appendChild(t)}if(f.project&&!ne.some(e=>e.slug===f.project)){const e=document.createElement("option");e.value=f.project,e.textContent=`${f.project} (not found)`,ye.appendChild(e)}const Ue=[...i.querySelectorAll('.cex-op select[data-model$="Access.value"]')],we=e=>{for(const t of Ue){const o=t.getAttribute("data-model").split(".")[0],a=e[o];t.replaceChildren(...ct(T,a,{token:t.id!=="export-access"}).map(r=>new Option(r.label,r.value))),t.value=a}};we(f);const He=i.querySelector("#storage-connection"),We=Array.isArray(L?.connections)&&L.connections.length?L.connections:["default"];for(const e of new Set([...We,f.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,He.appendChild(t)}const Y=e=>n.fields.peek().findIndex(t=>t._key===e),j=e=>{n.fields.value=e};function z(e,t){const o=Y(e);o<0||(j(Le(n.fields.peek(),o,o+t)),requestAnimationFrame(()=>i.querySelector(`.cex-row[data-key="${e}"]`)?.focus()))}async function oe(e){const t=Y(e);if(t<0)return;const o=n.fields.peek()[t];!k.peek()&&u&&x[e]&&!await E.confirm(`Remove the field "${Z(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 G(e,{isAdded:t=!1}={}){const o=Y(e);if(o<0)return;const a=n.fields.peek()[o],r=gt(a),v={};for(const[l,$]of Object.entries(r))v[l]=M.observable($);const p=JSON.stringify(r),J=()=>Object.fromEntries(Object.keys(v).map(l=>[l,v[l].peek()])),$e=()=>JSON.stringify(J())!==p;let Ce=!!a.name;const Xe=M.effect(()=>{const l=v.label.value;Ce||(v.name.value=ht(l))}),Ze=new Set(n.fields.peek().filter(l=>l._key!==e).map(l=>l.name)),Oe=M.computed(()=>{const l=Ne(a,Object.fromEntries(Object.keys(v).map($=>[$,v[$].value])));return l.name&&Ze.has(l.name)?`"${l.name}" is used by another field.`:ft(l,x[e])}),et=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.`:""),tt={...v,problem:Oe,renamed:et,isOptions:M.computed(()=>rt.has(v.type.value)),isGrid:M.computed(()=>n.layout.value==="grid")},b=E.slideover({title:t?"New field":`Field: ${Z(a.label||a.name||"Untitled")}`,size:"md",position:"right"}),F=$t(`
8
+ </div>`,Z=i=>String(i??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Ot(i){const p=document.createElement("template");return p.innerHTML=i.trim(),p.content.firstElementChild}function Le(i,p){const r=i.querySelector(".dm-plugin-banner-name");if(!r)return;const $=document.createElement("span");$.className="dm-plugin-banner-parent",$.textContent="Collections";const x=document.createElement("span");x.className="dm-plugin-banner-sep",x.setAttribute("aria-hidden","true"),x.textContent="\u203A",r.replaceChildren($,x,document.createTextNode(p))}let y=null,qe=!1;const ee=i=>String(i||"").split("?")[0];function jt(){qe||!window.R?.use||(qe=!0,R.use((i,p,r)=>{if(!(y&&Ct.test(p?.path||"")&&ee(i?.path)!==ee(p?.path))||!y.dirty())return r();const x=`#${p.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?r():history.replaceState(null,"",x)})}),M.subscribe("router:afterChange",({to:i}={})=>{if(y&&ee(i?.path)!==y.path){const p=y;y=null,p.dispose()}}))}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(i){if(jt(),y){const e=y;y=null,e.dispose()}const p=i.get(0),r=p.querySelector(".cex")||p,$=ee(location.hash.replace(/^#/,""))||"/collections/new",x=$.match(/^\/collections\/edit\/([^/?#]+)/),d=x?decodeURIComponent(x[1]):null,P=!d,ue=[],v=e=>(ue.push(e),e);let fe=!1;const te=()=>!fe&&r.isConnected,me=E.loader(p,{type:"dots"});let A=null,ae=[],D=null,he=[],u=0,N=[];try{[A,ae,D,he,N]=await Promise.all([d?g.collections.get(d):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(Et).catch(()=>[])]),d&&(u=(await g.collections.listEntries(d,{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(),!r.isConnected)return;if(d&&!A){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const ve=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",m=Pe(A,{project:ve,defaultRole:N.at(-1)?.name||""}),w=Object.fromEntries(m.fields.map(e=>[e._key,e.name])),_e=A?.meta||{};let K=m.storageAdapter;const Ke=new Set(["public","token",...N.map(e=>e.name)]),be=(e,t)=>{if(!N.length)return"";const n=[...new Set(e.filter(l=>l&&!Ke.has(l)))];return n.length?`Not a role on this site: ${n.join(", ")}. Until you pick another, ${t}.`:""},a={};for(const[e,t]of Object.entries(m))a[e]=M.observable(t);const k=M.observable(P),U=M.observable(!1),H=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].peek()])),ge=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].value])),F=M.observable(_(m)),B=M.computed(()=>_(ge())!==F.value);let Y=!P;v(M.effect(()=>{const e=a.title.value;Y||(a.slug.value=Q(e))}));const h=M.computed(()=>ft(ge(),{isNew:k.value,taken:he,original:w})),ye=M.observable([]);v(M.effect(()=>{const e=h.value.fields||{},t=a.layout.value==="grid";ye.value=a.fields.value.map(n=>bt(n,e,{grid:t}))}));const Ue=M.computed(()=>new Set(a.fields.value.map(e=>e.name))),He={...a,isNew:k,saving:U,dirty:B,rows:ye,errTitle:M.computed(()=>h.value.title||""),errSlug:M.computed(()=>h.value.slug||""),errColumns:M.computed(()=>h.value.columns||""),errExport:M.computed(()=>h.value.export||""),problemCount:M.computed(()=>q(h.value)),problemText:M.computed(()=>{const e=q(h.value);return e?`${e} thing${e===1?"":"s"} to fix before it can be saved`:""}),settingsBad:M.computed(()=>!!(h.value.title||h.value.slug||h.value.columns)),fieldsBad:M.computed(()=>!!h.value.fields),exportBad:M.computed(()=>!!h.value.export),fieldCount:M.computed(()=>a.fields.value.length),slugHint:M.computed(()=>k.value?`Will be created as "${Q(a.slug.value||a.title.value)||"\u2026"}".`:"Cannot be changed once the collection exists."),apiPath:M.computed(()=>`/api/collections/${Q(a.slug.value||a.title.value)||"<slug>"}/public`),unknownReadFields:M.computed(()=>{const e=String(a.readFields.value||"").split(",").map(t=>t.trim()).filter(t=>t&&!Ue.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),missingApiRoles:M.computed(()=>be(dt.filter(e=>a[`${e}Enabled`].value).map(e=>a[`${e}Access`].value),"only the level-0 role passes")),missingExportRole:M.computed(()=>be(a.exportEnabled.value?[a.exportAccess.value]:[],"only the level-0 role is offered export")),showStorage:M.observable(!!D?.pro&&d!=="roles"),storageNote:M.computed(()=>{const e=Ne(K,a.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=r.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(m.project&&!ae.some(e=>e.slug===m.project)){const e=document.createElement("option");e.value=m.project,e.textContent=`${m.project} (not found)`,xe.appendChild(e)}const Ye=[...r.querySelectorAll('.cex-op select[data-model$="Access.value"]')],we=e=>{for(const t of Ye){const n=t.getAttribute("data-model").split(".")[0],l=e[n];t.replaceChildren(...ut(N,l,{token:t.id!=="export-access"}).map(c=>new Option(c.label,c.value))),t.value=l}};we(m);const We=r.querySelector("#storage-connection"),ze=Array.isArray(D?.connections)&&D.connections.length?D.connections:["default"];for(const e of new Set([...ze,m.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,We.appendChild(t)}const W=e=>a.fields.peek().findIndex(t=>t._key===e),O=e=>{a.fields.value=e};function z(e,t){const n=W(e);n<0||(O(De(a.fields.peek(),n,n+t)),requestAnimationFrame(()=>r.querySelector(`.cex-row[data-key="${e}"]`)?.focus()))}async function ne(e){const t=W(e);if(t<0)return;const n=a.fields.peek()[t];!k.peek()&&u&&w[e]&&!await E.confirm(`Remove the field "${Z(n.label||n.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.`)||O(a.fields.peek().filter(l=>l._key!==e))}function G(e,{isAdded:t=!1}={}){const n=W(e);if(n<0)return;const l=a.fields.peek()[n],c=wt(l),s={};for(const[o,C]of Object.entries(c))s[o]=M.observable(C);const f=JSON.stringify(c),V=()=>Object.fromEntries(Object.keys(s).map(o=>[o,s[o].peek()])),$e=()=>JSON.stringify(V())!==f;let Ae=!!l.name;const et=M.effect(()=>{const o=s.label.value;Ae||(s.name.value=yt(o))}),tt=new Set(a.fields.peek().filter(o=>o._key!==e).map(o=>o.name)),Oe=M.computed(()=>{const o=Fe(l,Object.fromEntries(Object.keys(s).map(C=>[C,s[C].value])));return o.name&&tt.has(o.name)?`"${o.name}" is used by another field.`:vt(o,w[e])}),at=M.computed(()=>!k.peek()&&u&&w[e]&&s.name.value.trim()!==w[e]?`Entries keep their value under "${w[e]}" - renaming the field does not move it.`:""),nt={...s,problem:Oe,renamed:at,isOptions:M.computed(()=>pt.has(s.type.value)),canAddon:M.computed(()=>rt.has(s.type.value)),canClear:M.computed(()=>pe.clear.has(s.type.value)),canStepper:M.computed(()=>pe.stepper.has(s.type.value)),canCounter:M.computed(()=>pe.counter.has(s.type.value)),isGrid:M.computed(()=>a.layout.value==="grid")},b=E.slideover({title:t?"New field":`Field: ${Z(l.label||l.name||"Untitled")}`,size:"md",position:"right"}),T=Ot(`
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 de}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
- ${st.map(l=>`<option value="${l.value}">${l.label}</option>`).join("")}
23
+ ${ct.map(o=>`<option value="${o.value}">${o.label}</option>`).join("")}
24
24
  </select>
25
25
  </div>
26
26
  <div class="cex-setting">
@@ -42,6 +42,37 @@ import{api as g}from"../api.js";import{addBannerAction as de}from"../lib/plugin-
42
42
  <span data-help-title="Options" data-help="One per line, as value: Label - the value is what is stored, the label what is shown. A line with no colon uses the same text for both."></span></label>
43
43
  <textarea id="cex-f-options" class="form-input font-mono" rows="5" placeholder="s: Small&#10;m: Medium" data-model="options.value"></textarea>
44
44
  </div>
45
+ <div class="cex-wide cex-addons" data-bind-hidden="!canAddon.value">
46
+ <span class="form-label">Joined to the input
47
+ <span data-help-title="Icon or text beside the input" data-help="An icon or a short text joined to the start or end of the input in the entry form - a magnifier before a search box, \xA3 before a price, kg after a weight. Pick an icon, or type text; an icon wins if both are set."></span></span>
48
+ <div class="cex-addon-row">
49
+ <span class="cex-addon-side">Before</span>
50
+ <input id="cex-f-prefix-icon" type="text" class="form-input" maxlength="60" placeholder="Icon" aria-label="Icon before the input" data-model="prefixIcon.value">
51
+ <button type="button" class="btn btn-sm btn-ghost" title="Browse icons" aria-label="Browse icons for before the input" data-on-click="pickPrefix"><span data-icon="grid" data-icon-size="14"></span></button>
52
+ <input type="text" class="form-input" maxlength="24" placeholder="or text, e.g. \xA3" aria-label="Text before the input" data-model="prefixText.value">
53
+ </div>
54
+ <div class="cex-addon-row">
55
+ <span class="cex-addon-side">After</span>
56
+ <input id="cex-f-suffix-icon" type="text" class="form-input" maxlength="60" placeholder="Icon" aria-label="Icon after the input" data-model="suffixIcon.value">
57
+ <button type="button" class="btn btn-sm btn-ghost" title="Browse icons" aria-label="Browse icons for after the input" data-on-click="pickSuffix"><span data-icon="grid" data-icon-size="14"></span></button>
58
+ <input type="text" class="form-input" maxlength="24" placeholder="or text, e.g. kg" aria-label="Text after the input" data-model="suffixText.value">
59
+ </div>
60
+ </div>
61
+ <div class="cex-setting" data-bind-hidden="!canClear.value">
62
+ <label class="form-label" for="cex-f-clear">Clear button
63
+ <span data-help="An x at the end of the input that empties it."></span></label>
64
+ <input id="cex-f-clear" type="checkbox" class="form-switch" data-model="clear.value">
65
+ </div>
66
+ <div class="cex-setting" data-bind-hidden="!canStepper.value">
67
+ <label class="form-label" for="cex-f-stepper">- / + buttons
68
+ <span data-help="Buttons either side of the number that step it up and down."></span></label>
69
+ <input id="cex-f-stepper" type="checkbox" class="form-switch" data-model="stepper.value">
70
+ </div>
71
+ <div class="cex-setting" data-bind-hidden="!canCounter.value">
72
+ <label class="form-label" for="cex-f-counter">Character counter
73
+ <span data-help="Shows how many characters have been typed under the input."></span></label>
74
+ <input id="cex-f-counter" type="checkbox" class="form-switch" data-model="counter.value">
75
+ </div>
45
76
  <div data-bind-hidden="!isGrid.value">
46
77
  <label class="form-label" for="cex-f-span">Column span
47
78
  <span data-help="How many of the grid's columns the field takes, in the entry form."></span></label>
@@ -61,4 +92,4 @@ import{api as g}from"../api.js";import{addBannerAction as de}from"../lib/plugin-
61
92
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
62
93
  <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
94
  </div>
64
- </div>`);let Q=!1,re=!1;const nt=b.close.bind(b);function je(){if(Oe.peek())return;const l=Ne(a,J());j(n.fields.peek().map($=>$._key===e?l:$)),Q=!0,b.close()}b.element.appendChild(F);const ot=Ae(F),at=M.applyBindings(tt,F,{methods:{done:je,cancel:()=>b.close(),remove:async()=>{Q=!0,b.close(),await oe(e)}}}),Re=F.querySelector("#cex-f-name");Re.addEventListener("input",()=>{Ce=!!Re.value}),F.addEventListener("keydown",l=>{(l.ctrlKey||l.metaKey)&&l.key==="Enter"&&(l.preventDefault(),je())}),lt(b,()=>{Xe(),at.dispose(),ot(),se.delete(b)}),b.close=async l=>{if(!Q&&l!==!0&&$e()){if(re)return;re=!0;const ce=await E.confirm("Discard the changes to this field?");if(re=!1,!ce)return}Q=!0;const $=nt();return t&&!$e()&&!a.name&&j(n.fields.peek().filter(ce=>ce._key!==e)),$},se.add(b),I.scan(F),b.open(),F.querySelector("#cex-f-label")?.focus()}function Ye(){const e=bt();j([...n.fields.peek(),e]),Ve.activate?.(1),G(e._key,{isAdded:!0})}function ze(e){const t=Y(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:()=>G(e)},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>z(e,-1)},{label:"Move down",icon:"arrow-down",disabled:t===a-1,action:()=>z(e,1)},{label:"Duplicate",icon:"copy",action:()=>j(pt(n.fields.peek(),t))},{type:"divider"},{label:"Remove\u2026",icon:"trash",danger:!0,action:()=>oe(e)}]}async function ae(){if(U.peek())return;const e=m.peek();if(B(e)){E.toast(`Not saved - ${B(e)} thing${B(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=yt(H(),{isNew:k.peek(),meta:Ie});U.value=!0;try{if(k.peek()){const a=await g.collections.create(t);N.value=_(H()),ie(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(a.slug)}`);return}const o=Te(K,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 r=await g.collections.migrateStorage(c,t.storage);E.toast(`Moved ${r.migrated} of ${r.total} entries.`,{type:"success"})}else if(!await E.confirm(`Switch without moving them? The ${u} ${u===1?"entry stays":"entries stay"} in ${Z(K)} storage and this collection will show none until they are moved.`))return}if(await g.collections.update(c,t),!te())return;K=t.storage.adapter,o&&(u=(await g.collections.listEntries(c,{limit:1}).catch(()=>null))?.total??u);for(const a of n.fields.peek())x[a._key]=a.name;N.value=_(H()),ie(),Pe(d,t.title),E.toast("Collection saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{U.value=!1}}async function Ge(){if(!P.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=De(O,{project:ve});for(const t of e.fields)x[t._key]=t.name;for(const[t,o]of Object.entries(e))n[t].value=o;N.value=_(e),W=!k.peek()}const q=i.querySelector(".cex-list");q.innerHTML=Mt;const Ve=E.tabs(i.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Je=M.applyBindings(Ke,i,{methods:{save:()=>ae(),discard:()=>Ge(),addField:()=>Ye()}});h(()=>Je.dispose()),h(Ae(i,{position:"bottom"}));const le=i.querySelector("#field-slug"),xe=()=>{W=!!le.value};le.addEventListener("input",xe),h(()=>le.removeEventListener("input",xe));const se=new Set;h(()=>[...se].forEach(e=>{try{e.close(!0)}catch{}}));const Qe=E.contextMenu(q,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>ze(e.dataset.key)});h(()=>Qe?.destroy?.());const ke={click:e=>{const t=e.target.closest(".cex-row");if(!t)return;const o=e.target.closest(".cex-kebab");if(o){Fe(o);return}G(t.dataset.key)},keydown:e=>{const t=e.target.closest(".cex-row");!t||e.target!==t||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Fe(t)):e.key==="Enter"?G(t.dataset.key):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),z(t.dataset.key,-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),z(t.dataset.key,1)):e.key==="Delete"&&(e.preventDefault(),oe(t.dataset.key)))}};for(const[e,t]of Object.entries(ke))q.addEventListener(e,t);if(h(()=>{for(const[e,t]of Object.entries(ke))q.removeEventListener(e,t)}),typeof E.sortable=="function"){const e=kt(),t=E.sortable(q,{items:".cex-row",key:"data-key",keyboard:!1,onStart:e.start,onEnd:({item:o})=>e.restore(o),onSort:({item:o,from:a,to:r})=>{e.restore(o),j(Le(n.fields.peek(),a,r))}});h(()=>t.destroy())}Pe(d,O?.title||"New collection"),de(s,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),c&&de(s,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(c)}/entries`)}),de(s,{id:"collection-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>ae()});const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!te()||(e.preventDefault(),e.stopImmediatePropagation(),ae())};document.addEventListener("keydown",Ee,!0),h(()=>document.removeEventListener("keydown",Ee,!0));const V=St(c);function ie(){try{S.remove?S.remove(V):S.set(V,null)}catch{}}const Se=()=>{if(P.peek())try{S.set(V,{form:ut(H()),base:N.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{P.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),h(()=>window.removeEventListener("beforeunload",Me)),y={path:C,dirty:()=>P.peek(),dispose(){pe=!0,Se(),ue.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let A=null;try{A=S.get(V)}catch{}if(A?.form){ie();const e=A.base&&A.base!==N.peek(),t=A.at?` (${xt(A.at)})`:"",o=c?`changes to "${Z(O?.title||c)}"`:"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)&&te()){const r=vt(A.form,f);D||(r.slug=f.slug);const v=Object.fromEntries(f.fields.filter(p=>p.id).map(p=>[p.id,p.name]));for(const p of r.fields)p.id&&v[p.id]!==void 0&&(x[p._key]=v[p.id]);we(r);for(const[p,J]of Object.entries(r))n[p]&&(n[p].value=J);D&&(W=!!r.slug&&r.slug!==X(r.title))}}}};
95
+ </div>`);let X=!1,re=!1;const ot=b.close.bind(b);function je(){if(Oe.peek())return;const o=Fe(l,V());O(a.fields.peek().map(C=>C._key===e?o:C)),X=!0,b.close()}b.element.appendChild(T);const lt=Re(T),st=M.applyBindings(nt,T,{methods:{done:je,cancel:()=>b.close(),pickPrefix:()=>Be(o=>{s.prefixIcon.value=o,s.prefixText.value=""}),pickSuffix:()=>Be(o=>{s.suffixIcon.value=o,s.suffixText.value=""}),remove:async()=>{X=!0,b.close(),await ne(e)}}}),Te=T.querySelector("#cex-f-name");Te.addEventListener("input",()=>{Ae=!!Te.value}),T.addEventListener("keydown",o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),je())}),it(b,()=>{et(),st.dispose(),lt(),se.delete(b)}),b.close=async o=>{if(!X&&o!==!0&&$e()){if(re)return;re=!0;const ce=await E.confirm("Discard the changes to this field?");if(re=!1,!ce)return}X=!0;const C=ot();return t&&!$e()&&!l.name&&O(a.fields.peek().filter(ce=>ce._key!==e)),C},se.add(b),I.scan(T),b.open(),T.querySelector("#cex-f-label")?.focus()}function Ge(){const e=xt();O([...a.fields.peek(),e]),Xe.activate?.(1),G(e._key,{isAdded:!0})}function Je(e){const t=W(e);if(t<0)return[];const n=a.fields.peek()[t],l=a.fields.peek().length;return[{header:n.label||n.name||"Field"},{label:"Edit\u2026",icon:"edit",action:()=>G(e)},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>z(e,-1)},{label:"Move down",icon:"arrow-down",disabled:t===l-1,action:()=>z(e,1)},{label:"Duplicate",icon:"copy",action:()=>O(ht(a.fields.peek(),t))},{type:"divider"},{label:"Remove\u2026",icon:"trash",danger:!0,action:()=>ne(e)}]}async function oe(){if(U.peek())return;const e=h.peek();if(q(e)){E.toast(`Not saved - ${q(e)} thing${q(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=kt(H(),{isNew:k.peek(),meta:_e});U.value=!0;try{if(k.peek()){const l=await g.collections.create(t);F.value=_(H()),ie(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(l.slug)}`);return}const n=Ne(K,t.storage.adapter);if(n&&u){if(await E.confirm(`You changed the storage (${n}). Move the ${u} existing ${u===1?"entry":"entries"} to the new storage?`)){const c=await g.collections.migrateStorage(d,t.storage);E.toast(`Moved ${c.migrated} of ${c.total} entries.`,{type:"success"})}else if(!await E.confirm(`Switch without moving them? The ${u} ${u===1?"entry stays":"entries stay"} in ${Z(K)} storage and this collection will show none until they are moved.`))return}if(await g.collections.update(d,t),!te())return;K=t.storage.adapter,n&&(u=(await g.collections.listEntries(d,{limit:1}).catch(()=>null))?.total??u);for(const l of a.fields.peek())w[l._key]=l.name;F.value=_(H()),ie(),Le(p,t.title),E.toast("Collection saved.",{type:"success"})}catch(n){E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{U.value=!1}}async function Ve(){if(!B.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=Pe(A,{project:ve});for(const t of e.fields)w[t._key]=t.name;for(const[t,n]of Object.entries(e))a[t].value=n;F.value=_(e),Y=!k.peek()}const L=r.querySelector(".cex-list");L.innerHTML=At;const Xe=E.tabs(r.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(He,r,{methods:{save:()=>oe(),discard:()=>Ve(),addField:()=>Ge()}});v(()=>Qe.dispose()),v(Re(r,{position:"bottom"}));const le=r.querySelector("#field-slug"),ke=()=>{Y=!!le.value};le.addEventListener("input",ke),v(()=>le.removeEventListener("input",ke));const se=new Set;v(()=>[...se].forEach(e=>{try{e.close(!0)}catch{}}));const Ze=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>Je(e.dataset.key)});v(()=>Ze?.destroy?.());const Ee={click:e=>{const t=e.target.closest(".cex-row");if(!t)return;const n=e.target.closest(".cex-kebab");if(n){Ie(n);return}G(t.dataset.key)},keydown:e=>{const t=e.target.closest(".cex-row");!t||e.target!==t||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Ie(t)):e.key==="Enter"?G(t.dataset.key):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),z(t.dataset.key,-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),z(t.dataset.key,1)):e.key==="Delete"&&(e.preventDefault(),ne(t.dataset.key)))}};for(const[e,t]of Object.entries(Ee))L.addEventListener(e,t);if(v(()=>{for(const[e,t]of Object.entries(Ee))L.removeEventListener(e,t)}),typeof E.sortable=="function"){const e=Mt(),t=E.sortable(L,{items:".cex-row",key:"data-key",keyboard:!1,onStart:e.start,onEnd:({item:n})=>e.restore(n),onSort:({item:n,from:l,to:c})=>{e.restore(n),O(De(a.fields.peek(),l,c))}});v(()=>t.destroy())}Le(p,A?.title||"New collection"),de(i,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),d&&de(i,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(d)}/entries`)}),de(i,{id:"collection-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>oe()});const Se=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!te()||(e.preventDefault(),e.stopImmediatePropagation(),oe())};document.addEventListener("keydown",Se,!0),v(()=>document.removeEventListener("keydown",Se,!0));const J=$t(d);function ie(){try{S.remove?S.remove(J):S.set(J,null)}catch{}}const Me=()=>{if(B.peek())try{S.set(J,{form:mt(H()),base:F.peek(),at:new Date().toISOString()})}catch{}},Ce=e=>{B.peek()&&(Me(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ce),v(()=>window.removeEventListener("beforeunload",Ce)),y={path:$,dirty:()=>B.peek(),dispose(){fe=!0,Me(),ue.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(r);let j=null;try{j=S.get(J)}catch{}if(j?.form){ie();const e=j.base&&j.base!==F.peek(),t=j.at?` (${St(j.at)})`:"",n=d?`changes to "${Z(A?.title||d)}"`:"a new collection",l=e?`You left unsaved ${n}${t}, but the saved collection has changed since. Restore your version anyway?`:`You left unsaved ${n}${t}. Restore them?`;if(await E.confirm(l)&&te()){const c=gt(j.form,m);P||(c.slug=m.slug);const s=Object.fromEntries(m.fields.filter(f=>f.id).map(f=>[f.id,f.name]));for(const f of c.fields)f.id&&s[f.id]!==void 0&&(w[f._key]=s[f.id]);we(c);for(const[f,V]of Object.entries(c))a[f]&&(a[f].value=V);P&&(Y=!!c.slug&&c.slug!==Q(c.title))}}}};
@@ -4,7 +4,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
4
4
  ${Array.from({length:m},(v,a)=>`<td data-col="${a}" title="{{t${a}}}">{{c${a}}}</td>`).join("")}
5
5
  <td class="cex-when" title="{{whenFull}}">{{when}}</td>
6
6
  <td class="cex-tools"><button type="button" class="cex-run" data-bind-hidden="!runs" title="Run one of this collection's actions">\u26A1 {{runs}}</button><button type="button" class="cex-kebab" aria-label="Actions for this entry" title="Actions">\u22EE</button></td>
7
- </tr>`}function wt(m,h){const v={};return m.forEach(a=>{const u={};a.fullWidth?u.span=h:a.span>1&&(u.span=a.span),a.placeholder&&(u.placeholder=a.placeholder),a.helper&&(u.hint=a.helper);const r=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;v[a.name]={type:r,label:a.label,required:a.required,options:a.options,...Object.keys(u).length?{formConfig:u}:{}}}),v}function ce(m,h){const v={};for(const a of h){const u=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!u.length)continue;const r=u[0];r.type==="radio"?v[a.name]=u.find(p=>p.checked)?.value??"":r.type==="checkbox"&&u.length>1?v[a.name]=u.filter(p=>p.checked).map(p=>p.value):r.type==="checkbox"?v[a.name]=r.checked:r.tagName==="SELECT"&&r.multiple?v[a.name]=[...r.selectedOptions].map(p=>p.value):v[a.name]=gt(a,r.value)}return v}function Fe(m,h,v){for(const a of h){if(!(a.name in v))continue;const u=v[a.name],r=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!r.length)continue;const p=r[0];p.type==="radio"?r.forEach(c=>{c.checked=c.value===String(u)}):p.type==="checkbox"&&r.length>1?r.forEach(c=>{c.checked=Array.isArray(u)&&u.includes(c.value)}):p.type==="checkbox"?p.checked=u===!0:p.tagName==="SELECT"&&p.multiple?[...p.options].forEach(c=>{c.selected=Array.isArray(u)&&u.includes(c.value)}):p.value=u??"",r.forEach(c=>c.dispatchEvent(new Event("input",{bubbles:!0})))}}const Pe=dt(Be);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(m){Pe.reset();const h=m.get(0).querySelector(".cex")||m.get(0),v=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=v?decodeURIComponent(v[1]):null,u=()=>Be.test(location.hash)&&h.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),r=e=>(Pe.add(e),e);if(!a){E.toast("No collection selected.",{type:"error"});return}const p=M.observable(null),c=M.observable([]),x=M.observable(0),Y=M.observable(!0),_=M.observable(""),_e=M.computed(()=>p.value!==null),le=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),y=M.observable([]),q=M.observable(X),J=M.observable([]),Q=M.observable({}),A=M.observable(!1);let ee=1;const ie=()=>p.peek()?.fields||[],re=M.computed(()=>(p.value?.fields||[]).slice(0,ut)),V=e=>c.peek().find(t=>t.id===e),ye=e=>J.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),T=M.observable([]),ve=M.observable([]);r(M.effect(()=>{const e=pt(c.value,{filters:w.value,query:le.value,sort:Z.value});T.value=e})),r(M.effect(()=>{const e=re.value,t=new Set(y.value);Q.value,J.value,ve.value=T.value.slice(0,q.value).map(n=>{const s={id:n.id,sel:t.has(n.id),rowCls:t.has(n.id)?"is-selected":""};e.forEach((i,d)=>{const f=he(i,n.data?.[i.name]);s[`c${d}`]=Ue(f),s[`t${d}`]=je(f)});const o=n.meta?.createdAt;s.when=o?Et(o):"",s.whenFull=o?`Created ${B(o)}${n.meta?.updatedAt?` \xB7 changed ${B(n.meta.updatedAt)}`:""}`:"";const l=ye(n.id).length;return s.runs=l?String(l):"",s})}));const Je=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:vt(e)}))),Ve=M.computed(()=>y.value.length),He={schema:p,loaded:c,loading:Y,loadError:_,ready:_e,query:le,rows:ve,chips:Je,busy:A,selectedCount:Ve,countLine:M.computed(()=>mt(T.value.length,c.value.length,x.value)),selectedText:M.computed(()=>`${y.value.length} selected`),canClear:M.computed(()=>ft(a)),introText:M.computed(()=>{const e=p.value;return e?`${e.description?`${e.description} - `:""}${(e.fields||[]).length} field${(e.fields||[]).length===1?"":"s"}${e.storage?.adapter==="mongodb"?", on MongoDB":""}.`:""}),emptyText:M.computed(()=>c.value.length?"No entries match the search or filters.":"No entries yet - Add entry in the banner makes one, or Import brings many."),moreText:M.computed(()=>{const e=T.value.length-q.value;if(e>0)return`Show ${Math.min(X,e)} more (${e} not shown)`;const t=x.value-c.value.length;return t>0?`Load the next ${Math.min(se,t)} from the server`:""})};async function H({reset:e=!0}={}){Y.value=!0;try{const t=e?Math.max(se,c.peek().length):se;e&&(ee=1);const n=await g.collections.listEntries(a,{limit:t,page:ee});if(!u())return;const s=n?.entries||(Array.isArray(n)?n:[]);c.value=e?s:[...c.peek(),...s.filter(l=>!V(l.id))],x.value=Number(n?.total)||c.peek().length,e&&(ee=Math.max(1,Math.ceil(c.peek().length/se)));const o=new Set(c.peek().map(l=>l.id));y.value=y.peek().filter(l=>o.has(l)),_.value="",await We()}catch(t){_.value=`Could not load the entries: ${t.message||t}`}finally{Y.value=!1}}async function Ke(){ee+=1,await H({reset:!1})}async function Ge(){try{if((await it("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await g.actions.forCollection(a);J.value=Array.isArray(t)?t:[]}catch{J.value=[]}}async function We(){const e=J.peek().filter(s=>s.access?.rowLevel);if(!e.length||!c.peek().length)return;const t=c.peek().map(s=>s.id),n={...Q.peek()};await Promise.all(e.map(async s=>{try{n[s.slug]=new Set((await g.actions.checkAccess(s.slug,t)).allowed)}catch{n[s.slug]=null}})),Q.value=n}const K=e=>{const t=ie()[0];return(t?Ue(he(t,e?.data?.[t.name]),40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function be(e){if(await E.confirm(`Delete \u201C${D(K(e))}\u201D? This cannot be undone.`))try{await g.collections.deleteEntry(a,e.id),c.value=c.peek().filter(t=>t.id!==e.id),x.value=Math.max(0,x.peek()-1),y.value=y.peek().filter(t=>t!==e.id),j("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function Xe(){const e=[...y.peek()];if(!e.length||!await E.confirm(`Delete ${e.length} entr${e.length===1?"y":"ies"}? This cannot be undone.`))return;A.value=!0;let t=0;const n=[];for(const s of e)try{await g.collections.deleteEntry(a,s),t++}catch(o){n.push(o.message||String(o))}A.value=!1,y.value=[],j("#/collections"),E.toast(n.length?`Deleted ${t}; ${n.length} refused (${n[0]}).`:`Deleted ${t} entr${t===1?"y":"ies"}.`,{type:n.length?"warning":"success",duration:n.length?7e3:4e3}),u()&&await H()}async function Ye(){const e=p.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${D(e?.title||a)}\u201D? This cannot be undone.`)){A.value=!0;try{await g.collections.clearEntries(a),c.value=[],x.value=0,y.value=[],j("#/collections"),E.toast("All entries deleted.",{type:"success"})}catch(t){E.toast(`Not cleared: ${t.message||t}`,{type:"error"})}finally{A.value=!1}}}async function Ze(e){try{const t=await g.collections.createEntry(a,{...e.data||{}});c.value=[t,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Duplicated - the copy is at the top.",{type:"success"})}catch(t){E.toast(`Not duplicated: ${t.message||t}`,{type:"error"})}}async function Qe(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(D(n)))){A.value=!0;try{const s=await g.actions.execute(e.slug,t.id);if(s?.success){const o=s.stepsCompleted??0;E.toast(`${e.trigger?.label||e.title||"Action"} done (${o} step${o===1?"":"s"}).`,{type:"success"}),u()&&await H()}else E.toast(s?.results?.find(o=>!o.success)?.error||"The action failed.",{type:"error"})}catch(s){E.toast(s.message||"The action failed.",{type:"error"})}finally{A.value=!1}}}async function ge(e){try{await navigator.clipboard.writeText(e),E.toast("Copied.",{type:"success",duration:1500})}catch{E.toast("Copy failed - your browser refused the clipboard.",{type:"error"})}}function de(e){const t=y.peek();y.value=t.includes(e)?t.filter(n=>n!==e):[...t,e]}function Ee(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],q.value=X}const ue=new Set;let N=null,ke=!1;function xe(e,t,n){const s=e.close.bind(e);let o=!1,l=!1;e.close=async i=>{if(!o&&i!==!0&&!ke&&t()){if(l)return;l=!0;const d=await E.confirm(n);if(l=!1,!d)return}return o=!0,s()}}function te(e,t=()=>{}){ue.add(e),rt(e,()=>{ue.delete(e),t()})}function O(e,t=null){const n=p.peek(),s=ie(),o=!!e,l=E.slideover({title:o?`Edit ${D(K(e))}`:"New entry",size:"md",position:"right"}),i=oe(`
7
+ </tr>`}function wt(m,h){const v={};return m.forEach(a=>{const r={};a.fullWidth?r.span=h:a.span>1&&(r.span=a.span),a.placeholder&&(r.placeholder=a.placeholder),a.helper&&(r.hint=a.helper),a.prefix&&(r.prefix=a.prefix),a.suffix&&(r.suffix=a.suffix);for(const u of["clear","stepper","counter"])a[u]===!0&&(r[u]=!0);const d=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;v[a.name]={type:d,label:a.label,required:a.required,options:a.options,...Object.keys(r).length?{formConfig:r}:{}}}),v}function ce(m,h){const v={};for(const a of h){const r=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!r.length)continue;const d=r[0];d.type==="radio"?v[a.name]=r.find(u=>u.checked)?.value??"":d.type==="checkbox"&&r.length>1?v[a.name]=r.filter(u=>u.checked).map(u=>u.value):d.type==="checkbox"?v[a.name]=d.checked:d.tagName==="SELECT"&&d.multiple?v[a.name]=[...d.selectedOptions].map(u=>u.value):v[a.name]=gt(a,d.value)}return v}function Fe(m,h,v){for(const a of h){if(!(a.name in v))continue;const r=v[a.name],d=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!d.length)continue;const u=d[0];u.type==="radio"?d.forEach(c=>{c.checked=c.value===String(r)}):u.type==="checkbox"&&d.length>1?d.forEach(c=>{c.checked=Array.isArray(r)&&r.includes(c.value)}):u.type==="checkbox"?u.checked=r===!0:u.tagName==="SELECT"&&u.multiple?[...u.options].forEach(c=>{c.selected=Array.isArray(r)&&r.includes(c.value)}):u.value=r??"",d.forEach(c=>c.dispatchEvent(new Event("input",{bubbles:!0})))}}const Pe=dt(Be);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(m){Pe.reset();const h=m.get(0).querySelector(".cex")||m.get(0),v=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=v?decodeURIComponent(v[1]):null,r=()=>Be.test(location.hash)&&h.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),d=e=>(Pe.add(e),e);if(!a){E.toast("No collection selected.",{type:"error"});return}const u=M.observable(null),c=M.observable([]),x=M.observable(0),Y=M.observable(!0),_=M.observable(""),_e=M.computed(()=>u.value!==null),le=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),y=M.observable([]),q=M.observable(X),J=M.observable([]),Q=M.observable({}),A=M.observable(!1);let ee=1;const ie=()=>u.peek()?.fields||[],re=M.computed(()=>(u.value?.fields||[]).slice(0,ut)),V=e=>c.peek().find(t=>t.id===e),ye=e=>J.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),T=M.observable([]),ve=M.observable([]);d(M.effect(()=>{const e=pt(c.value,{filters:w.value,query:le.value,sort:Z.value});T.value=e})),d(M.effect(()=>{const e=re.value,t=new Set(y.value);Q.value,J.value,ve.value=T.value.slice(0,q.value).map(n=>{const s={id:n.id,sel:t.has(n.id),rowCls:t.has(n.id)?"is-selected":""};e.forEach((i,p)=>{const f=he(i,n.data?.[i.name]);s[`c${p}`]=Ue(f),s[`t${p}`]=je(f)});const o=n.meta?.createdAt;s.when=o?Et(o):"",s.whenFull=o?`Created ${B(o)}${n.meta?.updatedAt?` \xB7 changed ${B(n.meta.updatedAt)}`:""}`:"";const l=ye(n.id).length;return s.runs=l?String(l):"",s})}));const Je=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:vt(e)}))),Ve=M.computed(()=>y.value.length),He={schema:u,loaded:c,loading:Y,loadError:_,ready:_e,query:le,rows:ve,chips:Je,busy:A,selectedCount:Ve,countLine:M.computed(()=>mt(T.value.length,c.value.length,x.value)),selectedText:M.computed(()=>`${y.value.length} selected`),canClear:M.computed(()=>ft(a)),introText:M.computed(()=>{const e=u.value;return e?`${e.description?`${e.description} - `:""}${(e.fields||[]).length} field${(e.fields||[]).length===1?"":"s"}${e.storage?.adapter==="mongodb"?", on MongoDB":""}.`:""}),emptyText:M.computed(()=>c.value.length?"No entries match the search or filters.":"No entries yet - Add entry in the banner makes one, or Import brings many."),moreText:M.computed(()=>{const e=T.value.length-q.value;if(e>0)return`Show ${Math.min(X,e)} more (${e} not shown)`;const t=x.value-c.value.length;return t>0?`Load the next ${Math.min(se,t)} from the server`:""})};async function H({reset:e=!0}={}){Y.value=!0;try{const t=e?Math.max(se,c.peek().length):se;e&&(ee=1);const n=await g.collections.listEntries(a,{limit:t,page:ee});if(!r())return;const s=n?.entries||(Array.isArray(n)?n:[]);c.value=e?s:[...c.peek(),...s.filter(l=>!V(l.id))],x.value=Number(n?.total)||c.peek().length,e&&(ee=Math.max(1,Math.ceil(c.peek().length/se)));const o=new Set(c.peek().map(l=>l.id));y.value=y.peek().filter(l=>o.has(l)),_.value="",await We()}catch(t){_.value=`Could not load the entries: ${t.message||t}`}finally{Y.value=!1}}async function Ke(){ee+=1,await H({reset:!1})}async function Ge(){try{if((await it("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await g.actions.forCollection(a);J.value=Array.isArray(t)?t:[]}catch{J.value=[]}}async function We(){const e=J.peek().filter(s=>s.access?.rowLevel);if(!e.length||!c.peek().length)return;const t=c.peek().map(s=>s.id),n={...Q.peek()};await Promise.all(e.map(async s=>{try{n[s.slug]=new Set((await g.actions.checkAccess(s.slug,t)).allowed)}catch{n[s.slug]=null}})),Q.value=n}const K=e=>{const t=ie()[0];return(t?Ue(he(t,e?.data?.[t.name]),40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function be(e){if(await E.confirm(`Delete \u201C${D(K(e))}\u201D? This cannot be undone.`))try{await g.collections.deleteEntry(a,e.id),c.value=c.peek().filter(t=>t.id!==e.id),x.value=Math.max(0,x.peek()-1),y.value=y.peek().filter(t=>t!==e.id),j("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function Xe(){const e=[...y.peek()];if(!e.length||!await E.confirm(`Delete ${e.length} entr${e.length===1?"y":"ies"}? This cannot be undone.`))return;A.value=!0;let t=0;const n=[];for(const s of e)try{await g.collections.deleteEntry(a,s),t++}catch(o){n.push(o.message||String(o))}A.value=!1,y.value=[],j("#/collections"),E.toast(n.length?`Deleted ${t}; ${n.length} refused (${n[0]}).`:`Deleted ${t} entr${t===1?"y":"ies"}.`,{type:n.length?"warning":"success",duration:n.length?7e3:4e3}),r()&&await H()}async function Ye(){const e=u.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${D(e?.title||a)}\u201D? This cannot be undone.`)){A.value=!0;try{await g.collections.clearEntries(a),c.value=[],x.value=0,y.value=[],j("#/collections"),E.toast("All entries deleted.",{type:"success"})}catch(t){E.toast(`Not cleared: ${t.message||t}`,{type:"error"})}finally{A.value=!1}}}async function Ze(e){try{const t=await g.collections.createEntry(a,{...e.data||{}});c.value=[t,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Duplicated - the copy is at the top.",{type:"success"})}catch(t){E.toast(`Not duplicated: ${t.message||t}`,{type:"error"})}}async function Qe(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(D(n)))){A.value=!0;try{const s=await g.actions.execute(e.slug,t.id);if(s?.success){const o=s.stepsCompleted??0;E.toast(`${e.trigger?.label||e.title||"Action"} done (${o} step${o===1?"":"s"}).`,{type:"success"}),r()&&await H()}else E.toast(s?.results?.find(o=>!o.success)?.error||"The action failed.",{type:"error"})}catch(s){E.toast(s.message||"The action failed.",{type:"error"})}finally{A.value=!1}}}async function ge(e){try{await navigator.clipboard.writeText(e),E.toast("Copied.",{type:"success",duration:1500})}catch{E.toast("Copy failed - your browser refused the clipboard.",{type:"error"})}}function de(e){const t=y.peek();y.value=t.includes(e)?t.filter(n=>n!==e):[...t,e]}function Ee(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],q.value=X}const ue=new Set;let N=null,ke=!1;function xe(e,t,n){const s=e.close.bind(e);let o=!1,l=!1;e.close=async i=>{if(!o&&i!==!0&&!ke&&t()){if(l)return;l=!0;const p=await E.confirm(n);if(l=!1,!p)return}return o=!0,s()}}function te(e,t=()=>{}){ue.add(e),rt(e,()=>{ue.delete(e),t()})}function O(e,t=null){const n=u.peek(),s=ie(),o=!!e,l=E.slideover({title:o?`Edit ${D(K(e))}`:"New entry",size:"md",position:"right"}),i=oe(`
8
8
  <div class="cex-form">
9
9
  <div class="cex-fields"></div>
10
10
  <small class="cex-hint cex-when-line"></small>
@@ -15,7 +15,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
15
15
  <button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
16
16
  <button type="button" class="btn btn-sm btn-primary" data-act="save"><span data-icon="check" data-icon-size="13"></span> <span class="cex-save-text"></span></button>
17
17
  </div>
18
- </div>`);i.querySelector(".cex-save-text").textContent=o?"Save":"Add entry";const d=i.querySelector(".cex-fields");F.create(wt(s,n?.columns||2),e?.data||{},{layout:n?.layout||"stacked",columns:n?.columns||2,showSubmitButton:!1}).renderTo(d),s.forEach(k=>{if(k.type!=="date"||!k.name)return;const U=d.querySelector(`[name="${CSS.escape(k.name)}"]`);U&&U.type!=="date"&&(U.type="date")}),e?.data&&Fe(d,s,e.data),o&&e.meta?.createdAt&&(i.querySelector(".cex-when-line").textContent=`Created ${B(e.meta.createdAt)}${e.meta.updatedAt?`, last changed ${B(e.meta.updatedAt)}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),l.element.appendChild(i);const f=ze(ce(d,s));t&&Fe(d,s,t);const b=()=>ze(ce(d,s))!==f,lt=i.querySelector(".cex-dirty"),fe=()=>{lt.hidden=!b()};d.addEventListener("input",fe),d.addEventListener("change",fe),fe();let me=!1;async function Ne(){if(me)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const k=ce(d,s);me=!0;const U=i.querySelector('[data-act="save"]');U.disabled=!0;try{if(o){const W=await g.collections.updateEntry(a,e.id,k);c.value=c.peek().map(De=>De.id===e.id?W:De),E.toast("Entry saved.",{type:"success"})}else{const W=await g.collections.createEntry(a,k);c.value=[W,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}l.close(!0)}catch(W){E.toast(W.message||"Not saved.",{type:"error"})}finally{me=!1,U.disabled=!1}}i.querySelector('[data-act="save"]').addEventListener("click",Ne),i.querySelector('[data-act="cancel"]').addEventListener("click",()=>l.close()),i.addEventListener("keydown",k=>{(k.ctrlKey||k.metaKey)&&(k.key==="Enter"||k.key==="s")&&(k.preventDefault(),Ne())});const Ie={id:e?.id??null,dirty:b,values:()=>ht(ce(d,s),s)};N=Ie,te(l,()=>{N===Ie&&(N=null)}),xe(l,b,"Discard the changes you have not saved?"),I.scan(i),l.open(),d.querySelector("input, select, textarea")?.focus()}function et(e){const t=E.slideover({title:D(K(e)),size:"md",position:"right"}),n=oe(`
18
+ </div>`);i.querySelector(".cex-save-text").textContent=o?"Save":"Add entry";const p=i.querySelector(".cex-fields");F.create(wt(s,n?.columns||2),e?.data||{},{layout:n?.layout||"stacked",columns:n?.columns||2,showSubmitButton:!1}).renderTo(p),s.forEach(k=>{if(k.type!=="date"||!k.name)return;const U=p.querySelector(`[name="${CSS.escape(k.name)}"]`);U&&U.type!=="date"&&(U.type="date")}),e?.data&&Fe(p,s,e.data),o&&e.meta?.createdAt&&(i.querySelector(".cex-when-line").textContent=`Created ${B(e.meta.createdAt)}${e.meta.updatedAt?`, last changed ${B(e.meta.updatedAt)}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),l.element.appendChild(i);const f=ze(ce(p,s));t&&Fe(p,s,t);const b=()=>ze(ce(p,s))!==f,lt=i.querySelector(".cex-dirty"),fe=()=>{lt.hidden=!b()};p.addEventListener("input",fe),p.addEventListener("change",fe),fe();let me=!1;async function Ne(){if(me)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const k=ce(p,s);me=!0;const U=i.querySelector('[data-act="save"]');U.disabled=!0;try{if(o){const W=await g.collections.updateEntry(a,e.id,k);c.value=c.peek().map(De=>De.id===e.id?W:De),E.toast("Entry saved.",{type:"success"})}else{const W=await g.collections.createEntry(a,k);c.value=[W,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}l.close(!0)}catch(W){E.toast(W.message||"Not saved.",{type:"error"})}finally{me=!1,U.disabled=!1}}i.querySelector('[data-act="save"]').addEventListener("click",Ne),i.querySelector('[data-act="cancel"]').addEventListener("click",()=>l.close()),i.addEventListener("keydown",k=>{(k.ctrlKey||k.metaKey)&&(k.key==="Enter"||k.key==="s")&&(k.preventDefault(),Ne())});const Ie={id:e?.id??null,dirty:b,values:()=>ht(ce(p,s),s)};N=Ie,te(l,()=>{N===Ie&&(N=null)}),xe(l,b,"Discard the changes you have not saved?"),I.scan(i),l.open(),p.querySelector("input, select, textarea")?.focus()}function et(e){const t=E.slideover({title:D(K(e)),size:"md",position:"right"}),n=oe(`
19
19
  <div class="cex-form">
20
20
  <dl class="cex-detail"></dl>
21
21
  <div class="cex-meta"></div>
@@ -24,7 +24,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
24
24
  <span class="cex-spacer"></span>
25
25
  <button type="button" class="btn btn-sm btn-primary" data-act="edit"><span data-icon="edit" data-icon-size="13"></span> Edit</button>
26
26
  </div>
27
- </div>`),s=n.querySelector(".cex-detail");for(const i of ie()){const d=je(he(i,e.data?.[i.name])),f=document.createElement("dt");f.textContent=i.label||i.name;const b=document.createElement("dd");b.textContent=d===""?"-":d,s.append(f,b)}const o=n.querySelector(".cex-meta"),l=i=>{const d=document.createElement("span");d.textContent=i,o.appendChild(d)};l(`Id: ${e.id}`),e.meta?.createdAt&&l(`Created: ${B(e.meta.createdAt)}`),e.meta?.updatedAt&&l(`Changed: ${B(e.meta.updatedAt)}`),e.meta?.source&&l(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>ge(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),O(e)}),t.element.appendChild(n),te(t),I.scan(n),t.open()}function tt(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=oe(`
27
+ </div>`),s=n.querySelector(".cex-detail");for(const i of ie()){const p=je(he(i,e.data?.[i.name])),f=document.createElement("dt");f.textContent=i.label||i.name;const b=document.createElement("dd");b.textContent=p===""?"-":p,s.append(f,b)}const o=n.querySelector(".cex-meta"),l=i=>{const p=document.createElement("span");p.textContent=i,o.appendChild(p)};l(`Id: ${e.id}`),e.meta?.createdAt&&l(`Created: ${B(e.meta.createdAt)}`),e.meta?.updatedAt&&l(`Changed: ${B(e.meta.updatedAt)}`),e.meta?.source&&l(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>ge(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),O(e)}),t.element.appendChild(n),te(t),I.scan(n),t.open()}function tt(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=oe(`
28
28
  <div class="cex-form">
29
29
  <p class="cex-hint">A JSON array of entries, each with a "data" object of field values - what Export gives you.
30
30
  <span data-help="Each entry is added as new, with a new id and today's date (the Data Transfer plugin from the Marketplace keeps ids and dates). Entries are checked like any other save - one that fails, or points a reference at an entry that does not exist, is skipped and reported. Values are stored as given, including fields the collection does not have."></span></p>
@@ -40,11 +40,11 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
40
40
  <button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
41
41
  <button type="button" class="btn btn-sm btn-primary" data-act="import"><span data-icon="upload" data-icon-size="13"></span> Import</button>
42
42
  </div>
43
- </div>`),n=t.querySelector("textarea"),s=t.querySelector(".cex-err"),o=t.querySelector(".cex-ok"),l=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>l.click()),l.addEventListener("change",async()=>{const d=l.files?.[0];l.value="",d&&(n.value=await d.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let i=!1;t.querySelector('[data-act="import"]').addEventListener("click",async()=>{s.hidden=!0,o.hidden=!0;let d;try{if(d=JSON.parse(n.value),!Array.isArray(d))throw new Error("it must be a JSON array")}catch(b){s.textContent=`Not valid: ${b.message}`,s.hidden=!1;return}const f=t.querySelector('[data-act="import"]');f.disabled=!0;try{const b=await g.collections.import(a,d);o.textContent=`Imported ${b.imported}; skipped ${b.skipped}.`,o.hidden=!1,i=!0,j("#/collections"),u()&&await H(),setTimeout(()=>e.close(!0),1200)}catch(b){s.textContent=b.message||"Import failed.",s.hidden=!1}finally{f.disabled=!1}}),e.element.appendChild(t),te(e),xe(e,()=>!i&&n.value.trim()!=="","Discard the entries you pasted?"),Oe(t),I.scan(t),e.open(),n.focus()}function nt(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=oe(`
43
+ </div>`),n=t.querySelector("textarea"),s=t.querySelector(".cex-err"),o=t.querySelector(".cex-ok"),l=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>l.click()),l.addEventListener("change",async()=>{const p=l.files?.[0];l.value="",p&&(n.value=await p.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let i=!1;t.querySelector('[data-act="import"]').addEventListener("click",async()=>{s.hidden=!0,o.hidden=!0;let p;try{if(p=JSON.parse(n.value),!Array.isArray(p))throw new Error("it must be a JSON array")}catch(b){s.textContent=`Not valid: ${b.message}`,s.hidden=!1;return}const f=t.querySelector('[data-act="import"]');f.disabled=!0;try{const b=await g.collections.import(a,p);o.textContent=`Imported ${b.imported}; skipped ${b.skipped}.`,o.hidden=!1,i=!0,j("#/collections"),r()&&await H(),setTimeout(()=>e.close(!0),1200)}catch(b){s.textContent=b.message||"Import failed.",s.hidden=!1}finally{f.disabled=!1}}),e.element.appendChild(t),te(e),xe(e,()=>!i&&n.value.trim()!=="","Discard the entries you pasted?"),Oe(t),I.scan(t),e.open(),n.focus()}function nt(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=oe(`
44
44
  <div class="cex-form">
45
45
  <p class="cex-hint">Every entry in the collection - not only the ones shown.</p>
46
46
  <div class="cex-export">
47
47
  <button type="button" class="btn btn-ghost" data-fmt="json"><span data-icon="download" data-icon-size="14"></span> Export as JSON</button>
48
48
  <button type="button" class="btn btn-ghost" data-fmt="csv"><span data-icon="download" data-icon-size="14"></span> Export as CSV</button>
49
49
  </div>
50
- </div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const s=n.dataset.fmt;try{const o=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${s}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!o.ok)throw new Error(`${o.status}`);const l=await o.blob(),i=document.createElement("a");i.href=URL.createObjectURL(l),i.download=`${a}-entries.${s}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(o){E.toast(`Export failed (${o.message}).`,{type:"error"})}})),e.element.appendChild(t),te(e),I.scan(t),e.open()}function at(e,t){const n=V(e.dataset.id);if(!n)return[];const s=t?.event?.target?.closest?.("td[data-col]"),o=s?re.peek()[Number(s.dataset.col)]:null,l=ye(n.id),i=y.peek().includes(n.id),d=o?yt(o,n.data?.[o.name]):[];return[{header:K(n)},{label:"Edit\u2026",icon:"edit",action:()=>O(n)},{label:"See all of it\u2026",icon:"eye",action:()=>et(n)},{label:"Duplicate",icon:"copy",action:()=>Ze(n)},{label:"Copy id",icon:"hash",action:()=>ge(n.id)},{label:i?"Deselect":"Select",icon:"check",action:()=>de(n.id)},...l.length?[{type:"divider"},...l.map(f=>({label:`${f.trigger?.label||f.title||f.slug}\u2026`,icon:f.trigger?.icon||"zap",disabled:A.peek(),action:()=>Qe(f,n)}))]:[],...o?[{type:"divider"},{label:`Filter by ${o.label||o.name}`,icon:"filter",submenu:[...d.map(f=>({label:f.label,action:()=>Ee(o,f.op,f.value)})),{label:`${o.label||o.name} contains\u2026`,action:async()=>{const f=await E.prompt(`Show entries whose ${D(o.label||o.name)} contains`,{inputPlaceholder:"text"});f&&String(f).trim()&&Ee(o,"contains",String(f).trim())}}]}]:[],...w.peek().length?[{label:`Clear filters (${w.peek().length})`,icon:"x",action:()=>{w.value=[]}}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>be(n)}]}const L=h.querySelector(".cex-rows"),G=h.querySelector(".cex-head"),pe=h.querySelector(".cex-chips-list");pe.innerHTML=kt;const st=E.loader(h,{type:"dots"});try{p.value=await g.collections.get(a)}catch(e){_.value=`Could not load the collection: ${e.message||e}`}finally{st.destroy()}if(!u())return;if(!p.peek()&&!_.peek()){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const we=re.peek();L.innerHTML=xt(we.length),G.replaceChildren();const ne=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),G.appendChild(n),n},$=document.createElement("input");$.type="checkbox",$.title="Select every entry shown",$.setAttribute("aria-label","Select every entry shown"),ne("cex-check",$);const Se=(e,t)=>{const n=document.createElement("button");return n.type="button",n.dataset.sort=e,n.append(document.createTextNode(t),Object.assign(document.createElement("span"),{className:"cex-sort"})),n};we.forEach(e=>ne("",Se(e.name,e.label||e.name))),ne("cex-when",Se("created","Created")),ne("cex-tools"),r(M.effect(()=>{const e=Z.value;G.querySelectorAll("[data-sort]").forEach(t=>{t.querySelector(".cex-sort").textContent=t.dataset.sort===e.key?e.dir==="asc"?"\u25B2":"\u25BC":"",t.parentElement.setAttribute("aria-sort",t.dataset.sort===e.key?e.dir==="asc"?"ascending":"descending":"none")})})),r(M.effect(()=>{const e=T.value.map(n=>n.id),t=new Set(y.value);$.checked=e.length>0&&e.every(n=>t.has(n)),$.indeterminate=!$.checked&&e.some(n=>t.has(n))}));const Ce=e=>{const t=e.target.closest("[data-sort]");if(!t)return;const n=Z.peek();Z.value={key:t.dataset.sort,dir:n.key===t.dataset.sort&&n.dir==="asc"?"desc":"asc"}};G.addEventListener("click",Ce),$.addEventListener("change",()=>{const e=T.peek().map(t=>t.id);y.value=$.checked?[...new Set([...y.peek(),...e])]:y.peek().filter(t=>!e.includes(t))}),r(()=>G.removeEventListener("click",Ce));const ot=M.applyBindings(He,h,{methods:{clearSelection:()=>{y.value=[]},deleteSelected:()=>Xe(),clearAll:()=>Ye(),clearFilters:()=>{w.value=[]},showMore:()=>{T.peek().length>q.peek()?q.value=q.peek()+X:Ke()}}});r(()=>ot.dispose()),r(Oe(h,{position:"bottom"})),r(M.effect(()=>{le.value,w.value,q.value=X}));const $e=e=>{const t=e.target.closest("button[data-idx]");if(!t)return;const n=Number(t.dataset.idx);w.value=w.peek().filter((s,o)=>o!==n)};pe.addEventListener("click",$e),r(()=>pe.removeEventListener("click",$e));const ct=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>at(e,t)});r(()=>ct?.destroy?.());const Ae=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){Re(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=V(t.dataset.id);n&&O(n)},Me=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&de(t.dataset.id)},Le=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=V(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Re(t)):e.key==="Enter"?(e.preventDefault(),O(n)):e.key===" "?(e.preventDefault(),de(n.id)):e.key==="Delete"&&(e.preventDefault(),be(n)))};L.addEventListener("click",Ae),L.addEventListener("change",Me),L.addEventListener("keydown",Le),r(()=>{L.removeEventListener("click",Ae),L.removeEventListener("change",Me),L.removeEventListener("keydown",Le)});const qe=p.peek(),ae=m.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(ae&&qe?.title){for(;ae.nextSibling;)ae.nextSibling.remove();ae.after(document.createTextNode(qe.title))}z(m,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),z(m,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),bt(a)&&z(m,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),z(m,{id:"entries-export",icon:"download",title:"Export",onClick:()=>nt()}),z(m,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>tt()}),z(m,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>O(null)});const Te=e=>{N?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Te),r(()=>window.removeEventListener("beforeunload",Te)),r(()=>{if(N?.dirty())try{S.set(P(a),{id:N.id,values:N.values(),at:new Date().toISOString()})}catch{}ke=!0,[...ue].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(h),!p.peek()){Y.value=!1;return}if(await Ge(),!u()||(await H(),!u()))return;let C=null;try{C=S.get(P(a))}catch{}if(C?.values){try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}let e=C.id?V(C.id):null;if(C.id&&!e&&(e=await g.collections.getEntry(a,C.id).catch(()=>null)),!u()||C.id&&!e)return;const t=e?`your changes to \u201C${D(K(e))}\u201D`:"a new entry",n=e?.meta?.updatedAt&&C.at&&e.meta.updatedAt>C.at?" It has been changed since - check before saving.":"";await E.confirm(`You left ${t} unsaved.${n} Pick up where you left off?`)&&u()&&O(e,C.values)}}};
50
+ </div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const s=n.dataset.fmt;try{const o=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${s}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!o.ok)throw new Error(`${o.status}`);const l=await o.blob(),i=document.createElement("a");i.href=URL.createObjectURL(l),i.download=`${a}-entries.${s}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(o){E.toast(`Export failed (${o.message}).`,{type:"error"})}})),e.element.appendChild(t),te(e),I.scan(t),e.open()}function at(e,t){const n=V(e.dataset.id);if(!n)return[];const s=t?.event?.target?.closest?.("td[data-col]"),o=s?re.peek()[Number(s.dataset.col)]:null,l=ye(n.id),i=y.peek().includes(n.id),p=o?yt(o,n.data?.[o.name]):[];return[{header:K(n)},{label:"Edit\u2026",icon:"edit",action:()=>O(n)},{label:"See all of it\u2026",icon:"eye",action:()=>et(n)},{label:"Duplicate",icon:"copy",action:()=>Ze(n)},{label:"Copy id",icon:"hash",action:()=>ge(n.id)},{label:i?"Deselect":"Select",icon:"check",action:()=>de(n.id)},...l.length?[{type:"divider"},...l.map(f=>({label:`${f.trigger?.label||f.title||f.slug}\u2026`,icon:f.trigger?.icon||"zap",disabled:A.peek(),action:()=>Qe(f,n)}))]:[],...o?[{type:"divider"},{label:`Filter by ${o.label||o.name}`,icon:"filter",submenu:[...p.map(f=>({label:f.label,action:()=>Ee(o,f.op,f.value)})),{label:`${o.label||o.name} contains\u2026`,action:async()=>{const f=await E.prompt(`Show entries whose ${D(o.label||o.name)} contains`,{inputPlaceholder:"text"});f&&String(f).trim()&&Ee(o,"contains",String(f).trim())}}]}]:[],...w.peek().length?[{label:`Clear filters (${w.peek().length})`,icon:"x",action:()=>{w.value=[]}}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>be(n)}]}const L=h.querySelector(".cex-rows"),G=h.querySelector(".cex-head"),pe=h.querySelector(".cex-chips-list");pe.innerHTML=kt;const st=E.loader(h,{type:"dots"});try{u.value=await g.collections.get(a)}catch(e){_.value=`Could not load the collection: ${e.message||e}`}finally{st.destroy()}if(!r())return;if(!u.peek()&&!_.peek()){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const we=re.peek();L.innerHTML=xt(we.length),G.replaceChildren();const ne=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),G.appendChild(n),n},$=document.createElement("input");$.type="checkbox",$.title="Select every entry shown",$.setAttribute("aria-label","Select every entry shown"),ne("cex-check",$);const Se=(e,t)=>{const n=document.createElement("button");return n.type="button",n.dataset.sort=e,n.append(document.createTextNode(t),Object.assign(document.createElement("span"),{className:"cex-sort"})),n};we.forEach(e=>ne("",Se(e.name,e.label||e.name))),ne("cex-when",Se("created","Created")),ne("cex-tools"),d(M.effect(()=>{const e=Z.value;G.querySelectorAll("[data-sort]").forEach(t=>{t.querySelector(".cex-sort").textContent=t.dataset.sort===e.key?e.dir==="asc"?"\u25B2":"\u25BC":"",t.parentElement.setAttribute("aria-sort",t.dataset.sort===e.key?e.dir==="asc"?"ascending":"descending":"none")})})),d(M.effect(()=>{const e=T.value.map(n=>n.id),t=new Set(y.value);$.checked=e.length>0&&e.every(n=>t.has(n)),$.indeterminate=!$.checked&&e.some(n=>t.has(n))}));const Ce=e=>{const t=e.target.closest("[data-sort]");if(!t)return;const n=Z.peek();Z.value={key:t.dataset.sort,dir:n.key===t.dataset.sort&&n.dir==="asc"?"desc":"asc"}};G.addEventListener("click",Ce),$.addEventListener("change",()=>{const e=T.peek().map(t=>t.id);y.value=$.checked?[...new Set([...y.peek(),...e])]:y.peek().filter(t=>!e.includes(t))}),d(()=>G.removeEventListener("click",Ce));const ot=M.applyBindings(He,h,{methods:{clearSelection:()=>{y.value=[]},deleteSelected:()=>Xe(),clearAll:()=>Ye(),clearFilters:()=>{w.value=[]},showMore:()=>{T.peek().length>q.peek()?q.value=q.peek()+X:Ke()}}});d(()=>ot.dispose()),d(Oe(h,{position:"bottom"})),d(M.effect(()=>{le.value,w.value,q.value=X}));const $e=e=>{const t=e.target.closest("button[data-idx]");if(!t)return;const n=Number(t.dataset.idx);w.value=w.peek().filter((s,o)=>o!==n)};pe.addEventListener("click",$e),d(()=>pe.removeEventListener("click",$e));const ct=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>at(e,t)});d(()=>ct?.destroy?.());const Ae=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){Re(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=V(t.dataset.id);n&&O(n)},Me=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&de(t.dataset.id)},Le=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=V(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Re(t)):e.key==="Enter"?(e.preventDefault(),O(n)):e.key===" "?(e.preventDefault(),de(n.id)):e.key==="Delete"&&(e.preventDefault(),be(n)))};L.addEventListener("click",Ae),L.addEventListener("change",Me),L.addEventListener("keydown",Le),d(()=>{L.removeEventListener("click",Ae),L.removeEventListener("change",Me),L.removeEventListener("keydown",Le)});const qe=u.peek(),ae=m.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(ae&&qe?.title){for(;ae.nextSibling;)ae.nextSibling.remove();ae.after(document.createTextNode(qe.title))}z(m,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),z(m,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),bt(a)&&z(m,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),z(m,{id:"entries-export",icon:"download",title:"Export",onClick:()=>nt()}),z(m,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>tt()}),z(m,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>O(null)});const Te=e=>{N?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Te),d(()=>window.removeEventListener("beforeunload",Te)),d(()=>{if(N?.dirty())try{S.set(P(a),{id:N.id,values:N.values(),at:new Date().toISOString()})}catch{}ke=!0,[...ue].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(h),!u.peek()){Y.value=!1;return}if(await Ge(),!r()||(await H(),!r()))return;let C=null;try{C=S.get(P(a))}catch{}if(C?.values){try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}let e=C.id?V(C.id):null;if(C.id&&!e&&(e=await g.collections.getEntry(a,C.id).catch(()=>null)),!r()||C.id&&!e)return;const t=e?`your changes to \u201C${D(K(e))}\u201D`:"a new entry",n=e?.meta?.updatedAt&&C.at&&e.meta.updatedAt>C.at?" It has been changed since - check before saving.":"";await E.confirm(`You left ${t} unsaved.${n} Pick up where you left off?`)&&r()&&O(e,C.values)}}};