domma-cms 1.6.4 → 1.6.6
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/README.md +1 -1
- package/admin/js/lib/sidebar-frequent.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +6 -6
- package/config/menus/admin-sidebar.json +1 -1
- package/package.json +1 -1
- package/server/routes/api/sidebar.js +16 -0
- package/server/services/sidebar-migration.js +15 -4
- package/server/services/sidebarFrequent.js +66 -0
- package/server/services/siteGitignore.js +2 -0
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ Permissions per resource are defined in `config/auth.json`.
|
|
|
264
264
|
|
|
265
265
|
The sidebar groups content by role:
|
|
266
266
|
|
|
267
|
-
- **
|
|
267
|
+
- **Dashboards** - Dashboard
|
|
268
268
|
- **Structure** - Navigation, Layouts *(admin and manager only)*
|
|
269
269
|
- **Content** - Pages, Media
|
|
270
270
|
- **Configuration** - Users, Site Settings *(admin and manager only)*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const FREQUENT_KEY="frequent",FREQUENT_MAX=12;export function findLeaf(t
|
|
1
|
+
export const FREQUENT_KEY="frequent",FREQUENT_MAX=12;export function findLeaf(e,t){for(const r of e||[])if(!(!r||r.type||r.hidden)){if(Array.isArray(r.items)&&r.items.length){const n=findLeaf(r.items,t);if(n)return n}else if(r.url===t)return r}return null}export function frequentFolder(e,t,{hidden:r=!1}={}){const n=[];for(const o of Array.isArray(t)?t:[]){const i=typeof o=="string"&&o?findLeaf(e,o):null;i&&n.push({text:i.text,url:i.url,...i.icon&&{icon:i.icon},...i.colour&&{colour:i.colour}})}return n.length?{text:"Frequent",key:FREQUENT_KEY,icon:"star",items:n,frequent:!0}:r?null:{text:"Frequent",key:FREQUENT_KEY,icon:"star",items:[],frequent:!0,empty:!0}}export function addFrequent(e,t){const r=(Array.isArray(e)?e:[]).filter(n=>typeof n=="string"&&n);return!t||r.includes(t)||r.length>=12?r:[...r,t]}export function removeFrequent(e,t){return(Array.isArray(e)?e:[]).filter(r=>r!==t)}export function moveFrequent(e,t,r){const n=Array.isArray(e)?[...e]:[],o=n.indexOf(t),i=o+r;return o<0||i<0||i>=n.length||([n[o],n[i]]=[n[i],n[o]]),n}export function storageKey(e){const t=e&&(e.id||e._id||e.email);return`sidebar.frequent.${t?String(t):"anon"}`}export function readFrequent(e){try{const t=S.get(storageKey(e));return Array.isArray(t)?t.filter(r=>typeof r=="string"&&r):[]}catch{return[]}}export function readFrequentHidden(e){try{return S.get(`${storageKey(e)}.hidden`)===!0}catch{return!1}}export function writeFrequentHidden(e,t){try{return S.set(`${storageKey(e)}.hidden`,!!t),!0}catch{return!1}}export function writeFrequent(e,t){try{return S.set(storageKey(e),Array.isArray(t)?t:[]),!0}catch{return!1}}let u=null;export function resetFrequentSync(){u=null}export function saveFrequent(e,t){return e.put("/sidebar/frequent",{urls:readFrequent(t),hidden:readFrequentHidden(t)})}export async function syncFrequent(e,t){const r=t&&(t.id||t._id||t.email);if(!r||u===String(r))return;const n=await e.get("/sidebar/frequent");if(!(!n||typeof n!="object")){if(u=String(r),!n.saved){(readFrequent(t).length||readFrequentHidden(t))&&await saveFrequent(e,t);return}writeFrequent(t,Array.isArray(n.urls)?n.urls:[]),writeFrequentHidden(t,n.hidden===!0)}}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{api as g,getUser as
|
|
2
|
-
<summary${
|
|
1
|
+
import{api as g,getUser as L}from"../api.js";import{isPathAllowed as ue}from"./admin-scope.js";import{colourToCss as V,applyFloat as fe,applyHighlight as me,itemHighlightAttrs as pe}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{placeTools as he,ensureLockedTools as ge,managePluginsItem as be,stripItemByUrl as ye,insertFoldersBeforeSystem as ve,pruneEmptyToolsFolder as xe,sidebarStateKey as X,canAny as O,filterByPermission as $e,MANAGE_PLUGINS_URL as Z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as we,canArrange as ke,startArrange as Se}from"./sidebar-arrange.js";import{collapseButtonMarkup as qe,wireSidebarSize as Ce,COLLAPSED_WIDTH as Ae}from"./sidebar-size.js";import{FREQUENT_MAX as Y,addFrequent as Fe,frequentFolder as Le,moveFrequent as J,readFrequent as z,readFrequentHidden as Ee,removeFrequent as Te,saveFrequent as Me,syncFrequent as He,writeFrequent as Ne,writeFrequentHidden as Q}from"./sidebar-frequent.js";import{hotkeyFor as ee,hotkeyTitle as je,setHotkeyTree as Be}from"./hotkeys-ui.js";import{hotkeyLabel as Pe}from"./hotkeys.js";function m(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const Re=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function _e(e){if(!O(e,"projects"))return[];let n=[];try{n=await g.projects.listWithCounts()}catch{return[]}if(!Array.isArray(n))return[];const s=[{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(t=>{const o="#/projects/"+encodeURIComponent(t.slug),a=t.counts||{},d=[{text:"Overview",url:o,icon:t.icon||"folder"}];for(const c of s){const r=Number(a[c.key])||0;r<=0||d.push({text:`${c.text} (${r})`,url:o+c.path,icon:c.icon})}d.push({text:"Settings",url:o+"/settings",icon:"settings"});const u=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:d,...u&&{colour:"neutral",badge:{text:"off"}}}})}function Ie(e,n){for(const s of e)if(!s.hidden&&(s.key==="projects"||!s.key&&(s.text||"").toLowerCase()==="projects")){s.items=Array.isArray(s.items)?s.items.slice():[],s.items.push(...n);return}}function te(e,n){const s=se.includes(n.tone)?n.tone:"info",t=n.label||String(n.count);n.details?j.set(e,n.details):j.delete(e);const i=!n.inventory&&A.some(o=>o.url===e&&!o.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${s}" data-count-for="${m(e)}"${i} data-badge-label="${m(t)}" aria-label="${m(t)}">${m(String(n.count))}</span>`}function ne(e){for(const n of e.querySelectorAll("details")){const s=n.querySelector(":scope > summary");if(!s)continue;let t=0,i=!1;const o=[];for(const r of n.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const f=Number(r.textContent)||0;f&&(t+=f,i||(i=r.classList.contains("sidebar-badge--danger")),o.push({text:r.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:r.dataset.badgeLabel,tone:r.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const a=`folder:${n.dataset.stateKey||s.textContent.trim()}`;let d=s.querySelector(":scope > .sidebar-badge--sum");if(!t){d?.remove(),j.delete(a);continue}d||(d=document.createElement("span"),s.querySelector(".sidebar-text")?.after(d));const u=s.querySelector(".sidebar-text")?.textContent.trim()||"This folder",c=o.map(r=>`${r.text}: ${r.meta}`).join(" \xB7 ");d.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,d.dataset.countFor=a,d.dataset.badgeLabel=c,d.setAttribute("aria-label",c),d.textContent=String(t),j.set(a,{title:u,items:o})}}const j=new Map;function Oe(e,n){if(!e.url||!n)return"";const s=n[e.url];return!s||!(s.count>0)&&!s.showZero?"":te(e.url,s)}const se=["info","attention","danger","live"],Ue={"#/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(e,{fallbackLabel:n="",tone:s="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const i=typeof e?.label=="string"&&e.label||(n?`${t} - ${n}`:String(t)),o={count:Math.round(t),label:i,tone:se.includes(e?.tone)?e.tone:s};return e?.details&&typeof e.details=="object"&&(o.details=e.details),e?.inventory===!0&&(o.inventory=!0),e?.showZero===!0&&(o.showZero=!0),o}function oe(e,n,s=null,{inFrequent:t=!1}={}){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${m(y)}" aria-hidden="true"></div>`}const i=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",o=e.icon?`<span data-icon="${m(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${m(i)}</span>`,a=m(e.text||""),d=e.url?m(e.url):"",u=Array.isArray(e.items)&&e.items.length>0,c=e.colour?V(e.colour):"",r=pe(e),f=[r.hover&&` data-menu-item-hover="${m(r.hover)}"`,r.active&&` data-menu-item-active="${m(r.active)}"`].filter(Boolean).join(""),x=[c&&`color:${m(c)}`,r.accent&&`--dm-menu-highlight:${m(r.accent)}`].filter(Boolean).join(";"),w=x?` style="${x}"`:"",k=e.url?je(e.url,e.text||"",{inFrequent:t}):"",q=k?` title="${m(k)}"`:"";let F="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const y=e.badge.variant?V(e.badge.variant):"";F=`<span class="dm-menu-badge"${y?` style="background:${m(y)};color:var(--dm-text-inverse,#fff)"`:""}>${m(String(e.badge.text))}</span>`}if(e.frequent&&e.empty){const y=X(e,s),C=S.get(y)!==!1;return`<details data-state-key="${m(y)}"${C?" open":""} data-frequent data-frequent-empty>
|
|
2
|
+
<summary${w}>${o} <span class="sidebar-text">${a}</span></summary>
|
|
3
3
|
<div class="sidebar-children"><p class="sidebar-frequent-empty">Right-click any Tool and choose <strong>Add to Frequent</strong> to keep it here.</p></div>
|
|
4
|
-
</details>`}if(u){const
|
|
5
|
-
<summary${
|
|
6
|
-
<div class="sidebar-children">${
|
|
7
|
-
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${v}${m}${C}>${c} <span class="sidebar-text">${a}</span>${Pe(e,n)}${q}</a>`}async function _e(e=[],n=[],o=null){const t=f=>f.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0);F=Ue(e,o);const s=new Set(F.map(f=>f.url)),c={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=(f,x)=>O(n,f)&&!s.has(c[f])?x():Promise.resolve(0),[i,u,d,r,m,y,v,k,C,q,b,A,j,B]=await Promise.all([a("pages",()=>t(g.pages.list())),a("media",()=>t(g.media.list())),a("menus",()=>t(g.menus.list())),a("context-menus",()=>t(g.contextMenus.list())),a("theme",()=>g.theme.get().then(f=>(f?.overrides||[]).length).catch(()=>0)),a("collections",()=>t(g.collections.list())),a("forms",()=>t(g.forms.list())),a("views",()=>t(g.views.list())),a("actions",()=>t(g.actions.list())),a("blocks",()=>t(g.blocks.list())),a("components",()=>t(g.components.list())),a("users",()=>t(g.users.list())),a("plugins",()=>t(g.plugins.list())),a("notifications",()=>g.system?.notifications?.unreadCount?.().then(f=>f?.count??0).catch(()=>0)??Promise.resolve(0))]),L={};await Promise.all(F.map(async f=>{const x=await oe(f);x&&(L[f.url]=x)}));const _={"#/pages":i,"#/media":u,"#/menus":d,"#/context-menus":r,"#/theme":m,"#/collections":y,"#/forms":v,"#/views":k,"#/actions":C,"#/blocks":q,"#/components":b,"#/users":A,"#/plugins":j,"#/system/notifications":B},I={};for(const[f,x]of Object.entries(_)){if(s.has(f))continue;const[l,w]=Re[f]??["item","items"],p=z(x);p&&(I[f]={...p,label:`${p.count} ${p.count===1?l:w}`})}return{...I,...L}}let F=[];const Ie="dm:sidebar-count",Oe=6e4;function Ue(e=[],n=null){const o=[],t=s=>{if(s&&!(n&&typeof s.url=="string"&&s.url.startsWith("#/")&&!ce(n,s.url.slice(1)))){s.url&&typeof s.countUrl=="string"&&s.countUrl&&o.push({url:s.url,countUrl:s.countUrl,text:s.text||"",countLabel:s.countLabel||"",inventory:s.inventory===!0});for(const c of[...s.children||[],...s.items||[]])t(c)}};for(const s of e??[])t(s?.item??s);return o}async function oe(e){try{const n=new Date,o=`${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,"0")}-${String(n.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${o}`;return z(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function ie(e,n,o){const t=[...e.querySelectorAll("a.sidebar-link")].filter(s=>s.dataset.url===n);if(t.length){for(const s of t){const c=s.querySelector(".sidebar-badge[data-count-for]");if(!o||!(o.count>0)&&!o.showZero)c?.remove();else{const a=document.createRange();a.selectNodeContents(s);const i=a.createContextualFragment(ee(n,o)).firstElementChild;c?c.replaceWith(i):s.querySelector(".sidebar-text")?.after(i)}}W.set(n,o),te(e)}}const W=new Map;async function D(e,n=null){const o=n?F.filter(t=>t.url===n):F;await Promise.all(o.map(async t=>{const s=await oe(t),c=W.get(t.url);JSON.stringify(c??null)!==JSON.stringify(s)&&ie(e,t.url,s)}))}function ze(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(Ie,n=>{const o=n.detail||{};if(o.url)if(o.count!==void 0){const t=F.find(s=>s.url===o.url);ie(e,o.url,z(o,{fallbackLabel:t?.countLabel||t?.text||""}))}else D(e,o.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&D(e)},Oe),document.addEventListener("visibilitychange",()=>{document.hidden||D(e)}),We(e))}function ae(e,n,o){const t=(a,i,u)=>{const d=document.createElement(a);return i&&(d.className=i),u!=null&&(d.textContent=String(u)),d};if(!o){e.textContent=n;return}e.appendChild(t("strong","dm-badge-pop-title",o.title||n)),(o.rows?.length||!o.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",n));const s=Array.isArray(o.rows)?o.rows.slice(0,6):[];if(s.length){const a=t("dl","dm-badge-pop-rows");for(const i of s)a.appendChild(t("dt",null,i.label)),a.appendChild(t("dd",i.tone==="danger"?"is-danger":null,i.value));e.appendChild(a)}const c=Array.isArray(o.items)?o.items.slice(0,6):[];if(c.length){const a=t("ul","dm-badge-pop-items");for(const i of c){const u=t("li",i.tone==="danger"?"is-danger":null);u.appendChild(t("span","dm-badge-pop-text",i.text)),i.meta&&u.appendChild(t("span","dm-badge-pop-meta",i.meta)),a.appendChild(u)}e.appendChild(a)}else o.empty&&e.appendChild(t("p","dm-badge-pop-empty",o.empty))}function We(e){let n=null,o=null;const t=()=>e.classList.contains("is-collapsed"),s=()=>{clearTimeout(o),n?.remove(),n=null},c=(d,r)=>{s(),o=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),r(n),document.body.appendChild(n);const m=d.getBoundingClientRect(),y=n.getBoundingClientRect(),v=Math.min(m.right+10,innerWidth-y.width-8);n.style.left=`${Math.max(8,v)}px`,n.style.top=`${Math.max(8,Math.min(m.top+m.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},a=d=>{const r=d?.dataset.badgeLabel;if(!r)return s();c(d,m=>ae(m,r,N.get(d.dataset.countFor)))},i=d=>{const r=d.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",m=d.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");c(d,y=>{const v=m&&N.get(m.dataset.countFor);if(v)return ae(y,m.dataset.badgeLabel,{...v,title:v.title||r});if(!m){y.textContent=r;return}const k=document.createElement("strong");k.className="dm-badge-pop-title",k.textContent=r;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=m.dataset.badgeLabel,y.append(k,C)})},u=d=>{const r=d.closest?.("a.sidebar-link, summary");return r&&e.contains(r)?r:null};e.addEventListener("mouseover",d=>{if(t()){const m=u(d.target);m&&!m.contains(d.relatedTarget)&&i(m);return}const r=d.target.closest?.(".sidebar-badge[data-badge-label]");r&&a(r)}),e.addEventListener("mouseout",d=>{if(t()){const r=u(d.target);r&&!r.contains(d.relatedTarget)&&s();return}d.target.closest?.(".sidebar-badge[data-badge-label]")&&s()}),e.addEventListener("focusin",d=>{const r=d.target.closest?.("a, summary");if(t()&&r&&e.contains(r)&&r.querySelector(":scope > .sidebar-text"))return i(r);const m=r?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");m?a(m):s()}),e.addEventListener("focusout",s),e.addEventListener("click",s),window.addEventListener("scroll",s,!0),window.addEventListener("hashchange",s)}async function De(e=[]){if(!O(e,"settings"))return"Admin";try{const n=await g.settings.get();return n?.adminBrand?.title||n?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:n,adminScope:o=null}){const t=$(e).get(0);if(!t)return;let s,c=null,a=null,i=null,u=null,d=null;try{const l=await g.get("/sidebar/menu");s=Array.isArray(l?.items)?l.items:null,c=l?.variant||null,a=l?.position||null,i=l?.style||null,u=l?.float||null,d=l?.highlight||null}catch{s=null}(!s||!s.some(l=>l&&(!l.hidden||l.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),s=Ne.slice());let r=[];try{r=await H.get("/api/sidebar/registered-items")||[]}catch{}s=ge(s,X),s=fe(s,r.filter(l=>(l?.item??l)?.url!==X)),s=pe(s),s=be(s,[he()]);const m=await je(n);m.length&&Be(s,m),s=ve(s,n,o),s=ye(s);const y=Ae(s,U(P()),{hidden:Fe(P())});y&&(s=[y,...s]),Me(s);const[v,k]=await Promise.all([_e(r.filter(l=>O(n,l?.item?.permission)),n,o).catch(()=>({})),De(n)]),C=c?` dm-admin-sidebar--${h(c)}`:"";function q(l,w="px"){if(l==null)return l;const p=String(l).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${w}`:p}let b="";if(i){const l=[];i.fontFamily&&l.push(`font-family: ${i.fontFamily}, sans-serif`),i.fontSize&&l.push(`font-size: ${q(i.fontSize)}`),i.fontWeight&&l.push(`font-weight: ${i.fontWeight}`),i.letterSpacing&&l.push(`letter-spacing: ${q(i.letterSpacing,"em")}`);const w=[];if(l.length&&w.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 { ${l.join("; ")} }`),i.iconSize){const p=q(i.iconSize);w.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}w.length&&(b=`<style data-admin-sidebar-style>${w.join(" ")}</style>`)}const A=$e(n)?xe():"",j=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${h(k)}</span>${A}${ke()}</div>`,B=s.some(l=>l&&l.type==="spacer"&&l.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",L=`${b}<nav class="dm-admin-sidebar${C}${B}">${j}${s.map(l=>se(l,v)).join("")}</nav>`,_=document.createRange();_.selectNodeContents(t);const I=_.createContextualFragment(L);t.replaceChildren(I),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>we(t));for(const l of F)W.set(l.url,v[l.url]??null);te(t),ze(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),ue(t,{highlight:d});const f=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(l=>{t.style[l]=""}),t.classList.remove("dm-menu-floating"),f&&(f.style.paddingLeft="",f.style.paddingRight=""),t.__dmGutter=()=>{if(a!=="floating"||!f)return;const l=u&&u.anchor||"TL",w=Number.isFinite(Number(u?.offsetX))?Number(u.offsetX):16,p=t.classList.contains("is-collapsed")?Ce:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,T=Math.round(p+w*2)+"px";l==="R"||l==="TR"||l==="BR"?f.style.paddingRight=T:f.style.paddingLeft=T},a==="floating"&&de(t,{position:"floating",float:u||{}}),t.querySelectorAll("details").forEach(l=>{l.addEventListener("toggle",function(){const w=this.getAttribute("data-state-key");w&&S.set(w,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const T=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=T+"px",p.addEventListener("transitionend",function le(){p.style.maxHeight="none",p.removeEventListener("transitionend",le)})})}else{const T=p.scrollHeight;p.style.maxHeight=T+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),Se(t),Xe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function x(){const l=/^#\/(dashboards|my-dashboard)\b/.test(location.hash)?"#/":location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${l}"]`).addClass("active")}x(),t.__dmSetActive=x}function re(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function G(e){return`${location.origin}${location.pathname}${e}`}function R(e,n){const o=P(),t=e(U(o));if(!Ee(o,t)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}t.length&&J(o,!1),n&&E.toast(n,{type:"success",duration:2500}),re()}function Ge(e){if(!J(P(),e)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}E.toast(e?"Frequent hidden. Add a Tool to it from its right-click menu to bring it back.":"Frequent shown.",{type:"success",duration:3500}),re()}function Ke(e){const n=e.dataset.url;if(!n)return[];const o=e.querySelector(".sidebar-text")?.textContent.trim()||n,t=U(P()),s=t.includes(n),c=!!e.closest("details[data-frequent]"),a=t.indexOf(n),i=!s&&t.length>=Z;return[{header:o},{label:"Open",icon:"arrow-right",...Q(n)&&{shortcut:He(Q(n))},action:()=>{location.hash=n}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(G(n),"_blank","noopener")},{type:"divider"},s?{label:"Remove from Frequent",icon:"star",action:()=>R(u=>Le(u,n),`${o} removed from Frequent.`)}:{label:i?`Frequent is full (${Z})`:"Add to Frequent",icon:"star",disabled:i,action:()=>R(u=>qe(u,n),`${o} added to Frequent.`)},...c?[{label:"Move up",icon:"arrow-up",disabled:a<=0,action:()=>R(u=>Y(u,n,-1))},{label:"Move down",icon:"arrow-down",disabled:a<0||a>=t.length-1,action:()=>R(u=>Y(u,n,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(G(n)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(G(n),{type:"info",duration:8e3})}}}]}function Ve(e,n){const o=e.closest("details"),t=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",s=c=>n.querySelectorAll("details").forEach(a=>{a.open!==c&&(a.open=c)});return[{header:t},{label:o?.open?"Close":"Open",icon:o?.open?"chevron-up":"chevron-down",action:()=>{o&&(o.open=!o.open)}},{label:"Open every folder",icon:"chevrons-down",action:()=>s(!0)},{label:"Close every folder",icon:"chevrons-up",action:()=>s(!1)},...o?.hasAttribute("data-frequent-empty")?[{type:"divider"},{label:"Hide Frequent until I add a Tool",icon:"eye-off",action:()=>Ge(!0)}]:o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>R(()=>[],"Frequent cleared.")}]:[]]}function Xe(e){e.dataset.ctxWired||typeof E>"u"||!E.contextMenu||(e.dataset.ctxWired="1",E.contextMenu(e,{match:"a.sidebar-link, details > summary",inherit:!1,density:"compact",items:n=>n.matches("summary")?Ve(n,e):Ke(n)}))}
|
|
4
|
+
</details>`}if(u){const y=X(e,s),C=S.get(y)!==!1,B=e.items.map(T=>oe(T,n,y,{inFrequent:!!e.frequent})).join(""),P=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${m(y)}"${C?" open":""}${P}>
|
|
5
|
+
<summary${w}${q}>${o} <span class="sidebar-text">${a}</span>${F}</summary>
|
|
6
|
+
<div class="sidebar-children">${B}</div>
|
|
7
|
+
</details>`}return`<a href="${d}" class="sidebar-link" data-url="${d}"${w}${f}${q}>${o} <span class="sidebar-text">${a}</span>${Oe(e,n)}${F}</a>`}async function ze(e=[],n=[],s=null){const t=p=>p.then(b=>Array.isArray(b)?b.length:Array.isArray(b?.entries)?b.entries.length:0).catch(()=>0);A=Ge(e,s);const i=new Set(A.map(p=>p.url)),o={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=(p,b)=>O(n,p)&&!i.has(o[p])?b():Promise.resolve(0),[d,u,c,r,f,x,w,k,q,F,y,C,B,P]=await Promise.all([a("pages",()=>t(g.pages.list())),a("media",()=>t(g.media.list())),a("menus",()=>t(g.menus.list())),a("context-menus",()=>t(g.contextMenus.list())),a("theme",()=>g.theme.get().then(p=>(p?.overrides||[]).length).catch(()=>0)),a("collections",()=>t(g.collections.list())),a("forms",()=>t(g.forms.list())),a("views",()=>t(g.views.list())),a("actions",()=>t(g.actions.list())),a("blocks",()=>t(g.blocks.list())),a("components",()=>t(g.components.list())),a("users",()=>t(g.users.list())),a("plugins",()=>t(g.plugins.list())),a("notifications",()=>g.system?.notifications?.unreadCount?.().then(p=>p?.count??0).catch(()=>0)??Promise.resolve(0))]),T={};await Promise.all(A.map(async p=>{const b=await ie(p);b&&(T[p.url]=b)}));const U={"#/pages":d,"#/media":u,"#/menus":c,"#/context-menus":r,"#/theme":f,"#/collections":x,"#/forms":w,"#/views":k,"#/actions":q,"#/blocks":F,"#/components":y,"#/users":C,"#/plugins":B,"#/system/notifications":P},R={};for(const[p,b]of Object.entries(U)){if(i.has(p))continue;const[I,l]=Ue[p]??["item","items"],v=W(b);v&&(R[p]={...v,label:`${v.count} ${v.count===1?I:l}`})}return{...R,...T}}let A=[];const We="dm:sidebar-count",De=6e4;function Ge(e=[],n=null){const s=[],t=i=>{if(i&&!(n&&typeof i.url=="string"&&i.url.startsWith("#/")&&!ue(n,i.url.slice(1)))){i.url&&typeof i.countUrl=="string"&&i.countUrl&&s.push({url:i.url,countUrl:i.countUrl,text:i.text||"",countLabel:i.countLabel||"",inventory:i.inventory===!0});for(const o of[...i.children||[],...i.items||[]])t(o)}};for(const i of e??[])t(i?.item??i);return s}async function ie(e){try{const n=new Date,s=`${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,"0")}-${String(n.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${s}`;return W(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function ae(e,n,s){const t=[...e.querySelectorAll("a.sidebar-link")].filter(i=>i.dataset.url===n);if(t.length){for(const i of t){const o=i.querySelector(".sidebar-badge[data-count-for]");if(!s||!(s.count>0)&&!s.showZero)o?.remove();else{const a=document.createRange();a.selectNodeContents(i);const d=a.createContextualFragment(te(n,s)).firstElementChild;o?o.replaceWith(d):i.querySelector(".sidebar-text")?.after(d)}}D.set(n,s),ne(e)}}const D=new Map;async function G(e,n=null){const s=n?A.filter(t=>t.url===n):A;await Promise.all(s.map(async t=>{const i=await ie(t),o=D.get(t.url);JSON.stringify(o??null)!==JSON.stringify(i)&&ae(e,t.url,i)}))}function Ke(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(We,n=>{const s=n.detail||{};if(s.url)if(s.count!==void 0){const t=A.find(i=>i.url===s.url);ae(e,s.url,W(s,{fallbackLabel:t?.countLabel||t?.text||""}))}else G(e,s.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&G(e)},De),document.addEventListener("visibilitychange",()=>{document.hidden||G(e)}),Ve(e))}function re(e,n,s){const t=(a,d,u)=>{const c=document.createElement(a);return d&&(c.className=d),u!=null&&(c.textContent=String(u)),c};if(!s){e.textContent=n;return}e.appendChild(t("strong","dm-badge-pop-title",s.title||n)),(s.rows?.length||!s.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",n));const i=Array.isArray(s.rows)?s.rows.slice(0,6):[];if(i.length){const a=t("dl","dm-badge-pop-rows");for(const d of i)a.appendChild(t("dt",null,d.label)),a.appendChild(t("dd",d.tone==="danger"?"is-danger":null,d.value));e.appendChild(a)}const o=Array.isArray(s.items)?s.items.slice(0,6):[];if(o.length){const a=t("ul","dm-badge-pop-items");for(const d of o){const u=t("li",d.tone==="danger"?"is-danger":null);u.appendChild(t("span","dm-badge-pop-text",d.text)),d.meta&&u.appendChild(t("span","dm-badge-pop-meta",d.meta)),a.appendChild(u)}e.appendChild(a)}else s.empty&&e.appendChild(t("p","dm-badge-pop-empty",s.empty))}function Ve(e){let n=null,s=null;const t=()=>e.classList.contains("is-collapsed"),i=()=>{clearTimeout(s),n?.remove(),n=null},o=(c,r)=>{i(),s=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),r(n),document.body.appendChild(n);const f=c.getBoundingClientRect(),x=n.getBoundingClientRect(),w=Math.min(f.right+10,innerWidth-x.width-8);n.style.left=`${Math.max(8,w)}px`,n.style.top=`${Math.max(8,Math.min(f.top+f.height/2-x.height/2,innerHeight-x.height-8))}px`},150)},a=c=>{const r=c?.dataset.badgeLabel;if(!r)return i();o(c,f=>re(f,r,j.get(c.dataset.countFor)))},d=c=>{const r=c.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",f=c.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");o(c,x=>{const w=f&&j.get(f.dataset.countFor);if(w)return re(x,f.dataset.badgeLabel,{...w,title:w.title||r});if(!f){x.textContent=r;return}const k=document.createElement("strong");k.className="dm-badge-pop-title",k.textContent=r;const q=document.createElement("p");q.className="dm-badge-pop-sub",q.textContent=f.dataset.badgeLabel,x.append(k,q)})},u=c=>{const r=c.closest?.("a.sidebar-link, summary");return r&&e.contains(r)?r:null};e.addEventListener("mouseover",c=>{if(t()){const f=u(c.target);f&&!f.contains(c.relatedTarget)&&d(f);return}const r=c.target.closest?.(".sidebar-badge[data-badge-label]");r&&a(r)}),e.addEventListener("mouseout",c=>{if(t()){const r=u(c.target);r&&!r.contains(c.relatedTarget)&&i();return}c.target.closest?.(".sidebar-badge[data-badge-label]")&&i()}),e.addEventListener("focusin",c=>{const r=c.target.closest?.("a, summary");if(t()&&r&&e.contains(r)&&r.querySelector(":scope > .sidebar-text"))return d(r);const f=r?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");f?a(f):i()}),e.addEventListener("focusout",i),e.addEventListener("click",i),window.addEventListener("scroll",i,!0),window.addEventListener("hashchange",i)}async function Xe(e=[]){if(!O(e,"settings"))return"Admin";try{const n=await g.settings.get();return n?.adminBrand?.title||n?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:n,adminScope:s=null}){const t=$(e).get(0);if(!t)return;const i=He(g,L()).catch(()=>{});let o,a=null,d=null,u=null,c=null,r=null;try{const l=await g.get("/sidebar/menu");o=Array.isArray(l?.items)?l.items:null,a=l?.variant||null,d=l?.position||null,u=l?.style||null,c=l?.float||null,r=l?.highlight||null}catch{o=null}(!o||!o.some(l=>l&&(!l.hidden||l.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),o=Re.slice());let f=[];try{f=await H.get("/api/sidebar/registered-items")||[]}catch{}o=ye(o,Z),o=he(o,f.filter(l=>(l?.item??l)?.url!==Z)),o=ge(o),o=ve(o,[be()]);const x=await _e(n);x.length&&Ie(o,x),o=$e(o,n,s),o=xe(o),await i;const w=Le(o,z(L()),{hidden:Ee(L())});w&&(o=[w,...o]),Be(o);const[k,q]=await Promise.all([ze(f.filter(l=>O(n,l?.item?.permission)),n,s).catch(()=>({})),Xe(n)]),F=a?` dm-admin-sidebar--${m(a)}`:"";function y(l,v="px"){if(l==null)return l;const h=String(l).trim();return/^\d+(\.\d+)?$/.test(h)?`${h}${v}`:h}let C="";if(u){const l=[];u.fontFamily&&l.push(`font-family: ${u.fontFamily}, sans-serif`),u.fontSize&&l.push(`font-size: ${y(u.fontSize)}`),u.fontWeight&&l.push(`font-weight: ${u.fontWeight}`),u.letterSpacing&&l.push(`letter-spacing: ${y(u.letterSpacing,"em")}`);const v=[];if(l.length&&v.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 { ${l.join("; ")} }`),u.iconSize){const h=y(u.iconSize);v.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${h} !important; height: ${h} !important; }`)}v.length&&(C=`<style data-admin-sidebar-style>${v.join(" ")}</style>`)}const B=ke(n)?we():"",P=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${m(q)}</span>${B}${qe()}</div>`,T=o.some(l=>l&&l.type==="spacer"&&l.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",U=`${C}<nav class="dm-admin-sidebar${F}${T}">${P}${o.map(l=>oe(l,k)).join("")}</nav>`,R=document.createRange();R.selectNodeContents(t);const p=R.createContextualFragment(U);t.replaceChildren(p),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>Se(t));for(const l of A)D.set(l.url,k[l.url]??null);ne(t),Ke(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),me(t,{highlight:r});const b=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(l=>{t.style[l]=""}),t.classList.remove("dm-menu-floating"),b&&(b.style.paddingLeft="",b.style.paddingRight=""),t.__dmGutter=()=>{if(d!=="floating"||!b)return;const l=c&&c.anchor||"TL",v=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,h=t.classList.contains("is-collapsed")?Ae:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,N=Math.round(h+v*2)+"px";l==="R"||l==="TR"||l==="BR"?b.style.paddingRight=N:b.style.paddingLeft=N},d==="floating"&&fe(t,{position:"floating",float:c||{}}),t.querySelectorAll("details").forEach(l=>{l.addEventListener("toggle",function(){const v=this.getAttribute("data-state-key");v&&S.set(v,this.open);const h=this.querySelector(":scope > .sidebar-children");if(h)if(this.open){const N=h.scrollHeight;h.style.maxHeight="0",requestAnimationFrame(()=>{h.style.maxHeight=N+"px",h.addEventListener("transitionend",function de(){h.style.maxHeight="none",h.removeEventListener("transitionend",de)})})}else{const N=h.scrollHeight;h.style.maxHeight=N+"px",requestAnimationFrame(()=>{h.style.maxHeight="0"})}})}),Ce(t),Qe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function I(){const l=/^#\/(dashboards|my-dashboard)\b/.test(location.hash)?"#/":location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${l}"]`).addClass("active")}I(),t.__dmSetActive=I}function le(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function K(e){return`${location.origin}${location.pathname}${e}`}function _(e,n){const s=L(),t=e(z(s));if(!Ne(s,t)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}t.length&&Q(s,!1),ce(s),n&&E.toast(n,{type:"success",duration:2500}),le()}function ce(e){Me(g,e).catch(()=>E.toast("Frequent is saved in this browser only - the server could not keep it.",{type:"warning"}))}function Ze(e){if(!Q(L(),e)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}ce(L()),E.toast(e?"Frequent hidden. Add a Tool to it from its right-click menu to bring it back.":"Frequent shown.",{type:"success",duration:3500}),le()}function Ye(e){const n=e.dataset.url;if(!n)return[];const s=e.querySelector(".sidebar-text")?.textContent.trim()||n,t=z(L()),i=t.includes(n),o=!!e.closest("details[data-frequent]"),a=t.indexOf(n),d=!i&&t.length>=Y;return[{header:s},{label:"Open",icon:"arrow-right",...ee(n)&&{shortcut:Pe(ee(n))},action:()=>{location.hash=n}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(K(n),"_blank","noopener")},{type:"divider"},i?{label:"Remove from Frequent",icon:"star",action:()=>_(u=>Te(u,n),`${s} removed from Frequent.`)}:{label:d?`Frequent is full (${Y})`:"Add to Frequent",icon:"star",disabled:d,action:()=>_(u=>Fe(u,n),`${s} added to Frequent.`)},...o?[{label:"Move up",icon:"arrow-up",disabled:a<=0,action:()=>_(u=>J(u,n,-1))},{label:"Move down",icon:"arrow-down",disabled:a<0||a>=t.length-1,action:()=>_(u=>J(u,n,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(K(n)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(K(n),{type:"info",duration:8e3})}}}]}function Je(e,n){const s=e.closest("details"),t=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",i=o=>n.querySelectorAll("details").forEach(a=>{a.open!==o&&(a.open=o)});return[{header:t},{label:s?.open?"Close":"Open",icon:s?.open?"chevron-up":"chevron-down",action:()=>{s&&(s.open=!s.open)}},{label:"Open every folder",icon:"chevrons-down",action:()=>i(!0)},{label:"Close every folder",icon:"chevrons-up",action:()=>i(!1)},...s?.hasAttribute("data-frequent-empty")?[{type:"divider"},{label:"Hide Frequent until I add a Tool",icon:"eye-off",action:()=>Ze(!0)}]:s?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>_(()=>[],"Frequent cleared.")}]:[]]}function Qe(e){e.dataset.ctxWired||typeof E>"u"||!E.contextMenu||(e.dataset.ctxWired="1",E.contextMenu(e,{match:"a.sidebar-link, details > summary",inherit:!1,density:"compact",items:n=>n.matches("summary")?Je(n,e):Ye(n)}))}
|
package/package.json
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /api/sidebar/menu - the admin-sidebar menu, filtered for the caller
|
|
5
5
|
* GET /api/sidebar/registered-items - plugin-registered sidebar items, filtered for the caller
|
|
6
|
+
* GET /api/sidebar/frequent - the caller's own Frequent list
|
|
7
|
+
* PUT /api/sidebar/frequent - keep it ({urls, hidden})
|
|
6
8
|
*
|
|
7
9
|
* Both need only a signed-in user, NOT `menus.read`: every user who can reach
|
|
8
10
|
* the admin needs a sidebar, and a role without menu-editing rights (a
|
|
@@ -19,6 +21,7 @@ import {getRegisteredSidebarItems} from '../../services/hooks.js';
|
|
|
19
21
|
import {filterItemsForUser, getMenu} from '../../services/menus.js';
|
|
20
22
|
import {isDashboardPro} from '../../services/widgets.js';
|
|
21
23
|
import {boardName, readPrefs, readSite} from '../../services/dashboardSettings.js';
|
|
24
|
+
import {frequentProblems, readFrequent, writeFrequent} from '../../services/sidebarFrequent.js';
|
|
22
25
|
|
|
23
26
|
export const ADMIN_SIDEBAR_SLUG = 'admin-sidebar';
|
|
24
27
|
|
|
@@ -61,4 +64,17 @@ export async function sidebarRoutes(fastify, opts = {}) {
|
|
|
61
64
|
? {...entry, item: {...entry.item, items: filterItemsForUser(entry.item.items, request.user)}}
|
|
62
65
|
: entry);
|
|
63
66
|
});
|
|
67
|
+
|
|
68
|
+
// Each person's own, so it follows them from browser to browser (sidebarFrequent.js).
|
|
69
|
+
fastify.get('/sidebar/frequent', signedIn, async (request) => readFrequent(request.user));
|
|
70
|
+
|
|
71
|
+
fastify.put('/sidebar/frequent', signedIn, async (request, reply) => {
|
|
72
|
+
const problems = frequentProblems(request.body);
|
|
73
|
+
if (problems.length) return reply.code(400).send({error: problems.join(' ')});
|
|
74
|
+
try {
|
|
75
|
+
return await writeFrequent(request.user, request.body);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return reply.code(400).send({error: err.message});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
64
80
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Runs once on server boot. Idempotent - guarded by the existence of
|
|
5
5
|
* config/menus/admin-sidebar.json. Seeds the admin sidebar menu with the
|
|
6
|
-
* standard tree (
|
|
6
|
+
* standard tree (Dashboards, Projects, Content, Data, Tools, System,
|
|
7
7
|
* Documentation) and patches config/menu-locations.json to map the
|
|
8
8
|
* `admin-sidebar` slot.
|
|
9
9
|
*
|
|
@@ -21,7 +21,7 @@ const DEFAULT_CONFIG_DIR = path.resolve(path.dirname(__filename), '..', '..', 'c
|
|
|
21
21
|
|
|
22
22
|
const SEED_ITEMS = [
|
|
23
23
|
{
|
|
24
|
-
text: '
|
|
24
|
+
text: 'Dashboards', key: 'overview', icon: 'home',
|
|
25
25
|
items: [
|
|
26
26
|
{text: 'Dashboard', url: '#/', icon: 'home'}
|
|
27
27
|
]
|
|
@@ -319,13 +319,14 @@ export async function ensureDocumentationSubmenus(opts = {}) {
|
|
|
319
319
|
|
|
320
320
|
/** Keys for the folders the seed has always created, matched by their original name. */
|
|
321
321
|
const SEED_FOLDER_KEYS = {
|
|
322
|
-
overview: 'overview', projects: 'projects', content: 'content', data: 'data',
|
|
322
|
+
overview: 'overview', dashboards: 'overview', projects: 'projects', content: 'content', data: 'data',
|
|
323
323
|
tools: 'tools', system: 'system', documentation: 'documentation'
|
|
324
324
|
};
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
327
|
* Give an existing install's top-level folders their stable `key`, and - once -
|
|
328
|
-
* a persisted Tools folder for enabled Tools to be placed into
|
|
328
|
+
* a persisted Tools folder for enabled Tools to be placed into, and - once -
|
|
329
|
+
* the name Dashboards for the folder that was Overview.
|
|
329
330
|
*
|
|
330
331
|
* - A folder is keyed only if it has no key yet and still carries one of the
|
|
331
332
|
* seed's names; a folder the admin made, or renamed before keys existed, is
|
|
@@ -361,6 +362,16 @@ export async function ensureFolderKeys(opts = {}) {
|
|
|
361
362
|
changed = true;
|
|
362
363
|
}
|
|
363
364
|
|
|
365
|
+
// The first folder was "Overview" until 1.6.6. Renamed once (`meta.dashboardsFolder`)
|
|
366
|
+
// and only while it still says Overview, so a name an admin gave it - even
|
|
367
|
+
// Overview again, later - stays. Its key is still `overview`.
|
|
368
|
+
if (!menu.meta?.dashboardsFolder && items.length) {
|
|
369
|
+
const first = items.find(n => isFolder(n) && n.key === 'overview');
|
|
370
|
+
if (first && String(first.text || '').trim() === 'Overview') first.text = 'Dashboards';
|
|
371
|
+
menu.meta = {...(menu.meta || {}), dashboardsFolder: true};
|
|
372
|
+
changed = true;
|
|
373
|
+
}
|
|
374
|
+
|
|
364
375
|
// An empty menu is left empty: the renderer's minimal fallback (Dashboard,
|
|
365
376
|
// Menus) is what an admin needs to repair it, and a lone Tools folder would
|
|
366
377
|
// stop that fallback from applying.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar's Frequent folder, kept per user on the server so it follows the
|
|
3
|
+
* person to every browser and device (admin/js/lib/sidebar-frequent.js keeps
|
|
4
|
+
* a copy in the browser to draw from). content/sidebar/frequent/<user id>.json
|
|
5
|
+
* - per user, so ignored in a site repo like Dashboard Pro's prefs.
|
|
6
|
+
*
|
|
7
|
+
* Only the list is kept here, not what it may open: the sidebar draws Frequent
|
|
8
|
+
* from the user's already-filtered tree, so a url they cannot open never shows.
|
|
9
|
+
*
|
|
10
|
+
* Tested in tests/services/sidebar-frequent.test.js.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
/** As FREQUENT_MAX in admin/js/lib/sidebar-frequent.js. */
|
|
16
|
+
export const FREQUENT_MAX = 12;
|
|
17
|
+
const URL_MAX = 500;
|
|
18
|
+
const USER_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
|
|
19
|
+
|
|
20
|
+
/** A list, cleaned: admin hash urls only, once each, at most FREQUENT_MAX. */
|
|
21
|
+
export function normaliseFrequent(input) {
|
|
22
|
+
const p = input && typeof input === 'object' ? input : {};
|
|
23
|
+
const urls = [];
|
|
24
|
+
for (const u of Array.isArray(p.urls) ? p.urls : []) {
|
|
25
|
+
if (typeof u !== 'string' || !u.startsWith('#/') || u.length > URL_MAX || urls.includes(u)) continue;
|
|
26
|
+
urls.push(u);
|
|
27
|
+
if (urls.length >= FREQUENT_MAX) break;
|
|
28
|
+
}
|
|
29
|
+
return {urls, hidden: p.hidden === true};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What is wrong with a body sent to PUT, or [] when nothing is. */
|
|
33
|
+
export function frequentProblems(input) {
|
|
34
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return ['Send {urls, hidden}.'];
|
|
35
|
+
const problems = [];
|
|
36
|
+
if (input.urls !== undefined && !Array.isArray(input.urls)) problems.push('urls must be a list.');
|
|
37
|
+
if (input.hidden !== undefined && typeof input.hidden !== 'boolean') problems.push('hidden must be true or false.');
|
|
38
|
+
return problems;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DIR = () => path.resolve('content', 'sidebar', 'frequent');
|
|
42
|
+
const fileFor = (user) => (USER_ID_RE.test(String(user?.id || '')) ? path.join(DIR(), `${user.id}.json`) : null);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The user's list. `saved` is false until they have kept one here, so the
|
|
46
|
+
* browser knows to bring up a list it kept before Frequent moved to the server.
|
|
47
|
+
*/
|
|
48
|
+
export async function readFrequent(user) {
|
|
49
|
+
const file = fileFor(user);
|
|
50
|
+
let data = null;
|
|
51
|
+
if (file) {
|
|
52
|
+
try { data = JSON.parse(await fs.readFile(file, 'utf8')); } catch { data = null; }
|
|
53
|
+
}
|
|
54
|
+
return {...normaliseFrequent(data), saved: !!data};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function writeFrequent(user, input) {
|
|
58
|
+
const file = fileFor(user);
|
|
59
|
+
if (!file) throw new Error('This account cannot keep a Frequent list.');
|
|
60
|
+
const data = normaliseFrequent(input);
|
|
61
|
+
await fs.mkdir(path.dirname(file), {recursive: true});
|
|
62
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
63
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
64
|
+
await fs.rename(tmp, file);
|
|
65
|
+
return {...data, saved: true};
|
|
66
|
+
}
|
|
@@ -320,6 +320,8 @@ export function buildGitignore({sitePlugins = [], pluginContent = {}, sitePublic
|
|
|
320
320
|
// Each admin's own Dashboard Pro layout (keyed by user id); the site default beside it is tracked.
|
|
321
321
|
'/content/dashboard/layouts/',
|
|
322
322
|
'/content/dashboard/prefs/',
|
|
323
|
+
// Each admin's own sidebar Frequent list (keyed by user id).
|
|
324
|
+
'/content/sidebar/frequent/',
|
|
323
325
|
'/config/connections.json*',
|
|
324
326
|
'/config/keys/',
|
|
325
327
|
'/plugins/*/data/*.jsonl',
|