domma-cms 0.92.1 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as C,apiRequest as _e}from"../api.js";import{addBannerAction as Y}from"../lib/plugin-chrome.js";import{helpPopovers as gt}from"../lib/help-popover.js";import{openMenuAt as ae,sidebarCount as he}from"../lib/tool-kit.js";import{OPERATORS as wt,STAGE_TYPES as Be,adminTierRoles as bt,cellText as St,checkView as kt,draftKey as w,errorCount as He,isGuided as xt,matchToRows as Et,move as Je,parseStageJson as qt,resultColumns as $t,rowsToMatch as Ct,rowsToSort as Mt,slugify as Ye,snapshot as P,sortToRows as Rt,stageSummary as Lt,stageText as Tt}from"../lib/view-editor-arrange.js";const At=/^\/views\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,ne=v=>String(v||"").split("?")[0].replace(/\/$/,""),Nt=["super-admin","admin","user"],Ot=v=>String(v??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function s(v,m={},...r){const h=document.createElement(v);for(const[q,d]of Object.entries(m))d==null||d===!1||(q==="class"?h.className=d:q==="text"?h.textContent=d:h.setAttribute(q,d===!0?"":d));for(const q of r)q&&h.append(q);return h}let A=null,Ge=!1;function Dt(){Ge||!window.R?.use||(Ge=!0,R.use((v,m,r)=>{const h=ne(m?.path);return!(At.test(m?.path||"")&&ne(v?.path)!==h)||!A?.dirty()?r():E.confirm("This view has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?r():history.replaceState(null,"",`#${m.path}`)})}),M.subscribe("router:afterChange",({to:v}={})=>{if(A&&ne(v?.path)!==A.path){const m=A;A=null,m.dispose()}}))}function It(v,m){const r=v.querySelector(".dm-plugin-banner-name");if(!r)return;const h=r.querySelector(".dm-plugin-banner-sep");if(h){for(;h.nextSibling;)h.nextSibling.remove();h.after(document.createTextNode(m))}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(v){if(Dt(),A){const e=A;A=null,e.dispose()}const m=v.get(0),r=m.querySelector(".vex")||m,h=$(r),q=[],d=e=>(q.push(e),e);let ge=!1;const j=()=>!ge&&r.isConnected,we=location.hash.match(/^#\/views\/edit\/([^/?#]+)/),f=we?decodeURIComponent(we[1]):null,b=!f,We=ne(location.hash.slice(1));let L=[],N=null,be=[],Se=[];const ke=E.loader(m,{type:"dots"});try{const[e,t,o,n,l,c]=await Promise.all([C.collections.list().catch(()=>[]),C.collections.getConnections().catch(()=>({})),C.blocks.list().catch(()=>[]),C.projects.list().catch(()=>[]),_e("/collections/roles/entries?limit=1000").catch(()=>null),C.views.list().catch(()=>[])]);be=(c||[]).map(a=>a.slug),F(r.querySelector("#view-source"),(e||[]).map(a=>[a.slug,`${a.title||a.slug} (${a.slug})`])),F(r.querySelector("#view-connection"),Object.keys(t||{}).filter(a=>a!=="default").map(a=>[a,a])),F(r.querySelector("#view-block-name"),(o||[]).map(a=>[a.name,`${a.name}.html`])),F(r.querySelector("#view-project"),(n||[]).map(a=>[a.slug,a.name||a.slug]));const i=(l?.entries||l?.data||[]).map(a=>a.data||a).filter(a=>a?.name).sort((a,p)=>(a.level??9)-(p.level??9)).map(a=>({name:a.name,label:a.label||a.name,level:a.level}));if(Se=bt(i),Qe(i.length?i:Nt.map(a=>({name:a,label:a}))),F(r.querySelector("#add-stage-type"),Be.map(a=>[a.type,`${a.type} - ${a.label}`])),!b&&(N=await C.views.get(f),!N))throw new Error("View not found")}catch(e){ke.destroy(),E.toast(e.message==="View not found"?"That view does not exist.":`Could not load the view: ${e.message||e}`,{type:"error"}),R.navigate("/views");return}if(ke.destroy(),!j())return;function F(e,t){if(e)for(const[o,n]of t)e.querySelector(`option[value="${CSS.escape(o)}"]`)||e.append(s("option",{value:o,text:n}))}function Qe(e){const t=r.querySelector("#view-roles-checkboxes");t.replaceChildren();for(const o of e)t.append(s("label",{},s("input",{type:"checkbox",class:"view-role-cb",value:o.name}),document.createTextNode(o.label)))}function Xe(){const e=(L||[]).map(t=>({value:`data.${t.name}`,label:`${t.label||t.name} (${t.name})`}));return e.push({value:"__meta.createdAt",label:"Created (meta)"},{value:"__meta.updatedAt",label:"Updated (meta)"}),e}function _(e,t,o,n){const l=s("select",{class:`form-input form-input--sm ${o||""}`,"aria-label":n});for(const c of e)l.append(s("option",{value:c.value,text:c.label}));return t&&!e.some(c=>String(c.value)===String(t))&&l.append(s("option",{value:t,text:t})),l.value=t??"",l}function G(e,t){return _([{value:"",label:"- field -"},...Xe()],e,t,"Field")}function xe(e,t,o){const n=(L||[]).find(i=>`data.${i.name}`===e);if(n?.type==="select"&&(t==="eq"||t==="ne")&&n.options?.length){const i=n.options.map(a=>typeof a=="string"?{value:a,label:a}:{value:a.value??"",label:a.label||a.value}).filter(a=>a.value&&a.value!=="undefined");return _([{value:"",label:"- value -"},...i],o,"match-val","Value")}const l=t==="in"?"a, b, c":t==="contains"?"text (a pattern)":["gt","lt","gte","lte"].includes(t)?"0":"value",c=s("input",{type:"text",class:"form-input form-input--sm match-val",placeholder:l,"aria-label":"Value"});return c.value=o??"",c}function Ze(e,t){const{logic:o,rows:n}=Et(t),l=_([{value:"and",label:"ALL conditions (and)"},{value:"or",label:"ANY condition (or)"}],o,"match-logic","Match"),c=s("div",{class:"vex-conds"}),i=s("button",{type:"button",class:"btn btn-sm btn-ghost"},s("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Condition")),a=(p={field:"",op:"eq",val:""})=>{const oe=G(p.field,"match-field"),yt=_(wt,p.op,"match-op","Operator"),ht=s("span",{class:"vex-val"},xe(p.field,p.op,p.val)),Ue=s("button",{type:"button",class:"vex-x",title:"Remove this condition","aria-label":"Remove this condition",text:"\xD7"});Ue.addEventListener("click",()=>{Fe.remove(),g()});const Fe=s("div",{class:"vex-cond match-condition-row"},oe,yt,ht,Ue);c.append(Fe)};i.addEventListener("click",()=>{a(),g()}),e.append(s("div",{class:"vex-logic"},s("span",{class:"vex-muted",text:"Keep rows matching"}),l),c,i),(n.length?n:[void 0]).forEach(p=>a(p))}function et(e,t){const o=s("div",{class:"vex-conds"}),n=s("button",{type:"button",class:"btn btn-sm btn-ghost"},s("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Sort field")),l=(i={field:"",dir:1})=>{const a=s("button",{type:"button",class:"vex-x",title:"Remove","aria-label":"Remove this sort",text:"\xD7"}),p=s("div",{class:"vex-sort-row sort-row"},G(i.field,"sort-field"),_([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(i.dir),"sort-dir","Direction"),a);a.addEventListener("click",()=>{p.remove(),g()}),o.append(p)};n.addEventListener("click",()=>{l(),g()}),e.append(o,n);const c=Rt(t);(c.length?c:[void 0]).forEach(i=>l(i))}const k=r.querySelector("#pipeline-stages-list");let tt=0;function Ee(e){const t=s("div",{class:"vex-stage stage-card","data-stage-type":e.type,"data-sid":String(++tt)}),o=s("div",{class:"vex-stage-head",tabindex:"0"},s("span",{class:"vex-stage-no"}),s("code",{class:"vex-stage-type",text:e.type}),s("span",{class:"vex-stage-sum"}),s("button",{type:"button",class:"vex-kebab","aria-label":`Actions for this ${e.type} stage`,title:"Actions",text:"\u22EE"})),n=s("div",{class:"vex-stage-body"});if(xt(e.type)){t.dataset.guided="true";const l=s("div",{class:"stage-guided"});n.append(l),e.type==="$match"?Ze(l,e.config):et(l,e.config)}else{const l=Be.find(i=>i.type===e.type),c=s("textarea",{class:"form-input stage-config",rows:"5",spellcheck:"false","aria-label":`${e.type} settings`,placeholder:l?.example||"{}"});c.value=Tt(e.type,e.config),n.append(s("small",{class:"vex-hint",text:`The inside of ${e.type} only - e.g. ${(l?.example||"{}").replace(/\s+/g," ")}`}),c,s("small",{class:"vex-stage-err",hidden:!0}))}return t.append(o,n),t}function qe(e,t=null){const o=Ee(e);return t?t.after(o):k.append(o),I.scan(o),g(),o}function $e(e){const t=e.dataset.stageType;if(e.dataset.guided){const l=e.querySelector(".stage-guided");if(t==="$match"){const i=[...l.querySelectorAll(".match-condition-row")].map(a=>({field:a.querySelector(".match-field")?.value,op:a.querySelector(".match-op")?.value,val:a.querySelector(".match-val")?.value}));return{type:t,config:Ct(l.querySelector(".match-logic")?.value,i),error:""}}const c=[...l.querySelectorAll(".sort-row")].map(i=>({field:i.querySelector(".sort-field")?.value,dir:i.querySelector(".sort-dir")?.value}));return{type:t,config:Mt(c),error:""}}const{config:o,error:n}=qt(t,e.querySelector(".stage-config")?.value);return{type:t,config:o,error:n}}const V=()=>[...k.querySelectorAll(":scope > .stage-card")],x=r.querySelector("#view-columns-builder");let B=null;function Ce(e={key:"",label:""}){const t=L.length?G(e.key,"col-key"):s("input",{type:"text",class:"form-input form-input--sm col-key font-mono",placeholder:"data.field","aria-label":"Field"});L.length||(t.value=e.key||"");const o=s("input",{type:"text",class:"form-input form-input--sm col-label",placeholder:"Heading","aria-label":"Heading"});o.value=e.label||"";const n=s("button",{type:"button",class:"vex-kebab","aria-label":"Actions for this column",title:"Actions",text:"\u22EE"});return s("div",{class:"vex-col col-row",tabindex:"0"},t,o,n)}function Me(e){x.replaceChildren();const t=e??L.map(o=>({key:`data.${o.name}`,label:o.label||o.name}));for(const o of t)x.append(Ce(o));B=s("button",{type:"button",class:"btn btn-sm btn-ghost vex-col-add"},s("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Column")),B.addEventListener("click",()=>{B.before(Ce()),I.scan(x),g()}),x.append(B),I.scan(x)}const le=()=>[...x.querySelectorAll(":scope > .col-row")];async function Re(e){if(!e){L=[];return}try{L=(await C.collections.get(e))?.fields||[]}catch{L=[]}}function ot(){for(const e of r.querySelectorAll(".match-field, .sort-field, select.col-key")){const t=e.value;e.replaceWith(G(t,[...e.classList].find(o=>["match-field","sort-field","col-key"].includes(o))))}}const u=e=>r.querySelector(`#${e}`);let W={};function Q(){const e=[],t=[];V().forEach((i,a)=>{const p=$e(i);p.error&&t.push({index:a,type:p.type,error:p.error}),e.push({type:p.type,config:p.error?{}:p.config})});const o=le().map(i=>({key:i.querySelector(".col-key")?.value?.trim(),label:i.querySelector(".col-label")?.value?.trim()})).filter(i=>i.key).map(i=>({key:i.key,label:i.label||i.key})),n=u("view-rowlevel-enabled").checked,l=u("view-rowlevel-mode").value||"owner";return{payload:{title:u("view-title").value.trim(),slug:u("view-slug").value.trim()||void 0,description:u("view-description").value.trim(),...u("view-bundled").checked?{bundled:!0}:{},connection:u("view-connection").value||"default",pipeline:{source:u("view-source").value,stages:e},display:{mode:u("view-display-mode").value||"table",columns:o,pageSize:Number.parseInt(u("view-page-size").value,10),block:u("view-block-name").value||""},access:{roles:[...r.querySelectorAll(".view-role-cb:checked")].map(i=>i.value),public:u("view-public").checked,rowLevel:n?{mode:l,userKey:u("view-rowlevel-userkey").value||"id",...l==="field"?{field:u("view-rowlevel-field").value.trim()}:{}}:null},meta:{...W,project:u("view-project").value||null}},stageErrors:t}}async function se(e){W=e?.meta||{},u("view-title").value=e?.title||"",u("view-slug").value=e?.slug||"",u("view-description").value=e?.description||"",u("view-source").value=e?.pipeline?.source||"",u("view-bundled").checked=!!e?.bundled,u("view-project").value=e?.meta?.project||"",u("view-connection").value=e?.connection||"default",u("view-display-mode").value=e?.display?.mode||"table",u("view-page-size").value=String(e?.display?.pageSize??25),u("view-block-name").value=e?.display?.block||"";const t=Array.isArray(e?.access?.roles)?e.access.roles:[],o=t.length||e?.access?.public?t:Se;r.querySelectorAll(".view-role-cb").forEach(l=>{l.checked=o.includes(l.value)});for(const l of o)r.querySelector(`.view-role-cb[value="${CSS.escape(l)}"]`)||r.querySelector("#view-roles-checkboxes").append(s("label",{title:"This role no longer exists"},s("input",{type:"checkbox",class:"view-role-cb",value:l,checked:!0}),document.createTextNode(`${l} (missing)`)));u("view-public").checked=!!e?.access?.public;const n=e?.access?.rowLevel;u("view-rowlevel-enabled").checked=!!n,u("view-rowlevel-mode").value=n?.mode||"owner",u("view-rowlevel-userkey").value=n?.userKey||"id",u("view-rowlevel-field").value=n?.field||"",await Re(e?.pipeline?.source||""),k.replaceChildren();for(const l of e?.pipeline?.stages||[])k.append(Ee(l));I.scan(k),Me(e?e.display?.columns||[]:null),Le(),g()}function Le(){const e=u("view-display-mode").value||"table";r.querySelector("#view-columns-section").hidden=e!=="table",r.querySelector("#view-block-section").hidden=e!=="block";const t=u("view-rowlevel-enabled").checked;r.querySelector("#view-rowlevel-config").hidden=!t,r.querySelector("#view-rowlevel-field-group").hidden=!t||u("view-rowlevel-mode").value!=="field",u("view-slug").readOnly=!b}const ie=M.observable(0);function g(){ie.value=ie.peek()+1}const y=M.computed(()=>(ie.value,Q())),O=M.observable(""),X=M.observable(!1),H=M.computed(()=>kt(y.value.payload,{stageErrors:y.value.stageErrors,isNew:b,taken:be})),Z=M.computed(()=>{if(P(y.value.payload)===O.value)return!1;if(b){const t=y.value.payload;return!!(t.title||t.description||t.pipeline.source||t.pipeline.stages.length)}return!0}),at=M.computed(()=>y.value.payload.pipeline.stages.length),Te=M.computed(()=>{const e=He(H.value);if(!e)return"";const t=Object.values(H.value).flat()[0];return e===1?t:`${t} (+${e-1} more)`}),nt=M.computed(()=>{if(y.value,!b)return"Cannot change once the view exists.";const e=y.value.payload,t=e.slug?Ye(e.slug):Ye(e.title);return t?`Saved as "${t}".`:""});d(M.effect(()=>{const{payload:e,stageErrors:t}=y.value,o=new Set(t.map(n=>n.index));V().forEach((n,l)=>{n.querySelector(".vex-stage-no").textContent=String(l+1);const c=t.find(a=>a.index===l);n.querySelector(".vex-stage-sum").textContent=c?"":Lt(e.pipeline.stages[l]),n.classList.toggle("has-error",o.has(l));const i=n.querySelector(".vex-stage-err");i&&(i.hidden=!c,i.textContent=c?.error||"")})})),d(M.effect(()=>{const e=y.value.payload.title;It(m,e||(b?"New view":f))}));const J=r.querySelector(".vex-results"),T=M.observable(0),ee=M.observable(""),z=M.observable("Open this tab to run the pipeline."),re=M.observable("");let U=[],ce=!1,ue=0,de=null;async function pe(){const{payload:e,stageErrors:t}=y.peek(),o=++ue;if(re.value="",!e.pipeline.source){z.value="Choose the source collection (Source tab) to see results.",J.replaceChildren(),ee.value="";return}const n=T.peek()||e.pipeline.stages.length,l=t.find(c=>c.index<n);if(l){z.value=`Fix stage ${l.index+1} first: ${l.error}`;return}z.value="Running\u2026";try{const c=await _e("/views/_preview",{method:"POST",body:JSON.stringify({source:e.pipeline.source,connection:e.connection,stages:e.pipeline.stages.slice(0,n),limit:Math.min(e.display.pageSize||10,25)})});if(o!==ue||!j())return;U=c.results||[],z.value=U.length?"":"No rows come out of the pipeline.",ee.value=`${c.total} row${c.total===1?"":"s"}${c.total>U.length?` - the first ${U.length}`:""}${T.peek()?` \xB7 up to stage ${T.peek()}`:""}`,lt(U,e)}catch(c){if(o!==ue)return;z.value="",ee.value="",J.replaceChildren(),re.value=`The pipeline did not run: ${c.message||c}`}}function lt(e,t){if(J.replaceChildren(),!e.length)return;const o=$t(e,!T.peek()&&t.display.mode==="table"?t.display.columns:[]),n=s("table");n.append(s("thead",{},s("tr",{},...o.map(c=>s("th",{text:c.label})))));const l=s("tbody");e.forEach((c,i)=>{const a=s("tr",{tabindex:"0","data-i":String(i)});for(const p of o){const oe=St(c,p.key);a.append(s("td",{text:oe,title:oe}))}l.append(a)}),n.append(l),J.append(n)}d(M.effect(()=>{const{payload:e}=y.value;T.value;const t=P({s:e.pipeline,c:e.connection,d:e.display.columns,m:e.display.mode,p:e.display.pageSize});clearTimeout(de),ce&&(de=setTimeout(()=>{j()&&t&&pe()},600))})),d(()=>clearTimeout(de));const st=E.contextMenu(J,{cursor:"glyph",match:"tbody tr",inherit:!1,density:"compact",items:e=>[{header:`Row ${Number(e.dataset.i)+1}`},{label:"Copy as JSON",icon:"copy",action:()=>it(JSON.stringify(U[Number(e.dataset.i)],null,2),"Row copied.")}]});d(()=>st?.destroy?.());function it(e,t){navigator.clipboard?.writeText(e).then(()=>E.toast(t,{type:"success"}),()=>E.toast("Copy failed.",{type:"error"}))}const Ae=E.tabs(r.querySelector(".vex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),rt=[...r.querySelectorAll(".vex-tabs > .tab-list > .tab-item")],ve=4,Ne=()=>{Ae.activate?.(ve),fe(ve)};function fe(e){ce=e===ve,ce&&pe()}rt.forEach((e,t)=>e.addEventListener("click",()=>fe(t)));function ct(e){const t=V().indexOf(e),o=V().length;return[{header:`Stage ${t+1} \xB7 ${e.dataset.stageType}`},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>me(t,-1)},{label:"Move down",icon:"arrow-down",disabled:t===o-1,action:()=>me(t,1)},{label:"Duplicate",icon:"copy",action:()=>{const n=$e(e);if(n.error){E.toast(`Fix this stage first: ${n.error}`,{type:"warning"});return}qe({type:n.type,config:n.config},e).querySelector(".vex-stage-head")?.focus()}},{label:"Results up to here",icon:"play",action:()=>{T.value=t+1,Ne()}},{type:"divider"},{label:"Delete stage",icon:"trash",danger:!0,action:()=>{e.remove(),g()}}]}function me(e,t){const o=Je(V(),e,t);o.forEach(n=>k.append(n)),g(),o[e+t]?.querySelector(".vex-stage-head")?.focus()}const ut=E.contextMenu(k,{cursor:"glyph",match:".stage-card",inherit:!1,density:"compact",items:e=>ct(e)});d(()=>ut?.destroy?.());const Oe=e=>{const t=e.target.closest(".vex-kebab");t&&ae(t)},De=e=>{const t=e.target.closest(".vex-stage-head");if(t){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),ae(t);else if(e.altKey&&(e.key==="ArrowUp"||e.key==="ArrowDown")){e.preventDefault();const o=V().indexOf(t.closest(".stage-card"));me(o,e.key==="ArrowUp"?-1:1)}}};k.addEventListener("click",Oe),k.addEventListener("keydown",De),d(()=>{k.removeEventListener("click",Oe),k.removeEventListener("keydown",De)});const dt=E.contextMenu(x,{cursor:"glyph",match:".col-row",inherit:!1,density:"compact",items:e=>{const t=le(),o=t.indexOf(e),n=l=>{Je(t,o,l).forEach(i=>B.before(i)),g()};return[{header:e.querySelector(".col-label")?.value||e.querySelector(".col-key")?.value||"Column"},{label:"Move up",icon:"arrow-up",disabled:o===0,action:()=>n(-1)},{label:"Move down",icon:"arrow-down",disabled:o===t.length-1,action:()=>n(1)},{type:"divider"},{label:"Remove column",icon:"trash",danger:!0,action:()=>{e.remove(),g()}}]}});d(()=>dt?.destroy?.());const Ie=e=>{const t=e.target.closest(".vex-kebab");t&&ae(t)},Ke=e=>{const t=e.target.closest(".col-row");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ae(t))};x.addEventListener("click",Ie),x.addEventListener("keydown",Ke),d(()=>{x.removeEventListener("click",Ie),x.removeEventListener("keydown",Ke)});function pt(e){const t=e.querySelector(".vex-val"),o=t.querySelector(".match-val")?.value??"";t.replaceChildren(xe(e.querySelector(".match-field")?.value,e.querySelector(".match-op")?.value,o))}const te=e=>{if(!e.target.closest?.(".vex-results")){if(e.type==="change"&&e.target.matches?.(".match-field, .match-op")&&pt(e.target.closest(".match-condition-row")),e.type==="change"&&e.target.matches?.("select.col-key")){const t=e.target.closest(".col-row")?.querySelector(".col-label"),o=L.find(n=>`data.${n.name}`===e.target.value);t&&!t.value&&o&&(t.value=o.label||o.name)}e.target.id==="view-source"&&e.type==="change"&&Re(e.target.value).then(()=>{j()&&(ot(),le().length||Me(null),g())}),["view-display-mode","view-rowlevel-enabled","view-rowlevel-mode"].includes(e.target.id)&&Le(),g()}};r.addEventListener("input",te),r.addEventListener("change",te);const Pe=e=>{e.target.closest?.("input, textarea, select")&&e.stopPropagation()};r.addEventListener("contextmenu",Pe),d(()=>{r.removeEventListener("input",te),r.removeEventListener("change",te),r.removeEventListener("contextmenu",Pe)}),r.querySelector("#add-stage-btn").addEventListener("click",()=>{qe({type:r.querySelector("#add-stage-type").value||"$match",config:{}}).querySelector("select, textarea, input")?.focus()});async function ye(){if(X.peek())return;const{payload:e}=y.peek();if(He(H.peek())){const o=["source","pipeline","display","access"].findIndex(n=>H.peek()[n].length);o>=0&&(Ae.activate?.(o),fe(o)),E.toast(`Not saved - ${Te.peek()}`,{type:"warning",duration:6e3});return}X.value=!0;try{if(b){const n=await C.views.create(e);O.value=P(e);try{S.remove?S.remove(w(null)):S.set(w(null),null)}catch{}he("#/views"),E.toast(`Created "${n.title}".`,{type:"success"}),R.navigate(`/views/edit/${encodeURIComponent(n.slug)}`);return}const o=await C.views.update(f,e);N=o||N,W=o?.meta||W,O.value=P(Q().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}he("#/views"),E.toast("Saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{X.value=!1}}async function vt(){if(await E.confirm("Throw away the changes you have not saved?")){await se(N),O.value=P(Q().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}}}async function ft(){if(!(b||!N)&&await E.confirm(`Delete the view "${Ot(N.title||f)}"? Pages that show it with [view] will show nothing. This cannot be undone.`))try{await C.views.delete(f),O.value=P(y.peek().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}he("#/views"),E.toast("View deleted.",{type:"success"}),R.navigate("/views")}catch(e){E.toast(`Not deleted: ${e.message||e}`,{type:"error"})}}const mt=M.applyBindings({dirty:Z,saving:X,errs:H,stageCount:at,errorSummary:Te,slugHint:nt,upTo:T,previewMeta:ee,previewNote:z,previewError:re},r,{methods:{save:()=>ye(),discard:()=>vt(),runPreview:()=>pe(),allStages:()=>{T.value=0}}});d(()=>mt.dispose()),d(gt(r,{position:"bottom"})),Y(v,{id:"view-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>ye()}),Y(v,{id:"view-editor-results",icon:"play",title:"Results",onClick:()=>{T.value=0,Ne()}}),b||(Y(v,{id:"view-editor-preview",icon:"eye",title:"Open the preview page",onClick:()=>{location.hash=`#/views/${encodeURIComponent(f)}/preview`}}),Y(v,{id:"view-editor-delete",icon:"trash",title:"Delete this view",onClick:()=>ft()})),Y(v,{id:"view-editor-all",icon:"list",title:"All views",onClick:()=>{location.hash="#/views"}});const je=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),ye())};if(document.addEventListener("keydown",je,!0),d(()=>document.removeEventListener("keydown",je,!0)),await se(N),!j())return;O.value=P(Q().payload);const Ve=()=>{if(Z.peek())try{S.set(w(f),{payload:y.peek().payload,base:O.peek(),at:new Date().toISOString()})}catch{}},ze=e=>{Z.peek()&&(Ve(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ze),d(()=>window.removeEventListener("beforeunload",ze)),A={path:We,dirty:()=>Z.peek(),dispose(){ge=!0,Ve(),q.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(r);let K=null;try{K=S.get(w(f))}catch{}if(K?.payload){try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}const e=K.base&&K.base!==O.peek(),t=K.at?` (${D(K.at).format("D MMM HH:mm")})`:"",o=b?"the new view you started":"this view",n=e?`You left unsaved changes to ${o}${t}, but the saved view has changed since. Restore your version anyway?`:`You left unsaved changes to ${o}${t}. Restore them?`;if(await E.confirm(n)&&j()){const l=b?K.payload.slug:f;await se({...K.payload,slug:l})}}}};
|
|
1
|
+
import{api as C,apiRequest as Fe}from"../api.js";import{addBannerAction as Y}from"../lib/plugin-chrome.js";import{helpPopovers as ht}from"../lib/help-popover.js";import{openMenuAt as oe,sidebarCount as ye}from"../lib/tool-kit.js";import{OPERATORS as gt,STAGE_TYPES as _e,adminTierRoles as wt,cellText as bt,checkView as St,draftKey as w,errorCount as Be,isGuided as kt,matchToRows as xt,move as Je,parseStageJson as Et,resultColumns as qt,rowsToMatch as $t,rowsToSort as Ct,slugify as Ye,snapshot as K,sortToRows as Mt,stageSummary as Rt,stageText as Tt}from"../lib/view-editor-arrange.js";import{fmtDateTime as Lt}from"../lib/dates.js";const At=/^\/views\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,ae=v=>String(v||"").split("?")[0].replace(/\/$/,""),Nt=["super-admin","admin","user"],Ot=v=>String(v??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function s(v,m={},...r){const h=document.createElement(v);for(const[q,d]of Object.entries(m))d==null||d===!1||(q==="class"?h.className=d:q==="text"?h.textContent=d:h.setAttribute(q,d===!0?"":d));for(const q of r)q&&h.append(q);return h}let A=null,He=!1;function Dt(){He||!window.R?.use||(He=!0,R.use((v,m,r)=>{const h=ae(m?.path);return!(At.test(m?.path||"")&&ae(v?.path)!==h)||!A?.dirty()?r():E.confirm("This view has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?r():history.replaceState(null,"",`#${m.path}`)})}),M.subscribe("router:afterChange",({to:v}={})=>{if(A&&ae(v?.path)!==A.path){const m=A;A=null,m.dispose()}}))}function It(v,m){const r=v.querySelector(".dm-plugin-banner-name");if(!r)return;const h=r.querySelector(".dm-plugin-banner-sep");if(h){for(;h.nextSibling;)h.nextSibling.remove();h.after(document.createTextNode(m))}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(v){if(Dt(),A){const e=A;A=null,e.dispose()}const m=v.get(0),r=m.querySelector(".vex")||m,h=$(r),q=[],d=e=>(q.push(e),e);let he=!1;const P=()=>!he&&r.isConnected,ge=location.hash.match(/^#\/views\/edit\/([^/?#]+)/),f=ge?decodeURIComponent(ge[1]):null,b=!f,Ge=ae(location.hash.slice(1));let T=[],N=null,we=[],be=[];const Se=E.loader(m,{type:"dots"});try{const[e,t,o,n,l,c]=await Promise.all([C.collections.list().catch(()=>[]),C.collections.getConnections().catch(()=>({})),C.blocks.list().catch(()=>[]),C.projects.list().catch(()=>[]),Fe("/collections/roles/entries?limit=1000").catch(()=>null),C.views.list().catch(()=>[])]);we=(c||[]).map(a=>a.slug),U(r.querySelector("#view-source"),(e||[]).map(a=>[a.slug,`${a.title||a.slug} (${a.slug})`])),U(r.querySelector("#view-connection"),Object.keys(t||{}).filter(a=>a!=="default").map(a=>[a,a])),U(r.querySelector("#view-block-name"),(o||[]).map(a=>[a.name,`${a.name}.html`])),U(r.querySelector("#view-project"),(n||[]).map(a=>[a.slug,a.name||a.slug]));const i=(l?.entries||l?.data||[]).map(a=>a.data||a).filter(a=>a?.name).sort((a,p)=>(a.level??9)-(p.level??9)).map(a=>({name:a.name,label:a.label||a.name,level:a.level}));if(be=wt(i),We(i.length?i:Nt.map(a=>({name:a,label:a}))),U(r.querySelector("#add-stage-type"),_e.map(a=>[a.type,`${a.type} - ${a.label}`])),!b&&(N=await C.views.get(f),!N))throw new Error("View not found")}catch(e){Se.destroy(),E.toast(e.message==="View not found"?"That view does not exist.":`Could not load the view: ${e.message||e}`,{type:"error"}),R.navigate("/views");return}if(Se.destroy(),!P())return;function U(e,t){if(e)for(const[o,n]of t)e.querySelector(`option[value="${CSS.escape(o)}"]`)||e.append(s("option",{value:o,text:n}))}function We(e){const t=r.querySelector("#view-roles-checkboxes");t.replaceChildren();for(const o of e)t.append(s("label",{},s("input",{type:"checkbox",class:"view-role-cb",value:o.name}),document.createTextNode(o.label)))}function Qe(){const e=(T||[]).map(t=>({value:`data.${t.name}`,label:`${t.label||t.name} (${t.name})`}));return e.push({value:"__meta.createdAt",label:"Created (meta)"},{value:"__meta.updatedAt",label:"Updated (meta)"}),e}function F(e,t,o,n){const l=s("select",{class:`form-input form-input--sm ${o||""}`,"aria-label":n});for(const c of e)l.append(s("option",{value:c.value,text:c.label}));return t&&!e.some(c=>String(c.value)===String(t))&&l.append(s("option",{value:t,text:t})),l.value=t??"",l}function H(e,t){return F([{value:"",label:"- field -"},...Qe()],e,t,"Field")}function ke(e,t,o){const n=(T||[]).find(i=>`data.${i.name}`===e);if(n?.type==="select"&&(t==="eq"||t==="ne")&&n.options?.length){const i=n.options.map(a=>typeof a=="string"?{value:a,label:a}:{value:a.value??"",label:a.label||a.value}).filter(a=>a.value&&a.value!=="undefined");return F([{value:"",label:"- value -"},...i],o,"match-val","Value")}const l=t==="in"?"a, b, c":t==="contains"?"text (a pattern)":["gt","lt","gte","lte"].includes(t)?"0":"value",c=s("input",{type:"text",class:"form-input form-input--sm match-val",placeholder:l,"aria-label":"Value"});return c.value=o??"",c}function Xe(e,t){const{logic:o,rows:n}=xt(t),l=F([{value:"and",label:"ALL conditions (and)"},{value:"or",label:"ANY condition (or)"}],o,"match-logic","Match"),c=s("div",{class:"vex-conds"}),i=s("button",{type:"button",class:"btn btn-sm btn-ghost"},s("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Condition")),a=(p={field:"",op:"eq",val:""})=>{const te=H(p.field,"match-field"),mt=F(gt,p.op,"match-op","Operator"),yt=s("span",{class:"vex-val"},ke(p.field,p.op,p.val)),ze=s("button",{type:"button",class:"vex-x",title:"Remove this condition","aria-label":"Remove this condition",text:"\xD7"});ze.addEventListener("click",()=>{Ue.remove(),g()});const Ue=s("div",{class:"vex-cond match-condition-row"},te,mt,yt,ze);c.append(Ue)};i.addEventListener("click",()=>{a(),g()}),e.append(s("div",{class:"vex-logic"},s("span",{class:"vex-muted",text:"Keep rows matching"}),l),c,i),(n.length?n:[void 0]).forEach(p=>a(p))}function Ze(e,t){const o=s("div",{class:"vex-conds"}),n=s("button",{type:"button",class:"btn btn-sm btn-ghost"},s("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Sort field")),l=(i={field:"",dir:1})=>{const a=s("button",{type:"button",class:"vex-x",title:"Remove","aria-label":"Remove this sort",text:"\xD7"}),p=s("div",{class:"vex-sort-row sort-row"},H(i.field,"sort-field"),F([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(i.dir),"sort-dir","Direction"),a);a.addEventListener("click",()=>{p.remove(),g()}),o.append(p)};n.addEventListener("click",()=>{l(),g()}),e.append(o,n);const c=Mt(t);(c.length?c:[void 0]).forEach(i=>l(i))}const k=r.querySelector("#pipeline-stages-list");let et=0;function xe(e){const t=s("div",{class:"vex-stage stage-card","data-stage-type":e.type,"data-sid":String(++et)}),o=s("div",{class:"vex-stage-head",tabindex:"0"},s("span",{class:"vex-stage-no"}),s("code",{class:"vex-stage-type",text:e.type}),s("span",{class:"vex-stage-sum"}),s("button",{type:"button",class:"vex-kebab","aria-label":`Actions for this ${e.type} stage`,title:"Actions",text:"\u22EE"})),n=s("div",{class:"vex-stage-body"});if(kt(e.type)){t.dataset.guided="true";const l=s("div",{class:"stage-guided"});n.append(l),e.type==="$match"?Xe(l,e.config):Ze(l,e.config)}else{const l=_e.find(i=>i.type===e.type),c=s("textarea",{class:"form-input stage-config",rows:"5",spellcheck:"false","aria-label":`${e.type} settings`,placeholder:l?.example||"{}"});c.value=Tt(e.type,e.config),n.append(s("small",{class:"vex-hint",text:`The inside of ${e.type} only - e.g. ${(l?.example||"{}").replace(/\s+/g," ")}`}),c,s("small",{class:"vex-stage-err",hidden:!0}))}return t.append(o,n),t}function Ee(e,t=null){const o=xe(e);return t?t.after(o):k.append(o),I.scan(o),g(),o}function qe(e){const t=e.dataset.stageType;if(e.dataset.guided){const l=e.querySelector(".stage-guided");if(t==="$match"){const i=[...l.querySelectorAll(".match-condition-row")].map(a=>({field:a.querySelector(".match-field")?.value,op:a.querySelector(".match-op")?.value,val:a.querySelector(".match-val")?.value}));return{type:t,config:$t(l.querySelector(".match-logic")?.value,i),error:""}}const c=[...l.querySelectorAll(".sort-row")].map(i=>({field:i.querySelector(".sort-field")?.value,dir:i.querySelector(".sort-dir")?.value}));return{type:t,config:Ct(c),error:""}}const{config:o,error:n}=Et(t,e.querySelector(".stage-config")?.value);return{type:t,config:o,error:n}}const j=()=>[...k.querySelectorAll(":scope > .stage-card")],x=r.querySelector("#view-columns-builder");let _=null;function $e(e={key:"",label:""}){const t=T.length?H(e.key,"col-key"):s("input",{type:"text",class:"form-input form-input--sm col-key font-mono",placeholder:"data.field","aria-label":"Field"});T.length||(t.value=e.key||"");const o=s("input",{type:"text",class:"form-input form-input--sm col-label",placeholder:"Heading","aria-label":"Heading"});o.value=e.label||"";const n=s("button",{type:"button",class:"vex-kebab","aria-label":"Actions for this column",title:"Actions",text:"\u22EE"});return s("div",{class:"vex-col col-row",tabindex:"0"},t,o,n)}function Ce(e){x.replaceChildren();const t=e??T.map(o=>({key:`data.${o.name}`,label:o.label||o.name}));for(const o of t)x.append($e(o));_=s("button",{type:"button",class:"btn btn-sm btn-ghost vex-col-add"},s("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Column")),_.addEventListener("click",()=>{_.before($e()),I.scan(x),g()}),x.append(_),I.scan(x)}const ne=()=>[...x.querySelectorAll(":scope > .col-row")];async function Me(e){if(!e){T=[];return}try{T=(await C.collections.get(e))?.fields||[]}catch{T=[]}}function tt(){for(const e of r.querySelectorAll(".match-field, .sort-field, select.col-key")){const t=e.value;e.replaceWith(H(t,[...e.classList].find(o=>["match-field","sort-field","col-key"].includes(o))))}}const u=e=>r.querySelector(`#${e}`);let G={};function W(){const e=[],t=[];j().forEach((i,a)=>{const p=qe(i);p.error&&t.push({index:a,type:p.type,error:p.error}),e.push({type:p.type,config:p.error?{}:p.config})});const o=ne().map(i=>({key:i.querySelector(".col-key")?.value?.trim(),label:i.querySelector(".col-label")?.value?.trim()})).filter(i=>i.key).map(i=>({key:i.key,label:i.label||i.key})),n=u("view-rowlevel-enabled").checked,l=u("view-rowlevel-mode").value||"owner";return{payload:{title:u("view-title").value.trim(),slug:u("view-slug").value.trim()||void 0,description:u("view-description").value.trim(),...u("view-bundled").checked?{bundled:!0}:{},connection:u("view-connection").value||"default",pipeline:{source:u("view-source").value,stages:e},display:{mode:u("view-display-mode").value||"table",columns:o,pageSize:Number.parseInt(u("view-page-size").value,10),block:u("view-block-name").value||""},access:{roles:[...r.querySelectorAll(".view-role-cb:checked")].map(i=>i.value),public:u("view-public").checked,rowLevel:n?{mode:l,userKey:u("view-rowlevel-userkey").value||"id",...l==="field"?{field:u("view-rowlevel-field").value.trim()}:{}}:null},meta:{...G,project:u("view-project").value||null}},stageErrors:t}}async function le(e){G=e?.meta||{},u("view-title").value=e?.title||"",u("view-slug").value=e?.slug||"",u("view-description").value=e?.description||"",u("view-source").value=e?.pipeline?.source||"",u("view-bundled").checked=!!e?.bundled,u("view-project").value=e?.meta?.project||"",u("view-connection").value=e?.connection||"default",u("view-display-mode").value=e?.display?.mode||"table",u("view-page-size").value=String(e?.display?.pageSize??25),u("view-block-name").value=e?.display?.block||"";const t=Array.isArray(e?.access?.roles)?e.access.roles:[],o=t.length||e?.access?.public?t:be;r.querySelectorAll(".view-role-cb").forEach(l=>{l.checked=o.includes(l.value)});for(const l of o)r.querySelector(`.view-role-cb[value="${CSS.escape(l)}"]`)||r.querySelector("#view-roles-checkboxes").append(s("label",{title:"This role no longer exists"},s("input",{type:"checkbox",class:"view-role-cb",value:l,checked:!0}),document.createTextNode(`${l} (missing)`)));u("view-public").checked=!!e?.access?.public;const n=e?.access?.rowLevel;u("view-rowlevel-enabled").checked=!!n,u("view-rowlevel-mode").value=n?.mode||"owner",u("view-rowlevel-userkey").value=n?.userKey||"id",u("view-rowlevel-field").value=n?.field||"",await Me(e?.pipeline?.source||""),k.replaceChildren();for(const l of e?.pipeline?.stages||[])k.append(xe(l));I.scan(k),Ce(e?e.display?.columns||[]:null),Re(),g()}function Re(){const e=u("view-display-mode").value||"table";r.querySelector("#view-columns-section").hidden=e!=="table",r.querySelector("#view-block-section").hidden=e!=="block";const t=u("view-rowlevel-enabled").checked;r.querySelector("#view-rowlevel-config").hidden=!t,r.querySelector("#view-rowlevel-field-group").hidden=!t||u("view-rowlevel-mode").value!=="field",u("view-slug").readOnly=!b}const se=M.observable(0);function g(){se.value=se.peek()+1}const y=M.computed(()=>(se.value,W())),O=M.observable(""),Q=M.observable(!1),B=M.computed(()=>St(y.value.payload,{stageErrors:y.value.stageErrors,isNew:b,taken:we})),X=M.computed(()=>{if(K(y.value.payload)===O.value)return!1;if(b){const t=y.value.payload;return!!(t.title||t.description||t.pipeline.source||t.pipeline.stages.length)}return!0}),ot=M.computed(()=>y.value.payload.pipeline.stages.length),Te=M.computed(()=>{const e=Be(B.value);if(!e)return"";const t=Object.values(B.value).flat()[0];return e===1?t:`${t} (+${e-1} more)`}),at=M.computed(()=>{if(y.value,!b)return"Cannot change once the view exists.";const e=y.value.payload,t=e.slug?Ye(e.slug):Ye(e.title);return t?`Saved as "${t}".`:""});d(M.effect(()=>{const{payload:e,stageErrors:t}=y.value,o=new Set(t.map(n=>n.index));j().forEach((n,l)=>{n.querySelector(".vex-stage-no").textContent=String(l+1);const c=t.find(a=>a.index===l);n.querySelector(".vex-stage-sum").textContent=c?"":Rt(e.pipeline.stages[l]),n.classList.toggle("has-error",o.has(l));const i=n.querySelector(".vex-stage-err");i&&(i.hidden=!c,i.textContent=c?.error||"")})})),d(M.effect(()=>{const e=y.value.payload.title;It(m,e||(b?"New view":f))}));const J=r.querySelector(".vex-results"),L=M.observable(0),Z=M.observable(""),V=M.observable("Open this tab to run the pipeline."),ie=M.observable("");let z=[],re=!1,ce=0,ue=null;async function de(){const{payload:e,stageErrors:t}=y.peek(),o=++ce;if(ie.value="",!e.pipeline.source){V.value="Choose the source collection (Source tab) to see results.",J.replaceChildren(),Z.value="";return}const n=L.peek()||e.pipeline.stages.length,l=t.find(c=>c.index<n);if(l){V.value=`Fix stage ${l.index+1} first: ${l.error}`;return}V.value="Running\u2026";try{const c=await Fe("/views/_preview",{method:"POST",body:JSON.stringify({source:e.pipeline.source,connection:e.connection,stages:e.pipeline.stages.slice(0,n),limit:Math.min(e.display.pageSize||10,25)})});if(o!==ce||!P())return;z=c.results||[],V.value=z.length?"":"No rows come out of the pipeline.",Z.value=`${c.total} row${c.total===1?"":"s"}${c.total>z.length?` - the first ${z.length}`:""}${L.peek()?` \xB7 up to stage ${L.peek()}`:""}`,nt(z,e)}catch(c){if(o!==ce)return;V.value="",Z.value="",J.replaceChildren(),ie.value=`The pipeline did not run: ${c.message||c}`}}function nt(e,t){if(J.replaceChildren(),!e.length)return;const o=qt(e,!L.peek()&&t.display.mode==="table"?t.display.columns:[]),n=s("table");n.append(s("thead",{},s("tr",{},...o.map(c=>s("th",{text:c.label})))));const l=s("tbody");e.forEach((c,i)=>{const a=s("tr",{tabindex:"0","data-i":String(i)});for(const p of o){const te=bt(c,p.key);a.append(s("td",{text:te,title:te}))}l.append(a)}),n.append(l),J.append(n)}d(M.effect(()=>{const{payload:e}=y.value;L.value;const t=K({s:e.pipeline,c:e.connection,d:e.display.columns,m:e.display.mode,p:e.display.pageSize});clearTimeout(ue),re&&(ue=setTimeout(()=>{P()&&t&&de()},600))})),d(()=>clearTimeout(ue));const lt=E.contextMenu(J,{cursor:"glyph",match:"tbody tr",inherit:!1,density:"compact",items:e=>[{header:`Row ${Number(e.dataset.i)+1}`},{label:"Copy as JSON",icon:"copy",action:()=>st(JSON.stringify(z[Number(e.dataset.i)],null,2),"Row copied.")}]});d(()=>lt?.destroy?.());function st(e,t){navigator.clipboard?.writeText(e).then(()=>E.toast(t,{type:"success"}),()=>E.toast("Copy failed.",{type:"error"}))}const Le=E.tabs(r.querySelector(".vex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),it=[...r.querySelectorAll(".vex-tabs > .tab-list > .tab-item")],pe=4,Ae=()=>{Le.activate?.(pe),ve(pe)};function ve(e){re=e===pe,re&&de()}it.forEach((e,t)=>e.addEventListener("click",()=>ve(t)));function rt(e){const t=j().indexOf(e),o=j().length;return[{header:`Stage ${t+1} \xB7 ${e.dataset.stageType}`},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>fe(t,-1)},{label:"Move down",icon:"arrow-down",disabled:t===o-1,action:()=>fe(t,1)},{label:"Duplicate",icon:"copy",action:()=>{const n=qe(e);if(n.error){E.toast(`Fix this stage first: ${n.error}`,{type:"warning"});return}Ee({type:n.type,config:n.config},e).querySelector(".vex-stage-head")?.focus()}},{label:"Results up to here",icon:"play",action:()=>{L.value=t+1,Ae()}},{type:"divider"},{label:"Delete stage",icon:"trash",danger:!0,action:()=>{e.remove(),g()}}]}function fe(e,t){const o=Je(j(),e,t);o.forEach(n=>k.append(n)),g(),o[e+t]?.querySelector(".vex-stage-head")?.focus()}const ct=E.contextMenu(k,{cursor:"glyph",match:".stage-card",inherit:!1,density:"compact",items:e=>rt(e)});d(()=>ct?.destroy?.());const Ne=e=>{const t=e.target.closest(".vex-kebab");t&&oe(t)},Oe=e=>{const t=e.target.closest(".vex-stage-head");if(t){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),oe(t);else if(e.altKey&&(e.key==="ArrowUp"||e.key==="ArrowDown")){e.preventDefault();const o=j().indexOf(t.closest(".stage-card"));fe(o,e.key==="ArrowUp"?-1:1)}}};k.addEventListener("click",Ne),k.addEventListener("keydown",Oe),d(()=>{k.removeEventListener("click",Ne),k.removeEventListener("keydown",Oe)});const ut=E.contextMenu(x,{cursor:"glyph",match:".col-row",inherit:!1,density:"compact",items:e=>{const t=ne(),o=t.indexOf(e),n=l=>{Je(t,o,l).forEach(i=>_.before(i)),g()};return[{header:e.querySelector(".col-label")?.value||e.querySelector(".col-key")?.value||"Column"},{label:"Move up",icon:"arrow-up",disabled:o===0,action:()=>n(-1)},{label:"Move down",icon:"arrow-down",disabled:o===t.length-1,action:()=>n(1)},{type:"divider"},{label:"Remove column",icon:"trash",danger:!0,action:()=>{e.remove(),g()}}]}});d(()=>ut?.destroy?.());const De=e=>{const t=e.target.closest(".vex-kebab");t&&oe(t)},Ie=e=>{const t=e.target.closest(".col-row");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),oe(t))};x.addEventListener("click",De),x.addEventListener("keydown",Ie),d(()=>{x.removeEventListener("click",De),x.removeEventListener("keydown",Ie)});function dt(e){const t=e.querySelector(".vex-val"),o=t.querySelector(".match-val")?.value??"";t.replaceChildren(ke(e.querySelector(".match-field")?.value,e.querySelector(".match-op")?.value,o))}const ee=e=>{if(!e.target.closest?.(".vex-results")){if(e.type==="change"&&e.target.matches?.(".match-field, .match-op")&&dt(e.target.closest(".match-condition-row")),e.type==="change"&&e.target.matches?.("select.col-key")){const t=e.target.closest(".col-row")?.querySelector(".col-label"),o=T.find(n=>`data.${n.name}`===e.target.value);t&&!t.value&&o&&(t.value=o.label||o.name)}e.target.id==="view-source"&&e.type==="change"&&Me(e.target.value).then(()=>{P()&&(tt(),ne().length||Ce(null),g())}),["view-display-mode","view-rowlevel-enabled","view-rowlevel-mode"].includes(e.target.id)&&Re(),g()}};r.addEventListener("input",ee),r.addEventListener("change",ee);const Ke=e=>{e.target.closest?.("input, textarea, select")&&e.stopPropagation()};r.addEventListener("contextmenu",Ke),d(()=>{r.removeEventListener("input",ee),r.removeEventListener("change",ee),r.removeEventListener("contextmenu",Ke)}),r.querySelector("#add-stage-btn").addEventListener("click",()=>{Ee({type:r.querySelector("#add-stage-type").value||"$match",config:{}}).querySelector("select, textarea, input")?.focus()});async function me(){if(Q.peek())return;const{payload:e}=y.peek();if(Be(B.peek())){const o=["source","pipeline","display","access"].findIndex(n=>B.peek()[n].length);o>=0&&(Le.activate?.(o),ve(o)),E.toast(`Not saved - ${Te.peek()}`,{type:"warning",duration:6e3});return}Q.value=!0;try{if(b){const n=await C.views.create(e);O.value=K(e);try{S.remove?S.remove(w(null)):S.set(w(null),null)}catch{}ye("#/views"),E.toast(`Created "${n.title}".`,{type:"success"}),R.navigate(`/views/edit/${encodeURIComponent(n.slug)}`);return}const o=await C.views.update(f,e);N=o||N,G=o?.meta||G,O.value=K(W().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}ye("#/views"),E.toast("Saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{Q.value=!1}}async function pt(){if(await E.confirm("Throw away the changes you have not saved?")){await le(N),O.value=K(W().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}}}async function vt(){if(!(b||!N)&&await E.confirm(`Delete the view "${Ot(N.title||f)}"? Pages that show it with [view] will show nothing. This cannot be undone.`))try{await C.views.delete(f),O.value=K(y.peek().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}ye("#/views"),E.toast("View deleted.",{type:"success"}),R.navigate("/views")}catch(e){E.toast(`Not deleted: ${e.message||e}`,{type:"error"})}}const ft=M.applyBindings({dirty:X,saving:Q,errs:B,stageCount:ot,errorSummary:Te,slugHint:at,upTo:L,previewMeta:Z,previewNote:V,previewError:ie},r,{methods:{save:()=>me(),discard:()=>pt(),runPreview:()=>de(),allStages:()=>{L.value=0}}});d(()=>ft.dispose()),d(ht(r,{position:"bottom"})),Y(v,{id:"view-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>me()}),Y(v,{id:"view-editor-results",icon:"play",title:"Results",onClick:()=>{L.value=0,Ae()}}),b||(Y(v,{id:"view-editor-preview",icon:"eye",title:"Open the preview page",onClick:()=>{location.hash=`#/views/${encodeURIComponent(f)}/preview`}}),Y(v,{id:"view-editor-delete",icon:"trash",title:"Delete this view",onClick:()=>vt()})),Y(v,{id:"view-editor-all",icon:"list",title:"All views",onClick:()=>{location.hash="#/views"}});const Pe=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),me())};if(document.addEventListener("keydown",Pe,!0),d(()=>document.removeEventListener("keydown",Pe,!0)),await le(N),!P())return;O.value=K(W().payload);const je=()=>{if(X.peek())try{S.set(w(f),{payload:y.peek().payload,base:O.peek(),at:new Date().toISOString()})}catch{}},Ve=e=>{X.peek()&&(je(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ve),d(()=>window.removeEventListener("beforeunload",Ve)),A={path:Ge,dirty:()=>X.peek(),dispose(){he=!0,je(),q.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(r);let D=null;try{D=S.get(w(f))}catch{}if(D?.payload){try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}const e=D.base&&D.base!==O.peek(),t=D.at?` (${Lt(D.at)})`:"",o=b?"the new view you started":"this view",n=e?`You left unsaved changes to ${o}${t}, but the saved view has changed since. Restore your version anyway?`:`You left unsaved changes to ${o}${t}. Restore them?`;if(await E.confirm(n)&&P()){const l=b?D.payload.slug:f;await le({...D.payload,slug:l})}}}};
|
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Domma CMS - Project Scaffolder
|
|
4
|
-
* Usage: npx domma-cms <project-name> [--no-install] [--no-setup] [--seed]
|
|
4
|
+
* Usage: npx domma-cms <project-name> [--no-install] [--no-setup] [--no-seed]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from 'node:fs';
|
|
@@ -9,6 +9,7 @@ import {spawnSync} from 'node:child_process';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import {fileURLToPath} from 'node:url';
|
|
11
11
|
import {composeSmtp} from './lib/smtp-defaults.js';
|
|
12
|
+
import {minNodeMajor, nodeVersionOk} from './lib/node-version.js';
|
|
12
13
|
import {buildGitignore} from '../server/services/siteGitignore.js';
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
@@ -23,9 +24,13 @@ const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
|
23
24
|
// Node version check
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// The minimum comes from package.json `engines`, and below it the CLI stops:
|
|
28
|
+
// scaffolding or updating on an older Node leaves a site that will not run.
|
|
29
|
+
// The fleet (the manager and every site it forks) runs on Node 24, so fleet
|
|
30
|
+
// rolls pass this check.
|
|
31
|
+
const NODE_RANGE = JSON.parse(readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).engines?.node;
|
|
32
|
+
if (!nodeVersionOk(process.versions.node, NODE_RANGE)) {
|
|
33
|
+
console.error(`\n ✗ Domma CMS requires Node.js ${minNodeMajor(NODE_RANGE)} or higher (this is ${process.versions.node}). Upgrade Node and run this again.\n`);
|
|
29
34
|
process.exit(1);
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -244,14 +249,9 @@ const DEFAULT_NAV = {
|
|
|
244
249
|
position: 'sticky'
|
|
245
250
|
};
|
|
246
251
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
'analytics',
|
|
251
|
-
'theme-roller',
|
|
252
|
-
]);
|
|
253
|
-
|
|
254
|
-
// Discover all plugins and set enabled state accordingly
|
|
252
|
+
// Discover the bundled plugins and list them in plugins.json, all switched off:
|
|
253
|
+
// a fresh project enables what it wants from the Marketplace. (Analytics, the
|
|
254
|
+
// theme switcher and the effects are core now, not plugins.)
|
|
255
255
|
const DEFAULT_PLUGINS = {};
|
|
256
256
|
try {
|
|
257
257
|
const pluginsRoot = path.join(target, 'plugins');
|
|
@@ -262,7 +262,7 @@ try {
|
|
|
262
262
|
const manifestPath = path.join(pluginsRoot, name, 'plugin.json');
|
|
263
263
|
try {
|
|
264
264
|
JSON.parse(readFileSync(manifestPath, 'utf8')); // validate manifest exists
|
|
265
|
-
DEFAULT_PLUGINS[name] = {enabled:
|
|
265
|
+
DEFAULT_PLUGINS[name] = {enabled: false, settings: {}};
|
|
266
266
|
} catch { /* no valid plugin.json - skip */
|
|
267
267
|
}
|
|
268
268
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js version gate for the scaffolder - reads the minimum from package.json
|
|
3
|
+
* `engines.node` so the check and the declared requirement cannot drift apart.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The minimum major version in an `engines.node` range such as ">=24" or ">=24.1.0".
|
|
8
|
+
*
|
|
9
|
+
* @param {string|undefined} range
|
|
10
|
+
* @returns {number|null} null when the range names no minimum
|
|
11
|
+
*/
|
|
12
|
+
export function minNodeMajor(range) {
|
|
13
|
+
const match = /(\d+)/.exec(String(range || ''));
|
|
14
|
+
return match ? Number(match[1]) : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a running Node version (e.g. "24.3.0") meets the range's minimum major.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} version - process.versions.node
|
|
21
|
+
* @param {string|undefined} range - package.json engines.node
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function nodeVersionOk(version, range) {
|
|
25
|
+
const min = minNodeMajor(range);
|
|
26
|
+
if (min === null) return true;
|
|
27
|
+
const [major] = String(version).split('.').map(Number);
|
|
28
|
+
return major >= min;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -70,6 +70,7 @@ function formatSize(bytes) {
|
|
|
70
70
|
|
|
71
71
|
import {applyIdentity, findIdentity, identityFrom, swapSignature} from './identity.js';
|
|
72
72
|
import {describeWhen, quickTimes} from './scheduling.js';
|
|
73
|
+
import {fmtDateTime} from '../ui/dates.js';
|
|
73
74
|
import {applyTemplate, expandTemplate} from './templates.js';
|
|
74
75
|
import {makeResizable} from './resizable.js';
|
|
75
76
|
import {whenClosed} from './when-closed.js';
|
|
@@ -433,7 +434,7 @@ export function openCompose({
|
|
|
433
434
|
const at = scheduledAt.value;
|
|
434
435
|
sendLabel.value = at ? `Send ${describeWhen(at)}` : 'Send';
|
|
435
436
|
sendTitle.value = at
|
|
436
|
-
?
|
|
437
|
+
? fmtDateTime(at)
|
|
437
438
|
: (undoSeconds > 0 ? `Held for ${undoSeconds} seconds so you can take it back.` : '');
|
|
438
439
|
})
|
|
439
440
|
];
|
|
@@ -734,7 +735,7 @@ export function openCompose({
|
|
|
734
735
|
button.type = 'button';
|
|
735
736
|
button.className = 'btn btn-sm btn-ghost';
|
|
736
737
|
button.textContent = option.label;
|
|
737
|
-
button.title =
|
|
738
|
+
button.title = fmtDateTime(option.at);
|
|
738
739
|
button.addEventListener('click', () => choose(option.at));
|
|
739
740
|
pick('choices').appendChild(button);
|
|
740
741
|
}
|
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
const {imageSendersSection} = await import(versioned('image-senders-section.js'));
|
|
37
37
|
const {createSplitters, fitToViewport} = await import(versioned('panes.js'));
|
|
38
38
|
const {createDiagnosticsSection} = await import(versioned('diagnostics-section.js'));
|
|
39
|
+
const {fmtDate, fmtDateTime} = await import(new URL(`../ui/dates.js?v=${ASSET_VERSION}`, import.meta.url).href);
|
|
39
40
|
// Renamed: onMount wraps it in its own `openCompose`, which tracks each window it opens.
|
|
40
41
|
const {openCompose: openComposeWindow} = await import(versioned('compose-window.js'));
|
|
41
42
|
const {makeResizable} = await import(versioned('resizable.js'));
|
|
@@ -92,9 +93,9 @@ async function api(url, method = 'GET', body) {
|
|
|
92
93
|
function formatDate(value) {
|
|
93
94
|
if (!value) return '';
|
|
94
95
|
const d = D(value);
|
|
95
|
-
return d.format('DD
|
|
96
|
+
return d.format('YYYY-MM-DD') === D().format('YYYY-MM-DD')
|
|
96
97
|
? d.format('HH:mm')
|
|
97
|
-
:
|
|
98
|
+
: fmtDate(value);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -3177,7 +3178,7 @@ return {
|
|
|
3177
3178
|
const button = document.createElement('button');
|
|
3178
3179
|
button.className = 'btn btn-sm btn-ghost';
|
|
3179
3180
|
button.textContent = option.label;
|
|
3180
|
-
button.title =
|
|
3181
|
+
button.title = fmtDateTime(option.at);
|
|
3181
3182
|
button.addEventListener('click', () => finish(option.at));
|
|
3182
3183
|
choices.appendChild(button);
|
|
3183
3184
|
}
|
|
@@ -3237,7 +3238,7 @@ return {
|
|
|
3237
3238
|
|
|
3238
3239
|
removeFromList(message.uid);
|
|
3239
3240
|
offerUndo(
|
|
3240
|
-
`Snoozed until ${
|
|
3241
|
+
`Snoozed until ${fmtDateTime(result.at)}.`,
|
|
3241
3242
|
// Bringing it straight back is the undo. The message has
|
|
3242
3243
|
// already moved, so there is no "leave it where it was".
|
|
3243
3244
|
async () => {
|
|
@@ -3511,7 +3512,7 @@ return {
|
|
|
3511
3512
|
text.textContent = [
|
|
3512
3513
|
from && `From ${from}`,
|
|
3513
3514
|
formatAddresses(message.to) && `to ${formatAddresses(message.to)}`,
|
|
3514
|
-
message.date &&
|
|
3515
|
+
message.date && fmtDateTime(message.date)
|
|
3515
3516
|
].filter(Boolean).join(' \u00b7 ');
|
|
3516
3517
|
summary.appendChild(text);
|
|
3517
3518
|
|
|
@@ -3536,7 +3537,7 @@ return {
|
|
|
3536
3537
|
// "Priority: Normal" is saying nothing at length.
|
|
3537
3538
|
['Priority', message.priority === 'high' ? 'High'
|
|
3538
3539
|
: (message.priority === 'low' ? 'Low' : '')],
|
|
3539
|
-
['Date', message.date ? D(message.date).format('
|
|
3540
|
+
['Date', message.date ? `${fmtDateTime(message.date)}:${D(message.date).format('ss')}` : ''],
|
|
3540
3541
|
['Subject', message.subject],
|
|
3541
3542
|
['Message ID', message.messageId]
|
|
3542
3543
|
];
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* @module _lib/admin/mail/scheduling
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import {fmtDate, fmtTime} from '../ui/dates.js';
|
|
24
|
+
|
|
23
25
|
/** How long a sent message may be held back so it can be recalled. */
|
|
24
26
|
export const UNDO_SECONDS = [0, 5, 10, 20, 30];
|
|
25
27
|
|
|
@@ -168,7 +170,7 @@ export function describeWhen(value, now = new Date()) {
|
|
|
168
170
|
const minutes = Math.round(seconds / 60);
|
|
169
171
|
if (minutes < 60) return `in ${minutes} minutes`;
|
|
170
172
|
|
|
171
|
-
const clock = when
|
|
173
|
+
const clock = fmtTime(when);
|
|
172
174
|
const sameDay = when.toDateString() === now.toDateString();
|
|
173
175
|
if (sameDay) return `at ${clock}`;
|
|
174
176
|
|
|
@@ -180,7 +182,7 @@ export function describeWhen(value, now = new Date()) {
|
|
|
180
182
|
// date is - "Thursday" three weeks out tells you nothing.
|
|
181
183
|
const days = Math.round((when.getTime() - now.getTime()) / 86_400_000);
|
|
182
184
|
if (days < 7) return `on ${when.toLocaleDateString([], {weekday: 'long'})} at ${clock}`;
|
|
183
|
-
return `on ${when
|
|
185
|
+
return `on ${fmtDate(when)} at ${clock}`;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
/**
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* @module _lib/admin/mail/templates
|
|
27
27
|
*/
|
|
28
28
|
import {signatureToText, textToHtml} from './identity.js';
|
|
29
|
+
import {fmtDate, fmtTime} from '../ui/dates.js';
|
|
29
30
|
|
|
30
31
|
/** How many templates one mailbox may hold. */
|
|
31
32
|
export const MAX_TEMPLATES = 50;
|
|
@@ -144,10 +145,9 @@ export function templateValues({to = [], subject = '', identity = null, fromAddr
|
|
|
144
145
|
'my.name': String(identity?.displayName ?? '').trim(),
|
|
145
146
|
'my.email': String(identity?.address || fromAddress || '').trim(),
|
|
146
147
|
subject: String(subject ?? '').trim(),
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
time: now.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})
|
|
148
|
+
// dd/mm/yyyy and 24-hour, whatever the browser's locale - never 9/20/2026.
|
|
149
|
+
date: fmtDate(now),
|
|
150
|
+
time: fmtTime(now)
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dates as a plugin shows them to a person: dd/mm/yyyy, and dd/mm/yyyy HH:mm
|
|
3
|
+
* where a time matters. The engine's twins are admin/js/lib/dates.js and
|
|
4
|
+
* server/services/dates.js; plugins must not import either (the updater
|
|
5
|
+
* replaces admin/ and server/), so they import this one.
|
|
6
|
+
*
|
|
7
|
+
* Isomorphic: a plugin's admin views import it over HTTP
|
|
8
|
+
* (`/plugins/_lib/admin/ui/dates.js`), its server code by relative path
|
|
9
|
+
* (`../_lib/admin/ui/dates.js`).
|
|
10
|
+
*
|
|
11
|
+
* Display only. Stored values, API payloads, `<input type="date">` values,
|
|
12
|
+
* sitemap/RSS/iCal/JSON-LD, filenames, logs and sort keys stay ISO.
|
|
13
|
+
*
|
|
14
|
+
* Local time unless `{utc: true}` is passed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
18
|
+
|
|
19
|
+
/** A bare calendar day. Parsed as a LOCAL day: `new Date('2026-09-21')` is UTC midnight. */
|
|
20
|
+
const DAY_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A Date from whatever the API sent - ISO string, bare 'YYYY-MM-DD', epoch ms
|
|
24
|
+
* or a Date. Null for empty or unparseable input.
|
|
25
|
+
*
|
|
26
|
+
* @param {string|number|Date|null|undefined} value
|
|
27
|
+
* @returns {Date|null}
|
|
28
|
+
*/
|
|
29
|
+
export function toDate(value) {
|
|
30
|
+
if (value == null || value === '') return null;
|
|
31
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
const m = DAY_ONLY.exec(value.trim());
|
|
34
|
+
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
35
|
+
}
|
|
36
|
+
const d = new Date(value);
|
|
37
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isDayOnly = (value) => typeof value === 'string' && DAY_ONLY.test(value.trim());
|
|
41
|
+
|
|
42
|
+
/** Empty for nothing; the raw text for something that is not a date, so it is never silently lost. */
|
|
43
|
+
const unparsed = (value) => (value == null ? '' : String(value));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 'dd/mm/yyyy'.
|
|
47
|
+
*
|
|
48
|
+
* @param {string|number|Date|null|undefined} value
|
|
49
|
+
* @param {{utc?: boolean}} [opts]
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function fmtDate(value, {utc = false} = {}) {
|
|
53
|
+
const d = toDate(value);
|
|
54
|
+
if (!d) return unparsed(value);
|
|
55
|
+
if (utc && !isDayOnly(value)) return `${pad(d.getUTCDate())}/${pad(d.getUTCMonth() + 1)}/${d.getUTCFullYear()}`;
|
|
56
|
+
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 'HH:mm' (24-hour).
|
|
61
|
+
*
|
|
62
|
+
* @param {string|number|Date|null|undefined} value
|
|
63
|
+
* @param {{utc?: boolean}} [opts]
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function fmtTime(value, {utc = false} = {}) {
|
|
67
|
+
const d = toDate(value);
|
|
68
|
+
if (!d) return unparsed(value);
|
|
69
|
+
if (utc) return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
70
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 'dd/mm/yyyy HH:mm'. A bare 'YYYY-MM-DD' has no time to show, so it gets the date alone.
|
|
75
|
+
*
|
|
76
|
+
* @param {string|number|Date|null|undefined} value
|
|
77
|
+
* @param {{utc?: boolean}} [opts]
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function fmtDateTime(value, {utc = false} = {}) {
|
|
81
|
+
if (isDayOnly(value)) return fmtDate(value);
|
|
82
|
+
const d = toDate(value);
|
|
83
|
+
if (!d) return unparsed(value);
|
|
84
|
+
return `${fmtDate(d, {utc})} ${fmtTime(d, {utc})}`;
|
|
85
|
+
}
|
package/plugins/blog/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Public-facing blog for the CMS. Supports posts with categories, tags, and optional moderated comments. Authors can draft posts; only blog-
|
|
5
|
+
Public-facing blog for the CMS. Supports posts with categories, tags, and optional moderated comments. Authors can draft posts; only holders of the `blog-posts.publish` permission (Blog Editor, admins) can publish or schedule them. Public SSR routes serve an archive index, category/tag/author archives, individual post pages, and an RSS feed. All collection I/O goes through `server/services/collections.js` (FileAdapter by default).
|
|
6
6
|
|
|
7
7
|
## Files
|
|
8
8
|
|
|
@@ -33,26 +33,27 @@ All routes are under `/api/plugins/blog`. All require authentication unless note
|
|
|
33
33
|
|
|
34
34
|
| Method | Path | Purpose | Auth |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| GET | `/posts` | List posts;
|
|
37
|
-
| POST | `/posts` | Create a new post (draft by default) |
|
|
38
|
-
| GET | `/posts/:id` | Get a single post by ID |
|
|
39
|
-
| PUT | `/posts/:id` | Update post;
|
|
40
|
-
| DELETE | `/posts/:id` | Delete post
|
|
41
|
-
| POST | `/posts/:id/publish` | Set status to `published
|
|
42
|
-
| POST | `/posts/:id/schedule` | Set status to `scheduled` + `publishedAt
|
|
43
|
-
| GET | `/categories` | List all categories |
|
|
44
|
-
| POST | `/categories` | Create category
|
|
45
|
-
| PUT | `/categories/:id` | Update category
|
|
46
|
-
| DELETE | `/categories/:id` | Delete category
|
|
47
|
-
| GET | `/comments` | List comments; supports `?status=` and `?postId=` filters |
|
|
36
|
+
| GET | `/posts` | List posts; a writer without publish sees only their own | `blog-posts.read` |
|
|
37
|
+
| POST | `/posts` | Create a new post (draft by default) | `blog-posts.create` |
|
|
38
|
+
| GET | `/posts/:id` | Get a single post by ID (others' posts 404 for a writer) | `blog-posts.read` |
|
|
39
|
+
| PUT | `/posts/:id` | Update post; a writer without publish may only edit their own | `blog-posts.update` |
|
|
40
|
+
| DELETE | `/posts/:id` | Delete post | `blog-posts.delete` |
|
|
41
|
+
| POST | `/posts/:id/publish` | Set status to `published` | `blog-posts.publish` |
|
|
42
|
+
| POST | `/posts/:id/schedule` | Set status to `scheduled` + `publishedAt` | `blog-posts.publish` |
|
|
43
|
+
| GET | `/categories` | List all categories | `blog-categories.read` |
|
|
44
|
+
| POST | `/categories` | Create category | `blog-categories.create` |
|
|
45
|
+
| PUT | `/categories/:id` | Update category | `blog-categories.update` |
|
|
46
|
+
| DELETE | `/categories/:id` | Delete category | `blog-categories.delete` |
|
|
47
|
+
| GET | `/comments` | List comments; supports `?status=` and `?postId=` filters | `blog-comments.read` |
|
|
48
|
+
| GET | `/comments/pending-count` | The sidebar badge | `blog-comments.read` |
|
|
48
49
|
| POST | `/comments` | Submit a comment; **no authentication required** (public route) | - |
|
|
49
|
-
| PUT | `/comments/:id` | Moderate comment (update status field) |
|
|
50
|
-
| DELETE | `/comments/:id` | Delete comment |
|
|
51
|
-
| GET | `/layouts` | Post layouts and listing displays on offer, plus the defaults |
|
|
52
|
-
| POST | `/preview` | A post page from UNSAVED fields, in any layout (`{html, layout}`) |
|
|
50
|
+
| PUT | `/comments/:id` | Moderate comment (update status field) | `blog-comments.update` |
|
|
51
|
+
| DELETE | `/comments/:id` | Delete comment | `blog-comments.delete` |
|
|
52
|
+
| GET | `/layouts` | Post layouts and listing displays on offer, plus the defaults | `blog-posts.read` |
|
|
53
|
+
| POST | `/preview` | A post page from UNSAVED fields, in any layout (`{html, layout}`) | `blog-posts.read` |
|
|
53
54
|
| POST | `/samples` | Add the sample posts (idempotent by slug) | authenticate + requireAdmin |
|
|
54
55
|
| DELETE | `/samples` | Remove `sample: true` posts, and sample categories nothing uses | authenticate + requireAdmin |
|
|
55
|
-
| GET | `/settings` | Read blog plugin settings |
|
|
56
|
+
| GET | `/settings` | Read blog plugin settings | `blog-posts.read` |
|
|
56
57
|
| PUT | `/settings` | Save blog plugin settings | authenticate + requireAdmin |
|
|
57
58
|
|
|
58
59
|
## Public Routes
|
|
@@ -134,7 +135,9 @@ Two custom roles are seeded for this plugin:
|
|
|
134
135
|
- **`blog-author`** - can create posts and edit their own; cannot publish, schedule, or delete
|
|
135
136
|
- **`blog-editor`** - can do everything including publish, schedule, delete, and manage categories/comments
|
|
136
137
|
|
|
137
|
-
Both roles are at **level 2** in the role hierarchy (same as `editor`)
|
|
138
|
+
Both roles are at **level 2** in the role hierarchy (same as `editor`), so no level comparison can tell them apart.
|
|
139
|
+
What they may do is decided by **permission** since 1.8.0 (see "Access - by permission" below), never by role name
|
|
140
|
+
or level: any role can be given the same powers under Roles.
|
|
138
141
|
|
|
139
142
|
## Embed Shortcodes
|
|
140
143
|
|
|
@@ -155,7 +158,7 @@ A post page and a listing card are BLOCKS. `blog-post-<name>` is a post layout,
|
|
|
155
158
|
card; any block with those prefixes is offered automatically (`lib/layouts.js`). The shipped ones are
|
|
156
159
|
copied into content/blocks/ on start, once each (remembered as `seededBlocks` in config/plugins.json),
|
|
157
160
|
so a site edits them in the Blocks tool; a deleted one falls back to `blocks/` here. Placeholders are
|
|
158
|
-
documented in each block's header comment: `{{x}}` escaped, `{{x:
|
|
161
|
+
documented in each block's header comment: `{{x}}` escaped, `{{x:DD/MM/YYYY}}` a date (any format; the shipped blocks use DD/MM/YYYY), `{{{x}}}` raw
|
|
159
162
|
for fragments this plugin builds and escapes (content, image, categories, tags, toc, share, prevNext,
|
|
160
163
|
related, comments). Block CSS is scoped under `[data-block="name"]` like every other block.
|
|
161
164
|
|
|
@@ -252,7 +255,13 @@ with `?display=` and swaps the cards, since each display is different block mark
|
|
|
252
255
|
`createEntry` and came back 500 where the categories and comments routes both
|
|
253
256
|
answer 400.
|
|
254
257
|
|
|
255
|
-
- **
|
|
258
|
+
- **The publish gate is the `blog-posts.publish` permission** (1.8.0, `lib/access.js`). Before that it was a
|
|
259
|
+
named-role list (`['blog-editor', 'admin', 'super-admin'].includes(user.role)`). Never go back to either a role
|
|
260
|
+
name or a level check: `blog-author` and `blog-editor` are both level 2, so a level check would give authors
|
|
261
|
+
publish access, and a role list ignores additional roles and roles an admin made.
|
|
262
|
+
|
|
263
|
+
- **Settings has no sidebar item, on purpose**: it is the banner cog on every Blog screen (`admin/views/settings-cog.js`),
|
|
264
|
+
the house rule in `domma-cms/plugins/CLAUDE.md`. The old `#/plugins/blog/settings` page still opens it.
|
|
256
265
|
|
|
257
266
|
- **The base path is read per request, so the public pages are an `onRequest` hook, not routes.**
|
|
258
267
|
Fastify routes are fixed once the server is ready, so pages registered at `/blog` stayed there after
|
|
@@ -274,7 +283,7 @@ with `?display=` and swaps the cards, since each display is different block mark
|
|
|
274
283
|
|
|
275
284
|
- **plugin.public.js registered at Fastify root**: This file is registered without the `/api/plugins/blog` prefix. Routes use the full absolute path starting from `base` (default `/blog`). The server registers it separately from `plugin.js`.
|
|
276
285
|
|
|
277
|
-
- **`storage.adapter: 'file'` default**:
|
|
286
|
+
- **`storage.adapter: 'file'` default**: blog defaults to `file` so it works on free installs without MongoDB. Do not change this default. (Messages likewise has no MongoDB default - older notes said it had.)
|
|
278
287
|
|
|
279
288
|
- **`listEntries` called with bare slug strings**: In `plugin.public.js`, `listEntries('posts')`, `listEntries('categories')`, and `listEntries('comments')` are called with plain string slugs (not constants). This is consistent with the rest of the public file but differs from `plugin.js`, which uses `POSTS_SLUG` / `CATEGORIES_SLUG` / `COMMENTS_SLUG` constants.
|
|
280
289
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// The loader's cache-busting token, passed on so helpers are refetched with this view.
|
|
14
|
+
import {fmtDate, fmtDateTime} from '/plugins/_lib/admin/ui/dates.js';
|
|
14
15
|
const ASSET_VERSION = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
15
16
|
|
|
16
17
|
const STATUSES = [
|
|
@@ -96,8 +97,8 @@ export const blogView = {
|
|
|
96
97
|
status: p.status ?? 'draft',
|
|
97
98
|
badge: BADGE[p.status] || 'badge-secondary',
|
|
98
99
|
cats: (p.categories || []).map(catName).join(', '),
|
|
99
|
-
date: at ?
|
|
100
|
-
dateTitle: at ? `${label} ${
|
|
100
|
+
date: at ? fmtDate(at) : '',
|
|
101
|
+
dateTitle: at ? `${label} ${fmtDateTime(at)}` : ''
|
|
101
102
|
};
|
|
102
103
|
});
|
|
103
104
|
}));
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// The loader's cache-busting token, passed on so helpers are refetched with this view.
|
|
14
|
+
import {fmtDateTime} from '/plugins/_lib/admin/ui/dates.js';
|
|
14
15
|
const ASSET_VERSION = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
15
16
|
|
|
16
17
|
const STATUSES = [
|
|
@@ -93,7 +94,7 @@ export const commentsView = {
|
|
|
93
94
|
status: c.status || 'pending',
|
|
94
95
|
badge: BADGE[c.status] || 'badge-secondary',
|
|
95
96
|
date: c.createdAt ? D(c.createdAt).fromNow() : '',
|
|
96
|
-
dateTitle: c.createdAt ?
|
|
97
|
+
dateTitle: c.createdAt ? fmtDateTime(c.createdAt) : ''
|
|
97
98
|
}));
|
|
98
99
|
}));
|
|
99
100
|
add(M.effect(() => {
|