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.
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/config/menus/admin-sidebar.json +21 -8
- package/config/plugins.json +0 -18
- package/package.json +1 -1
- package/plugins/_lib/mail/diagnostics.js +3 -2
- package/plugins/blog/CLAUDE.md +33 -0
- package/plugins/blog/lib/access.js +64 -0
- package/plugins/blog/lib/notices.js +38 -0
- package/plugins/blog/plugin.js +84 -26
- package/plugins/blog/plugin.json +173 -20
- package/plugins/blog/roles/blog-author.json +15 -2
- package/plugins/blog/tests/access.test.js +32 -0
- package/plugins/blog/tests/comment-notices.test.js +140 -0
- package/plugins/blog/tests/notices.test.js +20 -0
- package/plugins/blog/tests/public.test.js +3 -1
- package/plugins/free-tier.lock.json +15 -10
- package/plugins/mail-reader/plugin.js +12 -1
- package/plugins/mail-reader/plugin.json +23 -4
- package/server/routes/api/notifications.js +4 -3
- package/server/routes/api/todo.js +11 -0
- package/server/services/notification-sources.js +62 -1
- package/server/services/notify.js +4 -2
- package/server/services/pluginScope.js +85 -0
- package/server/services/plugins.js +7 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as p}from"../api.js";import{isPathAllowed as
|
|
2
|
-
<summary${m}>${
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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-
|
|
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
|
}
|
package/config/plugins.json
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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
|
}
|
package/plugins/blog/CLAUDE.md
CHANGED
|
@@ -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
|
+
}
|