domma-cms 0.76.0 → 0.77.1
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 +2 -2
- 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/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/form-editor-arrange.js +1 -0
- package/admin/js/lib/form-submissions-arrange.js +2 -0
- package/admin/js/lib/menu-editor-arrange.js +2 -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/project-page-arrange.js +1 -0
- package/admin/js/lib/role-editor-arrange.js +3 -0
- package/admin/js/lib/user-editor-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/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/form-editor.html +41 -26
- package/admin/js/templates/form-submissions.html +78 -26
- package/admin/js/templates/menu-editor.html +57 -17
- package/admin/js/templates/menu-locations.html +54 -11
- package/admin/js/templates/page-editor.html +47 -26
- package/admin/js/templates/plugin-code.html +91 -73
- 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/user-editor.html +150 -14
- 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/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/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/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/page-editor.js +26 -26
- package/admin/js/views/plugin-code.js +23 -4
- 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/user-editor.js +1 -1
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/package.json +1 -1
- package/server/routes/api/dashboard.js +32 -4
- package/server/routes/api/forms.js +104 -13
- package/server/routes/api/users.js +12 -2
- package/server/routes/api/views.js +55 -1
- package/server/services/components.js +9 -0
- package/server/services/users.js +2 -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/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
|
@@ -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,typedValue as bt,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 gt='<span class="cex-chip">{{text}}<button type="button" data-idx="{{idx}}" title="Remove this filter" aria-label="Remove filter {{text}}">\xD7</button></span>';function Et(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 kt(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]=bt(a,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(kt(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=gt;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=Et(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()}};
|
|
@@ -1,55 +1,54 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</div>`;const w=r.querySelector(".ctx-icon-slot");if(w){const l=B("edit",e.icon||"");l.input.className+=" ctx-item-icon",l.input.dataset.i=String(i),w.appendChild(l.el)}const N=r.querySelector(".ctx-url-slot");if(N){const l=L(o.placeholder,e.action?.url||"");l.input.className+=" ctx-item-arg",l.input.dataset.i=String(i),l.input.dataset.key="url",N.appendChild(l.el)}t.appendChild(r)}),Domma.icons.scan("#ctx-items"),p()}c.on("change",".ctx-item-type",function(){const t=Number(this.dataset.i);a[t].type=this.value,this.value!=="item"?delete a[t].action:a[t].action||(a[t].action={type:"navigate"}),u()}),c.on("input",".ctx-item-text",function(){a[Number(this.dataset.i)].text=this.value,p()}),c.on("input",".ctx-item-icon",function(){a[Number(this.dataset.i)].icon=this.value,p()}),c.on("change",".ctx-item-action",function(){const t=Number(this.dataset.i);a[t].action={type:this.value},u()}),c.on("input",".ctx-item-arg",function(){const t=Number(this.dataset.i);a[t].action=a[t].action||{},a[t].action[this.dataset.key]=this.value,p()}),c.on("change",".ctx-item-danger",function(){a[Number(this.dataset.i)].danger=this.checked,p()}),c.on("click",".ctx-item-remove",function(){a.splice(Number(this.dataset.i),1),u()}),c.on("click",".ctx-item-up",function(){const t=Number(this.dataset.i);[a[t-1],a[t]]=[a[t],a[t-1]],u()}),c.on("click",".ctx-item-down",function(){const t=Number(this.dataset.i);[a[t+1],a[t]]=[a[t],a[t+1]],u()}),$("#ctx-add-item").on("click",()=>{a.push({type:"item",text:"New item",action:{type:"navigate",url:""}}),u()});function k(){const t={},s=$("#ctx-accent").val(),e=$("#ctx-transition").val(),i=$("#ctx-density").val(),n=$("#ctx-radius").val(),o=$("#ctx-shadow").val(),r=$("#ctx-opacity").val();return s&&(t.accent=s),e&&(t.transition=e),i&&(t.density=i),n&&(t.radius=n),o&&(t.shadow=o),r!==""&&r!=null&&(t.opacity=Number(r)),t}function A(){const t=$("#ctx-inherit").val(),s=j($("#ctx-pages").val()),e=j($("#ctx-projects").val()),i={slug:$("#ctx-slug").val().trim(),name:$("#ctx-name").val().trim(),enabled:$("#ctx-enabled").is(":checked"),target:{selector:$("#ctx-selector").val().trim(),match:$("#ctx-match").val().trim(),exclude:$("#ctx-exclude").val().trim()},inherit:t==="false"?!1:t,items:a},n=k();return Object.keys(n).length&&(i.theme=n),(s.length||e.length)&&(i.binding={},s.length&&(i.binding.pages=s),e.length&&(i.binding.projects=e)),i}E.tooltip&&E.tooltip(".ctx-help",{trigger:"click",position:"right"});function p(){b();const t=a.map(e=>{const i=e.type||"item";return i==="divider"?{type:"divider"}:i==="header"?{type:"header",header:e.text||""}:{label:e.text||"(no text)",icon:e.icon||void 0,danger:!!e.danger}});if(!t.some(e=>e.label))return;const s=k();g=E.contextMenu("#ctx-preview",{cursor:"glyph",items:t,inherit:!1,accent:s.accent,transition:s.transition,density:s.density,radius:s.radius,shadow:s.shadow,opacity:s.opacity})}c.on("change","#ctx-accent, #ctx-transition, #ctx-density, #ctx-radius, #ctx-shadow",p),c.on("input","#ctx-opacity",p);function h(t){const s=document.getElementById("ctx-errors");if(!t){s.style.display="none";return}s.style.display="",s.innerHTML=String(t).split("; ").map(e=>`<div>${d(e)}</div>`).join(""),s.scrollIntoView({behavior:"smooth",block:"nearest"})}$("#ctx-save").on("click",async function(){h(null);const t=A();if(!t.slug)return h("Slug is required");if(!t.name)return h("Name is required");if(!t.target.selector)return h("Container selector is required");this.disabled=!0;try{x?(await v.contextMenus.create(t),E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(await v.contextMenus.update(y,t),E.toast("Context menu saved.",{type:"success"}))}catch(s){h(s.message||String(s))}finally{this.disabled=!1}}),u(),Domma.icons.scan(c.get(0))},onUnmount(){E.contextMenu&&E.contextMenu.closeAll&&E.contextMenu.closeAll(),b()}};
|
|
1
|
+
import{api as ue}from"../api.js";import{addBannerAction as qe}from"../lib/plugin-chrome.js";import{helpPopovers as Ne}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Re}from"../lib/tool-kit.js";import{makeIconInput as rt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as ct}from"/admin/js/lib/page-picker.js";import{ACCENTS as lt,ACTION_ARG as B,ACTION_GROUPS as dt,EDITABLE_TYPES as ut,NEEDS_TARGET as mt,SELF_DISABLING as pt,checkMenu as vt,duplicateItem as ft,formFromMenu as Fe,insertItem as ht,itemForm as bt,itemFromForm as He,itemRow as yt,moveItem as gt,newItem as xt,previewItems as kt,previewable as Pe,recordFromForm as wt,removeItem as Q,slugify as Et,snapshot as me,splitList as X,themeFromForm as Mt}from"../lib/context-menu-editor-arrange.js";const St=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=c=>`context-menu-editor.draft.${c||"new"}`,At=`
|
|
2
|
+
<div class="cmx-row" data-index="{{index}}" tabindex="0" data-bind-class="cls">
|
|
3
|
+
<span class="cmx-icon" aria-hidden="true" data-name="{{icon}}"></span>
|
|
4
|
+
<span class="cmx-type">{{typeLabel}}</span>
|
|
5
|
+
<span class="cmx-main"><strong>{{text}}</strong><small data-bind-hidden="!does">{{does}}</small><small data-bind-hidden="!note">{{note}}</small><small class="cmx-bad" data-bind-hidden="!problem">{{problem}}</small></span>
|
|
6
|
+
<span class="cmx-chip is-danger" data-bind-hidden="!danger">Destructive</span>
|
|
7
|
+
<span class="cmx-move">
|
|
8
|
+
<button type="button" class="cmx-up" data-bind-disabled="first" aria-label="Move up" title="Move up (Alt+\u2191)">\u2191</button>
|
|
9
|
+
<button type="button" class="cmx-down" data-bind-disabled="last" aria-label="Move down" title="Move down (Alt+\u2193)">\u2193</button>
|
|
10
|
+
</span>
|
|
11
|
+
<button type="button" class="cmx-kebab" aria-label="Actions for {{text}}" title="Actions">\u22EE</button>
|
|
12
|
+
</div>`,It='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',Lt="<small>{{text}}</small>";function C(c){return String(c??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function Tt(c){const u=document.createElement("template");return u.innerHTML=c.trim(),u.content.firstElementChild}let Z=null;function pe(){if(Z){try{Z.destroy()}catch{}Z=null}const c=document.getElementById("ctx-preview");if(c&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(c))try{u.destroy()}catch{}}function ve(c,u){const l=c.querySelector(".dm-plugin-banner-name");if(!l)return;const T=l.querySelector(".dm-plugin-banner-sep");if(T){for(;T.nextSibling;)T.nextSibling.remove();l.appendChild(document.createTextNode(u))}}let k=null,Ke=!1;const ee=c=>String(c||"").split("?")[0].replace(/\/$/,"");function Ct(){Ke||!window.R?.use||(Ke=!0,R.use((c,u,l)=>!(St.test(u?.path||"")&&ee(c?.path)!==ee(u?.path))||!k?.dirty()?l():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(w=>{w?l():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:c}={})=>{if(k&&ee(c?.path)!==k.path){const u=k;k=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(c){if(Ct(),k){const e=k;k=null,e.dispose()}pe();const u=[],l=e=>(u.push(e),e);let T=!1;const w=c.get(0),m=w.querySelector(".cmx")||w,_=()=>!T&&m.isConnected,fe=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),p=fe?decodeURIComponent(fe[1]):null,h=!p,Be=ee(location.hash.replace(/^#/,""));let g=null;if(!h){const e=E.loader(w,{type:"dots"});try{g=await ue.contextMenus.get(p)}catch(n){e.destroy(),E.toast(`Could not load "${p}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!m.isConnected)return}const O=Fe(g),r={};for(const[e,n]of Object.entries(O))r[e]=M.observable(n);const U=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),he=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(g?.items)?g.items:[]),z=M.observable(me(O,s.peek())),G=M.computed(()=>me(he(),s.value)!==z.value),V=M.observable(!1),J=M.observable(!1),H=M.observable([]),$=M.computed(()=>vt(he(),s.value,{isNew:h})),_e=M.computed(()=>{const e=$.value.errors;if(J.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),be=M.computed(()=>{const e=J.value?$.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||$.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let te=!h;l(M.effect(()=>{const e=r.name.value;te||(r.slug.value=Et(e))}));const ne=M.observable([]);l(M.effect(()=>{const e=s.value,n=$.value.itemErrors;ne.value=e.map((t,i)=>({...yt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const x=m.querySelector("#ctx-items");l(M.effect(()=>{ne.value,requestAnimationFrame(()=>{x?.isConnected&&(x.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(x))})}));const Ue=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=X(r.pages.value).length,t=X(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(o=>!["divider","header"].includes(o?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ae=null;function ze(){if(!_())return;pe();const e=s.peek();if(!Pe(e))return;const n=Mt(U());Z=E.contextMenu("#ctx-preview",{cursor:"glyph",items:kt(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}l(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ae),ae=setTimeout(ze,120)})),l(()=>{clearTimeout(ae),pe()});const Ge=M.computed(()=>Pe(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),oe=e=>ye(e),A=e=>{s.value=e};function P(e){requestAnimationFrame(()=>x?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function j(e,n){n<0||n>=s.peek().length||(A(gt(s.peek(),e,n)),P(n))}function q(e,n=s.peek().length){const t=xt(e);A(ht(s.peek(),n,t)),e!=="divider"&&ye(n,{isAdd:!0})}function Ve(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>oe(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",disabled:e===0,action:()=>j(e,e-1)},{label:"Move down",icon:"arrow-down",disabled:e===n.length-1,action:()=>j(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{A(ft(s.peek(),e)),P(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>q("item",e+1)},{label:"Heading",action:()=>q("header",e+1)},{label:"Divider",action:()=>q("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{A(Q(s.peek(),e)),P(Math.max(0,e-1))}}]}let v=null;function ye(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(v)try{v.so.close(!0)}catch{}const i=bt(t),o={};for(const[a,d]of Object.entries(i))o[a]=M.observable(d);const W=()=>Object.fromEntries(Object.keys(o).map(a=>[a,o[a].peek()])),Ze=JSON.stringify(i),ie=()=>JSON.stringify(W())!==Ze,Ie=!ut.includes(i.type),re=Array.isArray(t.submenu)&&t.submenu.length,Le={...o,locked:Ie,notLocked:!Ie,hasSub:re,subText:re?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>o.type.value!=="divider"&&o.type.value!=="header"),hasText:M.computed(()=>o.type.value!=="divider"),showAction:M.computed(()=>o.type.value!=="divider"&&o.type.value!=="header"&&!re),argLabel:M.computed(()=>B[o.action.value]?.label||""),note:M.computed(()=>pt[o.action.value]||mt[o.action.value]||""),problem:M.computed(()=>{const a=He(Object.fromEntries(Object.keys(o).map(d=>[d,o[d].value])),t);return a.type==="divider"?"":String(a.text||"").trim()?B[a.action?.type]?.required&&!String(a.action?.[B[a.action.type].key]||"").trim()?`Needs a ${B[a.action.type].label.toLowerCase()}.`:"":"Needs text."})},y=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${C(i.text||"item")}`,size:"sm",position:"right"}),et=dt.map(a=>`<optgroup label="${C(a.label)}">${Object.entries(a.actions).map(([d,Y])=>`<option value="${C(d)}">${C(Y)}</option>`).join("")}</optgroup>`).join(""),L=Tt(`
|
|
13
|
+
<div class="cmx-form">
|
|
14
|
+
<div data-bind-hidden="locked">
|
|
15
|
+
<label class="form-label" for="cmx-i-type">Type
|
|
16
|
+
<span data-help="An Item does something when clicked, a Heading labels a group, a Divider draws a line."></span></label>
|
|
17
|
+
<select id="cmx-i-type" class="form-select" data-model="type.value">
|
|
18
|
+
<option value="item">Item</option><option value="header">Heading</option><option value="divider">Divider</option>
|
|
19
|
+
</select>
|
|
20
|
+
</div>
|
|
21
|
+
<p class="cmx-note" data-bind-hidden="notLocked">A ${C(i.type)} item - its text, icon and action can be changed here; its type is kept.</p>
|
|
22
|
+
<div data-bind-hidden="!hasText.value">
|
|
23
|
+
<label class="form-label" for="cmx-i-text">Text</label>
|
|
24
|
+
<input id="cmx-i-text" type="text" class="form-input" maxlength="80" data-model="text.value">
|
|
25
|
+
</div>
|
|
26
|
+
<div data-bind-hidden="!isItem.value">
|
|
27
|
+
<label class="form-label" for="cmx-i-icon">Icon <small class="cmx-hint-inline">(optional)</small></label>
|
|
28
|
+
<div class="cmx-icon-mount"></div>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="cmx-note" data-bind-hidden="!hasSub">${C(Le.subText)}</p>
|
|
31
|
+
<div data-bind-hidden="!showAction.value">
|
|
32
|
+
<label class="form-label" for="cmx-i-action">Does
|
|
33
|
+
<span data-help-title="What it does" data-help="A fixed set of actions, on purpose: nothing typed here runs as script on your pages. Some grey themselves out when they do not apply to the click - the note under the choice says when."></span></label>
|
|
34
|
+
<select id="cmx-i-action" class="form-select" data-model="action.value">${et}</select>
|
|
35
|
+
<small class="cmx-hint" data-bind-hidden="!note.value" data-bind-text="note.value"></small>
|
|
36
|
+
</div>
|
|
37
|
+
<div data-bind-hidden="!argLabel.value || !showAction.value">
|
|
38
|
+
<label class="form-label" for="cmx-i-arg" data-bind-text="argLabel.value">Argument</label>
|
|
39
|
+
<div class="cmx-arg-mount"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="cmx-setting" data-bind-hidden="!isItem.value">
|
|
42
|
+
<label class="form-label" for="cmx-i-danger">Destructive (red)
|
|
43
|
+
<span data-help="Drawn in red, for things like Delete - a warning, not a guard."></span></label>
|
|
44
|
+
<input id="cmx-i-danger" type="checkbox" class="form-switch" data-model="danger.value">
|
|
45
|
+
</div>
|
|
46
|
+
<small class="cmx-err" data-bind-hidden="!problem.value" data-bind-text="problem.value"></small>
|
|
47
|
+
<div class="cmx-actions">
|
|
48
|
+
<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>
|
|
49
|
+
<span class="cmx-spacer"></span>
|
|
50
|
+
<small class="cmx-hint-inline">Ctrl+Enter</small>
|
|
51
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
52
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="done"><span data-icon="check" data-icon-size="13"></span> Done</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>`),N=rt("edit",i.icon);N.input.id="cmx-i-icon",N.input.classList.add("form-input");const Te=()=>{o.icon.value=N.input.value};N.input.addEventListener("input",Te),L.querySelector(".cmx-icon-mount").appendChild(N.el);const ce=L.querySelector(".cmx-arg-mount");let f=null;const le=()=>{o.arg.value=f.value};function Ce(){f?.removeEventListener("input",le),ce.replaceChildren();const a=B[o.action.peek()];if(!a){f=null;return}if(a.key==="url"){const d=ct(a.placeholder,o.arg.peek());f=d.input,ce.appendChild(d.el)}else f=document.createElement("input"),f.type="text",f.className="form-input",f.placeholder=a.placeholder,f.value=o.arg.peek(),ce.appendChild(f);f.id="cmx-i-arg",f.addEventListener("input",le)}let Oe=o.action.peek();const tt=M.effect(()=>{const a=o.action.value;a!==Oe&&(Oe=a,o.arg.value="",Ce())});Ce();const $e=()=>{const a=He(W(),t),d=[...s.peek()],Y=d.indexOf(t);Y!==-1&&(d[Y]=a,A(d))};let K=!1,de=!1;const De={done:()=>{$e(),K=!0,y.close(),P(e)},cancel:()=>y.close(),remove:()=>{const a=s.peek().indexOf(t);a!==-1&&A(Q(s.peek(),a)),K=!0,y.close()}};y.element.appendChild(L);const nt=Ne(L),at=M.applyBindings(Le,L,{methods:De}),ot=a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),De.done())};L.addEventListener("keydown",ot);const je={so:y,dirty:ie,apply:$e};v=je,it(y,()=>{tt(),at.dispose(),nt(),N.input.removeEventListener("input",Te),f?.removeEventListener("input",le),v===je&&(v=null)});const st=y.close.bind(y);y.close=async a=>{if(!K&&a!==!0&&ie()){if(de)return;de=!0;const d=await E.confirm("Discard the changes to this item?");if(de=!1,!d)return}if(!K&&n&&a!==!0&&!ie()){const d=s.peek().indexOf(t);d!==-1&&A(Q(s.peek(),d))}return K=!0,st()},I.scan(L),y.open(),requestAnimationFrame(()=>L.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}l(()=>{if(v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function se(){if(V.peek())return;J.value=!0,H.value=[];const e=$.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=be.peek(),o=i.binding?0:i.items?1:i.appearance?2:-1;o>=0&&Ye?.activate?.(o);return}V.value=!0;const n=wt(U(),s.peek());try{if(h)await ue.contextMenus.create(n);else{const t=await ue.contextMenus.update(p,n);t?.items&&(s.value=t.items)}if(!_())return;z.value=me(U(),s.peek());try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),h?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),ve(w,r.name.peek()||p))}catch(t){H.value=String(t.message||t).split("; ").map((i,o)=>({key:String(o),text:i})),m.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{V.value=!1}}function Je(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!_())return;const n=Fe(g);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(g?.items)?g.items:[],H.value=[]})}m.querySelector("#ctx-items").innerHTML=At,m.querySelector(".cmx-chips").innerHTML=It,m.querySelector(".cmx-server-errors").innerHTML=Lt;const ge=m.querySelector("#ctx-accent");if(O.accent&&!lt.includes(O.accent)&&ge){const e=document.createElement("option");e.value=O.accent,e.textContent=`Custom (${O.accent})`,ge.appendChild(e)}const We={...r,items:s,rows:ne,chips:Ue,dirty:G,saving:V,err:_e,tabBad:be,serverErrors:H,isNew:h,notNew:!h,previewText:Ge,serverError:M.computed(()=>H.value.length>0),saveText:M.computed(()=>h?"Create menu":"Save"),problemText:M.computed(()=>J.value&&!$.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=X(r.pages.value),n=X(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},Ye=E.tabs(m.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(We,m,{methods:{save:()=>se(),discard:()=>Je(),addItem:()=>q("item"),addHeader:()=>q("header"),addDivider:()=>q("divider")}});l(()=>Qe.dispose()),l(Ne(m,{position:"bottom"}));const xe=m.querySelector("#ctx-slug"),ke=()=>{te=!0};xe?.addEventListener("input",ke),l(()=>xe?.removeEventListener("input",ke));const Xe=E.contextMenu(x,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>Ve(Number(e.dataset.index))});l(()=>Xe?.destroy?.());const we=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Re(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){j(t,t-1);return}if(e.target.closest(".cmx-down")){j(t,t+1);return}oe(t)},Ee=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Re(n)):e.key==="Enter"?(e.preventDefault(),oe(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),j(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),j(t,t+1)):e.key==="Delete"&&(e.preventDefault(),A(Q(s.peek(),t)),P(Math.max(0,t-1)))};x.addEventListener("click",we),x.addEventListener("keydown",Ee),l(()=>{x.removeEventListener("click",we),x.removeEventListener("keydown",Ee)}),h?ve(w,"New menu"):ve(w,g?.name||p),qe(c,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),qe(c,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>se()});const Me=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||v||(e.preventDefault(),e.stopImmediatePropagation(),se())};document.addEventListener("keydown",Me,!0),l(()=>document.removeEventListener("keydown",Me,!0));const Se=()=>{if(G.peek())try{S.set(F(p),{form:U(),items:s.peek(),base:z.peek(),at:new Date().toISOString()})}catch{}},Ae=e=>{v?.dirty()&&v.apply(),G.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ae),l(()=>window.removeEventListener("beforeunload",Ae)),k={path:Be,dirty:()=>G.peek()||!!v?.dirty(),dispose(){if(T=!0,v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Se(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let b=null;try{b=S.get(F(p))}catch{}if(b?.form){try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}const e=b.base&&b.base!==z.peek(),n=b.at?` (${D(b.at).format("D MMM HH:mm")})`:"",t=h?"the new context menu you started":`"${C(g?.name||p)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&_()){for(const[o,W]of Object.entries(b.form))r[o]&&o!==(h?"":"slug")&&(r[o].value=W);h&&b.form.slug&&(te=!0),Array.isArray(b.items)&&(s.value=b.items)}}}};
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as T,apiRequest as m}from"../api.js";import{addBannerAction as ae}from"../lib/plugin-chrome.js";import{helpPopovers as we}from"../lib/help-popover.js";import{openMenuAt as se,viewLifetime as xe}from"../lib/tool-kit.js";import{BADGE_SOURCES as ke,activityRows as Ce,attentionFrom as Me,canAny as j,chartSeries as $e,isSitePath as L,kpisFrom as Ee}from"../lib/dashboard-arrange.js";const oe=/^(#\/?)?(\?.*)?$/,Se=3e4,Pe="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js",Ae=`
|
|
2
|
+
<div class="dbx-row" data-id="{{id}}" data-href="{{href}}" data-url="{{url}}" tabindex="0" data-bind-class="rowCls">
|
|
3
|
+
<span class="dbx-main"><strong>{{text}}</strong><small data-bind-hidden="!meta">{{meta}}</small></span>
|
|
4
|
+
<span class="dbx-value" data-bind-hidden="!value">{{value}}</span>
|
|
5
|
+
<span class="dbx-chip" data-bind-hidden="!chip" data-bind-class="chipCls">{{chip}}</span>
|
|
6
|
+
<button type="button" class="dbx-kebab" aria-label="Actions" title="Actions" data-bind-hidden="!menu">\u22EE</button>
|
|
7
|
+
</div>`;let b=null;function Re(){return window.Chart?Promise.resolve(!0):(b||(b=new Promise(i=>{const n=document.createElement("script");n.src=Pe,n.async=!0,n.onload=()=>i(!0),n.onerror=()=>{b=null,i(!1)},document.head.appendChild(n)})),b)}const c=i=>Number(i||0).toLocaleString("en-GB"),Te=i=>`${Number(i)>=0?"+":""}${Number(i).toFixed(0)}%`,v=(i,n,d,l={})=>({id:`${i}${n}`,text:d,meta:"",href:"",url:d,value:"",chip:"",chipCls:"",rowCls:"",menu:!0,...l}),u=xe(oe);export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(i){u.reset();const n=i.get(0).querySelector(".dbx")||i.get(0),d=()=>oe.test(location.hash)&&n.isConnected,l=M.observable(null),g=M.observable({}),y=M.observable(!1),w=M.observable(""),x=M.observable(null),k=M.observable(Date.now()),p=M.observable(null),C=M.observable(!!window.Chart);let f=[],_=new Set;const ne=M.computed(()=>Ee(l.value,g.value)),re=M.computed(()=>{const e=x.value;if(!e)return"";const a=Math.floor((k.value-e)/6e4);return a<1?"Updated just now":`Updated ${a} min ago`}),O=M.observable([]),B=M.observable([]),F=M.observable([]),N=M.observable([]),D=M.observable([]),U=M.observable([]),q=M.observable([]),G=M.observable([]);u.add(M.effect(()=>{const e=l.value,a=t=>t==="danger"?"is-danger":"is-warn";O.value=Me(g.value,e?.health?.checks||[]).map(t=>({id:t.id,text:`${t.title}: ${t.text}`,meta:t.meta,href:t.href,url:"",value:"",chip:t.tone==="danger"?"Urgent":"Check",chipCls:a(t.tone),rowCls:a(t.tone)+(t.href?"":" is-static"),menu:!!t.href})),B.value=Ce(e?.activity||[],k.value).map(t=>({id:t.id,text:t.text,meta:t.meta,href:t.href,url:t.url,value:"",chip:"",chipCls:"",rowCls:"",menu:!0})),F.value=(e?.topPages||[]).map((t,r)=>v("t",r,t.url,{value:c(t.hits),chip:t.deltaPct==null?"":Te(t.deltaPct),chipCls:Number(t.deltaPct)>=0?"is-ok":"is-danger"})),N.value=(e?.spikes||[]).map((t,r)=>v("s",r,t.url,{value:c(t.hits),chip:`\xD7${Number(t.ratio)||0}`,chipCls:"is-warn"}));const s=e?.journeys;D.value=(s?.entry||[]).map((t,r)=>v("e",r,t.url,{value:c(t.count)})),U.value=(s?.exit||[]).map((t,r)=>v("x",r,t.url,{value:c(t.count)})),q.value=(s?.paths||[]).map((t,r)=>({id:`p${r}`,text:`${t.from} \u2192 ${t.to}`,meta:"",href:"",url:"",value:c(t.count),chip:"",chipCls:"",rowCls:"is-static",menu:!1}));const o={ok:["OK","is-ok"],warn:["Look","is-warn"],fail:["Failing","is-danger"]};G.value=(e?.health?.checks||[]).map((t,r)=>({id:`c${r}`,text:String(t.label||t.id),meta:Array.isArray(t.detail)?t.detail.slice(0,5).join(", "):"",href:"",url:"",value:String(t.value??""),chip:(o[t.level]||o.warn)[0],chipCls:(o[t.level]||o.warn)[1],rowCls:"is-static",menu:!1}))}));const ie=M.computed(()=>!!l.value?.journeys?.totalSessions),le=M.computed(()=>{const e=l.value?.journeys;return e?.totalSessions?`Bounce rate ${(Number(e.bounceRate)*100).toFixed(1)}%`:""}),$=M.computed(()=>$e(l.value?.traffic?.series)),S=M.computed(()=>C.value&&!$.value.empty),ce=M.computed(()=>y.value?l.value?C.value?"No page views recorded yet.":"The chart could not load (Chart.js).":"Traffic needs an administrator.":"Loading\u2026"),de=M.computed(()=>!!p.value?.enabled),ue=M.computed(()=>{const e=p.value||{},a=(e.hits||0)+(e.misses||0),s=o=>{if(!o)return"never";const t=Math.max(0,Math.floor((Date.now()-new Date(o).getTime())/1e3));return t<60?`${t}s ago`:t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} h ago`:`${Math.floor(t/86400)} days ago`};return{hitRate:e.hitRate==null?"-":`${(e.hitRate*100).toFixed(1)}%`,hitsMisses:a?`${c(e.hits)} hits \xB7 ${c(e.misses)} misses`:"no visits yet",size:e.maxItems?`${c(e.size)} / ${c(e.maxItems)}`:c(e.size),driver:e.driver?`driver: ${e.driver}`:"",writes:c(e.writes),invalidations:`${c(e.invalidations)} invalidations`,cleared:s(e.lastClearedAt)}});let P=0;async function H(){const e=ke.filter(s=>j(f,s.permission)),a=await Promise.all(e.map(s=>m(s.countUrl,{method:"GET"}).catch(()=>null)));d()&&(g.value=Object.fromEntries(e.map((s,o)=>[s.key,a[o]]).filter(([,s])=>s)))}async function K(){const e=++P;try{f=(await m("/auth/permissions",{method:"GET"}).catch(()=>({})))?.permissions||[];const[s,o,t]=await Promise.all([T.dashboard.summary().catch(r=>({__error:r})),m("/cache/status",{method:"GET"}).catch(()=>null),j(f,"pages")?T.pages.list().catch(()=>[]):Promise.resolve([]),H()]);if(e!==P||!d())return;_=new Set((Array.isArray(t)?t:[]).map(r=>r.urlPath).filter(Boolean)),s?.__error?(l.value=null,w.value=/403|forbidden/i.test(String(s.__error?.message))?"":`The dashboard could not load: ${s.__error.message}`):(l.value=s,w.value=s?.warnings?.length?`Some parts could not load: ${s.warnings.join(" \xB7 ")}`:""),p.value=o,x.value=Date.now()}finally{e===P&&(y.value=!0)}}let he=0;async function pe(){if(!(!d()||!l.peek())){k.value=Date.now();try{const e=await T.dashboard.summaryLite();if(!d())return;const a={...l.peek()};e.traffic&&(a.traffic={...a.traffic,...e.traffic}),e.realtime&&(a.realtime=e.realtime),e.spikes&&(a.spikes=e.spikes),e.health&&(a.health={...a.health,status:e.health.status}),l.value=a,x.value=Date.now()}catch{}++he%2===0&&H(),m("/cache/status",{method:"GET"}).then(e=>{d()&&(p.value=e)}).catch(()=>{})}}const me=setInterval(pe,Se);u.add(()=>clearInterval(me));let h=null;const A=n.querySelector(".dbx-canvas"),fe=()=>{const e=getComputedStyle(n),a=(s,o)=>e.getPropertyValue(s).trim()||o;return{line:a("--dm-primary","#5b8cff"),text:a("--dm-text-muted","#94a3b8"),grid:a("--dm-border","rgba(127,127,127,.2)")}},be=e=>/^#[0-9a-f]{6}$/i.test(e)?`${e}26`:"transparent";function V(){if(!S.peek()||!A?.isConnected||!A.offsetParent)return;const{labels:e,values:a}=$.peek(),{line:s,text:o,grid:t}=fe();if(h){h.data.labels=e,h.data.datasets[0].data=a,h.update("none");return}h=new window.Chart(A,{type:"line",data:{labels:e,datasets:[{data:a,borderColor:s,backgroundColor:be(s),fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:o},grid:{color:t}},y:{beginAtZero:!0,ticks:{precision:0,color:o},grid:{color:t}}}}})}u.add(M.effect(()=>{$.value,S.value,requestAnimationFrame(V)})),u.add(()=>{try{h?.destroy()}catch{}h=null});const z=()=>[...document.body.classList].filter(e=>/^dm-(theme|custom)-/.test(e)).sort().join(" ");let J=z();const W=new MutationObserver(()=>{const e=z();if(e!==J){J=e;try{h?.destroy()}catch{}h=null,requestAnimationFrame(V)}});W.observe(document.body,{attributes:!0,attributeFilter:["class"]}),u.add(()=>W.disconnect());const R=e=>new URL(e,location.origin).href,Z=()=>j(f,"pages"),Q=e=>L(e)&&_.has(e.split(/[?#]/)[0].replace(/\/$/,"")||"/");async function X(e,a){try{await navigator.clipboard.writeText(e),E.toast(`${a} copied.`,{type:"success"})}catch{E.toast("Could not copy - select it and copy it by hand.",{type:"error"})}}function Y(e){const{href:a,url:s}=e.dataset;if(a){location.hash=a;return}if(s&&Z()&&Q(s)){location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`;return}s&&L(s)&&window.open(R(s),"_blank","noopener")}function ve(e){const{href:a,url:s}=e.dataset,o=[{header:(e.querySelector(".dbx-main strong")?.textContent||"").slice(0,60)}];return a&&o.push({label:"Open",icon:"arrow-right",action:()=>{location.hash=a}}),s&&L(s)?(o.push({label:"Open on the site",icon:"external-link",action:()=>window.open(R(s),"_blank","noopener")}),o.push({label:"Copy address",icon:"copy",action:()=>X(R(s),"Address")}),Z()&&o.push({label:"Edit the page",icon:"edit",disabled:!Q(s),action:()=>{location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`}})):s&&o.push({label:"Copy address",icon:"copy",action:()=>X(s,"Address")}),o.length>1?o:[]}for(const e of[".dbx-attention",".dbx-activity",".dbx-top-pages",".dbx-spikes",".dbx-entry",".dbx-exit",".dbx-paths",".dbx-checks"]){const a=n.querySelector(e);a&&(a.innerHTML=Ae)}const ge=M.applyBindings({summary:l,loaded:y,warning:w,asOf:re,kpi:ne,attention:O,activity:B,topPages:F,spikes:N,entry:D,exit:U,paths:q,checks:G,hasJourneys:ie,bounce:le,hasChart:S,chartNote:ce,cache:p,cacheOn:de,cacheView:ue},n,{methods:{async toggleCache(e){const a=!!e?.target?.checked;try{p.value=await m("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})}),E.toast(`The cache is ${a?"on":"off"}.`,{type:"success"})}catch(s){e?.target&&(e.target.checked=!a),E.toast(s.message||"Could not change the cache.",{type:"error"})}},async clearCache(){if(await E.confirm("Clear the whole response cache? Pages render afresh until it fills again."))try{p.value=await m("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}}}});u.add(()=>ge.dispose()),u.add(we(n,{position:"bottom"}));const ye=E.contextMenu(n,{cursor:"glyph",match:".dbx-row",inherit:!1,density:"compact",items:e=>ve(e)});u.add(()=>ye?.destroy?.());const ee=e=>{const a=e.target.closest(".dbx-row");if(!a||!n.contains(a))return;const s=e.target.closest(".dbx-kebab");if(s){se(s);return}Y(a)},te=e=>{const a=e.target.closest(".dbx-row");!a||!n.contains(a)||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),se(a)):e.key==="Enter"&&e.target===a&&Y(a))};n.addEventListener("click",ee),n.addEventListener("keydown",te),u.add(()=>{n.removeEventListener("click",ee),n.removeEventListener("keydown",te)}),ae(i,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>K()}),ae(i,{id:"dashboard-new-page",icon:"plus",title:"New page",onClick:()=>{location.hash="#/pages/new"}}),I.scan(n),Re().then(e=>{d()&&(C.value=e)}),await K(),d()&&I.scan(n)}};
|