domma-cms 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{api as p}from"../api.js";import{isPathAllowed as te}from"./admin-scope.js";import{colourToCss as X,applyFloat as ne,applyHighlight as se,itemHighlightAttrs as ie}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as oe,ensureLockedTools as ae,managePluginsItem as re,stripItemByUrl as le,insertFoldersBeforeSystem as ce,pruneEmptyToolsFolder as de,MANAGE_PLUGINS_URL as K}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function N(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function V(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!N(s,i.permission))continue;const o=typeof i.url=="string"?i.url:"";if(n&&o.startsWith("#/")&&!te(n,o.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,a=l?V(i.items,s,n):[];l&&!a.length&&!o||e.push({...i,items:a})}return e}async function me(t){if(!N(t,"projects"))return[];let s=[];try{s=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(e=>{const o="#/projects/"+encodeURIComponent(e.slug),l=e.counts||{},a=[{text:"Overview",url:o,icon:e.icon||"folder"}];for(const u of n){const m=Number(l[u.key])||0;m<=0||a.push({text:`${u.text} (${m})`,url:o+u.path,icon:u.icon})}a.push({text:"Settings",url:o+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:a,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function G(t,s){const n=Y.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?P.set(t,s.details):P.delete(t);const i=!s.inventory&&C.some(o=>o.url===t&&!o.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${h(t)}"${i} data-badge-label="${h(e)}" aria-label="${h(e)}">${h(String(s.count))}</span>`}function J(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const o=[];for(const m of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const v=Number(m.textContent)||0;v&&(e+=v,i||(i=m.classList.contains("sidebar-badge--danger")),o.push({text:m.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:m.dataset.badgeLabel,tone:m.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let a=n.querySelector(":scope > .sidebar-badge--sum");if(!e){a?.remove(),P.delete(l);continue}a||(a=document.createElement("span"),n.querySelector(".sidebar-text")?.after(a));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",u=o.map(m=>`${m.text}: ${m.meta}`).join(" \xB7 ");a.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,a.dataset.countFor=l,a.dataset.badgeLabel=u,a.setAttribute("aria-label",u),a.textContent=String(e),P.set(l,{title:c,items:o})}}const P=new Map;function pe(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":G(t.url,n)}const Y=["info","attention","danger","live"],he={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function U(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),o={count:Math.round(e),label:i,tone:Y.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(o.details=t.details),t?.inventory===!0&&(o.inventory=!0),o}function ge(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Q(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(y)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${h(t.icon)}"></span>`:"",e=h(t.text||""),i=t.url?h(t.url):"",o=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",a=ie(t),c=[a.hover&&` data-menu-item-hover="${h(a.hover)}"`,a.active&&` data-menu-item-active="${h(a.active)}"`].filter(Boolean).join(""),u=[l&&`color:${h(l)}`,a.accent&&`--dm-menu-highlight:${h(a.accent)}`].filter(Boolean).join(";"),m=u?` style="${u}"`:"";let v="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const y=t.badge.variant?X(t.badge.variant):"";v=`<span class="dm-menu-badge"${y?` style="background:${h(y)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(t.badge.text))}</span>`}if(o){const y=ge(t),k=S.get(y)!==!1,E=t.items.map(w=>Q(w,s)).join("");return`<details data-state-key="${h(y)}"${k?" open":""}>
2
- <summary${m}>${n} <span class="sidebar-text">${e}</span>${v}</summary>
1
+ import{api as p}from"../api.js";import{isPathAllowed as X}from"./admin-scope.js";import{colourToCss as K,applyFloat as ne,applyHighlight as se,itemHighlightAttrs as ie}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as oe,ensureLockedTools as ae,managePluginsItem as re,stripItemByUrl as le,insertFoldersBeforeSystem as ce,pruneEmptyToolsFolder as de,MANAGE_PLUGINS_URL as V}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function F(t,n){return n?!t||!t.length?!1:t.includes(n)?!0:["read","create","update","delete"].some(i=>t.includes(`${n}.${i}`)):!0}function G(t,n,i=null){const e=[];for(const s of t){if(s.hidden||s.permission&&!F(n,s.permission))continue;const l=typeof s.url=="string"?s.url:"";if(i&&l.startsWith("#/")&&!X(i,l.slice(1)))continue;const a=Array.isArray(s.items)&&s.items.length>0,o=a?G(s.items,n,i):[];a&&!o.length&&!l||e.push({...s,items:o})}return e}async function me(t){if(!F(t,"projects"))return[];let n=[];try{n=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(n))return[];const i=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return n.map(e=>{const l="#/projects/"+encodeURIComponent(e.slug),a=e.counts||{},o=[{text:"Overview",url:l,icon:e.icon||"folder"}];for(const u of i){const m=Number(a[u.key])||0;m<=0||o.push({text:`${u.text} (${m})`,url:l+u.path,icon:u.icon})}o.push({text:"Settings",url:l+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:o,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,n){for(const i of t)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...n);return}}function J(t,n){const i=Q.includes(n.tone)?n.tone:"info",e=n.label||String(n.count);n.details?N.set(t,n.details):N.delete(t);const s=!n.inventory&&C.some(l=>l.url===t&&!l.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${h(t)}"${s} data-badge-label="${h(e)}" aria-label="${h(e)}">${h(String(n.count))}</span>`}function Y(t){for(const n of t.querySelectorAll("details")){const i=n.querySelector(":scope > summary");if(!i)continue;let e=0,s=!1;const l=[];for(const m of n.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const v=Number(m.textContent)||0;v&&(e+=v,s||(s=m.classList.contains("sidebar-badge--danger")),l.push({text:m.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:m.dataset.badgeLabel,tone:m.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const a=`folder:${n.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!e){o?.remove(),N.delete(a);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const c=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",u=l.map(m=>`${m.text}: ${m.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${s?"danger":"info"}`,o.dataset.countFor=a,o.dataset.badgeLabel=u,o.setAttribute("aria-label",u),o.textContent=String(e),N.set(a,{title:c,items:l})}}const N=new Map;function pe(t,n){if(!t.url||!n)return"";const i=n[t.url];return!i||!(i.count>0)?"":J(t.url,i)}const Q=["info","attention","danger","live"],he={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function W(t,{fallbackLabel:n="",tone:i="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const s=typeof t?.label=="string"&&t.label||(n?`${e} - ${n}`:String(e)),l={count:Math.round(e),label:s,tone:Q.includes(t?.tone)?t.tone:i};return t?.details&&typeof t.details=="object"&&(l.details=t.details),t?.inventory===!0&&(l.inventory=!0),l}function ge(t){const n=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),i=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${i?`project-${decodeURIComponent(i[1])}.`:""}${n}`}function Z(t,n){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(y)}" aria-hidden="true"></div>`}const i=t.icon?`<span data-icon="${h(t.icon)}"></span>`:"",e=h(t.text||""),s=t.url?h(t.url):"",l=Array.isArray(t.items)&&t.items.length>0,a=t.colour?K(t.colour):"",o=ie(t),c=[o.hover&&` data-menu-item-hover="${h(o.hover)}"`,o.active&&` data-menu-item-active="${h(o.active)}"`].filter(Boolean).join(""),u=[a&&`color:${h(a)}`,o.accent&&`--dm-menu-highlight:${h(o.accent)}`].filter(Boolean).join(";"),m=u?` style="${u}"`:"";let v="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const y=t.badge.variant?K(t.badge.variant):"";v=`<span class="dm-menu-badge"${y?` style="background:${h(y)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(t.badge.text))}</span>`}if(l){const y=ge(t),L=S.get(y)!==!1,E=t.items.map(w=>Z(w,n)).join("");return`<details data-state-key="${h(y)}"${L?" open":""}>
2
+ <summary${m}>${i} <span class="sidebar-text">${e}</span>${v}</summary>
3
3
  <div class="sidebar-children">${E}</div>
4
- </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${m}${c}>${n} <span class="sidebar-text">${e}</span>${pe(t,s)}${v}</a>`}async function be(t=[],s=[]){const n=d=>d.then(g=>Array.isArray(g)?g.length:Array.isArray(g?.entries)?g.entries.length:0).catch(()=>0);C=xe(t);const e=new Set(C.map(d=>d.url)),i={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},o=(d,g)=>N(s,d)&&!e.has(i[d])?g():Promise.resolve(0),[l,a,c,u,m,v,y,k,E,w,R,B,q,T]=await Promise.all([o("pages",()=>n(p.pages.list())),o("media",()=>n(p.media.list())),o("menus",()=>n(p.menus.list())),o("context-menus",()=>n(p.contextMenus.list())),o("theme",()=>p.theme.get().then(d=>(d?.overrides||[]).length).catch(()=>0)),o("collections",()=>n(p.collections.list())),o("forms",()=>n(p.forms.list())),o("views",()=>n(p.views.list())),o("actions",()=>n(p.actions.list())),o("blocks",()=>n(p.blocks.list())),o("components",()=>n(p.components.list())),o("users",()=>n(p.users.list())),o("plugins",()=>n(p.plugins.list())),o("notifications",()=>p.system?.notifications?.unreadCount?.().then(d=>d?.count??0).catch(()=>0)??Promise.resolve(0))]),j={};await Promise.all(C.map(async d=>{const g=await Z(d);g&&(j[d.url]=g)}));const I={"#/pages":l,"#/media":a,"#/menus":c,"#/context-menus":u,"#/theme":m,"#/collections":v,"#/forms":y,"#/views":k,"#/actions":E,"#/blocks":w,"#/components":R,"#/users":B,"#/plugins":q,"#/system/notifications":T},x={};for(const[d,g]of Object.entries(I)){if(e.has(d))continue;const[L,_]=he[d]??["item","items"],A=U(g);A&&(x[d]={...A,label:`${A.count} ${A.count===1?L:_}`})}return{...x,...j}}let C=[];const ye="dm:sidebar-count",ve=6e4;function xe(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||"",inventory:e.inventory===!0});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Z(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return U(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function ee(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(o=>o.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const o=document.createRange();o.selectNodeContents(e);const l=o.createContextualFragment(G(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}O.set(s,n),J(t)}const O=new Map;async function W(t,s=null){const n=s?C.filter(e=>e.url===s):C;await Promise.all(n.map(async e=>{const i=await Z(e),o=O.get(e.url);JSON.stringify(o??null)!==JSON.stringify(i)&&ee(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=C.find(i=>i.url===n.url);ee(t,n.url,U(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else W(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&W(t)},ve),document.addEventListener("visibilitychange",()=>{document.hidden||W(t)}),Se(t))}function Ce(t,s,n){const e=(l,a,c)=>{const u=document.createElement(l);return a&&(u.className=a),c!=null&&(u.textContent=String(c)),u};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const a of i)l.appendChild(e("dt",null,a.label)),l.appendChild(e("dd",a.tone==="danger"?"is-danger":null,a.value));t.appendChild(l)}const o=Array.isArray(n.items)?n.items.slice(0,6):[];if(o.length){const l=e("ul","dm-badge-pop-items");for(const a of o){const c=e("li",a.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",a.text)),a.meta&&c.appendChild(e("span","dm-badge-pop-meta",a.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=o=>{e();const l=o?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,P.get(o.dataset.countFor)),document.body.appendChild(s);const a=o.getBoundingClientRect(),c=s.getBoundingClientRect(),u=Math.min(a.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,u)}px`,s.style.top=`${Math.max(8,Math.min(a.top+a.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",o=>{const l=o.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",o=>{o.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",o=>{const l=o.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(t=[]){if(!N(t,"settings"))return"Admin";try{const s=await p.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,o=null,l=null,a=null,c=null,u=null;try{const r=await p.get("/sidebar/menu");i=Array.isArray(r?.items)?r.items:null,o=r?.variant||null,l=r?.position||null,a=r?.style||null,c=r?.float||null,u=r?.highlight||null}catch{i=null}(!i||!i.some(r=>r&&(!r.hidden||r.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let m=[];try{m=await H.get("/api/sidebar/registered-items")||[]}catch{}i=le(i,K),i=oe(i,m.filter(r=>(r?.item??r)?.url!==K)),i=ae(i),i=ce(i,[re()]);const v=await me(s);v.length&&fe(i,v),i=V(i,s,n),i=de(i);const[y,k]=await Promise.all([be(m.filter(r=>N(s,r?.item?.permission)),s).catch(()=>({})),we(s)]),E=o?` dm-admin-sidebar--${h(o)}`:"";function w(r,b="px"){if(r==null)return r;const f=String(r).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${b}`:f}let R="";if(a){const r=[];a.fontFamily&&r.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&r.push(`font-size: ${w(a.fontSize)}`),a.fontWeight&&r.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&r.push(`letter-spacing: ${w(a.letterSpacing,"em")}`);const b=[];if(r.length&&b.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${r.join("; ")} }`),a.iconSize){const f=w(a.iconSize);b.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}b.length&&(R=`<style data-admin-sidebar-style>${b.join(" ")}</style>`)}const B=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(k)}</div>`,q=i.some(r=>r&&r.type==="spacer"&&r.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",T=`${R}<nav class="dm-admin-sidebar${E}${q}">${B}${i.map(r=>Q(r,y)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const I=j.createContextualFragment(T);e.replaceChildren(I),Domma.icons.scan(e);for(const r of C)O.set(r.url,y[r.url]??null);J(e),$e(e);const x=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(x)&&x>=180&&x<=480&&(e.style.width=x+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),se(e,{highlight:u});const d=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(r=>{e.style[r]=""}),e.classList.remove("dm-menu-floating"),d&&(d.style.paddingLeft="",d.style.paddingRight=""),l==="floating"){ne(e,{position:"floating",float:c||{}});const b=c&&c.anchor||"TL",f=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,F=e.getBoundingClientRect().width||x||260;if(d){const z=Math.round(F+f*2)+"px";b==="R"||b==="TR"||b==="BR"?d.style.paddingRight=z:d.style.paddingLeft=z}}const g=document.createElement("div");g.className="dm-admin-sidebar-handle",g.setAttribute("aria-label","Resize sidebar"),e.appendChild(g),e.querySelectorAll("details").forEach(r=>{r.addEventListener("toggle",function(){const b=this.getAttribute("data-state-key");b&&S.set(b,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const F=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=F+"px",f.addEventListener("transitionend",function z(){f.style.maxHeight="none",f.removeEventListener("transitionend",z)})})}else{const F=f.scrollHeight;f.style.maxHeight=F+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})});let L=!1,_=0,A=0;g.addEventListener("mousedown",r=>{L=!0,_=r.clientX,A=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",r.preventDefault()}),e.dataset.resizeWired||(e.dataset.resizeWired="1",document.addEventListener("mousemove",r=>e.__dmResize?.move(r)),document.addEventListener("mouseup",()=>e.__dmResize?.up()),M.subscribe("router:afterChange",()=>e.__dmSetActive?.())),e.__dmResize={move(r){if(!L)return;const b=Math.max(180,Math.min(480,A+(r.clientX-_)));e.style.width=b+"px"},up(){L&&(L=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))}};function D(){const r=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${r}"]`).addClass("active")}D(),e.__dmSetActive=D}
4
+ </details>`}return`<a href="${s}" class="sidebar-link" data-url="${s}"${m}${c}>${i} <span class="sidebar-text">${e}</span>${pe(t,n)}${v}</a>`}async function be(t=[],n=[],i=null){const e=d=>d.then(g=>Array.isArray(g)?g.length:Array.isArray(g?.entries)?g.entries.length:0).catch(()=>0);C=xe(t,i);const s=new Set(C.map(d=>d.url)),l={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},a=(d,g)=>F(n,d)&&!s.has(l[d])?g():Promise.resolve(0),[o,c,u,m,v,y,L,E,w,P,T,I,U,R]=await Promise.all([a("pages",()=>e(p.pages.list())),a("media",()=>e(p.media.list())),a("menus",()=>e(p.menus.list())),a("context-menus",()=>e(p.contextMenus.list())),a("theme",()=>p.theme.get().then(d=>(d?.overrides||[]).length).catch(()=>0)),a("collections",()=>e(p.collections.list())),a("forms",()=>e(p.forms.list())),a("views",()=>e(p.views.list())),a("actions",()=>e(p.actions.list())),a("blocks",()=>e(p.blocks.list())),a("components",()=>e(p.components.list())),a("users",()=>e(p.users.list())),a("plugins",()=>e(p.plugins.list())),a("notifications",()=>p.system?.notifications?.unreadCount?.().then(d=>d?.count??0).catch(()=>0)??Promise.resolve(0))]),_={};await Promise.all(C.map(async d=>{const g=await ee(d);g&&(_[d.url]=g)}));const A={"#/pages":o,"#/media":c,"#/menus":u,"#/context-menus":m,"#/theme":v,"#/collections":y,"#/forms":L,"#/views":E,"#/actions":w,"#/blocks":P,"#/components":T,"#/users":I,"#/plugins":U,"#/system/notifications":R},x={};for(const[d,g]of Object.entries(A)){if(s.has(d))continue;const[z,B]=he[d]??["item","items"],k=W(g);k&&(x[d]={...k,label:`${k.count} ${k.count===1?z:B}`})}return{...x,..._}}let C=[];const ye="dm:sidebar-count",ve=6e4;function xe(t=[],n=null){const i=[],e=s=>{if(s&&!(n&&typeof s.url=="string"&&s.url.startsWith("#/")&&!X(n,s.url.slice(1)))){s.url&&typeof s.countUrl=="string"&&s.countUrl&&i.push({url:s.url,countUrl:s.countUrl,text:s.text||"",countLabel:s.countLabel||"",inventory:s.inventory===!0});for(const l of[...s.children||[],...s.items||[]])e(l)}};for(const s of t??[])e(s?.item??s);return i}async function ee(t){try{const n=new Date,i=`${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,"0")}-${String(n.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${i}`;return W(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function te(t,n,i){const e=[...t.querySelectorAll("a.sidebar-link")].find(l=>l.dataset.url===n);if(!e)return;const s=e.querySelector(".sidebar-badge[data-count-for]");if(!i||!(i.count>0))s?.remove();else{const l=document.createRange();l.selectNodeContents(e);const a=l.createContextualFragment(J(n,i)).firstElementChild;s?s.replaceWith(a):e.querySelector(".sidebar-text")?.after(a)}O.set(n,i),Y(t)}const O=new Map;async function D(t,n=null){const i=n?C.filter(e=>e.url===n):C;await Promise.all(i.map(async e=>{const s=await ee(e),l=O.get(e.url);JSON.stringify(l??null)!==JSON.stringify(s)&&te(t,e.url,s)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,n=>{const i=n.detail||{};if(i.url)if(i.count!==void 0){const e=C.find(s=>s.url===i.url);te(t,i.url,W(i,{fallbackLabel:e?.countLabel||e?.text||""}))}else D(t,i.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&D(t)},ve),document.addEventListener("visibilitychange",()=>{document.hidden||D(t)}),Se(t))}function Ce(t,n,i){const e=(a,o,c)=>{const u=document.createElement(a);return o&&(u.className=o),c!=null&&(u.textContent=String(c)),u};if(!i){t.textContent=n;return}t.appendChild(e("strong","dm-badge-pop-title",i.title||n)),(i.rows?.length||!i.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",n));const s=Array.isArray(i.rows)?i.rows.slice(0,6):[];if(s.length){const a=e("dl","dm-badge-pop-rows");for(const o of s)a.appendChild(e("dt",null,o.label)),a.appendChild(e("dd",o.tone==="danger"?"is-danger":null,o.value));t.appendChild(a)}const l=Array.isArray(i.items)?i.items.slice(0,6):[];if(l.length){const a=e("ul","dm-badge-pop-items");for(const o of l){const c=e("li",o.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",o.text)),o.meta&&c.appendChild(e("span","dm-badge-pop-meta",o.meta)),a.appendChild(c)}t.appendChild(a)}else i.empty&&t.appendChild(e("p","dm-badge-pop-empty",i.empty))}function Se(t){let n=null,i=null;const e=()=>{clearTimeout(i),n?.remove(),n=null},s=l=>{e();const a=l?.dataset.badgeLabel;a&&(i=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),Ce(n,a,N.get(l.dataset.countFor)),document.body.appendChild(n);const o=l.getBoundingClientRect(),c=n.getBoundingClientRect(),u=Math.min(o.right+10,innerWidth-c.width-8);n.style.left=`${Math.max(8,u)}px`,n.style.top=`${Math.max(8,Math.min(o.top+o.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",l=>{const a=l.target.closest?.(".sidebar-badge[data-badge-label]");a&&s(a)}),t.addEventListener("mouseout",l=>{l.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",l=>{const a=l.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");a?s(a):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(t=[]){if(!F(t,"settings"))return"Admin";try{const n=await p.settings.get();return n?.adminBrand?.title||n?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:n,adminScope:i=null}){const e=$(t).get(0);if(!e)return;let s,l=null,a=null,o=null,c=null,u=null;try{const r=await p.get("/sidebar/menu");s=Array.isArray(r?.items)?r.items:null,l=r?.variant||null,a=r?.position||null,o=r?.style||null,c=r?.float||null,u=r?.highlight||null}catch{s=null}(!s||!s.some(r=>r&&(!r.hidden||r.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),s=ue.slice());let m=[];try{m=await H.get("/api/sidebar/registered-items")||[]}catch{}s=le(s,V),s=oe(s,m.filter(r=>(r?.item??r)?.url!==V)),s=ae(s),s=ce(s,[re()]);const v=await me(n);v.length&&fe(s,v),s=G(s,n,i),s=de(s);const[y,L]=await Promise.all([be(m.filter(r=>F(n,r?.item?.permission)),n,i).catch(()=>({})),we(n)]),E=l?` dm-admin-sidebar--${h(l)}`:"";function w(r,b="px"){if(r==null)return r;const f=String(r).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${b}`:f}let P="";if(o){const r=[];o.fontFamily&&r.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&r.push(`font-size: ${w(o.fontSize)}`),o.fontWeight&&r.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&r.push(`letter-spacing: ${w(o.letterSpacing,"em")}`);const b=[];if(r.length&&b.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${r.join("; ")} }`),o.iconSize){const f=w(o.iconSize);b.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}b.length&&(P=`<style data-admin-sidebar-style>${b.join(" ")}</style>`)}const T=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(L)}</div>`,I=s.some(r=>r&&r.type==="spacer"&&r.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",U=`${P}<nav class="dm-admin-sidebar${E}${I}">${T}${s.map(r=>Z(r,y)).join("")}</nav>`,R=document.createRange();R.selectNodeContents(e);const _=R.createContextualFragment(U);e.replaceChildren(_),Domma.icons.scan(e);for(const r of C)O.set(r.url,y[r.url]??null);Y(e),$e(e);const A=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(A)&&A>=180&&A<=480&&(e.style.width=A+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),se(e,{highlight:u});const x=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(r=>{e.style[r]=""}),e.classList.remove("dm-menu-floating"),x&&(x.style.paddingLeft="",x.style.paddingRight=""),a==="floating"){ne(e,{position:"floating",float:c||{}});const b=c&&c.anchor||"TL",f=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,j=e.getBoundingClientRect().width||A||260;if(x){const q=Math.round(j+f*2)+"px";b==="R"||b==="TR"||b==="BR"?x.style.paddingRight=q:x.style.paddingLeft=q}}const d=document.createElement("div");d.className="dm-admin-sidebar-handle",d.setAttribute("aria-label","Resize sidebar"),e.appendChild(d),e.querySelectorAll("details").forEach(r=>{r.addEventListener("toggle",function(){const b=this.getAttribute("data-state-key");b&&S.set(b,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const j=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=j+"px",f.addEventListener("transitionend",function q(){f.style.maxHeight="none",f.removeEventListener("transitionend",q)})})}else{const j=f.scrollHeight;f.style.maxHeight=j+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})});let g=!1,z=0,B=0;d.addEventListener("mousedown",r=>{g=!0,z=r.clientX,B=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",r.preventDefault()}),e.dataset.resizeWired||(e.dataset.resizeWired="1",document.addEventListener("mousemove",r=>e.__dmResize?.move(r)),document.addEventListener("mouseup",()=>e.__dmResize?.up()),M.subscribe("router:afterChange",()=>e.__dmSetActive?.())),e.__dmResize={move(r){if(!g)return;const b=Math.max(180,Math.min(480,B+(r.clientX-z)));e.style.width=b+"px"},up(){g&&(g=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))}};function k(){const r=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${r}"]`).addClass("active")}k(),e.__dmSetActive=k}
@@ -14,7 +14,8 @@
14
14
  "url": "#/",
15
15
  "icon": "home"
16
16
  }
17
- ]
17
+ ],
18
+ "key": "overview"
18
19
  },
19
20
  {
20
21
  "text": "Projects",
@@ -28,7 +29,8 @@
28
29
  "icon": "folder",
29
30
  "permission": "projects"
30
31
  }
31
- ]
32
+ ],
33
+ "key": "projects"
32
34
  },
33
35
  {
34
36
  "text": "Content",
@@ -65,7 +67,8 @@
65
67
  "icon": "droplet",
66
68
  "permission": "theme"
67
69
  }
68
- ]
70
+ ],
71
+ "key": "content"
69
72
  },
70
73
  {
71
74
  "text": "Data",
@@ -114,7 +117,14 @@
114
117
  "icon": "code",
115
118
  "permission": "api-endpoints"
116
119
  }
117
- ]
120
+ ],
121
+ "key": "data"
122
+ },
123
+ {
124
+ "text": "Tools",
125
+ "key": "tools",
126
+ "icon": "tool",
127
+ "items": []
118
128
  },
119
129
  {
120
130
  "text": "System",
@@ -174,7 +184,8 @@
174
184
  "icon": "search",
175
185
  "permission": "settings"
176
186
  }
177
- ]
187
+ ],
188
+ "key": "system"
178
189
  },
179
190
  {
180
191
  "text": "Documentation",
@@ -354,14 +365,16 @@
354
365
  }
355
366
  ]
356
367
  }
357
- ]
368
+ ],
369
+ "key": "documentation"
358
370
  }
359
371
  ],
360
372
  "meta": {
361
373
  "createdAt": "2026-05-25T11:11:15.223Z",
362
- "updatedAt": "2026-09-20T11:19:46.452Z",
374
+ "updatedAt": "2026-09-26T19:15:57.495Z",
363
375
  "bundled": false,
364
376
  "presetOwner": null,
365
- "project": null
377
+ "project": null,
378
+ "toolsFolder": true
366
379
  }
367
380
  }
@@ -1,27 +1,9 @@
1
1
  {
2
- "todo": {
3
- "enabled": false,
4
- "bundled": true,
5
- "settings": {}
6
- },
7
- "notes": {
8
- "enabled": false,
9
- "bundled": true,
10
- "settings": {}
11
- },
12
- "contacts": {
13
- "enabled": false,
14
- "bundled": true,
15
- "settings": {}
16
- },
17
2
  "blog": {
18
3
  "enabled": false,
19
4
  "bundled": false,
20
5
  "settings": {}
21
6
  },
22
- "analytics": {
23
- "enabled": true
24
- },
25
7
  "mail-reader": {
26
8
  "enabled": false,
27
9
  "bundled": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -110,7 +110,7 @@ async function expectedStamp(pluginName) {
110
110
  * Assemble the report for one user.
111
111
  *
112
112
  * @param {{pluginName: string, userId: string, config: object,
113
- * extra?: () => Promise<object>|object}} options
113
+ * extra?: (who: {userId: string}) => Promise<object>|object}} options
114
114
  * @returns {Promise<object>}
115
115
  */
116
116
  export async function buildDiagnostics({pluginName, userId, config, extra = null}) {
@@ -149,6 +149,7 @@ export async function buildDiagnostics({pluginName, userId, config, extra = null
149
149
  trustedImageSenders: (account.imageSenders ?? []).length,
150
150
  rememberImageSenders: account.rememberImageSenders !== false
151
151
  })),
152
- ...(extra ? {edition: await extra()} : {})
152
+ // The caller's id: an edition reports only this user's mailboxes (Email Pro 0.4 relies on it).
153
+ ...(extra ? {edition: await extra({userId})} : {})
153
154
  };
154
155
  }
@@ -271,3 +271,36 @@ with `?display=` and swaps the cards, since each display is different block mark
271
271
  - **`storage.adapter: 'file'` default**: Unlike the messages plugin (which defaults to `mongodb`), blog defaults to `file` so it works on free installs without MongoDB. Do not change this default.
272
272
 
273
273
  - **`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.
274
+
275
+ ## Notifications (1.7.0, Notifications Phase 4)
276
+
277
+ A comment held for approval (`POST /comments`, status `pending`) raises a notice for the site's staff - source
278
+ `blog:comments`, severity info, the source's audience (Super Admins by default), link to the Comments screen. The
279
+ wording is `lib/notices.js` (pure, tested): one line each, trimmed; the commenter's words stay TEXT (the
280
+ Notifications screen escapes, desktop pop-ups are plain text). Key `comment:<id>`: moderating it away from
281
+ pending (`PUT /comments/:id`) or deleting it resolves the notice. Feature-detected - an older core sends none.
282
+ `tests/comment-notices.test.js` swaps in this plugin's own collection schemas for the run: the domma-cms
283
+ checkout's `content/` has an old demo `blog-comments` schema (`post_slug`...) that is not this plugin's.
284
+
285
+ ## Access - by permission (1.8.0)
286
+
287
+ Declared in plugin.json `permissions` (granted once to `admin`), checked on EVERY authenticated route via
288
+ `need(key)`; decisions go through `lib/access.js` (`permissionChecker`, tested), never role names:
289
+
290
+ | Resource | Actions | Blog Author | Blog Editor |
291
+ |---|---|---|---|
292
+ | `blog-posts` | read · create · update · delete · publish | read, create, update | all |
293
+ | `blog-categories` | read · create · update · delete | read | all |
294
+ | `blog-comments` | read · update · delete | - | all |
295
+
296
+ - An **editor** is whoever holds `blog-posts.publish` (additional roles count). A writer who is not one sees and
297
+ opens only their own posts (GET /posts, GET /posts/:id -> 404 for others').
298
+ - Until this release: `PUBLISH_ROLES.includes(user.role)` (primary role only), and many routes checked only
299
+ "signed in" - any account read every draft and commenters' emails, created drafts, and (free Blog) moderated
300
+ or deleted comments.
301
+ - **Blog Author's bare `blog-posts`** (which a permission check reads as every action, publish and delete too)
302
+ is made explicit ONCE at boot (`authorPatch`, marker `blog:author-actions` in grantedBy); the role file now
303
+ ships the explicit list.
304
+ - `usePermissionLookup()` points the checks at a lookup: the plugin passes core's `getPermissionsFor` (or
305
+ `options.getPermissionsFor` in tests).
306
+
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Who may do what on the blog - by PERMISSION, never by role name.
3
+ *
4
+ * Until Blog 1.8 / Blog Pro 0.4 the plugin asked `user.role` against a list
5
+ * ('blog-editor', 'admin', 'super-admin'): a primary role only (an editor held
6
+ * as an additional role was refused), and every route that asked nothing but
7
+ * "signed in?" answered ANY account - drafts, commenters' emails, moderation.
8
+ *
9
+ * The three resources (plugin.json `permissions`, granted to admin):
10
+ * blog-posts read · create · update · delete · publish
11
+ * blog-categories read · create · update · delete
12
+ * blog-comments read · update · delete
13
+ * Blog Author holds posts read/create/update and categories read; Blog Editor
14
+ * holds all three whole. A writer (posts.create) sees only their own posts;
15
+ * an editor (posts.publish) sees and runs everything.
16
+ *
17
+ * Pure apart from the lookup, which is passed in (core's getPermissionsFor).
18
+ *
19
+ * @module blog/lib/access
20
+ */
21
+
22
+ /** What Blog Author holds - explicit, so it never widens to delete or publish. */
23
+ export const AUTHOR_PERMISSIONS = Object.freeze(['blog-posts.read', 'blog-posts.create', 'blog-posts.update', 'blog-categories.read']);
24
+
25
+ /** The marker a Blog Author role carries once its bare 'blog-posts' was made explicit. */
26
+ export const AUTHOR_MARKER = 'blog:author-actions';
27
+
28
+ /** Every role a user holds, primary first. */
29
+ export function rolesOf(user) {
30
+ if (!user || typeof user.role !== 'string') return [];
31
+ const extra = Array.isArray(user.additionalRoles) ? user.additionalRoles : [];
32
+ return [user.role, ...extra.filter(r => r && r !== user.role)];
33
+ }
34
+
35
+ /**
36
+ * A `can(user, 'blog-posts.publish')` bound to a permission lookup.
37
+ *
38
+ * @param {(key: string) => string[]} permissionsFor - roles holding a permission
39
+ */
40
+ export function permissionChecker(permissionsFor) {
41
+ return (user, key) => {
42
+ const allowed = permissionsFor(key) || [];
43
+ return rolesOf(user).some(r => allowed.includes(r));
44
+ };
45
+ }
46
+
47
+ /**
48
+ * The one-time patch for a Blog Author role made before this release: its bare
49
+ * 'blog-posts' - which a permission check reads as EVERY action, publish and
50
+ * delete included - becomes the explicit author set. Only the blog's own role,
51
+ * only while it still holds exactly the shipped bare permission; an admin's
52
+ * later edits are left alone (the marker).
53
+ *
54
+ * @param {object|null} data - the stored role
55
+ * @returns {object|null} the fields to merge, or null
56
+ */
57
+ export function authorPatch(data) {
58
+ if (!data || data.name !== 'blog-author' || !['blog', 'blog-pro'].includes(data.plugin)) return null;
59
+ const grantedBy = Array.isArray(data.grantedBy) ? data.grantedBy : [];
60
+ if (grantedBy.includes(AUTHOR_MARKER)) return null;
61
+ const perms = Array.isArray(data.permissions) ? data.permissions : [];
62
+ const bare = perms.length === 1 && perms[0] === 'blog-posts';
63
+ return {grantedBy: [...grantedBy, AUTHOR_MARKER], ...(bare ? {permissions: [...AUTHOR_PERMISSIONS]} : {})};
64
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What the blog tells the site's staff (Notifications Phase 4): a comment
3
+ * waiting for moderation. Pure - tests/notices.test.js.
4
+ *
5
+ * The words are a stranger's, so they are only ever text: the notification
6
+ * screen escapes them and desktop notifications are plain text. Trimmed so a
7
+ * long comment is a line, not a page.
8
+ *
9
+ * @module blog/lib/notices
10
+ */
11
+
12
+ /** The dedupe key of a comment's notice - resolved when it is moderated or deleted. */
13
+ export const commentKey = (id) => `comment:${id}`;
14
+
15
+ const line = (s, max) => {
16
+ const t = String(s ?? '').replace(/\s+/g, ' ').trim();
17
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
18
+ };
19
+
20
+ /**
21
+ * The notice for a comment held for approval, or null for one that is not.
22
+ *
23
+ * @param {{id: string, status: string, authorName?: string, body?: string}} comment
24
+ * @param {string} postTitle
25
+ * @param {string} link - the admin's comments screen
26
+ * @returns {object|null} a notify() payload
27
+ */
28
+ export function commentNotice(comment, postTitle, link) {
29
+ if (!comment?.id || comment.status !== 'pending') return null;
30
+ return {
31
+ title: `New comment on "${line(postTitle || 'a post', 80)}"`,
32
+ body: `${line(comment.authorName || 'Someone', 60)}: ${line(comment.body, 200)}\n\nWaiting for approval.`,
33
+ severity: 'info',
34
+ link,
35
+ source: 'comments',
36
+ dedupeKey: commentKey(comment.id)
37
+ };
38
+ }