domma-cms 0.74.0 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/actions-arrange.js +1 -0
- package/admin/js/lib/api-endpoints-arrange.js +1 -0
- package/admin/js/lib/blocks-arrange.js +1 -0
- package/admin/js/lib/collections-arrange.js +1 -0
- package/admin/js/lib/components-arrange.js +1 -0
- package/admin/js/lib/context-menus-arrange.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -0
- package/admin/js/lib/media-arrange.js +1 -0
- package/admin/js/lib/menus-arrange.js +1 -0
- package/admin/js/lib/pages-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/theme-settings-arrange.js +1 -0
- package/admin/js/lib/views-arrange.js +1 -0
- package/admin/js/templates/actions-list.html +73 -14
- package/admin/js/templates/api-endpoints.html +63 -10
- package/admin/js/templates/blocks.html +76 -15
- package/admin/js/templates/collections.html +108 -52
- package/admin/js/templates/components.html +82 -15
- package/admin/js/templates/context-menus.html +55 -13
- package/admin/js/templates/forms.html +67 -14
- package/admin/js/templates/media.html +99 -13
- package/admin/js/templates/menus.html +57 -11
- package/admin/js/templates/pages.html +78 -23
- package/admin/js/templates/theme.html +164 -132
- package/admin/js/templates/views-list.html +63 -9
- package/admin/js/views/actions-list.js +37 -1
- package/admin/js/views/api-endpoints.js +11 -7
- package/admin/js/views/blocks.js +34 -5
- package/admin/js/views/collections.js +36 -1
- package/admin/js/views/components.js +27 -5
- package/admin/js/views/context-menus.js +10 -5
- package/admin/js/views/forms.js +57 -1
- package/admin/js/views/media.js +40 -1
- package/admin/js/views/menus.js +11 -5
- package/admin/js/views/pages.js +28 -8
- package/admin/js/views/theme.js +55 -1
- package/admin/js/views/views-list.js +11 -1
- package/package.json +1 -1
- package/server/routes/api/actions.js +64 -2
- package/server/routes/api/api-endpoints.js +41 -0
- package/server/routes/api/blocks.js +170 -8
- package/server/routes/api/collections.js +53 -1
- package/server/routes/api/components.js +46 -0
- package/server/routes/api/context-menus.js +8 -0
- package/server/routes/api/forms.js +123 -10
- package/server/routes/api/media.js +56 -1
- package/server/routes/api/menus.js +53 -6
- package/server/routes/api/pages.js +46 -0
- package/server/routes/api/theme.js +55 -0
- package/server/routes/api/views.js +27 -3
- package/server/server.js +16 -1
- package/server/services/contextMenus.js +41 -0
- package/server/services/views.js +38 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Views API
|
|
3
3
|
*
|
|
4
4
|
* Admin endpoints (authenticated + views permission):
|
|
5
|
-
* GET /views - List all view configs
|
|
5
|
+
* GET /views - List all view configs (?sources=1 adds `sourceInfo` per view)
|
|
6
|
+
* GET /views/_count - The sidebar badge (the underscore is outside the slug alphabet)
|
|
6
7
|
* POST /views - Create view config
|
|
7
8
|
* GET /views/:slug - Get view config
|
|
8
9
|
* PUT /views/:slug - Update view config
|
|
@@ -13,11 +14,17 @@
|
|
|
13
14
|
* Public endpoint (role-checked per view access config):
|
|
14
15
|
* GET /views/:slug/public - Execute view publicly
|
|
15
16
|
*/
|
|
16
|
-
import {createView, deleteView, executeView, getView, listViews, updateView} from '../../services/views.js';
|
|
17
|
+
import {createView, deleteView, executeView, getView, listViews, updateView, viewsBadge} from '../../services/views.js';
|
|
17
18
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
18
19
|
import {getRoleLevel} from '../../services/roles.js';
|
|
19
20
|
import {getEffectiveLevel} from '../../services/userRoles.js';
|
|
20
21
|
|
|
22
|
+
/** Every collection by slug - a missing one is a view whose source was deleted. */
|
|
23
|
+
async function collectionsBySlug() {
|
|
24
|
+
const {listCollections} = await import('../../services/collections.js');
|
|
25
|
+
return new Map((await listCollections().catch(() => [])).map(c => [c.slug, c]));
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export async function viewsRoutes(fastify) {
|
|
22
29
|
const canRead = {preHandler: [authenticate, requirePermission('views', 'read')]};
|
|
23
30
|
const canCreate = {preHandler: [authenticate, requirePermission('views', 'create')]};
|
|
@@ -33,7 +40,18 @@ export async function viewsRoutes(fastify) {
|
|
|
33
40
|
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
34
41
|
const all = await listViews();
|
|
35
42
|
// listViews returns full records with meta inline - filter directly
|
|
36
|
-
|
|
43
|
+
const visible = all.filter(v => canSeeArtefact(request.user, v));
|
|
44
|
+
if (request.query?.sources !== '1') return visible;
|
|
45
|
+
// The Views screen: what each view reads from - whether that collection still
|
|
46
|
+
// exists, its title and its storage (MongoDB runs native aggregation, file
|
|
47
|
+
// collections the JS pipeline). A views user need not hold collections.read.
|
|
48
|
+
const bySlug = await collectionsBySlug();
|
|
49
|
+
return visible.map(v => {
|
|
50
|
+
const c = bySlug.get(v.pipeline?.source);
|
|
51
|
+
return {...v, sourceInfo: c
|
|
52
|
+
? {exists: true, title: c.title || c.slug, adapter: c.storage?.adapter || 'file'}
|
|
53
|
+
: {exists: false}};
|
|
54
|
+
});
|
|
37
55
|
} catch (err) {
|
|
38
56
|
return reply.status(503).send({ error: err.message });
|
|
39
57
|
}
|
|
@@ -49,6 +67,12 @@ export async function viewsRoutes(fastify) {
|
|
|
49
67
|
}
|
|
50
68
|
});
|
|
51
69
|
|
|
70
|
+
fastify.get('/views/_count', canRead, async (request) => {
|
|
71
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
72
|
+
const visible = (await listViews()).filter(v => canSeeArtefact(request.user, v));
|
|
73
|
+
return viewsBadge(visible, new Set((await collectionsBySlug()).keys()));
|
|
74
|
+
});
|
|
75
|
+
|
|
52
76
|
// Static sub-routes must be declared before the parameterised :slug route
|
|
53
77
|
// so Fastify's radix router gives them priority.
|
|
54
78
|
fastify.get('/views/collection/:collectionSlug', canRead, async (request, reply) => {
|
package/server/server.js
CHANGED
|
@@ -528,7 +528,22 @@ registerSidebarItem({item: {source: 'Built in', text: 'Contacts', url: '#/contac
|
|
|
528
528
|
// Manage projects lives in the menu (the Projects folder, sidebar-migration.js);
|
|
529
529
|
// registering it here only adds its badge - the renderer merges a registered
|
|
530
530
|
// item into the menu node with the same url rather than adding a second one.
|
|
531
|
-
registerSidebarItem({folder: 'projects', item: {source: 'Built in', text: 'Manage projects', url: '#/projects', icon: 'folder', permission: 'projects', countUrl: '/api/projects/_count'}});
|
|
531
|
+
registerSidebarItem({folder: 'projects', item: {source: 'Built in', text: 'Manage projects', url: '#/projects', icon: 'folder', permission: 'projects', countUrl: '/api/projects/_count', inventory: true}});
|
|
532
|
+
// Content and Data: the same - each merges into its menu entry and brings its badge
|
|
533
|
+
// (which keeps the number the sidebar always showed there; the popover says more).
|
|
534
|
+
// `inventory`: a how-many count, left out of a closed folder's total (sidebar-renderer).
|
|
535
|
+
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Pages', url: '#/pages', icon: 'file-text', permission: 'pages', countUrl: '/api/pages/_count', inventory: true}});
|
|
536
|
+
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Media', url: '#/media', icon: 'image', permission: 'media', countUrl: '/api/media/_count', inventory: true}});
|
|
537
|
+
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Menus', url: '#/menus', icon: 'menu', permission: 'menus', countUrl: '/api/menus/_count', inventory: true}});
|
|
538
|
+
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Context Menus', url: '#/context-menus', icon: 'menu', permission: 'context-menus', countUrl: '/api/context-menus/_count', inventory: true}});
|
|
539
|
+
registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Theme', url: '#/theme', icon: 'droplet', permission: 'theme', countUrl: '/api/theme/count', inventory: true}});
|
|
540
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'Collections', url: '#/collections', icon: 'database', permission: 'collections', countUrl: '/api/collections/_count', inventory: true}});
|
|
541
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'Forms', url: '#/forms', icon: 'layout', permission: 'collections', countUrl: '/api/forms/_count', inventory: true}});
|
|
542
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'Views', url: '#/views', icon: 'eye', permission: 'views', countUrl: '/api/views/_count', inventory: true}});
|
|
543
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'Actions', url: '#/actions', icon: 'zap', permission: 'actions', countUrl: '/api/actions/_count', inventory: true}});
|
|
544
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'API Builder', url: '#/api-endpoints', icon: 'code', permission: 'api-endpoints', countUrl: '/api/api-endpoints/_count', inventory: true}});
|
|
545
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'Blocks', url: '#/blocks', icon: 'box', permission: 'pages', countUrl: '/api/blocks/_count', inventory: true}});
|
|
546
|
+
registerSidebarItem({folder: 'data', item: {source: 'Built in', text: 'Components', url: '#/components', icon: 'component', permission: 'components', countUrl: '/api/components/_count', inventory: true}});
|
|
532
547
|
|
|
533
548
|
await registerPlugins(app);
|
|
534
549
|
|
|
@@ -475,3 +475,44 @@ export function toClientPayload(menu) {
|
|
|
475
475
|
items: menu.items || []
|
|
476
476
|
};
|
|
477
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The sidebar badge on Context Menus (its countUrl, GET /api/context-menus/_count):
|
|
481
|
+
* how many menus there are, with the ones switched off and the ones whose
|
|
482
|
+
* binding matches nothing called out - an enabled menu that never appears is
|
|
483
|
+
* the one thing here worth a second look. Plain text throughout: the popover
|
|
484
|
+
* sets it with textContent.
|
|
485
|
+
*
|
|
486
|
+
* @param {object[]} menus - as listContextMenus returns them
|
|
487
|
+
* @returns {{count: number, label: string, tone: string, details: object}}
|
|
488
|
+
*/
|
|
489
|
+
export function contextMenusBadge(menus = []) {
|
|
490
|
+
const list = [...menus].sort((a, b) => String(a.name || a.slug).localeCompare(String(b.name || b.slug)));
|
|
491
|
+
const off = list.filter(m => m.enabled === false);
|
|
492
|
+
const never = list.filter(m => m.enabled !== false && m.binding
|
|
493
|
+
&& !(m.binding.projects || []).length && !(m.binding.pages || []).length);
|
|
494
|
+
const siteWide = list.filter(m => m.enabled !== false && !m.binding).length;
|
|
495
|
+
const n = list.length;
|
|
496
|
+
const bits = [off.length && `${off.length} off`, never.length && `${never.length} never matching`].filter(Boolean);
|
|
497
|
+
return {
|
|
498
|
+
count: n,
|
|
499
|
+
label: `${n} context menu${n === 1 ? '' : 's'}${bits.length ? `, ${bits.join(', ')}` : ''}`,
|
|
500
|
+
tone: never.length ? 'attention' : 'info',
|
|
501
|
+
details: {
|
|
502
|
+
title: 'Context Menus',
|
|
503
|
+
rows: [
|
|
504
|
+
{label: 'On', value: String(n - off.length)},
|
|
505
|
+
{label: 'Off', value: String(off.length)},
|
|
506
|
+
{label: 'Site-wide', value: String(siteWide)},
|
|
507
|
+
{label: 'Never matching', value: String(never.length), ...(never.length && {tone: 'danger'})}
|
|
508
|
+
],
|
|
509
|
+
items: list.slice(0, 8).map(m => ({
|
|
510
|
+
text: String(m.name || m.slug),
|
|
511
|
+
meta: [m.selector || 'no target', `${m.itemsCount ?? 0} item${m.itemsCount === 1 ? '' : 's'}`,
|
|
512
|
+
m.enabled === false ? 'off' : '', never.includes(m) ? 'never matches' : ''].filter(Boolean).join(' · '),
|
|
513
|
+
...(never.includes(m) && {tone: 'danger'})
|
|
514
|
+
})),
|
|
515
|
+
empty: 'No context menus yet.'
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
package/server/services/views.js
CHANGED
|
@@ -475,3 +475,41 @@ export async function executeView(slug, {page = 1, limit = 25, user = null} = {}
|
|
|
475
475
|
? executeMongoView(view, { page, limit, user })
|
|
476
476
|
: executeFileView(view, { page, limit, user });
|
|
477
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The sidebar badge on Views (its countUrl, GET /api/views/_count): how many
|
|
481
|
+
* views, red when one reads from a collection that no longer exists - such a
|
|
482
|
+
* view errors on every page that shows it. Plain text: the popover sets it
|
|
483
|
+
* with textContent.
|
|
484
|
+
*
|
|
485
|
+
* @param {object[]} views
|
|
486
|
+
* @param {Set<string>} collectionSlugs - the collections that exist
|
|
487
|
+
* @returns {{count: number, label: string, tone: string, details: object}}
|
|
488
|
+
*/
|
|
489
|
+
export function viewsBadge(views = [], collectionSlugs = new Set()) {
|
|
490
|
+
const broken = views.filter(v => v.pipeline?.source && !collectionSlugs.has(v.pipeline.source));
|
|
491
|
+
const pub = views.filter(v => v.access?.public).length;
|
|
492
|
+
const n = views.length;
|
|
493
|
+
const ordered = [...broken, ...views.filter(v => !broken.includes(v))];
|
|
494
|
+
return {
|
|
495
|
+
count: n,
|
|
496
|
+
label: `${n} view${n === 1 ? '' : 's'}${broken.length ? `, ${broken.length} reading a missing collection` : ''}`,
|
|
497
|
+
tone: broken.length ? 'danger' : 'info',
|
|
498
|
+
details: {
|
|
499
|
+
title: 'Views',
|
|
500
|
+
rows: [
|
|
501
|
+
{label: 'Views', value: String(n)},
|
|
502
|
+
{label: 'Public', value: String(pub)},
|
|
503
|
+
{label: 'Missing source', value: String(broken.length), ...(broken.length && {tone: 'danger'})}
|
|
504
|
+
],
|
|
505
|
+
items: ordered.slice(0, 8).map(v => ({
|
|
506
|
+
text: String(v.title || v.slug),
|
|
507
|
+
meta: broken.includes(v)
|
|
508
|
+
? `source "${v.pipeline.source}" is gone`
|
|
509
|
+
: [v.pipeline?.source, v.display?.mode || 'table', v.access?.public ? 'public' : ''].filter(Boolean).join(' · '),
|
|
510
|
+
...(broken.includes(v) && {tone: 'danger'})
|
|
511
|
+
})),
|
|
512
|
+
empty: 'No views yet.'
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|