domma-cms 0.54.2 → 0.66.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 +151 -77
- package/README.md +4 -4
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +3 -3
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/menu-editor.html +19 -25
- package/admin/js/templates/plugin-code.html +1 -1
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +27 -97
- package/admin/js/templates/theme.html +173 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +25 -23
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/bin/cli.js +11 -2
- package/bin/lib/smtp-defaults.js +53 -0
- package/bin/update.js +13 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/theme.json +18 -0
- package/package.json +12 -4
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4453 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1002 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/analytics/admin/css/index.css +1 -0
- package/plugins/analytics/admin/templates/analytics.html +22 -13
- package/plugins/analytics/plugin.json +3 -0
- package/plugins/blog/admin/css/index.css +1 -0
- package/plugins/blog/admin/templates/blog.html +30 -18
- package/plugins/blog/admin/templates/categories.html +2 -2
- package/plugins/blog/admin/templates/comments.html +2 -2
- package/plugins/blog/admin/templates/post-editor.html +34 -34
- package/plugins/blog/admin/templates/settings.html +6 -3
- package/plugins/blog/admin/views/blog.js +8 -5
- package/plugins/blog/admin/views/categories.js +5 -10
- package/plugins/blog/admin/views/comments.js +5 -5
- package/plugins/blog/admin/views/post-editor.js +39 -20
- package/plugins/blog/admin/views/settings.js +52 -50
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +14 -13
- package/plugins/blog/plugin.js +36 -13
- package/plugins/blog/plugin.json +13 -5
- package/plugins/blog/plugin.public.js +312 -0
- package/plugins/contacts/admin/css/index.css +1 -0
- package/plugins/contacts/admin/templates/contacts.html +128 -0
- package/plugins/contacts/admin/views/contacts.js +237 -4
- package/plugins/contacts/collections/user-contacts/schema.json +108 -0
- package/plugins/contacts/plugin.js +214 -27
- package/plugins/contacts/plugin.json +4 -1
- package/plugins/invoice/admin/css/index.css +1 -0
- package/plugins/invoice/admin/templates/editor.html +140 -49
- package/plugins/invoice/admin/templates/index.html +153 -23
- package/plugins/invoice/admin/templates/issuers.html +2 -5
- package/plugins/invoice/admin/templates/receivers.html +2 -5
- package/plugins/invoice/admin/views/contacts-source.js +266 -0
- package/plugins/invoice/admin/views/editor.js +366 -199
- package/plugins/invoice/admin/views/export.js +199 -0
- package/plugins/invoice/admin/views/help-content.js +61 -0
- package/plugins/invoice/admin/views/index.js +582 -94
- package/plugins/invoice/admin/views/issuers.js +24 -17
- package/plugins/invoice/admin/views/media.js +172 -0
- package/plugins/invoice/admin/views/party-view.js +305 -67
- package/plugins/invoice/admin/views/payments.js +127 -0
- package/plugins/invoice/admin/views/print.js +130 -0
- package/plugins/invoice/admin/views/receivers.js +49 -16
- package/plugins/invoice/admin/views/send.js +212 -0
- package/plugins/invoice/admin/views/settings.js +594 -0
- package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
- package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
- package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
- package/plugins/invoice/collections/invoices/schema.json +19 -13
- package/plugins/invoice/config.js +27 -6
- package/plugins/invoice/pdf.js +164 -0
- package/plugins/invoice/plugin.js +1217 -44
- package/plugins/invoice/plugin.json +10 -9
- package/plugins/invoice/templates/_base.css +1 -0
- package/plugins/invoice/templates/classic-nologo.html +100 -0
- package/plugins/invoice/templates/classic.html +91 -0
- package/plugins/invoice/templates/invoice-print.html +24 -0
- package/plugins/invoice/templates/minimal.html +99 -0
- package/plugins/invoice/templates/modern-nologo.html +114 -0
- package/plugins/invoice/templates/modern.html +113 -0
- package/plugins/invoice/templates/templates.json +11 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +33 -0
- package/plugins/notes/admin/views/notes.js +1 -1
- package/plugins/notes/plugin.json +2 -2
- package/plugins/surveys/lib/audience.js +37 -0
- package/plugins/surveys/lib/campaigns.js +43 -0
- package/plugins/surveys/lib/ledger.js +110 -0
- package/plugins/surveys/lib/sending.js +106 -0
- package/plugins/surveys/lib/stats.js +62 -0
- package/plugins/surveys/lib/submit.js +95 -0
- package/plugins/surveys/lib/tokens.js +28 -0
- package/plugins/surveys/plugin.public.js +149 -0
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
- package/public/css/forms.css +1 -1
- package/public/css/menu-highlight.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/menu-decor.mjs +1 -1
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +37 -3
- package/scripts/copy-domma.js +48 -0
- package/scripts/seed.js +1996 -0
- package/scripts/setup.js +8 -0
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/forms.js +42 -3
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugins.js +50 -6
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +74 -0
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/email.js +29 -3
- package/server/services/health.js +23 -2
- package/server/services/markdown.js +70 -9
- package/server/services/menuRender.js +28 -4
- package/server/services/menus.js +25 -1
- package/server/services/permissionRegistry.js +24 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/plugins.js +229 -6
- package/server/services/renderer.js +148 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/themeSettings.js +541 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/todo/data/todos.json +0 -1
package/server/routes/public.js
CHANGED
|
@@ -18,6 +18,7 @@ import {getEffectiveRoles} from '../services/userRoles.js';
|
|
|
18
18
|
import {hooks} from '../services/hooks.js';
|
|
19
19
|
import {getConfig} from '../config.js';
|
|
20
20
|
import * as cache from '../services/cache/index.js';
|
|
21
|
+
import {createHash} from 'crypto';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Derive the absolute origin for the current request.
|
|
@@ -55,6 +56,40 @@ function escapeHtml(str) {
|
|
|
55
56
|
.replace(/'/g, ''');
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/*
|
|
60
|
+
* Send an HTML page with headers that make a browser CHECK before reusing it.
|
|
61
|
+
*
|
|
62
|
+
* Public pages went out with no Cache-Control, no ETag and no Last-Modified at
|
|
63
|
+
* all. With nothing to go on a browser applies heuristic caching, so an edited
|
|
64
|
+
* page could keep showing the old content on a normal refresh - the server was
|
|
65
|
+
* serving the new one the whole time. That is indistinguishable from a CMS bug
|
|
66
|
+
* and was reported as one.
|
|
67
|
+
*
|
|
68
|
+
* `no-cache` does not mean "do not store": it means store it, then revalidate
|
|
69
|
+
* before every reuse. Paired with an ETag that makes the common case a 304 with
|
|
70
|
+
* no body rather than a full re-download.
|
|
71
|
+
*
|
|
72
|
+
* `private` because these responses are role-dependent - the response cache
|
|
73
|
+
* keys on role for exactly that reason - and a shared proxy must never hand one
|
|
74
|
+
* visitor a page rendered for another's permissions.
|
|
75
|
+
*/
|
|
76
|
+
function sendHtml(request, reply, html) {
|
|
77
|
+
const etag = `"${createHash('sha1').update(html).digest('base64')}"`;
|
|
78
|
+
reply.header('cache-control', 'private, no-cache');
|
|
79
|
+
reply.header('etag', etag);
|
|
80
|
+
|
|
81
|
+
// If-None-Match may carry a list, and a weak validator's W/ prefix.
|
|
82
|
+
const seen = String(request.headers['if-none-match'] || '')
|
|
83
|
+
.split(',')
|
|
84
|
+
.map((t) => t.trim().replace(/^W\//, ''));
|
|
85
|
+
if (seen.includes(etag)) return reply.code(304).send();
|
|
86
|
+
|
|
87
|
+
// Deliberately no reply.code() here: the 404 handlers have already called
|
|
88
|
+
// reply.status(404), and forcing 200 would turn every not-found into a
|
|
89
|
+
// soft 404 that search engines index as a real page.
|
|
90
|
+
return reply.type('text/html').send(html);
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
export async function publicRoutes(fastify) {
|
|
59
94
|
// Admin panel: serve index.html for all /admin/* paths (SPA fallback)
|
|
60
95
|
fastify.get('/admin', async (request, reply) => {
|
|
@@ -104,13 +139,13 @@ export async function publicRoutes(fastify) {
|
|
|
104
139
|
const link = await verifyPreviewToken((request.query || {}).token, fastify.jwt);
|
|
105
140
|
if (!link) {
|
|
106
141
|
reply.status(404);
|
|
107
|
-
return reply
|
|
142
|
+
return sendHtml(request, reply, await render404('/_preview'));
|
|
108
143
|
}
|
|
109
144
|
|
|
110
145
|
const page = await getPage(link.urlPath);
|
|
111
146
|
if (!page) {
|
|
112
147
|
reply.status(404);
|
|
113
|
-
return reply
|
|
148
|
+
return sendHtml(request, reply, await render404(link.urlPath));
|
|
114
149
|
}
|
|
115
150
|
|
|
116
151
|
// A disabled project stays a hard kill switch. A share link narrows who
|
|
@@ -118,7 +153,7 @@ export async function publicRoutes(fastify) {
|
|
|
118
153
|
const project = await getProjectForPage(page.urlPath || link.urlPath, page.project);
|
|
119
154
|
if (!(await isProjectEnabled(project))) {
|
|
120
155
|
reply.status(404);
|
|
121
|
-
return reply
|
|
156
|
+
return sendHtml(request, reply, await render404(link.urlPath));
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
// Rendered as anonymous on purpose: the recipient has no account, so
|
|
@@ -161,7 +196,7 @@ export async function publicRoutes(fastify) {
|
|
|
161
196
|
|
|
162
197
|
if (!page) {
|
|
163
198
|
reply.status(404);
|
|
164
|
-
return reply
|
|
199
|
+
return sendHtml(request, reply, await render404(urlPath));
|
|
165
200
|
}
|
|
166
201
|
|
|
167
202
|
// Hard kill-switch: a disabled project takes its pages off the public
|
|
@@ -169,7 +204,7 @@ export async function publicRoutes(fastify) {
|
|
|
169
204
|
const pageProject = await getProjectForPage(page.urlPath || urlPath, page.project);
|
|
170
205
|
if (!(await isProjectEnabled(pageProject))) {
|
|
171
206
|
reply.status(404);
|
|
172
|
-
return reply
|
|
207
|
+
return sendHtml(request, reply, await render404(urlPath));
|
|
173
208
|
}
|
|
174
209
|
|
|
175
210
|
// Draft preview: a page that is not published stays invisible to the
|
|
@@ -209,7 +244,7 @@ export async function publicRoutes(fastify) {
|
|
|
209
244
|
|
|
210
245
|
if (isDraft && !canPreviewDrafts(userObj)) {
|
|
211
246
|
reply.status(404);
|
|
212
|
-
return reply
|
|
247
|
+
return sendHtml(request, reply, await render404(urlPath));
|
|
213
248
|
}
|
|
214
249
|
|
|
215
250
|
if (!isPublic && !checkVisibility(userObj, vis)) {
|
|
@@ -249,7 +284,7 @@ export async function publicRoutes(fastify) {
|
|
|
249
284
|
const cacheTags = [`page:${urlPath}`, ...(page.cacheTags || []), 'nav', 'site'];
|
|
250
285
|
const html = await cache.wrap(cacheKey, render, {tags: cacheTags});
|
|
251
286
|
hooks.emit('content:pageViewed', {urlPath, title: page.title || ''});
|
|
252
|
-
return reply
|
|
287
|
+
return sendHtml(request, reply, html);
|
|
253
288
|
});
|
|
254
289
|
}
|
|
255
290
|
|
package/server/server.js
CHANGED
|
@@ -218,6 +218,29 @@ await refreshComponentTagAllowlist();
|
|
|
218
218
|
// Menus - one-shot migration from legacy navigation.json + site footer links
|
|
219
219
|
// ---------------------------------------------------------------------------
|
|
220
220
|
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Theme - one-shot lift of the theme keys out of site.json into theme.json
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const {migrateThemeConfig} = await import('./services/themeSettings.js');
|
|
227
|
+
if (await migrateThemeConfig()) app.log.info('[theme] Migrated theme settings into config/theme.json');
|
|
228
|
+
} catch (err) {
|
|
229
|
+
app.log.warn(`[theme] Migration skipped: ${err.message}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Search - one-shot retirement of the site-search plugin (settings + files)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const {migrateSiteSearchPlugin} = await import('./services/search-migration.js');
|
|
238
|
+
const changes = migrateSiteSearchPlugin();
|
|
239
|
+
if (changes.length) app.log.info(`[search] Migrated from the site-search plugin: ${changes.join(', ')}`);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
app.log.warn(`[search] Migration skipped: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
221
244
|
try {
|
|
222
245
|
const {runMigration: runMenusMigration} = await import('./services/menus-migration.js');
|
|
223
246
|
await runMenusMigration();
|
|
@@ -237,6 +260,18 @@ try {
|
|
|
237
260
|
groupText: 'Data',
|
|
238
261
|
item: {text: 'API Builder', url: '#/api-endpoints', icon: 'code', permission: 'api-endpoints'}
|
|
239
262
|
});
|
|
263
|
+
await ensureSidebarItem({
|
|
264
|
+
groupText: 'Content',
|
|
265
|
+
item: {text: 'Context Menus', url: '#/context-menus', icon: 'menu', permission: 'context-menus'}
|
|
266
|
+
});
|
|
267
|
+
await ensureSidebarItem({
|
|
268
|
+
groupText: 'Content',
|
|
269
|
+
item: {text: 'Theme', url: '#/theme', icon: 'droplet', permission: 'theme'}
|
|
270
|
+
});
|
|
271
|
+
await ensureSidebarItem({
|
|
272
|
+
groupText: 'System',
|
|
273
|
+
item: {text: 'Search', url: '#/search', icon: 'search', permission: 'settings'}
|
|
274
|
+
});
|
|
240
275
|
// Restructure Documentation: Tutorials → submenu, add Components reference set.
|
|
241
276
|
await ensureDocumentationSubmenus();
|
|
242
277
|
} catch (err) {
|
|
@@ -245,6 +280,39 @@ try {
|
|
|
245
280
|
|
|
246
281
|
// Initialise response cache (Memory/None/Redis driver per config.cache).
|
|
247
282
|
await cache.initCache(config.cache);
|
|
283
|
+
|
|
284
|
+
/*
|
|
285
|
+
* An admin change clears the response cache.
|
|
286
|
+
*
|
|
287
|
+
* Every service that mutates something is meant to invalidate the tags its
|
|
288
|
+
* change affects, and mostly they do - but a miss is INVISIBLE. The cache is
|
|
289
|
+
* off in development and on in production, so a missed invalidation works
|
|
290
|
+
* perfectly on the machine it was written on and serves stale HTML live until
|
|
291
|
+
* the TTL expires an hour later. Context menus shipped that way for three
|
|
292
|
+
* releases: the call named a function the cache module does not export AND a
|
|
293
|
+
* tag nothing is ever stored with, and a try/catch ate the evidence.
|
|
294
|
+
*
|
|
295
|
+
* This is the safety net, not the mechanism. The targeted invalidations stay -
|
|
296
|
+
* they are what stops one collection edit clearing every unrelated page - and
|
|
297
|
+
* this catches whatever they missed.
|
|
298
|
+
*
|
|
299
|
+
* Scoped to AUTHENTICATED mutations on purpose. A public form submission is
|
|
300
|
+
* not an admin change, and clearing on each of those would cold-start the
|
|
301
|
+
* cache on a busy site; entry writes already invalidate their own collection
|
|
302
|
+
* tag. Runs on onResponse, after the reply has gone, so it costs the caller
|
|
303
|
+
* nothing - and a memory-driver clear is two Map resets.
|
|
304
|
+
*/
|
|
305
|
+
const CACHE_CLEARING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
306
|
+
app.addHook('onResponse', async (request, reply) => {
|
|
307
|
+
if (!CACHE_CLEARING_METHODS.has(request.method)) return;
|
|
308
|
+
if (reply.statusCode >= 400) return;
|
|
309
|
+
if (!request.user) return;
|
|
310
|
+
try {
|
|
311
|
+
await cache.clear();
|
|
312
|
+
} catch (err) {
|
|
313
|
+
app.log.warn(`[cache] Admin change did not clear the cache: ${err.message}`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
248
316
|
console.log(`[cache] driver=${cache.isEnabled() ? config.cache.driver : 'disabled'}`);
|
|
249
317
|
|
|
250
318
|
// Docs are rendered at request time from the shipped admin templates; clear any
|
|
@@ -332,9 +400,11 @@ await app.register(authRoutes, { prefix: '/api' });
|
|
|
332
400
|
|
|
333
401
|
const { pagesRoutes } = await import('./routes/api/pages.js');
|
|
334
402
|
const { settingsRoutes } = await import('./routes/api/settings.js');
|
|
403
|
+
const { themeRoutes } = await import('./routes/api/theme.js');
|
|
335
404
|
const { layoutsRoutes } = await import('./routes/api/layouts.js');
|
|
336
405
|
const { navigationRoutes } = await import('./routes/api/navigation.js');
|
|
337
406
|
const {menusRoutes} = await import('./routes/api/menus.js');
|
|
407
|
+
const {contextMenusRoutes} = await import('./routes/api/context-menus.js');
|
|
338
408
|
const {menuLocationsRoutes} = await import('./routes/api/menu-locations.js');
|
|
339
409
|
const {projectsRoutes} = await import('./routes/api/projects.js');
|
|
340
410
|
const {apiTokensRoutes} = await import('./routes/api/api-tokens.js');
|
|
@@ -353,6 +423,7 @@ const {blocksRoutes} = await import('./routes/api/blocks.js');
|
|
|
353
423
|
const {componentsRoutes} = await import('./routes/api/components.js');
|
|
354
424
|
const {versionsRoutes} = await import('./routes/api/versions.js');
|
|
355
425
|
const {effectsRoutes} = await import('./routes/api/effects.js');
|
|
426
|
+
const {searchRoutes} = await import('./routes/api/search.js');
|
|
356
427
|
const {notificationsRoutes} = await import('./routes/api/notifications.js');
|
|
357
428
|
const {dashboardRoutes} = await import('./routes/api/dashboard.js');
|
|
358
429
|
const {cacheRoutes} = await import('./routes/api/cache.js');
|
|
@@ -363,6 +434,8 @@ await app.register(settingsRoutes, { prefix: '/api' });
|
|
|
363
434
|
await app.register(layoutsRoutes, { prefix: '/api' });
|
|
364
435
|
await app.register(navigationRoutes, { prefix: '/api' });
|
|
365
436
|
await app.register(menusRoutes, {prefix: '/api'});
|
|
437
|
+
await app.register(contextMenusRoutes, {prefix: '/api'});
|
|
438
|
+
await app.register(themeRoutes, {prefix: '/api'});
|
|
366
439
|
await app.register(menuLocationsRoutes, {prefix: '/api'});
|
|
367
440
|
await app.register(projectsRoutes, {prefix: '/api'});
|
|
368
441
|
await app.register(apiTokensRoutes, {prefix: '/api'});
|
|
@@ -381,6 +454,7 @@ await app.register(blocksRoutes, {prefix: '/api'});
|
|
|
381
454
|
await app.register(componentsRoutes, {prefix: '/api'});
|
|
382
455
|
await app.register(versionsRoutes, {prefix: '/api'});
|
|
383
456
|
await app.register(effectsRoutes, {prefix: '/api'});
|
|
457
|
+
await app.register(searchRoutes, {prefix: '/api'});
|
|
384
458
|
await app.register(notificationsRoutes, {prefix: '/api'});
|
|
385
459
|
await app.register(dashboardRoutes, {prefix: '/api'});
|
|
386
460
|
await app.register(cacheRoutes, {prefix: '/api'});
|
|
@@ -86,7 +86,12 @@ export class FileAdapter {
|
|
|
86
86
|
entries.sort((a, b) => {
|
|
87
87
|
const aVal = sort === 'createdAt' ? a.meta?.createdAt : (a.data?.[sort] ?? '');
|
|
88
88
|
const bVal = sort === 'createdAt' ? b.meta?.createdAt : (b.data?.[sort] ?? '');
|
|
89
|
-
|
|
89
|
+
// Numeric collation, because a plain string compare orders a number
|
|
90
|
+
// field 1, 10, 11, 2 - so any collection sorted by `order` scrambles
|
|
91
|
+
// itself the moment it has ten entries. MongoAdapter sorts natively
|
|
92
|
+
// and has always ordered these correctly, so this also stops the two
|
|
93
|
+
// adapters disagreeing about the same data.
|
|
94
|
+
const cmp = String(aVal).localeCompare(String(bVal), undefined, {numeric: true});
|
|
90
95
|
return order === 'desc' ? -cmp : cmp;
|
|
91
96
|
});
|
|
92
97
|
|
|
@@ -122,6 +122,25 @@ export async function buildPreviewPage(urlPath, frontmatter, body, opts = {}) {
|
|
|
122
122
|
* @param {string} body
|
|
123
123
|
* @returns {Promise<object>}
|
|
124
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* Keep a page's tag list to actual tags.
|
|
127
|
+
*
|
|
128
|
+
* An empty tag input has a way of arriving as `[null]` - which is an array,
|
|
129
|
+
* passes every `Array.isArray` guard, and then throws the moment anything
|
|
130
|
+
* calls a string method on the contents. It reached disk on dozens of pages
|
|
131
|
+
* and took the whole of site search down with it, so it is cleaned on the way
|
|
132
|
+
* IN rather than defended against at each of the places that read it.
|
|
133
|
+
*
|
|
134
|
+
* @param {unknown} tags
|
|
135
|
+
* @returns {string[]}
|
|
136
|
+
*/
|
|
137
|
+
function cleanTags(tags) {
|
|
138
|
+
if (!Array.isArray(tags)) return [];
|
|
139
|
+
return [...new Set(
|
|
140
|
+
tags.filter(tag => typeof tag === 'string' && tag.trim()).map(tag => tag.trim())
|
|
141
|
+
)];
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
export async function createPage(urlPath, frontmatter, body) {
|
|
126
145
|
const filePath = urlPathToFilePath(urlPath);
|
|
127
146
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -138,6 +157,7 @@ export async function createPage(urlPath, frontmatter, body) {
|
|
|
138
157
|
sortOrder: frontmatter.sortOrder ?? defaults.sortOrder,
|
|
139
158
|
showInNav: frontmatter.showInNav ?? false,
|
|
140
159
|
sidebar: frontmatter.sidebar ?? false,
|
|
160
|
+
tags: cleanTags(frontmatter.tags),
|
|
141
161
|
seo: frontmatter.seo || {},
|
|
142
162
|
dconfig: frontmatter.dconfig || null,
|
|
143
163
|
createdAt: now,
|
|
@@ -172,6 +192,12 @@ export async function updatePage(urlPath, frontmatter, body, {author} = {}) {
|
|
|
172
192
|
updatedAt: new Date().toISOString()
|
|
173
193
|
};
|
|
174
194
|
|
|
195
|
+
// Cleaned on every save, so an existing page carrying `[null]` is repaired
|
|
196
|
+
// the next time anyone touches it rather than staying broken for ever.
|
|
197
|
+
// Only when tags are part of this save: a caller that sends no tags is not
|
|
198
|
+
// asking for the page's tags to be emptied.
|
|
199
|
+
if ('tags' in meta) meta.tags = cleanTags(meta.tags);
|
|
200
|
+
|
|
175
201
|
if (existingMeta.createdAt) meta.createdAt = existingMeta.createdAt;
|
|
176
202
|
|
|
177
203
|
// '' is the editor's "inherit by rootUrl" choice - drop the key so the
|