domma-cms 1.10.28 → 1.10.30
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.30] - 2026-10-26
|
|
4
|
+
|
|
5
|
+
- Admin navigation keeps My Profile in the top account menu, removing its redundant System entry. Search defaults to Content, and existing System entries move there once while retaining saved settings and renamed folders.
|
|
6
|
+
|
|
7
|
+
## [1.10.29] - 2026-10-26
|
|
8
|
+
|
|
9
|
+
- **Pro Tools open again on sites behind the Manager.** The admin's versioned-file check ran before the Manager handed a request to its site, so a site's request for a Tool's screen (Analytics Pro's dashboard, for one) was answered from the Manager's own copy of that plugin; where the two builds differ the fingerprint never matched and the screen 404'd. The check now runs after the plugins' hooks, so the site answers its own files.
|
|
10
|
+
|
|
3
11
|
## [1.10.28] - 2026-10-26
|
|
4
12
|
|
|
5
13
|
- Admin pages and Tools have clearer section boundaries: stronger outlines, coloured header bands, icon tiles and a shaded canvas, all using the active theme palette. Settings and editor sections have distinct identities, and optional generated CSS and template-variable references fold away with keyboard-accessible controls.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isPathAllowed as F}from"./admin-scope.js";export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Marketplace",url:MANAGE_PLUGINS_URL,icon:"store",permission:"plugins"};export function stripItemByUrl(e,t){const n=[];for(const r of e||[]){if(!r||r.url===t)continue;const s={...r};Array.isArray(r.items)&&r.items.length&&(s.items=stripItemByUrl(r.items,t)),n.push(s)}return n}export function managePluginsItem(){return{...MANAGE_PLUGINS_ITEM}}export const TOOLS_FOLDER_KEY="tools";const E=["countUrl","core","permission"],c=e=>String(e??"").toLowerCase();export function slugify(e){return c(e).trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}export function groupKeyFor(e,t){return`${c(e)}.${slugify(t)}`}export const GROUP_ICON="folder";const P=e=>e&&(e.type==="separator"||e.type==="spacer"||e.type==="theme-switcher"),A=e=>e&&!P(e)&&!e.url;function w(e){return(e||[]).filter(Boolean).map(t=>Array.isArray(t.items)?{...t,items:w(t.items)}:{...t})}function
|
|
1
|
+
import{isPathAllowed as F}from"./admin-scope.js";export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Marketplace",url:MANAGE_PLUGINS_URL,icon:"store",permission:"plugins"};export function stripItemByUrl(e,t){const n=[];for(const r of e||[]){if(!r||r.url===t)continue;const s={...r};Array.isArray(r.items)&&r.items.length&&(s.items=stripItemByUrl(r.items,t)),n.push(s)}return n}export function managePluginsItem(){return{...MANAGE_PLUGINS_ITEM}}export const TOOLS_FOLDER_KEY="tools";const E=["countUrl","core","permission"],c=e=>String(e??"").toLowerCase();export function slugify(e){return c(e).trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}export function groupKeyFor(e,t){return`${c(e)}.${slugify(t)}`}export const GROUP_ICON="folder";const P=e=>e&&(e.type==="separator"||e.type==="spacer"||e.type==="theme-switcher"),A=e=>e&&!P(e)&&!e.url;function w(e){return(e||[]).filter(Boolean).map(t=>Array.isArray(t.items)?{...t,items:w(t.items)}:{...t})}function N(e,t,n=[]){for(const r of e||[]){if(!r)continue;if(t(r))return[...n,r];const s=N(r.items,t,[...n,r]);if(s)return s}return null}function I(e,t,n=[],r=[]){for(const s of e||[])s&&(t(s)&&r.push([...n,s]),I(s.items,t,[...n,s],r));return r}const U=e=>!e.some(t=>t.hidden||t.restricted);function $(e,t){for(const n of e||[]){if(t(n))return n;const r=$(n?.items,t);if(r)return r}return null}export function findFolder(e,t){if(!t)return null;const n=c(t);return $(e,r=>A(r)&&c(r.key)===n)||$(e,r=>A(r)&&!r.key&&c(r.text)===n)}function b(e,t){const n={...t,...e};for(const r of E)t[r]!==void 0&&(n[r]=t[r]);return!(Array.isArray(e.items)&&e.items.length)&&Array.isArray(t.items)&&(n.items=t.items),e.text||(n.text=t.text),!e.icon&&t.icon&&(n.icon=t.icon),delete n.ref,delete n.folder,delete n.group,n}function O(e,{persisted:t,orphan:n=!1,node:r=null}){const s=e?.item||{};return{persisted:t,orphan:n,source:s.source||(s.core?"Built in":""),folder:e?.folder||TOOLS_FOLDER_KEY,defaults:n?{text:null,icon:null}:{text:s.text||"",icon:s.icon||""},...n&&r&&{label:r.text||r.url},...(e?.group||e?.item?.group)&&{group:e.group||e.item.group}}}export function placeTools(e,t,{forEditor:n=!1}={}){const r=w(e),s=(t||[]).filter(o=>o&&o.item&&typeof o.item.text=="string"),d=new Map;for(const o of s)o.item.url&&!d.has(o.item.url)&&d.set(o.item.url,o);const y=new Set,m=o=>{const i=[];for(const u of o){const h=u.url?d.get(u.url):null;if(h&&!y.has(u.url)){y.add(u.url);const g=b(u,h.item);Array.isArray(u.items)&&u.items.length?g.items=m(u.items):n&&(g.items=[]),n&&(g.$tool=O(h,{persisted:!0})),i.push(g);continue}if(u.ref){if(!n)continue;const{ref:g,...f}=u;i.push({...f,items:m(u.items||[]),$tool:O(null,{persisted:!0,orphan:!0,node:u})});continue}Array.isArray(u.items)&&(u.items=m(u.items)),i.push(u)}return i},a=m(r),T=new Set,S=o=>{for(const i of o||[])i.hidden&&i.text&&!i.url&&!(Array.isArray(i.items)&&i.items.length)&&T.add(c(i.text)),S(i.items)};S(a);let x=null;const L=()=>{const o=findFolder(a,TOOLS_FOLDER_KEY);if(o)return o;if(!x){x={text:TOOLS_FOLDER_TEXT,key:TOOLS_FOLDER_KEY,icon:"tool",items:[],synthesised:!0};const i=a.findIndex(u=>["system","documentation"].includes(c(u?.key||u?.text)));a.splice(i===-1?a.length:i,0,x)}return x},l=new Set,k=(o,i)=>{const u=groupKeyFor(o.key||slugify(o.text),i),h=$(a,p=>A(p)&&c(p.key)===u)||(o.items||[]).find(p=>A(p)&&(!p.key||c(p.key)===u)&&c(p.text)===c(i));if(h)return h;const g={text:String(i),key:u,icon:GROUP_ICON,items:[],synthesised:!0},f=o.items.findIndex(p=>l.has(p));return o.items.splice(f===-1?o.items.length:f,0,g),g};for(const o of s){const{item:i}=o;if(i.url&&y.has(i.url))continue;i.url&&y.add(i.url);const u=T.has(c(i.text));if(u&&!n)continue;const h=b({},i);n&&(h.items=[],h.$tool=O(o,{persisted:!1}),u&&(h.hidden=!0));let f=(o.parent?findFolder(a,o.parent)||$(a,_=>!P(_)&&c(_.text)===c(o.parent)):null)||findFolder(a,o.folder||i.folder)||L();f.items=Array.isArray(f.items)?f.items:[];const p=typeof(o.group||i.group)=="string"?(o.group||i.group).trim():"";p&&slugify(p)&&(f=k(f,p),f.items=Array.isArray(f.items)?f.items:[]),f.items.push(h),l.add(h)}return a}export const LOCKED_TOOLS=[{folder:"overview",item:{text:"Dashboard",url:"#/",icon:"home"}},{folder:"content",item:{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}},{folder:"system",item:{text:"Users",url:"#/users",icon:"users",permission:"users"}}];export function isLockedUrl(e){return LOCKED_TOOLS.some(t=>t.item.url===e)}export function ensureLockedTools(e,{leaveInHiddenFolders:t=!1}={}){const n=w(e);for(const{folder:r,item:s}of LOCKED_TOOLS){const d=I(n,l=>l?.url===s.url),y=d.find(l=>!l.some(k=>k.restricted));if(!y&&d.length)continue;const m=y?.at(-1);if(m){if(m.hidden&&delete m.hidden,!y.slice(0,-1).some(o=>o.hidden)||t)continue;const k=y.at(-2);k.items=k.items.filter(o=>o!==m)}const a=c(r),T=I(n,l=>A(l)&&c(l.key)===a).filter(U),S=I(n,l=>A(l)&&!l.key&&c(l.text)===a).filter(U),x=(T[0]||S[0])?.at(-1)||null,L=m||{...s};x?(x.items=Array.isArray(x.items)?x.items:[],x.items.push(L)):n.push(L)}return n}export function insertFoldersBeforeSystem(e,t){const n=(t||[]).filter(Boolean);if(!n.length)return e;const r=["system","documentation"],s=e.findIndex(d=>r.includes(c(d?.key||d?.text)));return s===-1?[...e,...n]:[...e.slice(0,s),...n,...e.slice(s)]}export function canAny(e,t){return t?!e||!e.length?!1:e.includes(t)?!0:["read","create","update","delete"].some(n=>e.includes(`${t}.${n}`)):!0}export function filterByPermission(e,t,n=null){const r=[];for(const s of e){if(s.hidden||s.permission&&!canAny(t,s.permission))continue;const d=typeof s.url=="string"?s.url:"";if(n&&d.startsWith("#/")&&!F(n,d.slice(1)))continue;const y=Array.isArray(s.items)&&s.items.length>0,m=y?filterByPermission(s.items,t,n):[];y&&!m.length&&!d||r.push({...s,items:m})}return r}export function sidebarStateKey(e,t=null){const n=String(e?.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),r=String(e?.url||"").match(/^#\/projects\/([^/]+)/);let s="";if(r)try{s=`project-${decodeURIComponent(r[1])}.`}catch{s=`project-${r[1]}.`}return t?e?.key?`sidebar.expanded.key.${c(e.key)}`:`${t}/${s}${n}`:`sidebar.expanded.${s}${n}`}export function pruneEmptyToolsFolder(e){return(e||[]).filter(t=>A(t)?!(c(t.key)===TOOLS_FOLDER_KEY||!t.key&&t.text===TOOLS_FOLDER_TEXT)||Array.isArray(t.items)&&t.items.length>0:!0)}
|
|
@@ -90,6 +90,12 @@
|
|
|
90
90
|
"url": "#/content-api",
|
|
91
91
|
"icon": "file-text",
|
|
92
92
|
"permission": "settings"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"text": "Search",
|
|
96
|
+
"url": "#/search",
|
|
97
|
+
"icon": "search",
|
|
98
|
+
"permission": "settings"
|
|
93
99
|
}
|
|
94
100
|
],
|
|
95
101
|
"key": "content"
|
|
@@ -173,22 +179,11 @@
|
|
|
173
179
|
"icon": "layout",
|
|
174
180
|
"permission": "layouts"
|
|
175
181
|
},
|
|
176
|
-
{
|
|
177
|
-
"text": "My Profile",
|
|
178
|
-
"url": "#/my-profile",
|
|
179
|
-
"icon": "user"
|
|
180
|
-
},
|
|
181
182
|
{
|
|
182
183
|
"text": "API Tokens",
|
|
183
184
|
"url": "#/api-tokens",
|
|
184
185
|
"icon": "key",
|
|
185
186
|
"permission": "api-tokens"
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"text": "Search",
|
|
189
|
-
"url": "#/search",
|
|
190
|
-
"icon": "search",
|
|
191
|
-
"permission": "settings"
|
|
192
187
|
}
|
|
193
188
|
],
|
|
194
189
|
"key": "system"
|
package/package.json
CHANGED
package/server/server.js
CHANGED
|
@@ -166,7 +166,7 @@ if (publicCorsPrefixes.length) {
|
|
|
166
166
|
await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
|
|
167
167
|
await app.register(rateLimit, rateLimitOptions());
|
|
168
168
|
|
|
169
|
-
registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
|
|
169
|
+
const adminAssets = registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
|
|
170
170
|
// Text assets (scripts, stylesheets, SVG, JSON) from memory, compressed once
|
|
171
171
|
// per encoding - services/assetCache.js. Its onSend hook must be added BEFORE
|
|
172
172
|
// compress registers its own, so compress finds content-encoding set and
|
|
@@ -338,7 +338,7 @@ try {
|
|
|
338
338
|
item: {text: 'Theme', url: '#/theme', icon: 'droplet', permission: 'theme'}
|
|
339
339
|
});
|
|
340
340
|
await ensureSidebarItem({
|
|
341
|
-
groupText: '
|
|
341
|
+
groupText: 'Content',
|
|
342
342
|
item: {text: 'Search', url: '#/search', icon: 'search', permission: 'settings'}
|
|
343
343
|
});
|
|
344
344
|
// Restructure Documentation: Tutorials → submenu, add Components reference set.
|
|
@@ -699,11 +699,10 @@ registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Roles',
|
|
|
699
699
|
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Users', url: '#/users', icon: 'users', permission: 'users', countUrl: '/api/users/_count', inventory: true}});
|
|
700
700
|
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Site Settings', url: '#/settings', icon: 'settings', permission: 'settings', countUrl: '/api/settings/_count'}});
|
|
701
701
|
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Effects', url: '#/effects', icon: 'sparkles', permission: 'settings', countUrl: '/api/effects/_count', inventory: true}});
|
|
702
|
-
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'My Profile', url: '#/my-profile', icon: 'user', countUrl: '/api/auth/me/_count'}});
|
|
703
702
|
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Layouts', url: '#/layouts', icon: 'layout', permission: 'layouts', countUrl: '/api/layouts/_count'}});
|
|
704
703
|
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Plugins', url: '#/plugins', icon: 'package', permission: 'plugins', countUrl: '/api/plugins/_count', inventory: true}});
|
|
705
704
|
registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'API Tokens', url: '#/api-tokens', icon: 'key', permission: 'api-tokens', countUrl: '/api/api-tokens/_count'}});
|
|
706
|
-
registerSidebarItem({folder: '
|
|
705
|
+
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Search', url: '#/search', icon: 'search', permission: 'settings', countUrl: '/api/search/_count', inventory: true}});
|
|
707
706
|
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Content API', url: '#/content-api', icon: 'file-text', permission: 'settings', countUrl: '/api/content-api/_count', inventory: true}});
|
|
708
707
|
// Documentation pages added after the sidebar menu was seeded (0.93.1). The menu is
|
|
709
708
|
// seeded once, so new pages reach existing sites only by registering here. The two
|
|
@@ -727,6 +726,8 @@ await registerCoreSources();
|
|
|
727
726
|
(await import('./services/passwordReset.js')).registerPasswordChangeNotice({log: app.log});
|
|
728
727
|
|
|
729
728
|
await registerPlugins(app);
|
|
729
|
+
// After the plugins' onRequest hooks - see serve() in services/adminAssets.js.
|
|
730
|
+
adminAssets.serve();
|
|
730
731
|
|
|
731
732
|
// ---------------------------------------------------------------------------
|
|
732
733
|
// Public Plugin Routes (root-level, before catch-all)
|
|
@@ -76,7 +76,10 @@ export function registerAdminAssets(app, {root, dommaDist, maxBytes = 16 * 1024
|
|
|
76
76
|
app.get(url, async (_request, reply) => reply.header('Cache-Control', 'no-store')
|
|
77
77
|
.type('text/html; charset=utf-8').send(await index()));
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// Added by serve(), after the plugins: a plugin's onRequest hook that answers a request
|
|
80
|
+
// itself (site-manager proxying another site's host) must run before this one, or this
|
|
81
|
+
// answers from the manager's own files - a different build of the same plugin 404s.
|
|
82
|
+
const serve = () => app.addHook('onRequest', async (request, reply) => {
|
|
80
83
|
if (!['GET', 'HEAD'].includes(request.method)) return;
|
|
81
84
|
let url;
|
|
82
85
|
try { url = new URL(request.raw.url, 'http://localhost'); } catch { return; }
|
|
@@ -113,5 +116,5 @@ export function registerAdminAssets(app, {root, dommaDist, maxBytes = 16 * 1024
|
|
|
113
116
|
reply.removeHeader('content-length');
|
|
114
117
|
return JSON.stringify(config);
|
|
115
118
|
});
|
|
116
|
-
return {version};
|
|
119
|
+
return {version, serve};
|
|
117
120
|
}
|
|
@@ -41,7 +41,8 @@ const SEED_ITEMS = [
|
|
|
41
41
|
{text: 'Blocks', url: '#/blocks', icon: 'box', permission: 'pages'},
|
|
42
42
|
{text: 'Components', url: '#/components', icon: 'component', permission: 'components'},
|
|
43
43
|
{text: 'Views', url: '#/views', icon: 'eye', permission: 'views'},
|
|
44
|
-
{text: 'Content API', url: '#/content-api', icon: 'file-text', permission: 'settings'}
|
|
44
|
+
{text: 'Content API', url: '#/content-api', icon: 'file-text', permission: 'settings'},
|
|
45
|
+
{text: 'Search', url: '#/search', icon: 'search', permission: 'settings'}
|
|
45
46
|
]
|
|
46
47
|
},
|
|
47
48
|
{
|
|
@@ -65,11 +66,9 @@ const SEED_ITEMS = [
|
|
|
65
66
|
{text: 'Users', url: '#/users', icon: 'users', permission: 'users'},
|
|
66
67
|
{text: 'Site Settings', url: '#/settings', icon: 'settings', permission: 'settings'},
|
|
67
68
|
{text: 'Effects', url: '#/effects', icon: 'sparkles', permission: 'settings'},
|
|
68
|
-
{text: 'Search', url: '#/search', icon: 'search', permission: 'settings'},
|
|
69
69
|
{text: 'Layouts', url: '#/layouts', icon: 'layout', permission: 'layouts'},
|
|
70
70
|
{text: 'Plugins', url: '#/plugins', icon: 'package', permission: 'plugins'},
|
|
71
|
-
{text: 'API Tokens', url: '#/api-tokens', icon: 'key', permission: 'api-tokens'}
|
|
72
|
-
{text: 'My Profile', url: '#/my-profile', icon: 'user'}
|
|
71
|
+
{text: 'API Tokens', url: '#/api-tokens', icon: 'key', permission: 'api-tokens'}
|
|
73
72
|
]
|
|
74
73
|
},
|
|
75
74
|
{
|
|
@@ -441,6 +440,35 @@ export async function ensureFolderKeys(opts = {}) {
|
|
|
441
440
|
changed = true;
|
|
442
441
|
}
|
|
443
442
|
|
|
443
|
+
// Account access lives in the top menu. Move Search out of the old
|
|
444
|
+
// System default once, preserving saved labels, permissions and folders.
|
|
445
|
+
if (!menu.meta?.accountSearchNavigation && items.length) {
|
|
446
|
+
const moving = [];
|
|
447
|
+
const removeProfile = nodes => nodes.filter(n => n?.url !== '#/my-profile').map(n => {
|
|
448
|
+
if (Array.isArray(n?.items)) n.items = removeProfile(n.items);
|
|
449
|
+
return n;
|
|
450
|
+
});
|
|
451
|
+
for (const folder of items.filter(n => isFolder(n) && n.key === 'system')) {
|
|
452
|
+
moving.push(...(folder.items || []).filter(n => n?.url === '#/search'));
|
|
453
|
+
folder.items = removeProfile((folder.items || []).filter(n => n?.url !== '#/search'));
|
|
454
|
+
}
|
|
455
|
+
const hasSearch = nodes => nodes.some(n => n?.url === '#/search'
|
|
456
|
+
|| (Array.isArray(n?.items) && hasSearch(n.items)));
|
|
457
|
+
let content = items.find(n => isFolder(n) && n.key === 'content');
|
|
458
|
+
for (const item of moving) {
|
|
459
|
+
if (hasSearch(items)) break;
|
|
460
|
+
if (!content) {
|
|
461
|
+
content = {text: 'Content', key: 'content', icon: 'edit', items: []};
|
|
462
|
+
const index = items.findIndex(n => isFolder(n) && n.key === 'system');
|
|
463
|
+
items.splice(index === -1 ? items.length : index, 0, content);
|
|
464
|
+
}
|
|
465
|
+
if (!Array.isArray(content.items)) content.items = [];
|
|
466
|
+
content.items.push(item);
|
|
467
|
+
}
|
|
468
|
+
menu.meta = {...(menu.meta || {}), accountSearchNavigation: true};
|
|
469
|
+
changed = true;
|
|
470
|
+
}
|
|
471
|
+
|
|
444
472
|
// An empty menu is left empty: the renderer's minimal fallback (Dashboard,
|
|
445
473
|
// Menus) is what an admin needs to repair it, and a lone Tools folder would
|
|
446
474
|
// stop that fallback from applying.
|