domma-cms 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/action-editor-arrange.js +1 -0
- package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
- package/admin/js/lib/api-tokens-arrange.js +3 -0
- package/admin/js/lib/block-editor-arrange.js +2 -0
- package/admin/js/lib/collection-editor-arrange.js +3 -0
- package/admin/js/lib/collection-entries-arrange.js +1 -0
- package/admin/js/lib/component-editor-arrange.js +27 -0
- package/admin/js/lib/context-menu-editor-arrange.js +1 -0
- package/admin/js/lib/dashboard-arrange.js +1 -0
- package/admin/js/lib/effects-arrange.js +3 -0
- package/admin/js/lib/form-editor-arrange.js +1 -0
- package/admin/js/lib/form-submissions-arrange.js +2 -0
- package/admin/js/lib/layouts-arrange.js +1 -0
- package/admin/js/lib/menu-editor-arrange.js +2 -0
- package/admin/js/lib/my-profile-arrange.js +1 -0
- package/admin/js/lib/notifications-arrange.js +1 -0
- package/admin/js/lib/page-editor-arrange.js +1 -0
- package/admin/js/lib/plugin-code-arrange.js +1 -0
- package/admin/js/lib/plugins-arrange.js +2 -0
- package/admin/js/lib/project-page-arrange.js +1 -0
- package/admin/js/lib/role-editor-arrange.js +3 -0
- package/admin/js/lib/roles-arrange.js +1 -0
- package/admin/js/lib/search-settings-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +2 -2
- package/admin/js/lib/site-settings-arrange.js +1 -0
- package/admin/js/lib/user-editor-arrange.js +1 -0
- package/admin/js/lib/users-arrange.js +1 -0
- package/admin/js/lib/view-editor-arrange.js +14 -0
- package/admin/js/templates/action-editor.html +223 -147
- package/admin/js/templates/api-endpoint-editor.html +172 -117
- package/admin/js/templates/api-tokens.html +83 -10
- package/admin/js/templates/block-editor.html +194 -203
- package/admin/js/templates/collection-editor.html +191 -113
- package/admin/js/templates/collection-entries.html +99 -36
- package/admin/js/templates/component-editor.html +128 -126
- package/admin/js/templates/context-menu-editor.html +208 -148
- package/admin/js/templates/dashboard.html +184 -22
- package/admin/js/templates/effects.html +155 -72
- package/admin/js/templates/form-editor.html +41 -26
- package/admin/js/templates/form-submissions.html +78 -26
- package/admin/js/templates/layouts.html +126 -43
- package/admin/js/templates/menu-editor.html +57 -17
- package/admin/js/templates/menu-locations.html +54 -11
- package/admin/js/templates/my-profile.html +100 -12
- package/admin/js/templates/notifications.html +70 -10
- package/admin/js/templates/page-editor.html +47 -26
- package/admin/js/templates/plugin-code.html +91 -73
- package/admin/js/templates/plugins.html +100 -28
- package/admin/js/templates/project-detail.html +83 -145
- package/admin/js/templates/project-settings.html +88 -53
- package/admin/js/templates/role-editor.html +158 -83
- package/admin/js/templates/roles.html +78 -7
- package/admin/js/templates/search.html +193 -216
- package/admin/js/templates/settings.html +404 -472
- package/admin/js/templates/user-editor.html +150 -14
- package/admin/js/templates/users.html +69 -9
- package/admin/js/templates/view-editor.html +203 -180
- package/admin/js/templates/view-preview.html +45 -45
- package/admin/js/views/action-editor.js +7 -1
- package/admin/js/views/api-endpoint-editor.js +2 -3
- package/admin/js/views/api-tokens.js +62 -8
- package/admin/js/views/block-editor.js +14 -8
- package/admin/js/views/collection-editor.js +64 -5
- package/admin/js/views/collection-entries.js +50 -1
- package/admin/js/views/component-editor.js +2 -28
- package/admin/js/views/context-menu-editor.js +54 -55
- package/admin/js/views/dashboard.js +7 -1
- package/admin/js/views/effects.js +31 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +11 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +81 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +21 -43
- package/admin/js/views/menu-locations.js +16 -1
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/notifications.js +18 -1
- package/admin/js/views/page-editor.js +26 -26
- package/admin/js/views/plugin-code.js +23 -4
- package/admin/js/views/plugins.js +97 -39
- package/admin/js/views/project-detail.js +9 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +11 -3
- package/admin/js/views/roles.js +49 -4
- package/admin/js/views/search.js +7 -1
- package/admin/js/views/settings.js +13 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +13 -4
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +97 -40
- package/plugins/_lib/admin/mail/contacts.js +4 -1
- package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
- package/plugins/_lib/admin/mail/folder-tree.js +1 -1
- package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/mail.html +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +303 -44
- package/plugins/_lib/admin/mail/when-closed.js +26 -0
- package/plugins/_lib/mail/readRoutes.js +43 -1
- package/plugins/_lib/mail/unreadBadge.js +42 -0
- package/plugins/free-tier.lock.json +5 -5
- package/plugins/mail-reader/CLAUDE.md +1 -0
- package/plugins/mail-reader/plugin.js +3 -1
- package/plugins/mail-reader/plugin.json +5 -4
- package/server/routes/api/api-tokens.js +56 -0
- package/server/routes/api/auth.js +7 -1
- package/server/routes/api/collections.js +13 -1
- package/server/routes/api/dashboard.js +32 -4
- package/server/routes/api/effects.js +68 -13
- package/server/routes/api/forms.js +104 -13
- package/server/routes/api/layouts.js +93 -2
- package/server/routes/api/notifications.js +140 -14
- package/server/routes/api/plugins.js +135 -4
- package/server/routes/api/roles.js +111 -0
- package/server/routes/api/search.js +38 -4
- package/server/routes/api/settings.js +52 -0
- package/server/routes/api/users.js +65 -3
- package/server/routes/api/views.js +55 -1
- package/server/server.js +12 -0
- package/server/services/components.js +9 -0
- package/server/services/plugins.js +7 -0
- package/server/services/roles.js +3 -0
- package/server/services/search.js +58 -0
- package/server/services/users.js +16 -2
- package/server/services/views.js +14 -0
- package/admin/js/templates/dashboard/activity-feed.html +0 -3
- package/admin/js/templates/dashboard/cache.html +0 -32
- package/admin/js/templates/dashboard/health-detail.html +0 -2
- package/admin/js/templates/dashboard/journeys.html +0 -17
- package/admin/js/templates/dashboard/kpi-strip.html +0 -34
- package/admin/js/templates/dashboard/spike-feed.html +0 -3
- package/admin/js/templates/dashboard/top-pages.html +0 -3
- package/admin/js/templates/dashboard/traffic-chart.html +0 -3
- package/admin/js/templates/plugin-marketplace.html +0 -29
- package/admin/js/views/block-editor-enhance.js +0 -1
- package/admin/js/views/dashboard/lib/escape.js +0 -1
- package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
- package/admin/js/views/dashboard/widgets/cache.js +0 -1
- package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
- package/admin/js/views/dashboard/widgets/journeys.js +0 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
- package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
- package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
- package/admin/js/views/plugin-marketplace.js +0 -1
|
@@ -1,5 +1,64 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
|
|
3
|
-
`),j.appendChild(Y),j.appendChild(W);const k=document.createElement("div");k.className="fb-grid-row",k.style.gridTemplateColumns="1fr auto",k.style.gap=".6rem",k.style.alignItems="end",k.style.marginBottom=".6rem",k.style.display=document.getElementById("collection-layout")?.value==="grid"?"grid":"none";const G=document.createElement("div"),J=document.createElement("label");J.className="form-label",J.textContent="Column Span";const S=document.createElement("input");S.id=`fb-span-${t}`,S.type="number",S.className="form-input",S.min="1",S.max="6",S.value=e.span>1?String(e.span):"1",G.appendChild(J),G.appendChild(S);const K=document.createElement("div");K.style.cssText="padding-bottom:.35rem;";const U=document.createElement("label");U.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const P=document.createElement("input");return P.id=`fb-fullwidth-${t}`,P.type="checkbox",P.checked=!!e.fullWidth,U.appendChild(P),U.appendChild(document.createTextNode("Full Width")),K.appendChild(U),k.appendChild(G),k.appendChild(K),n.appendChild(c),n.appendChild(f),n.appendChild(j),n.appendChild(k),o.addEventListener("click",()=>{const d=n.style.display!=="none";n.style.display=d?"none":"",b.style.transform=d?"":"rotate(180deg)"}),a.appendChild(o),a.appendChild(n),a}function O(e){if(e){if(e.textContent="",x.length===0){const t=document.createElement("p");t.className="text-muted",t.id="fields-empty-msg",t.style.cssText="text-align:center;padding:2rem 0;",t.textContent='No fields yet. Click "Add Field" to get started.',e.appendChild(t);return}x.forEach((t,a)=>{e.appendChild(de(t,a))})}}function ie(e,t,a){t.textContent="",le.forEach(i=>{const p=e?.[i]||{enabled:!1,access:"admin"},u=document.createElement("div");u.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--dm-border,#333);";const m=document.createElement("strong");m.textContent=i.charAt(0).toUpperCase()+i.slice(1),m.style.cssText="font-size:.9rem;";const C=document.createElement("label");C.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const L=document.createElement("input");L.type="checkbox",L.id=`api-${i}-enabled`,L.checked=!!p.enabled,C.appendChild(L),C.appendChild(document.createTextNode("Enable public access"));const v=document.createElement("select");if(v.id=`api-${i}-access`,v.className="form-input",te.forEach(f=>{const r=document.createElement("option");r.value=f,r.textContent=f.charAt(0).toUpperCase()+f.slice(1),f===p.access&&(r.selected=!0),v.appendChild(r)}),u.appendChild(m),u.appendChild(C),u.appendChild(v),t.appendChild(u),i==="read"){const f=document.createElement("div");f.style.cssText="display:grid;grid-template-columns:140px 1fr;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--dm-border,#333);";const r=document.createElement("label");r.htmlFor="api-read-fields",r.style.cssText="font-size:.85rem;color:var(--dm-text-muted,#aaa);",r.textContent="Read fields";const T=document.createElement("input");T.type="text",T.id="api-read-fields",T.className="form-input",T.placeholder="Comma-separated allowlist - empty = all fields",T.value=Array.isArray(p.fields)?p.fields.join(", "):"",f.appendChild(r),f.appendChild(T),t.appendChild(f)}});const o=a||{},s=document.createElement("div");s.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-top:2px solid var(--dm-border,#333);";const g=document.createElement("strong");g.textContent="Export",g.style.cssText="font-size:.9rem;";const y=document.createElement("label");y.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const b=document.createElement("input");b.type="checkbox",b.id="export-enabled",b.checked=o.enabled!==!1,y.appendChild(b),y.appendChild(document.createTextNode("Allow export"));const l=document.createElement("select");l.id="export-access",l.className="form-input",te.filter(i=>i!=="token").forEach(i=>{const p=document.createElement("option");p.value=i,p.textContent=i.charAt(0).toUpperCase()+i.slice(1),i===o.access&&(p.selected=!0),l.appendChild(p)}),s.appendChild(g),s.appendChild(y),s.appendChild(l),t.appendChild(s);const n=document.createElement("div");n.style.cssText="display:grid;grid-template-columns:140px 1fr;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--dm-border,#333);";const c=document.createElement("label");c.style.cssText="font-size:.85rem;color:var(--dm-text-muted,#aaa);",c.textContent="Formats";const h=document.createElement("div");h.style.cssText="display:flex;gap:1rem;";const w=Array.isArray(o.formats)?o.formats:["csv","json"];["csv","json"].forEach(i=>{const p=document.createElement("label");p.style.cssText="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.875rem;";const u=document.createElement("input");u.type="checkbox",u.id=`export-format-${i}`,u.checked=w.includes(i),p.appendChild(u),p.appendChild(document.createTextNode(i.toUpperCase())),h.appendChild(p)}),n.appendChild(c),n.appendChild(h),t.appendChild(n)}function ce(e,t){E.dropdown("#storage-adapter-trigger",{items:[{label:"File (default)",value:"file"},{label:"MongoDB",value:"mongodb"}],onSelect:({item:o})=>{e.find("#storage-adapter").val(o.value),e.find("#storage-adapter-label").text(o.label);const s=o.value==="mongodb";e.find("#storage-connection-group").toggle(s),e.find("#storage-migration-warning").toggle(s&&!q)}});const a=t.map(o=>({label:o,value:o}));E.dropdown("#storage-connection-trigger",{items:a.length?a:[{label:"default",value:"default"}],onSelect:({item:o})=>{e.find("#storage-connection").val(o.value),e.find("#storage-connection-label").text(o.label)}})}function se(){return(document.getElementById("storage-adapter")?.value||"file")==="mongodb"?{adapter:"mongodb",connection:document.getElementById("storage-connection")?.value||"default"}:{adapter:"file"}}function re(){const e={};return le.forEach(t=>{const a=document.getElementById(`api-${t}-enabled`)?.checked??!1,o=document.getElementById(`api-${t}-access`)?.value||"admin";if(e[t]={enabled:a,access:o},t==="read"){const s=(document.getElementById("api-read-fields")?.value||"").split(",").map(g=>g.trim()).filter(Boolean);s.length&&(e[t].fields=s)}}),e}function pe(){const e=["csv","json"].filter(t=>document.getElementById(`export-format-${t}`)?.checked);return{enabled:document.getElementById("export-enabled")?.checked??!0,access:document.getElementById("export-access")?.value||"subscriber",formats:e}}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(e){x=[],$=null,q=!0;const t=window.location.hash.match(/\/collections\/edit\/([^/?#]+)/);t&&($=t[1],q=!1),E.tabs(e.find("#collection-tabs").get(0)),e.find("#collection-layout").get(0)?.addEventListener("change",function(){const l=this.value==="grid";e.find("#collection-columns-group").get(0).style.display=l?"":"none",document.querySelectorAll(".fb-grid-row").forEach(n=>{n.style.display=l?"grid":"none"})});const a=e.find("#fields-list").get(0),o=e.find("#api-access-rows").get(0);X={};const s=e.find("#collection-project").get(0);if(s)try{(await B.projects.list()).forEach(n=>{const c=document.createElement("option");c.value=n.slug,c.textContent=n.name||n.slug,s.appendChild(c)})}catch{}const g=await B.collections.proStatus();g?.pro&&$!=="roles"&&(e.find("#storage-tab-btn").show(),ce(e,g.connections));let y={create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}},b={enabled:!0,access:"subscriber",formats:["csv","json"]};if(q){const l=e.find("#field-title").get(0),n=e.find("#field-slug").get(0);l&&n&&(l.addEventListener("input",()=>{n.dataset.manual||(n.value=ne(l.value))}),n.addEventListener("input",()=>{n.dataset.manual="1"}))}else try{const l=await B.collections.get($);if(!l){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const n=e.find("#editor-title-text").get(0);n&&(n.textContent=l.title),e.find("#field-title").val(l.title||""),e.find("#field-slug").val(l.slug||""),e.find("#field-slug").prop("readonly",!0),e.find("#slug-hint").get(0).textContent="Slug cannot be changed after creation.",e.find("#field-description").val(l.description||""),e.find("#collection-layout").val(l.layout||"stacked"),e.find("#collection-columns").val(l.columns||2),e.find("#collection-bundled").prop("checked",!!l.bundled),X=l.meta||{},e.find("#collection-project").val(l.meta?.project||""),e.find("#collection-columns-group").get(0).style.display=l.layout==="grid"?"":"none",x=l.fields||[],y=l.api||y,b=l.export||b,A=l.storage?.adapter||"file",l.storage&&(e.find("#storage-adapter").val(l.storage.adapter||"file"),e.find("#storage-adapter-label").text(l.storage.adapter==="mongodb"?"MongoDB":"File (default)"),l.storage.adapter==="mongodb"&&(e.find("#storage-connection-group").show(),e.find("#storage-connection").val(l.storage.connection||"default"),e.find("#storage-connection-label").text(l.storage.connection||"default"))),$==="roles"&&e.find("#storage-tab-btn").hide()}catch{E.toast("Failed to load collection.",{type:"error"}),R.navigate("/collections");return}O(a),ie(y,o,b),e.find("#add-field-btn").off("click").on("click",()=>{x=Z(),x.push({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null}),O(a);const l=a.querySelectorAll(".fb-field-card");if(l.length){const n=l[l.length-1],c=n.querySelector(".fb-field-body"),h=n.querySelector(".fb-field-chevron");c&&(c.style.display=""),h&&(h.style.transform="rotate(180deg)"),n.querySelector(`#fb-label-${x.length-1}`)?.focus()}}),e.find("#save-collection-btn").off("click").on("click",async()=>{const l=e.find("#field-title").val().trim(),n=e.find("#field-slug").val().trim(),c=e.find("#field-description").val().trim();if(!l){E.toast("Title is required.",{type:"warning"});return}const h=Z(),w=re(),i=pe(),p=e.find("#collection-layout").val()||"stacked",u=parseInt(e.find("#collection-columns").val(),10)||2,m=se(),C=e.find("#collection-bundled").is(":checked"),L=e.find("#collection-project").val()||"",v={...X||{},project:L||null},f=e.find("#save-collection-btn");f.prop("disabled",!0);try{if(q){const r=await B.collections.create({title:l,slug:n,description:c,layout:p,columns:u,fields:h,api:w,export:i,storage:m,meta:v,...C?{bundled:!0}:{}});$=r.slug,A=m.adapter||"file",q=!1,E.toast("Collection created.",{type:"success"}),R.navigate(`/collections/edit/${r.slug}`)}else if((m.adapter||"file")!==A){let r=0;try{r=(await B.collections.listEntries($,{limit:1}))?.total??0}catch{}const T=A==="file"?`file \u2192 ${m.adapter}`:`${A} \u2192 ${m.adapter||"file"}`;if(r>0&&await E.confirm(`You changed the storage adapter (${T}).
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import{api as C}from"../api.js";import{addBannerAction as ue}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{onceClosed as ot,openMenuAt as Fe}from"../lib/tool-kit.js";import{ACCESS_LEVELS as at,EXPORT_ACCESS as lt,FIELD_TYPES as st,OPTION_TYPES as it,checkForm as rt,draftOf as ct,duplicateField as dt,errorCount as K,fieldFromPanel as Le,fieldProblem as ut,fieldRow as pt,formFromCollection as De,formFromDraft as ft,moveField as Re,nameFromLabel as mt,newField as vt,panelForm as ht,payloadFromForm as bt,slugify as Z,snapshot as H,storageChange as Ne}from"../lib/collection-editor-arrange.js";const gt=/^\/collections\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,yt=s=>`collection-editor.draft.${s||"new"}`,wt=`
|
|
2
|
+
<div class="cex-row" data-key="{{key}}" tabindex="0" draggable="true" data-bind-class="rowCls">
|
|
3
|
+
<span class="cex-grip" title="Drag to reorder" aria-hidden="true">\u283F</span>
|
|
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
|
+
<span class="cex-chip is-ok" data-bind-hidden="!required">Required</span>
|
|
6
|
+
<span class="cex-chip" data-bind-hidden="!hidden">Hidden</span>
|
|
7
|
+
<button type="button" class="cex-kebab" aria-label="Actions for this field" title="Actions">\u22EE</button>
|
|
8
|
+
</div>`,ee=s=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),xt=s=>s.charAt(0).toUpperCase()+s.slice(1);function kt(s){const c=document.createElement("template");return c.innerHTML=s.trim(),c.content.firstElementChild}function Te(s,c){const i=s.querySelector(".dm-plugin-banner-name");if(!i)return;const $=document.createElement("span");$.className="dm-plugin-banner-parent",$.textContent="Collections";const y=document.createElement("span");y.className="dm-plugin-banner-sep",y.setAttribute("aria-hidden","true"),y.textContent="\u203A",i.replaceChildren($,y,document.createTextNode(c))}let g=null,qe=!1;const te=s=>String(s||"").split("?")[0];function Et(){qe||!window.R?.use||(qe=!0,R.use((s,c,i)=>{if(!(g&>.test(c?.path||"")&&te(s?.path)!==te(c?.path))||!g.dirty())return i();const y=`#${c.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(r=>{r?i():history.replaceState(null,"",y)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(g&&te(s?.path)!==g.path){const c=g;g=null,c.dispose()}}))}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(s){if(Et(),g){const e=g;g=null,e.dispose()}const c=s.get(0),i=c.querySelector(".cex")||c,$=te(location.hash.replace(/^#/,""))||"/collections/new",y=$.match(/^\/collections\/edit\/([^/?#]+)/),r=y?decodeURIComponent(y[1]):null,P=!r,pe=[],b=e=>(pe.push(e),e);let fe=!1;const ne=()=>!fe&&i.isConnected,me=E.loader(c,{type:"dots"});let O=null,oe=[],B=null,ve=[],d=0;try{[O,oe,B,ve]=await Promise.all([r?C.collections.get(r):Promise.resolve(null),C.projects.list().catch(()=>[]),C.collections.proStatus().catch(()=>null),P?C.collections.list().then(e=>(e||[]).map(t=>t.slug)).catch(()=>[]):Promise.resolve([])]),r&&(d=(await C.collections.listEntries(r,{limit:1}).catch(()=>null))?.total??0)}catch(e){me.destroy(),E.toast(e?.status===404?"Collection not found.":`Could not load the collection: ${e.message||e}`,{type:"error"}),R.navigate("/collections");return}if(me.destroy(),!i.isConnected)return;if(r&&!O){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const he=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",m=De(O,{project:he}),w=Object.fromEntries(m.fields.map(e=>[e._key,e.name])),Pe=O?.meta||{};let U=m.storageAdapter;const n={};for(const[e,t]of Object.entries(m))n[e]=M.observable(t);const x=M.observable(P),W=M.observable(!1),Y=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),be=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),L=M.observable(H(m)),_=M.computed(()=>H(be())!==L.value);let V=!P;b(M.effect(()=>{const e=n.title.value;V||(n.slug.value=Z(e))}));const p=M.computed(()=>rt(be(),{isNew:x.value,taken:ve,original:w})),ge=M.observable([]);b(M.effect(()=>{const e=p.value.fields||{},t=n.layout.value==="grid";ge.value=n.fields.value.map(o=>pt(o,e,{grid:t}))}));const Ie=M.computed(()=>new Set(n.fields.value.map(e=>e.name))),Be={...n,isNew:x,saving:W,dirty:_,rows:ge,errTitle:M.computed(()=>p.value.title||""),errSlug:M.computed(()=>p.value.slug||""),errColumns:M.computed(()=>p.value.columns||""),errExport:M.computed(()=>p.value.export||""),problemCount:M.computed(()=>K(p.value)),problemText:M.computed(()=>{const e=K(p.value);return e?`${e} thing${e===1?"":"s"} to fix before it can be saved`:""}),settingsBad:M.computed(()=>!!(p.value.title||p.value.slug||p.value.columns)),fieldsBad:M.computed(()=>!!p.value.fields),exportBad:M.computed(()=>!!p.value.export),fieldCount:M.computed(()=>n.fields.value.length),slugHint:M.computed(()=>x.value?`Will be created as "${Z(n.slug.value||n.title.value)||"\u2026"}".`:"Cannot be changed once the collection exists."),apiPath:M.computed(()=>`/api/collections/${Z(n.slug.value||n.title.value)||"<slug>"}/public`),unknownReadFields:M.computed(()=>{const e=String(n.readFields.value||"").split(",").map(t=>t.trim()).filter(t=>t&&!Ie.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),showStorage:M.observable(!!B?.pro&&r!=="roles"),storageNote:M.computed(()=>{const e=Ne(U,n.storageAdapter.value);return!e||x.value?"":d?`Switching ${e}: Save asks whether to move the ${d} existing ${d===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 oe){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,ye.appendChild(t)}if(m.project&&!oe.some(e=>e.slug===m.project)){const e=document.createElement("option");e.value=m.project,e.textContent=`${m.project} (not found)`,ye.appendChild(e)}const _e=(e,t)=>{for(const o of t){const l=document.createElement("option");l.value=o,l.textContent=xt(o),e.appendChild(l)}};i.querySelectorAll(".cex-op select").forEach(e=>_e(e,e.id==="export-access"?lt:at));const Ke=i.querySelector("#storage-connection"),He=Array.isArray(B?.connections)&&B.connections.length?B.connections:["default"];for(const e of new Set([...He,m.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,Ke.appendChild(t)}const N=e=>n.fields.peek().findIndex(t=>t._key===e),j=e=>{n.fields.value=e};function z(e,t){const o=N(e);o<0||(j(Re(n.fields.peek(),o,o+t)),requestAnimationFrame(()=>i.querySelector(`.cex-row[data-key="${e}"]`)?.focus()))}async function ae(e){const t=N(e);if(t<0)return;const o=n.fields.peek()[t];!x.peek()&&d&&w[e]&&!await E.confirm(`Remove the field "${ee(o.label||o.name)}"? The ${d} existing ${d===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(l=>l._key!==e))}function G(e,{isAdded:t=!1}={}){const o=N(e);if(o<0)return;const l=n.fields.peek()[o],v=ht(l),f={};for(const[a,k]of Object.entries(v))f[a]=M.observable(k);const u=JSON.stringify(v),X=()=>Object.fromEntries(Object.keys(f).map(a=>[a,f[a].peek()])),Ce=()=>JSON.stringify(X())!==u;let Me=!!l.name;const Je=M.effect(()=>{const a=f.label.value;Me||(f.name.value=mt(a))}),Xe=new Set(n.fields.peek().filter(a=>a._key!==e).map(a=>a.name)),$e=M.computed(()=>{const a=Le(l,Object.fromEntries(Object.keys(f).map(k=>[k,f[k].value])));return a.name&&Xe.has(a.name)?`"${a.name}" is used by another field.`:ut(a,w[e])}),Qe=M.computed(()=>!x.peek()&&d&&w[e]&&f.name.value.trim()!==w[e]?`Entries keep their value under "${w[e]}" - renaming the field does not move it.`:""),Ze={...f,problem:$e,renamed:Qe,isOptions:M.computed(()=>it.has(f.type.value)),isGrid:M.computed(()=>n.layout.value==="grid")},h=E.slideover({title:t?"New field":`Field: ${ee(l.label||l.name||"Untitled")}`,size:"md",position:"right"}),F=kt(`
|
|
9
|
+
<div class="cex-panel">
|
|
10
|
+
<div class="cex-grid">
|
|
11
|
+
<div>
|
|
12
|
+
<label class="form-label" for="cex-f-label">Label</label>
|
|
13
|
+
<input id="cex-f-label" type="text" class="form-input" maxlength="120" placeholder="e.g. Price" data-model="label.value">
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<label class="form-label" for="cex-f-name">Name
|
|
17
|
+
<span data-help-title="Field name" data-help="What each entry stores this field's value under - in the API, in exports and in block templates. Made from the label until you type one. Letters, digits, _ and -."></span></label>
|
|
18
|
+
<input id="cex-f-name" type="text" class="form-input font-mono" maxlength="80" data-model="name.value">
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<label class="form-label" for="cex-f-type">Type</label>
|
|
22
|
+
<select id="cex-f-type" class="form-input" data-model="type.value">
|
|
23
|
+
${st.map(a=>`<option value="${a.value}">${a.label}</option>`).join("")}
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="cex-setting">
|
|
27
|
+
<label class="form-label" for="cex-f-required">Required
|
|
28
|
+
<span data-help="An entry cannot be saved without it - in the admin and through the API."></span></label>
|
|
29
|
+
<input id="cex-f-required" type="checkbox" class="form-switch" data-model="required.value">
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<label class="form-label" for="cex-f-ph">Placeholder</label>
|
|
33
|
+
<input id="cex-f-ph" type="text" class="form-input" maxlength="200" data-model="placeholder.value">
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<label class="form-label" for="cex-f-help">Helper text
|
|
37
|
+
<span data-help="Shown under the field in the entry form."></span></label>
|
|
38
|
+
<input id="cex-f-help" type="text" class="form-input" maxlength="300" data-model="helper.value">
|
|
39
|
+
</div>
|
|
40
|
+
<div class="cex-wide" data-bind-hidden="!isOptions.value">
|
|
41
|
+
<label class="form-label" for="cex-f-options">Options
|
|
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
|
+
<textarea id="cex-f-options" class="form-input font-mono" rows="5" placeholder="s: Small m: Medium" data-model="options.value"></textarea>
|
|
44
|
+
</div>
|
|
45
|
+
<div data-bind-hidden="!isGrid.value">
|
|
46
|
+
<label class="form-label" for="cex-f-span">Column span
|
|
47
|
+
<span data-help="How many of the grid's columns the field takes, in the entry form."></span></label>
|
|
48
|
+
<input id="cex-f-span" type="number" class="form-input" min="1" max="6" data-model="span.value" data-bind-disabled="fullWidth.value">
|
|
49
|
+
</div>
|
|
50
|
+
<div class="cex-setting" data-bind-hidden="!isGrid.value">
|
|
51
|
+
<label class="form-label" for="cex-f-full">Full width</label>
|
|
52
|
+
<input id="cex-f-full" type="checkbox" class="form-switch" data-model="fullWidth.value">
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<small class="cex-err" data-bind-hidden="!problem.value" data-bind-text="problem.value"></small>
|
|
56
|
+
<p class="cex-warn" data-bind-hidden="!renamed.value"><span data-bind-text="renamed.value"></span></p>
|
|
57
|
+
<div class="cex-panel-actions">
|
|
58
|
+
<button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="remove"><span data-icon="trash" data-icon-size="13"></span> Remove</button>
|
|
59
|
+
<small class="cex-hint">Ctrl+Enter - Done. Save the collection to keep it.</small>
|
|
60
|
+
<span class="cex-spacer"></span>
|
|
61
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
62
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="done" data-bind-disabled="problem.value"><span data-icon="check" data-icon-size="13"></span> Done</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>`);let Q=!1,ce=!1;const et=h.close.bind(h);function Oe(){if($e.peek())return;const a=Le(l,X());j(n.fields.peek().map(k=>k._key===e?a:k)),Q=!0,h.close()}h.element.appendChild(F);const tt=Ae(F),nt=M.applyBindings(Ze,F,{methods:{done:Oe,cancel:()=>h.close(),remove:async()=>{Q=!0,h.close(),await ae(e)}}}),je=F.querySelector("#cex-f-name");je.addEventListener("input",()=>{Me=!!je.value}),F.addEventListener("keydown",a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),Oe())}),ot(h,()=>{Je(),nt.dispose(),tt(),ie.delete(h)}),h.close=async a=>{if(!Q&&a!==!0&&Ce()){if(ce)return;ce=!0;const de=await E.confirm("Discard the changes to this field?");if(ce=!1,!de)return}Q=!0;const k=et();return t&&!Ce()&&!l.name&&j(n.fields.peek().filter(de=>de._key!==e)),k},ie.add(h),I.scan(F),h.open(),F.querySelector("#cex-f-label")?.focus()}function Ue(){const e=vt();j([...n.fields.peek(),e]),Ve.activate?.(1),G(e._key,{isAdded:!0})}function We(e){const t=N(e);if(t<0)return[];const o=n.fields.peek()[t],l=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===l-1,action:()=>z(e,1)},{label:"Duplicate",icon:"copy",action:()=>j(dt(n.fields.peek(),t))},{type:"divider"},{label:"Remove\u2026",icon:"trash",danger:!0,action:()=>ae(e)}]}async function le(){if(W.peek())return;const e=p.peek();if(K(e)){E.toast(`Not saved - ${K(e)} thing${K(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=bt(Y(),{isNew:x.peek(),meta:Pe});W.value=!0;try{if(x.peek()){const l=await C.collections.create(t);L.value=H(Y()),re(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(l.slug)}`);return}const o=Ne(U,t.storage.adapter);if(o&&d){if(await E.confirm(`You changed the storage (${o}). Move the ${d} existing ${d===1?"entry":"entries"} to the new storage?`)){const v=await C.collections.migrateStorage(r,t.storage);E.toast(`Moved ${v.migrated} of ${v.total} entries.`,{type:"success"})}else if(!await E.confirm(`Switch without moving them? The ${d} ${d===1?"entry stays":"entries stay"} in ${ee(U)} storage and this collection will show none until they are moved.`))return}if(await C.collections.update(r,t),!ne())return;U=t.storage.adapter,o&&(d=(await C.collections.listEntries(r,{limit:1}).catch(()=>null))?.total??d);for(const l of n.fields.peek())w[l._key]=l.name;L.value=H(Y()),re(),Te(c,t.title),E.toast("Collection saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{W.value=!1}}async function Ye(){if(!_.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=De(O,{project:he});for(const t of e.fields)w[t._key]=t.name;for(const[t,o]of Object.entries(e))n[t].value=o;L.value=H(e),V=!x.peek()}const T=i.querySelector(".cex-list");T.innerHTML=wt;const Ve=E.tabs(i.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),ze=M.applyBindings(Be,i,{methods:{save:()=>le(),discard:()=>Ye(),addField:()=>Ue()}});b(()=>ze.dispose()),b(Ae(i,{position:"bottom"}));const se=i.querySelector("#field-slug"),we=()=>{V=!!se.value};se.addEventListener("input",we),b(()=>se.removeEventListener("input",we));const ie=new Set;b(()=>[...ie].forEach(e=>{try{e.close(!0)}catch{}}));const Ge=E.contextMenu(T,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>We(e.dataset.key)});b(()=>Ge?.destroy?.());let q=null;const xe={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(),ae(t.dataset.key)))},dragstart:e=>{const t=e.target.closest(".cex-row");t&&(q=t.dataset.key,t.classList.add("is-dragging"),e.dataTransfer.effectAllowed="move")},dragover:e=>{const t=e.target.closest(".cex-row");!t||!q||(e.preventDefault(),T.querySelectorAll(".is-drag-over").forEach(o=>o.classList.remove("is-drag-over")),t.classList.add("is-drag-over"))},drop:e=>{const t=e.target.closest(".cex-row");if(!t||!q)return;e.preventDefault();const o=N(q),l=N(t.dataset.key);q=null,j(Re(n.fields.peek(),o,l))},dragend:()=>{q=null,T.querySelectorAll(".is-drag-over, .is-dragging").forEach(e=>e.classList.remove("is-drag-over","is-dragging"))}};for(const[e,t]of Object.entries(xe))T.addEventListener(e,t);b(()=>{for(const[e,t]of Object.entries(xe))T.removeEventListener(e,t)}),Te(c,O?.title||"New collection"),ue(s,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),r&&ue(s,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(r)}/entries`)}),ue(s,{id:"collection-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>le()});const ke=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!ne()||(e.preventDefault(),e.stopImmediatePropagation(),le())};document.addEventListener("keydown",ke,!0),b(()=>document.removeEventListener("keydown",ke,!0));const J=yt(r);function re(){try{S.remove?S.remove(J):S.set(J,null)}catch{}}const Ee=()=>{if(_.peek())try{S.set(J,{form:ct(Y()),base:L.peek(),at:new Date().toISOString()})}catch{}},Se=e=>{_.peek()&&(Ee(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Se),b(()=>window.removeEventListener("beforeunload",Se)),g={path:$,dirty:()=>_.peek(),dispose(){fe=!0,Ee(),pe.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let A=null;try{A=S.get(J)}catch{}if(A?.form){re();const e=A.base&&A.base!==L.peek(),t=A.at?` (${D(A.at).format("D MMM HH:mm")})`:"",o=r?`changes to "${ee(O?.title||r)}"`:"a new collection",l=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(l)&&ne()){const v=ft(A.form,m);P||(v.slug=m.slug);const f=Object.fromEntries(m.fields.filter(u=>u.id).map(u=>[u.id,u.name]));for(const u of v.fields)u.id&&f[u.id]!==void 0&&(w[u._key]=f[u.id]);for(const[u,X]of Object.entries(v))n[u]&&(n[u].value=X);P&&(V=!!v.slug&&v.slug!==Z(v.title))}}}};
|
|
@@ -1 +1,50 @@
|
|
|
1
|
-
import{api as h}from"../api.js";import{keepTableIconsAlive as _}from"../lib/table-icons.js";function O(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}let y=null,g=null,u=[],H=1,L=[],k={},b=[];const te=50,P={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"};function U(e,r){const a=e.data?.[r.field],o=String(a??"");switch(r.op){case"eq":return o===String(r.value);case"ne":return o!==String(r.value);case"contains":return o.toLowerCase().includes(String(r.value).toLowerCase());case"gt":case"lt":{const c=parseFloat(o),t=parseFloat(r.value);if(!Number.isNaN(c)&&!Number.isNaN(t))return r.op==="gt"?c>t:c<t;const n=Date.parse(o),i=Date.parse(r.value);return!Number.isNaN(n)&&!Number.isNaN(i)?r.op==="gt"?n>i:n<i:r.op==="gt"?o>String(r.value):o<String(r.value)}default:return!0}}function x(e){const r=(e.find("#entry-search").get(0)?.value||"").toLowerCase().trim();let a=u;for(const o of b)a=a.filter(c=>U(c,o));r&&(a=a.filter(o=>Object.values(o.data||{}).some(c=>String(c).toLowerCase().includes(r)))),W(e),B(a,e)}function W(e){const r=e.find("#entry-filter-chips").get(0);if(!r)return;if(r.replaceChildren(),!b.length){r.style.display="none";return}r.style.display="flex",b.forEach((o,c)=>{const t=document.createElement("span");t.className="badge badge-secondary",t.style.cssText="display:inline-flex;align-items:center;gap:.35rem;font-size:.78rem;";const n=String(o.value);t.textContent=`${o.label} ${P[o.op]||o.op} \u201C${n.length>24?n.slice(0,24)+"\u2026":n}\u201D`;const i=document.createElement("button");i.type="button",i.textContent="\xD7",i.title="Remove filter",i.style.cssText="background:none;border:none;cursor:pointer;color:inherit;font-size:.9rem;line-height:1;padding:0;",i.addEventListener("click",()=>{b.splice(c,1),x(e)}),t.appendChild(i),r.appendChild(t)});const a=document.createElement("button");a.type="button",a.className="btn btn-ghost btn-sm",a.textContent="Clear all",a.addEventListener("click",()=>{b=[],x(e)}),r.appendChild(a)}let v=null;const J="position:fixed;z-index:10000;min-width:200px;max-height:60vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",G="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;background:none;border:none;color:inherit;font:inherit;padding:.35rem .6rem;border-radius:4px;cursor:pointer;";function C(){v&&(v.remove(),v=null,document.removeEventListener("mousedown",A,!0),document.removeEventListener("keydown",j,!0),window.removeEventListener("scroll",z,!0))}const A=e=>{v&&!v.contains(e.target)&&C()},j=e=>{e.key==="Escape"&&C()},z=e=>{v&&!v.contains(e.target)&&C()};function N(e,r){const a=document.createElement("button");return a.type="button",a.style.cssText=G,a.textContent=e,a.addEventListener("mouseenter",()=>{a.style.background="var(--dm-surface-2,rgba(255,255,255,.06))"}),a.addEventListener("mouseleave",()=>{a.style.background="none"}),a.addEventListener("click",r),a}function V(e){const r=document.createElement("div");return r.style.cssText="padding:.3rem .6rem;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted,#9a9aac);",r.textContent=e,r}function K(e,r,a,o,c){C();const t=document.createElement("div");t.style.cssText=J,v=t;const n=a.label||a.name,i=String(o).length>28?String(o).slice(0,28)+"\u2026":String(o),l=(m,f)=>{b.push({field:a.name,label:n,op:m,value:f}),x(c),C()};t.appendChild(V(n)),o!==""&&(t.appendChild(N(`Filter: = \u201C${i}\u201D`,()=>l("eq",o))),t.appendChild(N(`Filter: \u2260 \u201C${i}\u201D`,()=>l("ne",o))),["number","date","time"].includes(a.type)&&(t.appendChild(N(`Filter: > \u201C${i}\u201D`,()=>l("gt",o))),t.appendChild(N(`Filter: < \u201C${i}\u201D`,()=>l("lt",o)))));const d=document.createElement("div");d.style.cssText="padding:.35rem .6rem;";const s=document.createElement("input");if(s.type="text",s.placeholder=`${n} contains\u2026`,s.className="form-input",s.style.cssText="width:100%;font-size:.8rem;padding:.3rem .5rem;",s.addEventListener("mousedown",m=>m.stopPropagation()),s.addEventListener("keydown",m=>{m.key==="Enter"&&s.value.trim()&&l("contains",s.value.trim())}),d.appendChild(s),t.appendChild(d),b.length){const m=document.createElement("div");m.style.cssText="border-top:1px solid var(--dm-border,#2a2a40);margin:.25rem 0;",t.appendChild(m),t.appendChild(N(`Clear all filters (${b.length})`,()=>{b=[],x(c),C()}))}document.body.appendChild(t);const p=t.getBoundingClientRect();t.style.left=Math.min(e,window.innerWidth-p.width-8)+"px",t.style.top=Math.min(r,window.innerHeight-p.height-8)+"px",document.addEventListener("mousedown",A,!0),document.addEventListener("keydown",j,!0),window.addEventListener("scroll",z,!0),setTimeout(()=>s.focus(),0)}export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(e){u=[],H=1,g=null,L=[],k={},b=[],C();const a=window.location.hash.match(/\/collections\/([^/?#]+)\/entries/);if(y=a?a[1]:null,!y){E.toast("No collection selected.",{type:"error"});return}const o=E.loader(e.get(0),{type:"dots"});try{if(g=await h.collections.get(y),!g){o.destroy(),E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const t=e.find("#entries-title").get(0);t&&(t.textContent=g.title+" - Entries");const n=e.find("#edit-schema-btn").get(0);n&&n.setAttribute("href",`#/collections/edit/${y}`);const i=e.find("#form-btn").get(0);i&&(new Set(["roles","user-profiles"]).has(y)?i.style.display="none":i.setAttribute("href",`#/forms/edit/${y}`))}catch{o.destroy(),E.toast("Failed to load collection schema.",{type:"error"});return}try{L=await h.actions.forCollection(y)}catch{L=[]}try{await M(e)}finally{o.destroy()}await I(e);const c=e.find("#entry-search").get(0);c&&c.addEventListener("input",()=>x(e)),e.get(0).addEventListener("contextmenu",t=>{const n=t.target.closest("#entries-table tbody td");if(!n)return;const l=(g?.fields||[]).slice(0,5)[n.cellIndex];if(!l)return;t.preventDefault();const d=n.querySelector("span[title]")?.title??n.textContent.trim();K(t.clientX,t.clientY,l,d,e)}),e.find("#add-entry-btn").off("click").on("click",()=>{q(null,e)}),e.find("#export-btn").off("click").on("click",()=>{const t=E.slideover({title:"Export Entries",size:"sm",position:"right"}),n=document.createElement("div");n.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;",["json","csv"].forEach(i=>{const l=document.createElement("button");l.className="btn btn-ghost",l.style.cssText="justify-content:flex-start;gap:.5rem;",l.textContent=i==="json"?"Export as JSON":"Export as CSV",l.addEventListener("click",()=>{fetch(`/api/collections/${y}/export?format=${i}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}}).then(d=>d.blob()).then(d=>{const s=document.createElement("a");s.href=URL.createObjectURL(d),s.download=`${y}-entries.${i}`,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(s.href),t.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),n.appendChild(l)}),t.setContent(n),t.open()}),e.find("#import-btn").off("click").on("click",()=>{Q(e)}),e.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete ALL entries? This cannot be undone."))try{await h.collections.clearEntries(y),u=[],E.toast("All entries cleared.",{type:"success"}),x(e)}catch{E.toast("Failed to clear entries.",{type:"error"})}}),e.off("click",".js-run-action").on("click",".js-run-action",async function(t){t.stopPropagation();const n=this,{actionSlug:i,entryId:l,confirmMessage:d}=n.dataset;if(!(d&&!await E.confirm(d))){n.disabled=!0;try{const s=await h.actions.execute(i,l);if(s.success)E.toast(`Action completed (${s.stepsCompleted} step${s.stepsCompleted!==1?"s":""}).`,{type:"success"}),await M(e);else{const p=s.results?.find(m=>!m.success);E.toast(p?.error||"Action failed.",{type:"error"})}}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{n.disabled=!1}}}),e.off("click",".js-edit-entry").on("click",".js-edit-entry",function(t){t.stopPropagation();const n=u.find(i=>i.id===this.dataset.id);n&&q(n,e)}),e.off("click",".js-delete-entry").on("click",".js-delete-entry",async function(t){t.stopPropagation();const n=this.dataset.id;if(await E.confirm("Delete this entry?"))try{await h.collections.deleteEntry(y,n),u=u.filter(l=>l.id!==n),E.toast("Entry deleted.",{type:"success"}),x(e)}catch{E.toast("Failed to delete entry.",{type:"error"})}}),e.off("click","#entries-table tbody tr").on("click","#entries-table tbody tr",function(t){if(t.target.closest(".js-run-action, .js-edit-entry, .js-delete-entry"))return;const n=this.querySelector(".js-edit-entry")?.dataset.id,i=u.find(l=>l.id===n);i&&Z(i)}),Domma.icons.scan(),_(e.get(0))}};async function I(e){const r=L.filter(o=>o.access?.rowLevel);if(r.length===0)return;const a=u.map(o=>o.id);a.length!==0&&(await Promise.all(r.map(async o=>{try{const{allowed:c}=await h.actions.checkAccess(o.slug,a);k[o.slug]=new Set(c)}catch{k[o.slug]=null}})),x(e))}async function M(e){try{const r=await h.collections.listEntries(y,{limit:500});u=r.entries||r||[]}catch{u=[],E.toast("Could not load entries.",{type:"error"})}x(e),await I(e)}function B(e,r){const a=r.find("#entry-count").get(0);a&&(a.textContent=e.length===u.length?`${u.length} entr${u.length!==1?"ies":"y"}`:`Showing ${e.length} of ${u.length}`);const c=[...(g?.fields||[]).slice(0,5).map(n=>({key:`data.${n.name}`,title:n.label||n.name,render:(i,l)=>{const d=l.data?.[n.name]??"",s=String(d),p=document.createElement("span");return p.title=s,p.textContent=s.length>60?s.slice(0,60)+"\u2026":s,p.outerHTML}})),{key:"meta",title:"Created",render:n=>D(n?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:n=>{const i=document.createElement("div");i.style.cssText="display:flex;gap:.3rem;justify-content:flex-end;flex-wrap:wrap;",L.forEach(s=>{if(k[s.slug]!==void 0&&k[s.slug]!==null&&!k[s.slug].has(n))return;const p=document.createElement("button");p.className="btn btn-sm btn-ghost js-run-action",p.dataset.actionSlug=s.slug,p.dataset.entryId=n,p.dataset.confirmMessage=s.trigger?.confirmMessage||"",p.title=s.title||s.slug;const m=document.createElement("span");m.setAttribute("data-icon",O(s.trigger?.icon||"zap")),m.setAttribute("data-icon-size","14"),p.appendChild(m),p.appendChild(document.createTextNode(" "+(s.trigger?.label||s.title))),i.appendChild(p)});const l=document.createElement("button");l.className="btn btn-sm btn-ghost js-edit-entry",l.dataset.id=n,l.setAttribute("data-tooltip","Edit"),l.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const d=document.createElement("button");return d.className="btn btn-sm btn-ghost js-delete-entry",d.dataset.id=n,d.setAttribute("data-tooltip","Delete"),d.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',i.appendChild(l),i.appendChild(d),i.outerHTML}}];T.create("#entries-table",{pageSize:25,data:e,columns:c,emptyMessage:'No entries yet. Click "Add Entry" to get started.'});const t=document.querySelector("#entries-table");t&&(t.querySelectorAll("tbody tr").forEach(n=>{n.style.cursor="pointer"}),Domma.icons.scan(t),t.querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})}))}function X(e,r){const a={};return e.forEach(o=>{const c={};o.fullWidth?c.span=r:o.span>1&&(c.span=o.span),o.placeholder&&(c.placeholder=o.placeholder),o.helper&&(c.hint=o.helper);const t=o.type==="checkbox"?"boolean":o.type==="date"?"string":o.type;a[o.name]={type:t,label:o.label,required:o.required,options:o.options,...Object.keys(c).length?{formConfig:c}:{}}}),a}function q(e,r){const a=!!e,o=E.slideover({title:a?"Edit Entry":"Add Entry",size:"md",position:"right"}),c=document.createElement("div");c.style.cssText="padding:.25rem 0 .5rem;";const t=g?.fields||[],n=g?.columns||2,i=g?.layout||"stacked",l=document.createElement("div");F.create(X(t,n),e?.data||{},{layout:i,columns:n,showSubmitButton:!1}).renderTo(l),(t||[]).forEach(m=>{if(m.type!=="date"||!m.name)return;const f=l.querySelector(`[name="${m.name}"]`);f&&f.type!=="date"&&(f.type="date")}),c.appendChild(l);const d=document.createElement("div");d.style.cssText="display:flex;justify-content:flex-end;gap:1rem;margin-top:.5rem;";const s=document.createElement("button");s.className="btn btn-ghost",s.textContent="Cancel";const p=document.createElement("button");p.className="btn btn-primary",p.textContent=a?"Save Changes":"Add Entry",d.appendChild(s),d.appendChild(p),c.appendChild(d),o.element.appendChild(c),o.open(),s.addEventListener("click",()=>o.close()),p.addEventListener("click",async()=>{const m={};t.forEach(f=>{const w=l.querySelector(`[name="${f.name}"]`);w&&(m[f.name]=f.type==="checkbox"?w.checked:w.value)}),p.disabled=!0;try{if(a){const f=await h.collections.updateEntry(y,e.id,m),w=u.findIndex(Y=>Y.id===e.id);w!==-1&&(u[w]=f),E.toast("Entry updated.",{type:"success"})}else{const f=await h.collections.createEntry(y,m);u.unshift(f),E.toast("Entry added.",{type:"success"})}o.close(),r?x(r):B(u,{find:()=>({get:()=>null})})}catch(f){E.toast(f.message||"Failed to save entry.",{type:"error"}),p.disabled=!1}})}function Z(e){const r=document.createElement("div"),a=g?.fields||[],o=document.createElement("div");o.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",a.forEach(n=>{const i=e.data?.[n.name];if(i==null||i==="")return;const l=document.createElement("div");l.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const d=document.createElement("strong");d.textContent=n.label||n.name,d.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const s=document.createElement("p");s.textContent=String(i),s.style.cssText="margin:0;word-break:break-word;",l.appendChild(d),l.appendChild(s),o.appendChild(l)});const c=document.createElement("div");if(c.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",e.meta?.createdAt){const n=document.createElement("span");n.textContent=`Created: ${D(e.meta.createdAt).format("DD MMM YYYY HH:mm")}`,c.appendChild(n)}if(e.meta?.source){const n=document.createElement("span");n.textContent=`Source: ${e.meta.source}`,c.appendChild(n)}r.appendChild(o),r.appendChild(c);const t=E.slideover({title:"Entry Details",size:"md",position:"right"});t.element.appendChild(r),t.open()}function Q(e){const r=E.slideover({title:"Import Entries",size:"md",position:"right"}),a=document.createElement("div");a.style.cssText="padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;";const o=document.createElement("p");o.textContent='Paste a JSON array of entries. Each item should have a "data" object with field values.',o.style.cssText="font-size:.875rem;color:var(--dm-text-muted,#888);margin:0;",a.appendChild(o);const c=document.createElement("textarea");c.className="form-input",c.rows=10,c.placeholder='[{"data": {"name": "Example"}}, ...]',a.appendChild(c);const t=document.createElement("p");t.style.cssText="font-size:.875rem;margin:0;display:none;",a.appendChild(t);const n=document.createElement("div");n.style.cssText="display:flex;justify-content:flex-end;gap:1rem;";const i=document.createElement("button");i.className="btn btn-ghost",i.textContent="Cancel";const l=document.createElement("button");l.className="btn btn-primary",l.textContent="Import",n.appendChild(i),n.appendChild(l),a.appendChild(n),r.element.appendChild(a),r.open(),i.addEventListener("click",()=>r.close()),l.addEventListener("click",async()=>{let d;try{if(d=JSON.parse(c.value),!Array.isArray(d))throw new Error("Must be a JSON array")}catch(s){t.style.display="",t.style.color="var(--dm-danger,#f87171)",t.textContent=`Invalid JSON: ${s.message}`;return}l.disabled=!0;try{const s=await h.collections.import(y,d);t.style.display="",t.style.color="var(--dm-success,#4ade80)",t.textContent=`Imported ${s.imported} entries. Skipped: ${s.skipped}.`,await M(e||{find:()=>({get:()=>null,off:()=>({on:()=>{}})})}),setTimeout(()=>r.close(),1500)}catch(s){t.style.display="",t.style.color="var(--dm-danger,#f87171)",t.textContent=s.message||"Import failed."}finally{l.disabled=!1}})}
|
|
1
|
+
import{api as g,apiRequest as lt}from"../api.js";import{addBannerAction as U}from"../lib/plugin-chrome.js";import{helpPopovers as Ne}from"../lib/help-popover.js";import{onceClosed as rt,openMenuAt as Ie,sidebarCount as z,viewLifetime as it}from"../lib/tool-kit.js";import{MAX_COLUMNS as dt,PAGE_SIZE as se,SHOW_STEP as W,arrangeEntries as ut,canClear as pt,cellText as He,countText as mt,draftable as ft,filterChoices as ht,filterText as yt,hasForm as vt,snapshot as Oe,valueText as Re}from"../lib/collection-entries-arrange.js";const Ue=/^#\/collections\/[^/?#]+\/entries\/?(\?.*)?$/,j=m=>`collection-entries.draft.${m}`,N=m=>String(m??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function oe(m){const h=document.createElement("template");return h.innerHTML=m.trim(),h.content.firstElementChild}const bt='<span class="cex-chip">{{text}}<button type="button" data-idx="{{idx}}" title="Remove this filter" aria-label="Remove filter {{text}}">\xD7</button></span>';function gt(m){return`
|
|
2
|
+
<tr class="cex-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
|
|
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:m},(v,a)=>`<td data-col="${a}" title="{{t${a}}}">{{c${a}}}</td>`).join("")}
|
|
5
|
+
<td class="cex-when" title="{{whenFull}}">{{when}}</td>
|
|
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 Et(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 i=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;v[a.name]={type:i,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 i=u[0];i.type==="radio"?v[a.name]=u.find(p=>p.checked)?.value??"":i.type==="checkbox"&&u.length>1?v[a.name]=u.filter(p=>p.checked).map(p=>p.value):i.type==="checkbox"?v[a.name]=i.checked:i.tagName==="SELECT"&&i.multiple?v[a.name]=[...i.selectedOptions].map(p=>p.value):v[a.name]=i.value}return v}function ze(m,h,v){for(const a of h){if(!(a.name in v))continue;const u=v[a.name],i=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!i.length)continue;const p=i[0];p.type==="radio"?i.forEach(c=>{c.checked=c.value===String(u)}):p.type==="checkbox"&&i.length>1?i.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??"",i.forEach(c=>c.dispatchEvent(new Event("input",{bubbles:!0})))}}const je=it(Ue);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(m){je.reset();const h=m.get(0).querySelector(".cex")||m.get(0),v=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=v?decodeURIComponent(v[1]):null,u=()=>Ue.test(location.hash)&&h.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),i=e=>(je.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),X=M.observable(!0),B=M.observable(""),Fe=M.computed(()=>p.value!==null),le=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),y=M.observable([]),q=M.observable(W),P=M.observable([]),Q=M.observable({}),A=M.observable(!1);let ee=1;const re=()=>p.peek()?.fields||[],ie=M.computed(()=>(p.value?.fields||[]).slice(0,dt)),_=e=>c.peek().find(t=>t.id===e),he=e=>P.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),T=M.observable([]),ye=M.observable([]);i(M.effect(()=>{const e=ut(c.value,{filters:w.value,query:le.value,sort:Z.value});T.value=e})),i(M.effect(()=>{const e=ie.value,t=new Set(y.value);Q.value,P.value,ye.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((r,d)=>{s[`c${d}`]=He(n.data?.[r.name]),s[`t${d}`]=Re(n.data?.[r.name])});const o=n.meta?.createdAt;s.when=o?D(o).format("D MMM YYYY"):"",s.whenFull=o?`Created ${D(o).format("D MMM YYYY HH:mm")}${n.meta?.updatedAt?` \xB7 changed ${D(n.meta.updatedAt).format("D MMM YYYY HH:mm")}`:""}`:"";const l=he(n.id).length;return s.runs=l?String(l):"",s})}));const Be=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:yt(e)}))),Pe=M.computed(()=>y.value.length),_e={schema:p,loaded:c,loading:X,loadError:B,ready:Fe,query:le,rows:ye,chips:Be,busy:A,selectedCount:Pe,countLine:M.computed(()=>mt(T.value.length,c.value.length,x.value)),selectedText:M.computed(()=>`${y.value.length} selected`),canClear:M.computed(()=>pt(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(W,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 J({reset:e=!0}={}){X.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=>!_(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)),B.value="",await Ve()}catch(t){B.value=`Could not load the entries: ${t.message||t}`}finally{X.value=!1}}async function Je(){ee+=1,await J({reset:!1})}async function Ke(){try{if((await lt("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await g.actions.forCollection(a);P.value=Array.isArray(t)?t:[]}catch{P.value=[]}}async function Ve(){const e=P.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=re()[0];return(t?He(e?.data?.[t.name],40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function ve(e){if(await E.confirm(`Delete \u201C${N(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),z("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function Ge(){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=[],z("#/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 J()}async function We(){const e=p.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${N(e?.title||a)}\u201D? This cannot be undone.`)){A.value=!0;try{await g.collections.clearEntries(a),c.value=[],x.value=0,y.value=[],z("#/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 Xe(e){try{const t=await g.collections.createEntry(a,{...e.data||{}});c.value=[t,...c.peek()],x.value=x.peek()+1,z("#/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 Ze(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(N(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 J()}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 be(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 ge(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],q.value=W}const ue=new Set;let Y=null,Ee=!1;function ke(e,t,n){const s=e.close.bind(e);let o=!1,l=!1;e.close=async r=>{if(!o&&r!==!0&&!Ee&&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 H(e,t=null){const n=p.peek(),s=re(),o=!!e,l=E.slideover({title:o?`Edit ${N(K(e))}`:"New entry",size:"md",position:"right"}),r=oe(`
|
|
8
|
+
<div class="cex-form">
|
|
9
|
+
<div class="cex-fields"></div>
|
|
10
|
+
<small class="cex-hint cex-when-line"></small>
|
|
11
|
+
<div class="cex-actions">
|
|
12
|
+
<span class="cex-dirty" hidden>Unsaved</span>
|
|
13
|
+
<small class="cex-hint">Ctrl+Enter saves.</small>
|
|
14
|
+
<span class="cex-spacer"></span>
|
|
15
|
+
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
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
|
+
</div>
|
|
18
|
+
</div>`);r.querySelector(".cex-save-text").textContent=o?"Save":"Add entry";const d=r.querySelector(".cex-fields");F.create(Et(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 O=d.querySelector(`[name="${CSS.escape(k.name)}"]`);O&&O.type!=="date"&&(O.type="date")}),e?.data&&ze(d,s,e.data),o&&e.meta?.createdAt&&(r.querySelector(".cex-when-line").textContent=`Created ${D(e.meta.createdAt).format("D MMM YYYY HH:mm")}${e.meta.updatedAt?`, last changed ${D(e.meta.updatedAt).format("D MMM YYYY HH:mm")}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),l.element.appendChild(r);const f=Oe(ce(d,s));t&&ze(d,s,t);const b=()=>Oe(ce(d,s))!==f,ct=r.querySelector(".cex-dirty"),me=()=>{ct.hidden=!b()};d.addEventListener("input",me),d.addEventListener("change",me),me();let fe=!1;async function De(){if(fe)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const k=ce(d,s);fe=!0;const O=r.querySelector('[data-act="save"]');O.disabled=!0;try{if(o){const G=await g.collections.updateEntry(a,e.id,k);c.value=c.peek().map(Ye=>Ye.id===e.id?G:Ye),E.toast("Entry saved.",{type:"success"})}else{const G=await g.collections.createEntry(a,k);c.value=[G,...c.peek()],x.value=x.peek()+1,z("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(j(a)):S.set(j(a),null)}catch{}l.close(!0)}catch(G){E.toast(G.message||"Not saved.",{type:"error"})}finally{fe=!1,O.disabled=!1}}r.querySelector('[data-act="save"]').addEventListener("click",De),r.querySelector('[data-act="cancel"]').addEventListener("click",()=>l.close()),r.addEventListener("keydown",k=>{(k.ctrlKey||k.metaKey)&&(k.key==="Enter"||k.key==="s")&&(k.preventDefault(),De())});const Te={id:e?.id??null,dirty:b,values:()=>ft(ce(d,s),s)};Y=Te,te(l,()=>{Y===Te&&(Y=null)}),ke(l,b,"Discard the changes you have not saved?"),I.scan(r),l.open(),d.querySelector("input, select, textarea")?.focus()}function Qe(e){const t=E.slideover({title:N(K(e)),size:"md",position:"right"}),n=oe(`
|
|
19
|
+
<div class="cex-form">
|
|
20
|
+
<dl class="cex-detail"></dl>
|
|
21
|
+
<div class="cex-meta"></div>
|
|
22
|
+
<div class="cex-actions">
|
|
23
|
+
<button type="button" class="btn btn-sm btn-ghost" data-act="copy"><span data-icon="copy" data-icon-size="13"></span> Copy as JSON</button>
|
|
24
|
+
<span class="cex-spacer"></span>
|
|
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
|
+
</div>
|
|
27
|
+
</div>`),s=n.querySelector(".cex-detail");for(const r of re()){const d=Re(e.data?.[r.name]),f=document.createElement("dt");f.textContent=r.label||r.name;const b=document.createElement("dd");b.textContent=d===""?"-":d,s.append(f,b)}const o=n.querySelector(".cex-meta"),l=r=>{const d=document.createElement("span");d.textContent=r,o.appendChild(d)};l(`Id: ${e.id}`),e.meta?.createdAt&&l(`Created: ${D(e.meta.createdAt).format("D MMM YYYY HH:mm")}`),e.meta?.updatedAt&&l(`Changed: ${D(e.meta.updatedAt).format("D MMM YYYY HH:mm")}`),e.meta?.source&&l(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>be(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),H(e)}),t.element.appendChild(n),te(t),I.scan(n),t.open()}function et(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=oe(`
|
|
28
|
+
<div class="cex-form">
|
|
29
|
+
<p class="cex-hint">A JSON array of entries, each with a "data" object of field values - what Export gives you.
|
|
30
|
+
<span data-help="Entries keep the ids and dates they had only when you export and import with Data Transfer; here each one is added as new. Fields the collection does not have are ignored."></span></p>
|
|
31
|
+
<div class="cex-actions" style="border:0;padding:0">
|
|
32
|
+
<button type="button" class="btn btn-sm btn-ghost" data-act="file"><span data-icon="upload" data-icon-size="13"></span> Choose a file\u2026</button>
|
|
33
|
+
<input type="file" accept=".json,application/json" hidden>
|
|
34
|
+
</div>
|
|
35
|
+
<textarea class="form-input cex-import" spellcheck="false" placeholder='[{"data": {"name": "Example"}}]' aria-label="Entries as JSON"></textarea>
|
|
36
|
+
<small class="cex-err" hidden></small>
|
|
37
|
+
<small class="cex-ok" hidden></small>
|
|
38
|
+
<div class="cex-actions">
|
|
39
|
+
<span class="cex-spacer"></span>
|
|
40
|
+
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
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
|
+
</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 r=!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,r=!0,z("#/collections"),u()&&await J(),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),ke(e,()=>!r&&n.value.trim()!=="","Discard the entries you pasted?"),Ne(t),I.scan(t),e.open(),n.focus()}function tt(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=oe(`
|
|
44
|
+
<div class="cex-form">
|
|
45
|
+
<p class="cex-hint">Every entry in the collection - not only the ones shown.</p>
|
|
46
|
+
<div class="cex-export">
|
|
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
|
+
<button type="button" class="btn btn-ghost" data-fmt="csv"><span data-icon="download" data-icon-size="14"></span> Export as CSV</button>
|
|
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(),r=document.createElement("a");r.href=URL.createObjectURL(l),r.download=`${a}-entries.${s}`,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(r.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 nt(e,t){const n=_(e.dataset.id);if(!n)return[];const s=t?.event?.target?.closest?.("td[data-col]"),o=s?ie.peek()[Number(s.dataset.col)]:null,l=he(n.id),r=y.peek().includes(n.id),d=o?ht(o,n.data?.[o.name]):[];return[{header:K(n)},{label:"Edit\u2026",icon:"edit",action:()=>H(n)},{label:"See all of it\u2026",icon:"eye",action:()=>Qe(n)},{label:"Duplicate",icon:"copy",action:()=>Xe(n)},{label:"Copy id",icon:"hash",action:()=>be(n.id)},{label:r?"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:()=>Ze(f,n)}))]:[],...o?[{type:"divider"},{label:`Filter by ${o.label||o.name}`,icon:"filter",submenu:[...d.map(f=>({label:f.label,action:()=>ge(o,f.op,f.value)})),{label:`${o.label||o.name} contains\u2026`,action:async()=>{const f=await E.prompt(`Show entries whose ${N(o.label||o.name)} contains`,{inputPlaceholder:"text"});f&&String(f).trim()&&ge(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:()=>ve(n)}]}const L=h.querySelector(".cex-rows"),V=h.querySelector(".cex-head"),pe=h.querySelector(".cex-chips-list");pe.innerHTML=bt;const at=E.loader(h,{type:"dots"});try{p.value=await g.collections.get(a)}catch(e){B.value=`Could not load the collection: ${e.message||e}`}finally{at.destroy()}if(!u())return;if(!p.peek()&&!B.peek()){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const xe=ie.peek();L.innerHTML=gt(xe.length),V.replaceChildren();const ne=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),V.appendChild(n),n},$=document.createElement("input");$.type="checkbox",$.title="Select every entry shown",$.setAttribute("aria-label","Select every entry shown"),ne("cex-check",$);const we=(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};xe.forEach(e=>ne("",we(e.name,e.label||e.name))),ne("cex-when",we("created","Created")),ne("cex-tools"),i(M.effect(()=>{const e=Z.value;V.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")})})),i(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 Se=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"}};V.addEventListener("click",Se),$.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))}),i(()=>V.removeEventListener("click",Se));const st=M.applyBindings(_e,h,{methods:{clearSelection:()=>{y.value=[]},deleteSelected:()=>Ge(),clearAll:()=>We(),clearFilters:()=>{w.value=[]},showMore:()=>{T.peek().length>q.peek()?q.value=q.peek()+W:Je()}}});i(()=>st.dispose()),i(Ne(h,{position:"bottom"})),i(M.effect(()=>{le.value,w.value,q.value=W}));const Me=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",Me),i(()=>pe.removeEventListener("click",Me));const ot=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>nt(e,t)});i(()=>ot?.destroy?.());const Ce=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){Ie(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=_(t.dataset.id);n&&H(n)},$e=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&de(t.dataset.id)},Ae=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=_(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Ie(t)):e.key==="Enter"?(e.preventDefault(),H(n)):e.key===" "?(e.preventDefault(),de(n.id)):e.key==="Delete"&&(e.preventDefault(),ve(n)))};L.addEventListener("click",Ce),L.addEventListener("change",$e),L.addEventListener("keydown",Ae),i(()=>{L.removeEventListener("click",Ce),L.removeEventListener("change",$e),L.removeEventListener("keydown",Ae)});const Le=p.peek(),ae=m.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(ae&&Le?.title){for(;ae.nextSibling;)ae.nextSibling.remove();ae.after(document.createTextNode(Le.title))}U(m,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),U(m,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),vt(a)&&U(m,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),U(m,{id:"entries-export",icon:"download",title:"Export",onClick:()=>tt()}),U(m,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>et()}),U(m,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>H(null)});const qe=e=>{Y?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",qe),i(()=>window.removeEventListener("beforeunload",qe)),i(()=>{if(Y?.dirty())try{S.set(j(a),{id:Y.id,values:Y.values(),at:new Date().toISOString()})}catch{}Ee=!0,[...ue].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(h),!p.peek()){X.value=!1;return}if(await Ke(),!u()||(await J(),!u()))return;let C=null;try{C=S.get(j(a))}catch{}if(C?.values){try{S.remove?S.remove(j(a)):S.set(j(a),null)}catch{}let e=C.id?_(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${N(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()&&H(e,C.values)}}};
|
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<
|
|
3
|
-
</template>
|
|
4
|
-
<props>
|
|
5
|
-
{
|
|
6
|
-
"name": { "type": "string", "default": "world", "label": "Name" }
|
|
7
|
-
}
|
|
8
|
-
</props>
|
|
9
|
-
<script>
|
|
10
|
-
export default {
|
|
11
|
-
data() { return {}; }
|
|
12
|
-
};
|
|
13
|
-
<\/script>
|
|
14
|
-
<style>
|
|
15
|
-
div { font-family: system-ui, sans-serif; }
|
|
16
|
-
</style>
|
|
17
|
-
`;function L(e){const n=e.find(".component-tab").get()||[],o=e.find(".component-tab-pane").get()||[];for(const t of n)t.addEventListener("click",()=>{for(const s of n)s.classList.toggle("active",s===t);for(const s of o)s.hidden=s.dataset.pane!==t.dataset.tab})}function I(e){const n=o=>{const t=e.match(new RegExp(`<${o}>([\\s\\S]*?)</${o}>`));return t?t[1].replace(/^\n/,"").replace(/\n$/,""):""};return{template:n("template"),props:n("props"),script:n("script"),style:n("style")}}function M(e){return[`<template>
|
|
18
|
-
${e.template}
|
|
19
|
-
</template>`,`<props>
|
|
20
|
-
${e.props}
|
|
21
|
-
</props>`,`<script>
|
|
22
|
-
${e.script}
|
|
23
|
-
<\/script>`,e.style.trim()?`<style>
|
|
24
|
-
${e.style}
|
|
25
|
-
</style>`:""].filter(Boolean).join(`
|
|
26
|
-
`)+`
|
|
27
|
-
`}function w(e,n){const o=I(n);c.template?.setValue(o.template),c.props?.setValue(o.props),c.script?.setValue(o.script),c.style?.setValue(o.style)}function N(e){return M({template:c.template?.getValue()??"",props:c.props?.getValue()??"",script:c.script?.getValue()??"",style:c.style?.getValue()??""})}const O=["template","props","script","style"];function A(e){const n=()=>{b&&clearTimeout(b),b=setTimeout(()=>h(e),300)};for(const o of O){const t=e.find(`#component-src-${o}`).get(0);t&&(c[o]=S(t,{initialValue:"",onChange:n}))}}async function h(e){const n=(e.find("#component-name").val()||"").trim()||"preview",o=N(e);g(e,"compiling");let t;try{t=await u.components.compile(n,o)}catch(s){g(e,"error",s.message||"Compile request failed.");return}if(t.errors?.length){const s=t.errors.map(r=>`[${r.type}] ${r.message}`).join(`
|
|
28
|
-
`);g(e,"error",s),f=null;return}f=t.js,g(e,"ok"),P(e),B(e,{compiledJs:f,tagName:`dm-${n}`,props:l})}let y=null;function B(e,n){const o=e.find("#component-preview-iframe").get(0);if(!o)return;if(!o.dataset.dmMounted){o.dataset.dmMounted="true",C(e,{type:"mount",payload:n});return}y=n;const t=()=>{o.removeEventListener("load",t),y&&(C(e,{type:"mount",payload:y}),y=null)};o.addEventListener("load",t),o.src=o.src}function g(e,n,o=""){const t=e.find("#component-compile-indicator").get(0),s=e.find("#component-compile-status").get(0),r=e.find("#component-compile-errors").get(0),a=e.find("#save-component-btn").get(0);n==="compiling"?(t.style.background="var(--dm-text-muted)",s.textContent="Compiling\u2026",r.hidden=!0,a&&(a.disabled=!0)):n==="ok"?(t.style.background="var(--dm-success)",s.textContent="Compile OK",r.hidden=!0,a&&(a.disabled=!1)):(t.style.background="var(--dm-danger)",s.textContent="Compile failed - see below",r.hidden=!1,r.textContent=o,a&&(a.disabled=!0))}function j(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function k(e,n,o=!1){j(e);const t=document.createElement("p");if(t.className="text-muted",t.style.cssText="font-size:.8rem;margin:0;",o){t.appendChild(document.createTextNode("Invalid "));const s=document.createElement("code");s.textContent="<props>",t.appendChild(s),t.appendChild(document.createTextNode(" JSON - fix the tab above to see the props panel."))}else t.textContent=n;e.appendChild(t)}function P(e){const n=e.find("#component-preview-props-panel").get(0);if(!n)return;let o={};try{o=JSON.parse(c.props?.getValue()||"{}")}catch{k(n,"",!0);return}const t={};j(n);const s=Object.entries(o);if(s.length===0){k(n,"No props declared."),l={};return}for(const[r,a]of s){const i=document.createElement("div");i.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;";const m=document.createElement("label");m.style.cssText="font-size:.75rem;color:var(--dm-text-muted,#aaa);min-width:120px;",m.textContent=a.label||r,i.appendChild(m);const d=F(a,r,()=>{l[r]=J(d,a),C(e,{type:"update",payload:{props:l}})}),x=r in l?l[r]:a.default!==void 0?a.default:_(a.type);U(d,a,x),t[r]=x,i.appendChild(d),n.appendChild(i)}l=t}function F(e,n,o){let t;return e.type==="number"?(t=document.createElement("input"),t.type="number",t.className="form-input form-input--sm"):e.type==="boolean"?(t=document.createElement("input"),t.type="checkbox",t.className="form-check"):e.type==="array"||e.type==="object"?(t=document.createElement("textarea"),t.rows=2,t.className="form-input form-input--sm",t.style.fontFamily="var(--dm-font-mono, monospace)"):(t=document.createElement("input"),t.type="text",t.className="form-input form-input--sm"),t.style.flex="1",t.dataset.propName=n,t.addEventListener("input",o),t.addEventListener("change",o),t}function J(e,n){if(n.type==="number")return e.value===""?null:Number(e.value);if(n.type==="boolean")return!!e.checked;if(n.type==="array"||n.type==="object")try{return JSON.parse(e.value||"null")}catch{return null}return e.value}function U(e,n,o){n.type==="boolean"?e.checked=!!o:n.type==="array"||n.type==="object"?e.value=JSON.stringify(o??null):e.value=o??""}function _(e){return e==="number"?0:e==="boolean"?!1:e==="array"?[]:e==="object"?{}:""}function C(e,n){const o=e.find("#component-preview-iframe").get(0);o?.contentWindow&&o.contentWindow.postMessage(n,"*")}function z(e){const n=e.find("#save-component-btn").get(0),o=e.find("#export-component-btn").get(0);n&&n.addEventListener("click",()=>q(e)),o&&o.addEventListener("click",async()=>{if(p)try{await u.components.exportBundle(p)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}})}async function q(e){const n=(e.find("#component-name").val()||"").trim();if(!n){E.toast("Component name is required.",{type:"warning"});return}if(!/^[a-z][a-z0-9-]*$/.test(n)){E.toast("Name must start with a letter and contain only lowercase letters, digits, and hyphens.",{type:"warning"});return}const o=N(e),t=!!e.find("#component-bundled").is(":checked"),s={...v||{}};delete s.bundled;const r=(e.find("#component-project").val()||"").trim();r?s.project=r:delete s.project;const a=e.find("#save-component-btn").get(0);a&&(a.disabled=!0);try{await u.components.put(n,{source:o,bundled:t,meta:s}),E.toast(p?"Component updated.":"Component created.",{type:"success"}),p||(p=n,R.navigate(`/components/edit/${encodeURIComponent(n)}`))}catch(i){E.toast(i.message||"Failed to save component.",{type:"error"})}finally{a&&(a.disabled=!1)}}async function D(e){if(p)try{const n=await u.components.list(),o=e.find("#component-quick-switch").get(0);if(!o||!n?.length)return;const t=[...n].sort((s,r)=>s.name.localeCompare(r.name));for(const s of t){const r=document.createElement("option");r.value=s.name,r.textContent=s.name,s.name===p&&(r.selected=!0),o.appendChild(r)}o.style.display="",o.addEventListener("change",()=>{const s=o.value;s&&s!==p&&R.navigate(`/components/edit/${encodeURIComponent(s)}`)})}catch{}}
|
|
1
|
+
import{api as P}from"../api.js";import{createSimpleEditor as Ie}from"../lib/simple-editor.js";import{addBannerAction as Q}from"../lib/plugin-chrome.js";import{helpPopovers as Ue}from"../lib/help-popover.js";import{openMenuAt as xe,sidebarCount as Ke}from"../lib/tool-kit.js";import{PANES as J,STARTER_SOURCE as Be,assembleSource as Fe,attributeFor as Je,draftKey as Y,draftStatus as Ye,nameProblem as He,previewRows as Me,readValue as We,snapshot as ce,splitSource as _e,tagFor as ze,writeValue as $e}from"../lib/component-editor-arrange.js";const Ge=/^\/components\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,X=n=>String(n||"").split("?")[0],Ne=n=>String(n??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),H={get(n){try{return S.get(n)}catch{return null}},set(n,i){try{S.set(n,i)}catch{}},drop(n){try{S.remove?S.remove(n):S.set(n,null)}catch{}}};let h=null,je=!1;function Qe(){je||!window.R?.use||(je=!0,R.use((n,i,c)=>{const p=i?.path||"";return!(Ge.test(p)&&X(p)!==X(n?.path))||!h?.dirty()?c():E.confirm(h.question()).then(W=>{W?c():history.replaceState(null,"",`#${p}`)})}),M.subscribe("router:afterChange",({to:n}={})=>{if(h&&X(n?.path)!==h.path){const i=h;h=null,i.dispose()}}))}function Xe(n,i){const c=n.querySelector(".dm-plugin-banner-name");if(!c)return;const p=c.querySelector(".dm-plugin-banner-sep");if(p){for(;p.nextSibling;)p.nextSibling.remove();p.after(document.createTextNode(i))}else c.textContent=`Components \u203A ${i}`}export const componentEditorView={templateUrl:"/admin/js/templates/component-editor.html",async onMount(n){if(Qe(),h){const e=h;h=null,e.dispose()}const i=n.get(0),c=i.querySelector(".cex")||i,p=[],v=e=>(p.push(e),e);let W=!1;const x=()=>!W&&c.isConnected,le=location.hash.match(/^#\/components\/edit\/([^/?#]+)/);let a=le?decodeURIComponent(le[1]):null;const Te=X(location.hash.slice(1)),C=M.observable(!a),u=M.observable(!1),m=M.observable(a||""),d=M.observable(""),$=M.observable(!1),O=M.observable(0),j=M.observable(""),A=M.observable(!1),b=M.observable("compiling"),V=M.observable(""),Z=M.observable("template"),ie=M.observable([]),ee={value:{}},g={},I=()=>Fe(Object.fromEntries(J.map(e=>[e,g[e]?.getValue()??""]))),te=()=>({name:m.peek(),project:d.peek(),bundled:$.peek(),source:I()}),U=M.computed(()=>(O.value,u.value?!1:ce({name:m.value,project:d.value,bundled:$.value,source:I()})!==j.value)),oe=M.computed(()=>u.value?"":He(m.value,{isNew:C.value,taken:ie.value}));let K=null;v(()=>clearTimeout(K));const Le=()=>{O.value=O.peek()+1,clearTimeout(K),K=setTimeout(F,300)};for(const e of J){const t=c.querySelector(`#component-src-${e}`);t&&(g[e]=Ie(t,{initialValue:"",onChange:Le}))}v(()=>{for(const e of J)try{g[e]?.destroy?.()}catch{}});const _=e=>{const t=_e(e);for(const o of J)g[o]?.setValue(t[o]);O.value=O.peek()+1},B=c.querySelector("#component-project"),pe=E.loader(i,{type:"dots"});let T;try{const[e,t,o]=await Promise.all([a?P.components.get(a):Promise.resolve(null),P.projects.list().catch(()=>[]),P.components.list().catch(()=>[])]);T={comp:e,projects:t,list:o}}catch(e){pe.destroy(),E.toast(e.message||"Component not found.",{type:"error"}),x()&&(location.hash="#/components");return}if(pe.destroy(),!c.isConnected)return;for(const e of T.projects||[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,B?.appendChild(t)}ie.value=(T.list||[]).map(e=>e.name);const N=T.comp;if(N)u.value=N.readOnly===!0||N.origin==="plugin",ee.value=N.meta||{},d.value=N.meta?.project||"",$.value=!!N.bundled,_(N.source);else{const e=new URLSearchParams(location.hash.split("?")[1]||"").get("project");e&&(T.projects||[]).some(t=>t.slug===e)&&(d.value=e),_(Be)}if(j.value=ce(te()),u.peek())for(const e of J)g[e]?.textarea&&(g[e].textarea.readOnly=!0);Xe(i,u.peek()?`View source: ${a}`:a||"New component");const f=c.querySelector("#component-preview-iframe");let ne=null,y={},z=null,ae=0;const G=e=>{try{f?.contentWindow?.postMessage(e,"*")}catch{}};function Re(e){if(!f)return;if(!f.dataset.dmMounted){f.dataset.dmMounted="true",G({type:"mount",payload:e});return}z=e;const t=()=>{f.removeEventListener("load",t),z&&x()&&G({type:"mount",payload:z}),z=null};f.addEventListener("load",t),f.src=f.src}async function F(){const e=++ae,t=m.peek().trim()||"preview";b.value="compiling";let o;try{o=await P.components.compile(t,I())}catch(r){if(e!==ae||!x())return;b.value="error",V.value=r.message||"Compile request failed.";return}if(!(e!==ae||!x())){if(o.errors?.length){b.value="error",V.value=o.errors.map(r=>`[${r.type}] ${r.message}`).join(`
|
|
2
|
+
`),ne=null;return}ne=o.js,b.value="ok",V.value="",de(),Re({compiledJs:ne,tagName:`dm-${t}`,props:y})}}const w=c.querySelector("#component-preview-props-panel");let ue=[];function me(e){w.replaceChildren();const t=document.createElement("p");t.className="cex-note",t.textContent=e,w.appendChild(t)}function de(){const{rows:e,error:t}=Me(g.props?.getValue(),y);if(ue=e,t){me("The <props> block is not valid JSON - fix it to see the props here.");return}if(!e.length){y={},me("No props declared. Add them in the <props> tab.");return}y=Object.fromEntries(e.map(o=>[o.name,o.value])),w.replaceChildren();for(const o of e){const r=document.createElement("div");r.className="cex-prop",r.tabIndex=0,r.dataset.prop=o.name;const l=document.createElement("label");l.className="cex-prop-label",l.textContent=o.label,l.title=`${o.name} (${o.type})`;const s=o.type==="array"||o.type==="object"?document.createElement("textarea"):document.createElement("input");o.type==="boolean"?(s.type="checkbox",s.className="form-check",s.checked=$e(o.type,o.value)):(s.tagName==="INPUT"?s.type=o.type==="number"?"number":"text":s.rows=2,s.className="form-input form-input--sm cex-prop-input",s.value=$e(o.type,o.value)),s.setAttribute("aria-label",o.label);const Ce=()=>{y[o.name]=We(o.type,o.type==="boolean"?s.checked:s.value),G({type:"update",payload:{props:y}})};s.addEventListener("input",Ce),s.addEventListener("change",Ce);const q=document.createElement("button");q.type="button",q.className="cex-kebab",q.setAttribute("aria-label",`Actions for ${o.label}`),q.title="Actions",q.textContent="\u22EE",r.append(l,s,q),w.appendChild(r)}}const re=async(e,t)=>{try{await navigator.clipboard.writeText(e),E.toast(`${t} copied.`,{type:"success"})}catch{E.toast(e,{type:"info",duration:8e3})}};function qe(e){const t=ue.find(l=>l.name===e);if(!t)return[];const o=y[t.name],r=Je(t.name,t.type,o);return[{header:t.label},{label:"Reset to its default",icon:"rotate-ccw",action:()=>{delete y[t.name];const l=Me(g.props?.getValue(),{}).rows.find(s=>s.name===t.name);y[t.name]=l?l.value:null,de(),G({type:"update",payload:{props:y}})}},{label:"Copy as an attribute",icon:"copy",disabled:!r,action:()=>re(r,"Attribute")},{label:"Copy the tag with this value",icon:"code",action:()=>re(`<dm-${m.peek().trim()||"component"}${r?` ${r}`:""}></dm-${m.peek().trim()||"component"}>`,"Tag")},{label:"Copy the prop name",icon:"copy",action:()=>re(t.name,"Name")},{type:"divider"},{label:"Edit in <props>",icon:"edit",action:()=>{Z.value="props";const l=g.props?.textarea;if(!l)return;const s=l.value.indexOf(`"${t.name}"`);l.focus(),s>=0&&l.setSelectionRange(s,s+t.name.length+2)}}]}const Pe=E.contextMenu(w,{cursor:"glyph",match:".cex-prop",inherit:!1,density:"compact",items:e=>qe(e.dataset.prop)});v(()=>Pe?.destroy?.());const ve=e=>{const t=e.target.closest(".cex-kebab");t&&xe(t)},fe=e=>{const t=e.target.closest(".cex-prop");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),xe(t))};w.addEventListener("click",ve),w.addEventListener("keydown",fe),v(()=>{w.removeEventListener("click",ve),w.removeEventListener("keydown",fe)});async function se(){if(u.peek()||A.peek())return;const e=oe.peek();if(e){E.toast(e,{type:"warning"});return}if(b.peek()==="error"){E.toast("Fix the compile errors first - a component that does not compile is not saved.",{type:"warning"});return}const t=m.peek().trim(),o={...ee.value||{}};delete o.bundled,d.peek()?o.project=d.peek():delete o.project,A.value=!0;try{if(await P.components.put(t,{source:I(),bundled:$.peek(),meta:o}),!x())return;ee.value=o,j.value=ce(te()),H.drop(Y(a)),E.toast(C.peek()?`Created the component "${t}".`:"Saved.",{type:"success"}),C.peek()&&(Ke("#/components"),a=t,location.hash=`#/components/edit/${encodeURIComponent(t)}`)}catch(r){E.toast(`Not saved: ${r.message||r}`,{type:"error"})}finally{A.value=!1}}async function Oe(){if(!U.peek()||!await E.confirm("Throw away the changes you have not saved?"))return;const e=JSON.parse(j.peek());C.peek()&&(m.value=e[0]),d.value=e[1],$.value=e[2],_(e[3]),clearTimeout(K),F()}function ye(){H.set(Y(null),{name:`${a}-copy`,project:"",bundled:!1,source:I(),base:null,seed:!0,at:new Date().toISOString()}),location.hash="#/components/new"}const Ae={name:m,project:d,bundled:$,dirty:U,saving:A,readOnly:u,isNew:C,nameErr:oe,compileErrors:V,activeTab:Z,notNew:M.computed(()=>!C.value),canEdit:M.computed(()=>!u.value),tagText:M.computed(()=>ze(m.value)),statusText:M.computed(()=>({compiling:"Compiling\u2026",ok:"Compiles",error:"Does not compile - see below"})[b.value]),statusCls:M.computed(()=>`is-${b.value}`),hasErrors:M.computed(()=>b.value==="error"&&!!V.value),saveBlocked:M.computed(()=>A.value||!!oe.value||b.value==="error"),nameLocked:M.computed(()=>!C.value||u.value)},De=M.applyBindings(Ae,c,{methods:{save:()=>se(),discard:()=>Oe(),copyToNew:()=>ye(),showTab:e=>{Z.value=e}}});v(()=>De.dispose()),v(Ue(c,{position:"bottom"})),B&&(B.value=d.peek());const L=c.querySelector("#component-quick-switch");if(L&&a){for(const t of[...T.list||[]].sort((o,r)=>o.name.localeCompare(r.name))){const o=document.createElement("option");o.value=t.name,o.textContent=t.origin==="plugin"?`${t.name} (plugin)`:t.name,t.name===a&&(o.selected=!0),L.appendChild(o)}L.hidden=!1;const e=()=>{const t=L.value;t&&t!==a&&(location.hash=`#/components/edit/${encodeURIComponent(t)}`)};L.addEventListener("change",e),v(()=>L.removeEventListener("change",e))}Q(n,{id:"component-editor-back",icon:"arrow-left",title:"All components",onClick:()=>{location.hash="#/components"}}),a&&Q(n,{id:"component-editor-export",icon:"download",title:"Export .dmcomponent.json",onClick:async()=>{try{await P.components.exportBundle(a)}catch(e){E.toast(e.message||"Export failed.",{type:"error"})}}}),u.peek()?Q(n,{id:"component-editor-copy",icon:"copy",title:"Copy to a new component",onClick:ye}):Q(n,{id:"component-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>se()});const he=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),se())};document.addEventListener("keydown",he,!0),v(()=>document.removeEventListener("keydown",he,!0));const be=()=>{U.peek()&&H.set(Y(a),{...te(),base:j.peek(),at:new Date().toISOString()})},ge=e=>{U.peek()&&(be(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",ge),v(()=>window.removeEventListener("beforeunload",ge)),h={path:Te,dirty:()=>U.peek(),question:()=>`You have unsaved changes to ${a?`<dm-${Ne(a)}>`:"this new component"}. Leave anyway? They are kept as a draft for your next visit.`,dispose(){W=!0,be(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},Domma.icons.scan(c),f?.contentDocument?.readyState==="complete"?F():f?.addEventListener("load",()=>{x()&&F()},{once:!0}),u.peek())return;const k=H.get(Y(a)),we=Ye(k,j.peek());if(!we.ok)return;H.drop(Y(a));const ke=()=>{C.peek()&&typeof k.name=="string"&&(m.value=k.name),d.value=String(k.project??""),B&&(B.value=d.peek()),$.value=k.bundled===!0,_(k.source),clearTimeout(K),F()};if(k.seed){ke();return}const Ee=k.at?` (${D(k.at).format("D MMM HH:mm")})`:"",Se=a?`<dm-${Ne(a)}>`:"a new component",Ve=we.stale?`You left unsaved changes to ${Se}${Ee}, but the saved version has changed since. Restore your version anyway?`:`You left unsaved changes to ${Se}${Ee}. Restore them?`;await E.confirm(Ve)&&x()&&ke()}};
|