domma-cms 1.16.0 → 1.16.2
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 +8 -0
- package/admin/js/lib/context-menu-action-help.js +1 -0
- package/admin/js/lib/menu-item-help.js +1 -0
- package/admin/js/templates/context-menu-editor.html +20 -16
- package/admin/js/templates/docs/api-navigation.html +9 -3
- package/admin/js/templates/docs/usage-navigation.html +4 -0
- package/admin/js/templates/menu-editor.html +95 -53
- package/admin/js/views/context-menu-editor.js +17 -13
- package/admin/js/views/menu-editor.js +23 -22
- package/admin/js/views/menu-locations.js +3 -2
- package/docs/DommaFeatures.md +1 -0
- package/docs/screenshots/context-menu-action-help.png +0 -0
- package/docs/screenshots/context-menu-help-mobile.png +0 -0
- package/docs/screenshots/menu-option-help.png +0 -0
- package/package.json +1 -1
- package/server/routes/api/menus.js +8 -4
- package/server/services/menus.js +27 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* GET /api/menus/_count - the sidebar badge (the underscore can never be a slug)
|
|
6
6
|
* GET /api/menus/:slug - full menu
|
|
7
7
|
* POST /api/menus - create
|
|
8
|
-
* PUT /api/menus/:slug - replace
|
|
8
|
+
* PUT /api/menus/:slug - replace (409 if `revision` is given and the menu has changed since)
|
|
9
9
|
* DELETE /api/menus/:slug - delete (409 if mapped to a slot)
|
|
10
10
|
* POST /api/menus/:slug/duplicate - copy as <slug>-copy
|
|
11
11
|
*
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getLocations,
|
|
27
27
|
getMenu,
|
|
28
28
|
listMenus,
|
|
29
|
+
menuRevision,
|
|
29
30
|
updateMenu
|
|
30
31
|
} from '../../services/menus.js';
|
|
31
32
|
import * as cache from '../../services/cache/index.js';
|
|
@@ -110,7 +111,8 @@ export async function menusRoutes(fastify, opts = {}) {
|
|
|
110
111
|
if (!canSeeArtefact(request.user, menu)) {
|
|
111
112
|
return reply.status(403).send({error: 'Access denied for this project'});
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
// The fingerprint the editor sends back on save (see menuRevision).
|
|
115
|
+
return {...menu, revision: menuRevision(menu)};
|
|
114
116
|
});
|
|
115
117
|
|
|
116
118
|
fastify.post('/menus', canCreate, async (request, reply) => {
|
|
@@ -126,10 +128,12 @@ export async function menusRoutes(fastify, opts = {}) {
|
|
|
126
128
|
|
|
127
129
|
fastify.put('/menus/:slug', canUpdate, async (request, reply) => {
|
|
128
130
|
try {
|
|
129
|
-
const
|
|
131
|
+
const {revision, ...body} = request.body || {};
|
|
132
|
+
const menu = await updateMenu(request.params.slug, body, {baseRevision: revision});
|
|
130
133
|
await cache.invalidateTags([`menu:${menu.slug}`, 'nav']);
|
|
131
|
-
return menu;
|
|
134
|
+
return {...menu, revision: menuRevision(menu)};
|
|
132
135
|
} catch (err) {
|
|
136
|
+
if (err.code === 'MENU_STALE') return reply.status(409).send({error: err.message, stale: true});
|
|
133
137
|
const status = /not found/.test(err.message) ? 404 : 400;
|
|
134
138
|
return reply.status(status).send({error: err.message});
|
|
135
139
|
}
|
package/server/services/menus.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* concept from the map) is held in process memory and seeded by core +
|
|
9
9
|
* plugin hooks at startup.
|
|
10
10
|
*/
|
|
11
|
+
import {createHash} from 'crypto';
|
|
11
12
|
import fs from 'fs/promises';
|
|
12
13
|
import path from 'path';
|
|
13
14
|
import {fileURLToPath} from 'url';
|
|
@@ -645,18 +646,42 @@ export async function createMenu(input) {
|
|
|
645
646
|
return menu;
|
|
646
647
|
}
|
|
647
648
|
|
|
649
|
+
/**
|
|
650
|
+
* A short fingerprint of a menu as stored. The editor sends back the one it
|
|
651
|
+
* loaded, so a save over a menu changed since - in another tab, by someone
|
|
652
|
+
* else, or by hand on the server - is caught instead of quietly undone. It
|
|
653
|
+
* hashes the whole file, not meta.updatedAt, because a hand edit leaves that
|
|
654
|
+
* date alone.
|
|
655
|
+
*
|
|
656
|
+
* @param {object} menu
|
|
657
|
+
* @returns {string}
|
|
658
|
+
*/
|
|
659
|
+
export function menuRevision(menu) {
|
|
660
|
+
return createHash('sha256').update(JSON.stringify(menu)).digest('hex').slice(0, 16);
|
|
661
|
+
}
|
|
662
|
+
|
|
648
663
|
/**
|
|
649
664
|
* Replace an existing menu. Preserves meta.createdAt; updates meta.updatedAt.
|
|
650
665
|
* The slug in the path wins over any slug in the body.
|
|
651
666
|
*
|
|
667
|
+
* `opts.baseRevision` is the revision the caller loaded (menuRevision); when
|
|
668
|
+
* given and the menu has changed since, nothing is written and the error
|
|
669
|
+
* carries `code: 'MENU_STALE'`. Leave it out to overwrite regardless.
|
|
670
|
+
*
|
|
652
671
|
* @param {string} slug
|
|
653
672
|
* @param {object} input
|
|
673
|
+
* @param {{baseRevision?: string}} [opts]
|
|
654
674
|
* @returns {Promise<object>} the persisted menu
|
|
655
|
-
* @throws {Error} if the menu does not exist
|
|
675
|
+
* @throws {Error} if the menu does not exist, or has changed since baseRevision
|
|
656
676
|
*/
|
|
657
|
-
export async function updateMenu(slug, input) {
|
|
677
|
+
export async function updateMenu(slug, input, opts = {}) {
|
|
658
678
|
const existing = await getMenu(slug);
|
|
659
679
|
if (!existing) throw new Error(`Menu "${slug}" not found`);
|
|
680
|
+
if (opts.baseRevision && opts.baseRevision !== menuRevision(existing)) {
|
|
681
|
+
const err = new Error(`Menu "${slug}" has changed since it was opened`);
|
|
682
|
+
err.code = 'MENU_STALE';
|
|
683
|
+
throw err;
|
|
684
|
+
}
|
|
660
685
|
const binding = normaliseBinding(input.binding);
|
|
661
686
|
const errors = validateMenu({...input, slug, binding});
|
|
662
687
|
if (errors.length) {
|