domma-cms 1.10.15 → 1.10.17
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.
- package/CHANGELOG.md +14 -0
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/api.js +1 -1
- package/admin/js/lib/collection-editor-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/form-submissions-arrange.js +1 -1
- package/admin/js/lib/hotkeys-ui.js +7 -16
- package/admin/js/lib/hotkeys.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/media-picker.js +17 -5
- package/admin/js/lib/media-upload.js +1 -0
- package/admin/js/lib/palette-commands.js +1 -0
- package/admin/js/templates/collection-entries.html +4 -0
- package/admin/js/templates/form-submissions.html +3 -0
- package/admin/js/templates/media.html +19 -10
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/media.js +3 -3
- package/docs/DommaFeatures.md +5 -4
- package/docs/api-reference.md +6 -1
- package/docs/configuration.md +57 -10
- package/docs/guides/multi-page-forms.md +1 -1
- package/docs/markdown-shortcodes.md +3 -2
- package/docs/plugin-development.md +3 -0
- package/package.json +2 -2
- package/public/css/forms.css +1 -1
- package/public/css/site.css +1 -1
- package/public/js/collection-browser.js +2 -2
- package/public/js/collection-range.mjs +1 -0
- package/public/js/form-files.mjs +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/site.js +1 -1
- package/server/routes/api/forms.js +211 -60
- package/server/routes/api/media.js +25 -2
- package/server/server.js +8 -2
- package/server/services/collections.js +3 -0
- package/server/services/email.js +15 -4
- package/server/services/forms.js +11 -3
- package/server/services/markdown.js +20 -9
- package/server/services/mediaTypes.js +22 -0
- package/server/templates/page.html +8 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as k,apiRequest as pt}from"../api.js";import{addBannerAction as j}from"../lib/plugin-chrome.js";import{SHAPES as ft,showLoading as mt}from"../lib/loading.js";import{helpPopovers as ze}from"../lib/help-popover.js";import{onceClosed as ht,openMenuAt as je,sidebarCount as P,viewLifetime as yt}from"../lib/tool-kit.js";import{MAX_COLUMNS as vt,PAGE_SIZE as ce,SHOW_STEP as X,arrangeEntries as bt,canClear as gt,cellText as Pe,countText as Et,draftable as kt,fileRefsOf as xt,filterChoices as wt,filterText as St,hasForm as Ct,isFileValue as ve,safeFileHref as $t,shownValue as be,snapshot as _e,typedValue as At,valueText as ge}from"../lib/collection-entries-arrange.js";import{fmtDate as Mt,fmtDateTime as _}from"../lib/dates.js";const Be=/^#\/collections\/[^/?#]+\/entries\/?(\?.*)?$/,B=p=>`collection-entries.draft.${p}`,U=p=>String(p??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function le(p){const y=document.createElement("template");return y.innerHTML=p.trim(),y.content.firstElementChild}const Lt='<span class="cex-chip">{{text}}<button type="button" data-idx="{{idx}}" title="Remove this filter" aria-label="Remove filter {{text}}">\xD7</button></span>';function qt(p){return`
|
|
2
2
|
<tr class="cex-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
|
|
3
3
|
<td class="cex-check"><input type="checkbox" class="cex-sel" aria-label="Select this entry" data-bind-checked="sel"></td>
|
|
4
|
-
${Array.from({length:
|
|
4
|
+
${Array.from({length:p},(b,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
|
|
7
|
+
</tr>`}const Tt=(p,y)=>p?.type==="file"||ve(y?.[p?.name]);function He(p){const y=document.createElement("ul");y.className="cex-files";for(const b of xt(p)){const a=document.createElement("li"),c=$t(b.url),l=document.createElement(c?"a":"span");c&&(l.href=c,l.target="_blank",l.rel="noopener"),l.textContent=ge(b),a.appendChild(l),y.appendChild(a)}return y}function Nt(p,y){const b={};return p.forEach(a=>{const c={};a.fullWidth?c.span=y:a.span>1&&(c.span=a.span),a.placeholder&&(c.placeholder=a.placeholder),a.helper&&(c.hint=a.helper),a.prefix&&(c.prefix=a.prefix),a.suffix&&(c.suffix=a.suffix);for(const u of["clear","stepper","counter"])a[u]===!0&&(c[u]=!0);const l=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;b[a.name]={type:l,label:a.label,required:a.required,options:a.options,...Object.keys(c).length?{formConfig:c}:{}}}),b}function ie(p,y){const b={};for(const a of y){const c=[...p.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!c.length)continue;const l=c[0];l.type==="radio"?b[a.name]=c.find(u=>u.checked)?.value??"":l.type==="checkbox"&&c.length>1?b[a.name]=c.filter(u=>u.checked).map(u=>u.value):l.type==="checkbox"?b[a.name]=l.checked:l.tagName==="SELECT"&&l.multiple?b[a.name]=[...l.selectedOptions].map(u=>u.value):b[a.name]=At(a,l.value)}return b}function Ve(p,y,b){for(const a of y){if(!(a.name in b))continue;const c=b[a.name],l=[...p.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!l.length)continue;const u=l[0];u.type==="radio"?l.forEach(r=>{r.checked=r.value===String(c)}):u.type==="checkbox"&&l.length>1?l.forEach(r=>{r.checked=Array.isArray(c)&&c.includes(r.value)}):u.type==="checkbox"?u.checked=c===!0:u.tagName==="SELECT"&&u.multiple?[...u.options].forEach(r=>{r.selected=Array.isArray(c)&&c.includes(r.value)}):u.value=c??"",l.forEach(r=>r.dispatchEvent(new Event("input",{bubbles:!0})))}}const Je=yt(Be);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(p){Je.reset();const y=p.get(0).querySelector(".cex")||p.get(0),b=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=b?decodeURIComponent(b[1]):null,c=()=>Be.test(location.hash)&&y.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),l=e=>(Je.add(e),e);if(!a){E.toast("No collection selected.",{type:"error"});return}const u=M.observable(null),r=M.observable([]),x=M.observable(0),Y=M.observable(!0),H=M.observable(""),Ke=M.computed(()=>u.value!==null),re=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),g=M.observable([]),N=M.observable(X),V=M.observable([]),Q=M.observable({}),L=M.observable(!1);let ee=1;const te=()=>u.peek()?.fields||[],de=M.computed(()=>(u.value?.fields||[]).slice(0,vt)),J=e=>r.peek().find(t=>t.id===e),Ee=e=>V.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),D=M.observable([]),ke=M.observable([]);l(M.effect(()=>{const e=bt(r.value,{filters:w.value,query:re.value,sort:Z.value});D.value=e})),l(M.effect(()=>{const e=de.value,t=new Set(g.value);Q.value,V.value,ke.value=D.value.slice(0,N.value).map(n=>{const o={id:n.id,sel:t.has(n.id),rowCls:t.has(n.id)?"is-selected":""};e.forEach((i,m)=>{const d=be(i,n.data?.[i.name]);o[`c${m}`]=Pe(d),o[`t${m}`]=ge(d)});const s=n.meta?.createdAt;o.when=s?Mt(s):"",o.whenFull=s?`Created ${_(s)}${n.meta?.updatedAt?` \xB7 changed ${_(n.meta.updatedAt)}`:""}`:"";const f=Ee(n.id).length;return o.runs=f?String(f):"",o})}));const Ge=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:St(e)}))),We=M.computed(()=>g.value.length),Xe={schema:u,loaded:r,loading:Y,loadError:H,ready:Ke,query:re,rows:ke,chips:Ge,busy:L,selectedCount:We,countLine:M.computed(()=>Et(D.value.length,r.value.length,x.value)),selectedText:M.computed(()=>`${g.value.length} selected`),canClear:M.computed(()=>gt(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(()=>r.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=D.value.length-N.value;if(e>0)return`Show ${Math.min(X,e)} more (${e} not shown)`;const t=x.value-r.value.length;return t>0?`Load the next ${Math.min(ce,t)} from the server`:""})};async function K({reset:e=!0,pending:t=null}={}){Y.value=!0;try{const n=e?Math.max(ce,r.peek().length):ce;e&&(ee=1);const o=await k.collections.listEntries(a,{limit:n,page:ee});if(t&&await t.done(),!c())return;const s=o?.entries||(Array.isArray(o)?o:[]);r.value=e?s:[...r.peek(),...s.filter(i=>!J(i.id))],x.value=Number(o?.total)||r.peek().length,e&&(ee=Math.max(1,Math.ceil(r.peek().length/ce)));const f=new Set(r.peek().map(i=>i.id));g.value=g.peek().filter(i=>f.has(i)),H.value="",await Qe()}catch(n){H.value=`Could not load the entries: ${n.message||n}`}finally{t&&await t.done(),Y.value=!1}}async function Ye(){ee+=1,await K({reset:!1})}async function Ze(){try{if((await pt("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await k.actions.forCollection(a);V.value=Array.isArray(t)?t.filter(n=>n.canRun!==!1):[]}catch{V.value=[]}}async function Qe(){const e=V.peek().filter(o=>o.access?.rowLevel);if(!e.length||!r.peek().length)return;const t=r.peek().map(o=>o.id),n={...Q.peek()};await Promise.all(e.map(async o=>{try{n[o.slug]=new Set((await k.actions.checkAccess(o.slug,t)).allowed)}catch{n[o.slug]=null}})),Q.value=n}const G=e=>{const t=te()[0];return(t?Pe(be(t,e?.data?.[t.name]),40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function xe(e){if(await E.confirm(`Delete \u201C${U(G(e))}\u201D? This cannot be undone.`))try{await k.collections.deleteEntry(a,e.id),r.value=r.peek().filter(t=>t.id!==e.id),x.value=Math.max(0,x.peek()-1),g.value=g.peek().filter(t=>t!==e.id),P("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function et(){const e=[...g.peek()];if(!e.length||!await E.confirm(`Delete ${e.length} entr${e.length===1?"y":"ies"}? This cannot be undone.`))return;L.value=!0;let t=0;const n=[];for(const o of e)try{await k.collections.deleteEntry(a,o),t++}catch(s){n.push(s.message||String(s))}L.value=!1,g.value=[],P("#/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}),c()&&await K()}async function tt(){const e=u.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${U(e?.title||a)}\u201D? This cannot be undone.`)){L.value=!0;try{await k.collections.clearEntries(a),r.value=[],x.value=0,g.value=[],P("#/collections"),E.toast("All entries deleted.",{type:"success"})}catch(t){E.toast(`Not cleared: ${t.message||t}`,{type:"error"})}finally{L.value=!1}}}async function nt(e){try{const t=await k.collections.createEntry(a,{...e.data||{}});r.value=[t,...r.peek()],x.value=x.peek()+1,P("#/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 at(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(U(n)))){L.value=!0;try{const o=await k.actions.execute(e.slug,t.id);if(o?.success){const s=o.stepsCompleted??0;E.toast(`${e.trigger?.label||e.title||"Action"} done (${s} step${s===1?"":"s"}).`,{type:"success"}),c()&&await K()}else E.toast(o?.results?.find(s=>!s.success)?.error||"The action failed.",{type:"error"})}catch(o){E.toast(o.message||"The action failed.",{type:"error"})}finally{L.value=!1}}}async function we(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 ue(e){const t=g.peek();g.value=t.includes(e)?t.filter(n=>n!==e):[...t,e]}function Se(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],N.value=X}const pe=new Set;let O=null,Ce=!1;function $e(e,t,n){const o=e.close.bind(e);let s=!1,f=!1;e.close=async i=>{if(!s&&i!==!0&&!Ce&&t()){if(f)return;f=!0;const m=await E.confirm(n);if(f=!1,!m)return}return s=!0,o()}}function ne(e,t=()=>{}){pe.add(e),ht(e,()=>{pe.delete(e),t()})}function z(e,t=null){const n=u.peek(),o=te().filter(h=>Tt(h,e?.data)),s=te().filter(h=>!o.includes(h)),f=Object.fromEntries(o.filter(h=>e?.data&&h.name in e.data).map(h=>[h.name,e.data[h.name]])),i=!!e,m=E.slideover({title:i?`Edit ${U(G(e))}`:"New entry",size:"md",position:"right"}),d=le(`
|
|
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>`);
|
|
18
|
+
</div>`);d.querySelector(".cex-save-text").textContent=i?"Save":"Add entry";const v=d.querySelector(".cex-fields");for(const h of o){if(!ve(f[h.name]))continue;const $=document.createElement("div");$.className="cex-files-field";const T=document.createElement("span");T.textContent=h.label||h.name,$.append(T,He(f[h.name])),v.before($)}F.create(Nt(s,n?.columns||2),e?.data||{},{layout:n?.layout||"stacked",columns:n?.columns||2,showSubmitButton:!1}).renderTo(v),s.forEach(h=>{if(h.type!=="date"||!h.name)return;const $=v.querySelector(`[name="${CSS.escape(h.name)}"]`);$&&$.type!=="date"&&($.type="date")}),e?.data&&Ve(v,s,e.data),i&&e.meta?.createdAt&&(d.querySelector(".cex-when-line").textContent=`Created ${_(e.meta.createdAt)}${e.meta.updatedAt?`, last changed ${_(e.meta.updatedAt)}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),m.element.appendChild(d);const dt=_e(ie(v,s));t&&Ve(v,s,t);const me=()=>_e(ie(v,s))!==dt,ut=d.querySelector(".cex-dirty"),he=()=>{ut.hidden=!me()};v.addEventListener("input",he),v.addEventListener("change",he),he();let ye=!1;async function Re(){if(ye)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const h={...ie(v,s),...f};ye=!0;const $=d.querySelector('[data-act="save"]');$.disabled=!0;try{if(i){const T=await k.collections.updateEntry(a,e.id,h);r.value=r.peek().map(Ue=>Ue.id===e.id?T:Ue),E.toast("Entry saved.",{type:"success"})}else{const T=await k.collections.createEntry(a,h);r.value=[T,...r.peek()],x.value=x.peek()+1,P("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(B(a)):S.set(B(a),null)}catch{}m.close(!0)}catch(T){E.toast(T.message||"Not saved.",{type:"error"})}finally{ye=!1,$.disabled=!1}}d.querySelector('[data-act="save"]').addEventListener("click",Re),d.querySelector('[data-act="cancel"]').addEventListener("click",()=>m.close()),d.addEventListener("keydown",h=>{(h.ctrlKey||h.metaKey)&&(h.key==="Enter"||h.key==="s")&&(h.preventDefault(),Re())});const Fe={id:e?.id??null,dirty:me,values:()=>kt(ie(v,s),s)};O=Fe,ne(m,()=>{O===Fe&&(O=null)}),$e(m,me,"Discard the changes you have not saved?"),I.scan(d),m.open(),v.querySelector("input, select, textarea")?.focus()}function st(e){const t=E.slideover({title:U(G(e)),size:"md",position:"right"}),n=le(`
|
|
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>`),
|
|
27
|
+
</div>`),o=n.querySelector(".cex-detail");for(const i of te()){const m=ge(be(i,e.data?.[i.name])),d=document.createElement("dt");d.textContent=i.label||i.name;const v=document.createElement("dd");ve(e.data?.[i.name])?v.appendChild(He(e.data[i.name])):v.textContent=m===""?"-":m,o.append(d,v)}const s=n.querySelector(".cex-meta"),f=i=>{const m=document.createElement("span");m.textContent=i,s.appendChild(m)};f(`Id: ${e.id}`),e.meta?.createdAt&&f(`Created: ${_(e.meta.createdAt)}`),e.meta?.updatedAt&&f(`Changed: ${_(e.meta.updatedAt)}`),e.meta?.source&&f(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>we(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),z(e)}),t.element.appendChild(n),ne(t),I.scan(n),t.open()}function ot(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=le(`
|
|
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"),
|
|
43
|
+
</div>`),n=t.querySelector("textarea"),o=t.querySelector(".cex-err"),s=t.querySelector(".cex-ok"),f=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>f.click()),f.addEventListener("change",async()=>{const m=f.files?.[0];f.value="",m&&(n.value=await m.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let i=!1;t.querySelector('[data-act="import"]').addEventListener("click",async()=>{o.hidden=!0,s.hidden=!0;let m;try{if(m=JSON.parse(n.value),!Array.isArray(m))throw new Error("it must be a JSON array")}catch(v){o.textContent=`Not valid: ${v.message}`,o.hidden=!1;return}const d=t.querySelector('[data-act="import"]');d.disabled=!0;try{const v=await k.collections.import(a,m);s.textContent=`Imported ${v.imported}; skipped ${v.skipped}.`,s.hidden=!1,i=!0,P("#/collections"),c()&&await K(),setTimeout(()=>e.close(!0),1200)}catch(v){o.textContent=v.message||"Import failed.",o.hidden=!1}finally{d.disabled=!1}}),e.element.appendChild(t),ne(e),$e(e,()=>!i&&n.value.trim()!=="","Discard the entries you pasted?"),ze(t),I.scan(t),e.open(),n.focus()}function ct(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=le(`
|
|
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
|
|
50
|
+
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const o=n.dataset.fmt;try{const s=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${o}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!s.ok)throw new Error(`${s.status}`);const f=await s.blob(),i=document.createElement("a");i.href=URL.createObjectURL(f),i.download=`${a}-entries.${o}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(s){E.toast(`Export failed (${s.message}).`,{type:"error"})}})),e.element.appendChild(t),ne(e),I.scan(t),e.open()}function lt(e,t){const n=J(e.dataset.id);if(!n)return[];const o=t?.event?.target?.closest?.("td[data-col]"),s=o?de.peek()[Number(o.dataset.col)]:null,f=Ee(n.id),i=g.peek().includes(n.id),m=s?wt(s,n.data?.[s.name]):[];return[{header:G(n)},{label:"Edit\u2026",icon:"edit",action:()=>z(n)},{label:"See all of it\u2026",icon:"eye",action:()=>st(n)},{label:"Duplicate",icon:"copy",action:()=>nt(n)},{label:"Copy id",icon:"hash",action:()=>we(n.id)},{label:i?"Deselect":"Select",icon:"check",action:()=>ue(n.id)},...f.length?[{type:"divider"},...f.map(d=>({label:`${d.trigger?.label||d.title||d.slug}\u2026`,icon:d.trigger?.icon||"zap",disabled:L.peek(),action:()=>at(d,n)}))]:[],...s?[{type:"divider"},{label:`Filter by ${s.label||s.name}`,icon:"filter",submenu:[...m.map(d=>({label:d.label,action:()=>Se(s,d.op,d.value)})),{label:`${s.label||s.name} contains\u2026`,action:async()=>{const d=await E.prompt(`Show entries whose ${U(s.label||s.name)} contains`,{inputPlaceholder:"text"});d&&String(d).trim()&&Se(s,"contains",String(d).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:()=>xe(n)}]}const q=y.querySelector(".cex-rows"),W=y.querySelector(".cex-head"),fe=y.querySelector(".cex-chips-list");fe.innerHTML=Lt;const ae=mt(y.querySelector(".dm-loading-slot"),ft.table);try{u.value=await k.collections.get(a)}catch(e){H.value=`Could not load the collection: ${e.message||e}`}if(!c()){ae.done();return}if(!u.peek()&&(await ae.done(),!H.peek())){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const Ae=de.peek();q.innerHTML=qt(Ae.length),W.replaceChildren();const se=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),W.appendChild(n),n},A=document.createElement("input");A.type="checkbox",A.title="Select every entry shown",A.setAttribute("aria-label","Select every entry shown"),se("cex-check",A);const Me=(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};Ae.forEach(e=>se("",Me(e.name,e.label||e.name))),se("cex-when",Me("created","Created")),se("cex-tools"),l(M.effect(()=>{const e=Z.value;W.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")})})),l(M.effect(()=>{const e=D.value.map(n=>n.id),t=new Set(g.value);A.checked=e.length>0&&e.every(n=>t.has(n)),A.indeterminate=!A.checked&&e.some(n=>t.has(n))}));const Le=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"}};W.addEventListener("click",Le),A.addEventListener("change",()=>{const e=D.peek().map(t=>t.id);g.value=A.checked?[...new Set([...g.peek(),...e])]:g.peek().filter(t=>!e.includes(t))}),l(()=>W.removeEventListener("click",Le));const it=M.applyBindings(Xe,y,{methods:{clearSelection:()=>{g.value=[]},deleteSelected:()=>et(),clearAll:()=>tt(),clearFilters:()=>{w.value=[]},showMore:()=>{D.peek().length>N.peek()?N.value=N.peek()+X:Ye()}}});l(()=>it.dispose()),l(ze(y,{position:"bottom"})),l(M.effect(()=>{re.value,w.value,N.value=X}));const qe=e=>{const t=e.target.closest("button[data-idx]");if(!t)return;const n=Number(t.dataset.idx);w.value=w.peek().filter((o,s)=>s!==n)};fe.addEventListener("click",qe),l(()=>fe.removeEventListener("click",qe));const rt=E.contextMenu(q,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>lt(e,t)});l(()=>rt?.destroy?.());const Te=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){je(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=J(t.dataset.id);n&&z(n)},Ne=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&ue(t.dataset.id)},Ie=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=J(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),je(t)):e.key==="Enter"?(e.preventDefault(),z(n)):e.key===" "?(e.preventDefault(),ue(n.id)):e.key==="Delete"&&(e.preventDefault(),xe(n)))};q.addEventListener("click",Te),q.addEventListener("change",Ne),q.addEventListener("keydown",Ie),l(()=>{q.removeEventListener("click",Te),q.removeEventListener("change",Ne),q.removeEventListener("keydown",Ie)});const De=u.peek(),oe=p.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(oe&&De?.title){for(;oe.nextSibling;)oe.nextSibling.remove();oe.after(document.createTextNode(De.title))}j(p,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),j(p,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),Ct(a)&&j(p,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),j(p,{id:"entries-export",icon:"download",title:"Export",onClick:()=>ct()}),j(p,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>ot()}),j(p,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>z(null)});const Oe=e=>{O?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Oe),l(()=>window.removeEventListener("beforeunload",Oe)),l(()=>{if(O?.dirty())try{S.set(B(a),{id:O.id,values:O.values(),at:new Date().toISOString()})}catch{}Ce=!0,[...pe].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(y),!u.peek()){Y.value=!1;return}if(await Ze(),!c()){ae.done();return}if(await K({pending:ae}),!c())return;let C=null;try{C=S.get(B(a))}catch{}if(C?.values){try{S.remove?S.remove(B(a)):S.set(B(a),null)}catch{}let e=C.id?J(C.id):null;if(C.id&&!e&&(e=await k.collections.getEntry(a,C.id).catch(()=>null)),!c()||C.id&&!e)return;const t=e?`your changes to \u201C${U(G(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?`)&&c()&&z(e,C.values)}}};
|