domma-cms 0.92.1 → 0.94.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 +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import crypto from 'crypto';
|
|
14
14
|
import fs from 'fs/promises';
|
|
15
|
+
import {readFileSync} from 'fs';
|
|
15
16
|
import path from 'path';
|
|
16
17
|
import zlib from 'zlib';
|
|
17
18
|
import {fileURLToPath} from 'url';
|
|
@@ -227,6 +228,40 @@ async function extractTar(tarBuffer, slug, destDir) {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The version of the CMS this site runs. The engine's own checkout has it in
|
|
233
|
+
* package.json; a scaffolded or managed site's package.json is the site's, so
|
|
234
|
+
* there it comes from the `.domma` marker the scaffolder and updater write
|
|
235
|
+
* (or the manager's `.cmsVersion`). Null when none says.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} [root]
|
|
238
|
+
* @returns {string|null}
|
|
239
|
+
*/
|
|
240
|
+
export function installedCmsVersion(root = ROOT) {
|
|
241
|
+
const read = (file) => {
|
|
242
|
+
try { return JSON.parse(readFileSync(path.join(root, file), 'utf8')); } catch { return null; }
|
|
243
|
+
};
|
|
244
|
+
const pkg = read('package.json');
|
|
245
|
+
if (pkg?.name === 'domma-cms' && pkg.version) return pkg.version;
|
|
246
|
+
return read('.domma')?.cmsVersion || read('.cmsVersion')?.cmsVersion || null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The refusal for a manifest whose `minCmsVersion` is newer than this CMS, or
|
|
251
|
+
* null when it fits (or either version is unknown - an old site without a
|
|
252
|
+
* marker is not locked out of every plugin that sets the field).
|
|
253
|
+
*
|
|
254
|
+
* @param {{name?: string, displayName?: string, minCmsVersion?: string}} manifest
|
|
255
|
+
* @param {string|null} [cmsVersion]
|
|
256
|
+
* @returns {string|null}
|
|
257
|
+
*/
|
|
258
|
+
export function cmsVersionShortfall(manifest, cmsVersion = installedCmsVersion()) {
|
|
259
|
+
const needed = String(manifest?.minCmsVersion ?? '').trim().replace(/^[^\d]+/, '');
|
|
260
|
+
if (!needed || !cmsVersion) return null;
|
|
261
|
+
if (compareVersions(cmsVersion, needed) >= 0) return null;
|
|
262
|
+
return `${manifest.displayName || manifest.name} needs Domma CMS ${needed} or later; this site runs ${cmsVersion}. Update the CMS first.`;
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
// ---------------------------------------------------------------------------
|
|
231
266
|
// Public API
|
|
232
267
|
// ---------------------------------------------------------------------------
|
|
@@ -239,8 +274,10 @@ async function extractTar(tarBuffer, slug, destDir) {
|
|
|
239
274
|
* 2. Reject if plugins/<slug>/ already exists
|
|
240
275
|
* 3. Extract tarball to plugins/<slug>/ with path traversal guard
|
|
241
276
|
* 4. Validate manifest via discoverPlugins()
|
|
277
|
+
* 4b. Refuse a plugin whose `minCmsVersion` is newer than this CMS
|
|
242
278
|
* 5. Add { enabled: false, settings: {} } to config/plugins.json
|
|
243
|
-
*
|
|
279
|
+
* (onEnable runs when it is enabled, not here)
|
|
280
|
+
* 6. Write the envelope's entitlement, if any
|
|
244
281
|
* 7. Append install record to audit log
|
|
245
282
|
*
|
|
246
283
|
* A bundle from the marketplace is signed. One exported from a running CMS
|
|
@@ -331,6 +368,11 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId, all
|
|
|
331
368
|
await fs.rm(destDir, { recursive: true, force: true });
|
|
332
369
|
throw new Error(`Plugin manifest validation failed after extraction: ${slug}`);
|
|
333
370
|
}
|
|
371
|
+
const tooNew = cmsVersionShortfall(manifest);
|
|
372
|
+
if (tooNew) {
|
|
373
|
+
await fs.rm(destDir, { recursive: true, force: true });
|
|
374
|
+
throw new Error(tooNew);
|
|
375
|
+
}
|
|
334
376
|
|
|
335
377
|
// Step 5 - Update config/plugins.json
|
|
336
378
|
const cfg = await readPluginsConfig();
|
|
@@ -339,25 +381,19 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId, all
|
|
|
339
381
|
await writePluginsConfig(cfg);
|
|
340
382
|
}
|
|
341
383
|
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (typeof mod.onEnable === 'function') {
|
|
347
|
-
await mod.onEnable(null);
|
|
348
|
-
}
|
|
349
|
-
} catch (err) {
|
|
350
|
-
console.warn(`[pluginInstaller] onEnable hook for "${slug}" failed: ${err.message}`);
|
|
351
|
-
}
|
|
384
|
+
// No onEnable here: the plugin is installed switched OFF (step 5), and
|
|
385
|
+
// enabling it runs onEnable with its real context ({fastify, services, hooks})
|
|
386
|
+
// through runLifecycleHook(). Calling it at install too ran it twice, the
|
|
387
|
+
// first time with null, which threw in any plugin that destructured it.
|
|
352
388
|
|
|
353
|
-
// Step
|
|
389
|
+
// Step 6 - Entitlement, if the envelope carried one. Written beside the
|
|
354
390
|
// plugin so it travels with it and a renewal is a file drop.
|
|
355
391
|
if (entitlement) {
|
|
356
392
|
await fs.writeFile(path.join(destDir, '.entitlement.json'),
|
|
357
393
|
JSON.stringify(entitlement, null, 2) + '\n', 'utf8');
|
|
358
394
|
}
|
|
359
395
|
|
|
360
|
-
// Step
|
|
396
|
+
// Step 7 - Audit log. An unsigned install is recorded as such: it is the one
|
|
361
397
|
// that was a judgement call rather than a verified fact.
|
|
362
398
|
await writeAuditLog({
|
|
363
399
|
action: unsigned ? 'install:unsigned' : 'install',
|
|
@@ -412,6 +448,11 @@ async function upgradePlugin(slug, {tarball, unsigned, entitlement, allowDowngra
|
|
|
412
448
|
await fs.rm(incoming, {recursive: true, force: true});
|
|
413
449
|
throw new Error(`Refusing to upgrade ${slug}: the new version is ${next.reason}`);
|
|
414
450
|
}
|
|
451
|
+
const tooNew = cmsVersionShortfall(next.manifest);
|
|
452
|
+
if (tooNew) {
|
|
453
|
+
await fs.rm(incoming, {recursive: true, force: true});
|
|
454
|
+
throw new Error(tooNew);
|
|
455
|
+
}
|
|
415
456
|
|
|
416
457
|
const from = current.manifest?.version;
|
|
417
458
|
const to = next.manifest.version;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
registerSanitizeRules,
|
|
26
26
|
registerShortcode,
|
|
27
27
|
hideSidebarUrl,
|
|
28
|
+
registerComponent,
|
|
28
29
|
registerSidebarItem,
|
|
29
30
|
registerTransform
|
|
30
31
|
} from './hooks.js';
|
|
@@ -80,6 +81,23 @@ export async function validatePluginDir(dir, slug) {
|
|
|
80
81
|
return {ok: true, manifest};
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* `hooks.registerComponent({name, source})` as plugins get it: registers the
|
|
86
|
+
* component (read-only in the Components list) and refreshes the sanitiser's
|
|
87
|
+
* `dm-*` tag allowlist, which was built at boot before any plugin ran - without
|
|
88
|
+
* the refresh a plugin's `<dm-name>` tag would be stripped from every page.
|
|
89
|
+
*
|
|
90
|
+
* @param {{name: string, source: string}} component
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
export async function pluginComponentHook(component) {
|
|
94
|
+
registerComponent(component);
|
|
95
|
+
try {
|
|
96
|
+
const {refreshComponentTagAllowlist} = await import('./markdown.js');
|
|
97
|
+
await refreshComponentTagAllowlist();
|
|
98
|
+
} catch { /* the allowlist is refreshed again on the next component save */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
const failedLoads = [];
|
|
84
102
|
|
|
85
103
|
/**
|
|
@@ -530,7 +548,9 @@ export async function registerPlugins(fastify) {
|
|
|
530
548
|
...notifierFor(manifest.name, manifest.displayName || manifest.name),
|
|
531
549
|
// Sign-in and password checks (0.83) - Security / Security Pro. services/authChecks.js.
|
|
532
550
|
registerLoginCheck: (def) => registerLoginCheck(manifest.name, def),
|
|
533
|
-
registerPasswordCheck: (def) => registerPasswordCheck(manifest.name, def)
|
|
551
|
+
registerPasswordCheck: (def) => registerPasswordCheck(manifest.name, def),
|
|
552
|
+
// A Domma component (.dmc source) usable as <dm-name> / [component] on pages (0.92.2).
|
|
553
|
+
registerComponent: pluginComponentHook},
|
|
534
554
|
settings,
|
|
535
555
|
config: {}
|
|
536
556
|
});
|
|
@@ -586,6 +606,14 @@ export async function registerPlugins(fastify) {
|
|
|
586
606
|
}
|
|
587
607
|
}
|
|
588
608
|
|
|
609
|
+
// User guides (docs/*.md) of the plugins that loaded, under Documentation > Plugins.
|
|
610
|
+
try {
|
|
611
|
+
const {registerPluginGuides} = await import('./pluginGuides.js');
|
|
612
|
+
await registerPluginGuides(manifests.filter(m => loaded.includes(m.name)), PLUGINS_DIR);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
fastify.log.warn(`[plugins] Plugin guides skipped: ${err.message}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
589
617
|
if (loaded.length) {
|
|
590
618
|
fastify.log.info(`[plugins] Loaded ${loaded.length} plugin${loaded.length === 1 ? '' : 's'}: ${loaded.join(', ')}`);
|
|
591
619
|
} else {
|
|
@@ -122,10 +122,10 @@ const PRESETS = [
|
|
|
122
122
|
// them exactly preserves every existing entry; changing one would seed an
|
|
123
123
|
// empty collection beside a site's real one.
|
|
124
124
|
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
125
|
+
// `storage` is declared exactly as the plugins declared it. It is used
|
|
126
|
+
// only where the site has a working MongoDB connection and no notes or
|
|
127
|
+
// todos already in files (adapterRegistry.js resolveUserDataPreset); a
|
|
128
|
+
// site's own storage block, once written, is kept by seedAll().
|
|
129
129
|
{
|
|
130
130
|
"slug": "notes-notes",
|
|
131
131
|
"title": "Notes",
|
|
@@ -464,6 +464,28 @@ const PRESETS = [
|
|
|
464
464
|
/** Slugs exported for use in adapterRegistry and the delete guard. */
|
|
465
465
|
export const PRESET_COLLECTION_SLUGS = PRESETS.map(p => p.slug);
|
|
466
466
|
|
|
467
|
+
/**
|
|
468
|
+
* The presets that hold people's own data - the Notes, Todo and Contacts
|
|
469
|
+
* Tools - rather than the site's workings. These, and only these, may live in
|
|
470
|
+
* MongoDB: a site that declares it for one and has a working connection gets
|
|
471
|
+
* it (adapterRegistry.js), and their `storage` block survives the reseed below
|
|
472
|
+
* so a storage move sticks. Every other preset (roles, user profiles,
|
|
473
|
+
* projects, notifications, api tokens and endpoints) is files, always.
|
|
474
|
+
*/
|
|
475
|
+
export const USER_DATA_PRESET_SLUGS = Object.freeze(['notes-notes', 'todo-items', 'contacts-contacts', 'contacts-groups']);
|
|
476
|
+
|
|
477
|
+
const USER_DATA = new Set(USER_DATA_PRESET_SLUGS);
|
|
478
|
+
|
|
479
|
+
/** A storage block worth keeping from an existing schema.json, or null. */
|
|
480
|
+
async function keptStorage(schemaPath) {
|
|
481
|
+
try {
|
|
482
|
+
const storage = JSON.parse(await fs.readFile(schemaPath, 'utf8'))?.storage;
|
|
483
|
+
if (storage?.adapter === 'file') return {adapter: 'file'};
|
|
484
|
+
if (storage?.adapter === 'mongodb') return {adapter: 'mongodb', connection: storage.connection || 'default'};
|
|
485
|
+
} catch { /* new, or unreadable - the preset's own block it is */ }
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
467
489
|
/**
|
|
468
490
|
* Seed all preset collections at boot.
|
|
469
491
|
* Always overwrites schema.json; only creates data.json if absent.
|
|
@@ -477,7 +499,11 @@ export async function seedAll() {
|
|
|
477
499
|
const dataPath = path.join(dir, 'data.json');
|
|
478
500
|
|
|
479
501
|
await fs.mkdir(dir, {recursive: true});
|
|
480
|
-
|
|
502
|
+
// A user-data preset keeps the storage the site has (a move to or from
|
|
503
|
+
// MongoDB); rewriting it would flip the site to the other store.
|
|
504
|
+
const kept = USER_DATA.has(schema.slug) ? await keptStorage(schemaPath) : null;
|
|
505
|
+
const written = kept ? {...schema, storage: kept} : schema;
|
|
506
|
+
await fs.writeFile(schemaPath, JSON.stringify(written, null, 2) + '\n', 'utf8');
|
|
481
507
|
|
|
482
508
|
try {
|
|
483
509
|
await fs.access(dataPath);
|
|
@@ -15,6 +15,7 @@ import {resolveContextMenusForPage} from './contextMenus.js';
|
|
|
15
15
|
import {buildCustomThemeStyleTag, buildOverrideStyleTag, getThemeTokenMap, listThemeIds, loadThemeConfig, resolveThemeClasses} from './themeSettings.js';
|
|
16
16
|
import {getSearchSettings} from './search.js';
|
|
17
17
|
import {isToolEnabled} from './tools.js';
|
|
18
|
+
import {fmtDateTime} from './dates.js';
|
|
18
19
|
|
|
19
20
|
const VALID_LAYOUT_WIDTHS = new Set(['narrow', 'normal', 'wide', 'full']);
|
|
20
21
|
const CUSTOM_CSS_PATH = new URL('../../content/custom.css', import.meta.url).pathname;
|
|
@@ -783,8 +784,7 @@ function formatExpiry(iso) {
|
|
|
783
784
|
if (!iso) return '';
|
|
784
785
|
const when = new Date(iso);
|
|
785
786
|
if (Number.isNaN(when.getTime())) return '';
|
|
786
|
-
|
|
787
|
-
return ` This link expires ${escapeHtml(when.toLocaleString('en-GB', opts))}.`;
|
|
787
|
+
return ` This link expires ${escapeHtml(fmtDateTime(when))}.`;
|
|
788
788
|
}
|
|
789
789
|
|
|
790
790
|
function escapeHtml(str) {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* @module services/sidebarBadges
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import {fmtDate} from './dates.js';
|
|
15
|
+
|
|
14
16
|
/** "1 task", "3 tasks". */
|
|
15
17
|
export function plural(n, one, many = `${one}s`) {
|
|
16
18
|
return `${n} ${n === 1 ? one : many}`;
|
|
@@ -25,7 +27,7 @@ export function ago(iso, now = Date.now()) {
|
|
|
25
27
|
if (s < 3600) return `${Math.round(s / 60)} min ago`;
|
|
26
28
|
if (s < 86400) return plural(Math.round(s / 3600), 'hour') + ' ago';
|
|
27
29
|
if (s < 86400 * 14) return plural(Math.round(s / 86400), 'day') + ' ago';
|
|
28
|
-
return
|
|
30
|
+
return fmtDate(t);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** Shorten to `max` characters on a word boundary, with an ellipsis. */
|
package/server/services/tools.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ## Built-in Tools
|
|
5
5
|
*
|
|
6
|
-
* Contacts, Notes, Todo and Analytics were plugins until 0.67.0
|
|
7
|
-
* of the CMS
|
|
6
|
+
* Contacts, Notes, Todo and Analytics were plugins until 0.67.0; SEO joined them
|
|
7
|
+
* in 0.90.0 (CORE_TOOLS below). They are part of the CMS and ON unless a site
|
|
8
|
+
* says otherwise, in `config/tools.json`, switched at System > Plugins
|
|
9
|
+
* (#/plugins, the Marketplace screen):
|
|
8
10
|
*
|
|
9
11
|
* { "notes": { "enabled": false, "locked": true } }
|
|
10
12
|
*
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
</script>
|
|
137
137
|
|
|
138
138
|
<!-- Site initialisation -->
|
|
139
|
-
<script src="/public/js/site.js?v=
|
|
139
|
+
<script src="/public/js/site.js?v=20260927-scope-mine" type="module"></script>
|
|
140
140
|
<script src="/public/js/collection-context.js?v=20260927-ctx" type="module"></script>
|
|
141
141
|
{{ctxMenusModule}}
|
|
142
142
|
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
|
|
148
148
|
<!-- Interactive Collection Browser (used by [collection] shortcodes with
|
|
149
149
|
searchable / filterable / sortable / paginate attributes) -->
|
|
150
|
-
<script src="/public/js/collection-browser.js?v=20260927-
|
|
150
|
+
<script src="/public/js/collection-browser.js?v=20260927-scope-mine"></script>
|
|
151
151
|
|
|
152
152
|
<!-- Core Forms (logic engine must load before renderer) -->
|
|
153
153
|
<script src="/public/js/form-logic-engine.js?v=20260926-limits"></script>
|