domma-cms 1.6.6 → 1.6.7

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.
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Todo API - /api/todo
3
3
  *
4
- * Was the `todo` plugin until 0.67.0. The logic is unchanged; what moved is
5
- * where it lives and how it is gated.
4
+ * Was the `todo` plugin until 0.67.0. Personal entries now use recoverable Trash; normal reads omit trashed rows.
6
5
  *
7
6
  * **Per-user scope.** Entries carry a `userId` and every read is filtered to the
8
7
  * caller's own. The plugin made this configurable (`scope: 'user' | 'site'`) and
@@ -15,9 +14,10 @@
15
14
  * `setupPlugin()` read `schema.slug`, so the live collection has always been
16
15
  * called this, and existing entries are found only if it stays exactly so.
17
16
  */
17
+ import {registerToolTrash, trashToolEntry} from '../../services/tool-trash.js';
18
18
  import {clip, plural} from '../../services/sidebarBadges.js';
19
19
  import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
20
- import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
20
+ import {createEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
21
21
  import {registerCalendarSource} from '../../services/hooks.js';
22
22
  import {getPermissionsFor} from '../../services/roles.js';
23
23
 
@@ -83,6 +83,8 @@ export async function todoRoutes(fastify, opts = {}) {
83
83
  const requirePermission = opts.requirePermission || defaultRequirePermission;
84
84
 
85
85
 
86
+ registerToolTrash(fastify, {slug: SLUG, resource: 'todo', itemPath: '/items', authenticate, requirePermission, project: toTodo, onChange: tell});
87
+
86
88
  function userId(request) {
87
89
  return request.user?.id ?? request.user?.sub ?? null;
88
90
  }
@@ -213,7 +215,7 @@ export async function todoRoutes(fastify, opts = {}) {
213
215
  const uid = userId(request);
214
216
  const all = await loadAll(uid);
215
217
  const completed = all.filter(t => t.status === 'completed');
216
- await Promise.all(completed.map(t => deleteEntry(SLUG, t.id)));
218
+ await Promise.all(completed.map(async t => trashToolEntry(SLUG, await getEntry(SLUG, t.id))));
217
219
  if (completed.length) tell(uid);
218
220
  return reply.send({success: true, removed: completed.length});
219
221
  });
@@ -227,7 +229,7 @@ export async function todoRoutes(fastify, opts = {}) {
227
229
  if (!entry) return reply.code(404).send({error: 'Todo not found'});
228
230
  if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Todo not found'});
229
231
 
230
- await deleteEntry(SLUG, id);
232
+ await trashToolEntry(SLUG, entry);
231
233
  tell(entry.data.userId);
232
234
  return reply.send({ success: true });
233
235
  });
@@ -16,6 +16,7 @@ import {typedEntryData} from './entryValues.js';
16
16
  import {toCsv} from '../../public/js/collection-export.mjs';
17
17
 
18
18
  const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
19
+ const TOOL_TRASH_SLUGS = new Set(['notes-notes', 'todo-items', 'contacts-contacts']);
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // Internal helpers (schema only)
@@ -352,6 +353,7 @@ export async function validateReferences(schema, data) {
352
353
  * @param {string} [opts.search] - Substring match across all fields
353
354
  * @param {Record<string, unknown>} [opts.filter] - Structured filter; see filterEngine.js
354
355
  * @param {boolean} [opts.resolveRefs=false] - Batch-resolve reference fields to display labels
356
+ * @param {boolean} [opts.includeToolTrash=false] - Internal recovery/maintenance reads only
355
357
  * @returns {Promise<{ entries: object[], total: number, page: number, limit: number }>}
356
358
  * @throws {Error} If collection slug does not exist
357
359
  */
@@ -359,6 +361,11 @@ export async function listEntries(slug, opts = {}) {
359
361
  const schema = await getCollection(slug);
360
362
  if (!schema) throw new Error(`Collection "${slug}" not found`);
361
363
  const adapter = await getAdapter(slug);
364
+ // Personal Tool trash is retained for recovery, but must stay out of
365
+ // normal lists, badges, notifications and contact-sharing integrations.
366
+ if (TOOL_TRASH_SLUGS.has(slug) && !opts.includeToolTrash) {
367
+ opts = {...opts, filter: {...opts.filter, _toolTrashedAt_exists: false}};
368
+ }
362
369
  const result = await adapter.list(slug, opts);
363
370
  if (opts.resolveRefs) {
364
371
  const {resolveReferences} = await import('./references.js');
@@ -372,11 +379,13 @@ export async function listEntries(slug, opts = {}) {
372
379
  *
373
380
  * @param {string} slug
374
381
  * @param {string} entryId
382
+ * @param {{includeToolTrash?: boolean}} [opts] - Internal recovery/maintenance reads only
375
383
  * @returns {Promise<object|null>}
376
384
  */
377
- export async function getEntry(slug, entryId) {
385
+ export async function getEntry(slug, entryId, opts = {}) {
378
386
  const adapter = await getAdapter(slug);
379
- return adapter.get(slug, entryId);
387
+ const entry = await adapter.get(slug, entryId);
388
+ return TOOL_TRASH_SLUGS.has(slug) && entry?.data?._toolTrashedAt && !opts.includeToolTrash ? null : entry;
380
389
  }
381
390
 
382
391
  /**
@@ -1819,17 +1819,17 @@ function processEffectsBlocks(markdown) {
1819
1819
 
1820
1820
  apply('firework', (attrStr, body) => {
1821
1821
  const attrs = parseShortcodeAttrs(attrStr);
1822
+ const flag = name => attrs[name] === true || attrs[name] === 'true';
1822
1823
  const classes = ['firework'];
1823
- if (attrs.type) classes.push(`firework-${attrs.type}`);
1824
- if (attrs.colour) classes.push(`firework-${attrs.colour}`);
1825
- if (attrs.size) classes.push(`firework-${attrs.size}`);
1826
- if (attrs.continuous === 'true') classes.push('firework-continuous');
1827
- if (attrs.hover === 'true') classes.push('firework-on-hover');
1828
- if (body === null) return `<div class="${classes.join(' ')}"></div>`;
1829
- // Restore code placeholders before the nested parse (tabs-handler
1830
- // pattern) so code spans/fences render as <code>, not raw backticks.
1831
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1832
- return `<div class="${classes.join(' ')}">${innerHtml}</div>\n`;
1824
+ for (const name of ['type', 'colour', 'size']) if (attrs[name]) classes.push(`firework-${escapeAttr(attrs[name])}`);
1825
+ if (flag('continuous')) classes.push('firework-continuous');
1826
+ const hover = flag('hover');
1827
+ if (hover) classes.push('firework-on-hover');
1828
+ // Native hover fireworks need a hit target; the base decoration ignores pointer events.
1829
+ const style = hover ? ' style="pointer-events:auto;--dm-fw-size:var(--dm-fw-particle-size)"' : '';
1830
+ const particles = hover ? '' : '<div class="firework-launch"></div><div class="firework-particles"></div>';
1831
+ const innerHtml = body === null ? '' : marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1832
+ return `<div class="${classes.join(' ')}"${style}>${particles}${innerHtml}</div>\n`;
1833
1833
  });
1834
1834
 
1835
1835
  apply('fireworks', (_attrStr, body) => {
@@ -367,7 +367,7 @@ export async function renderPage(page, opts = {}) {
367
367
  ? `window.__CMS_CTXMENUS__ = ${JSON.stringify(ctxMenus).replace(/<\/script>/gi, '<\\/script>')};`
368
368
  : '';
369
369
  const ctxMenusModule = ctxMenus.length
370
- ? '<script src="/public/js/context-menus.js?v=20261009-classes" type="module"></script>'
370
+ ? '<script src="/public/js/context-menus.js?v=20261010-utf8-payloads" type="module"></script>'
371
371
  : '';
372
372
 
373
373
  const vars = {
@@ -1081,7 +1081,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
1081
1081
  ? `window.__CMS_CTXMENUS__ = ${JSON.stringify(ctxMenus).replace(/<\/script>/gi, '<\\/script>')};`
1082
1082
  : '';
1083
1083
  const ctxMenusModule = ctxMenus.length
1084
- ? '<script src="/public/js/context-menus.js?v=20261009-classes" type="module"></script>'
1084
+ ? '<script src="/public/js/context-menus.js?v=20261010-utf8-payloads" type="module"></script>'
1085
1085
  : '';
1086
1086
 
1087
1087
  const vars = {
@@ -0,0 +1,39 @@
1
+ import {deleteEntry, getEntry, listEntries, updateEntry} from './collections.js';
2
+
3
+ /** Keep the original row (including Pro fields and related IDs) until purged. */
4
+ export async function trashToolEntry(slug, entry) {
5
+ return updateEntry(slug, entry.id, {...entry.data, _toolTrashedAt: new Date().toISOString()});
6
+ }
7
+
8
+ /** Routes remain owner-scoped, even for administrators. Restore needs update. */
9
+ export function registerToolTrash(fastify, {slug, resource, itemPath, authenticate, requirePermission, project, onChange, onPurge}) {
10
+ const uid = request => request.user?.id ?? request.user?.sub ?? null;
11
+ const may = action => ({preHandler: [authenticate, requirePermission(resource, action)]});
12
+ const own = async request => {
13
+ const entry = await getEntry(slug, request.params.id, {includeToolTrash: true});
14
+ return uid(request) && entry?.data.userId === uid(request) && entry.data._toolTrashedAt ? entry : null;
15
+ };
16
+ fastify.get('/trash', may('read'), async request => {
17
+ if (!uid(request)) return [];
18
+ const {entries} = await listEntries(slug, {limit: 100000, includeToolTrash: true});
19
+ return entries.filter(e => e.data.userId === uid(request) && e.data._toolTrashedAt)
20
+ .sort((a, b) => String(b.data._toolTrashedAt).localeCompare(String(a.data._toolTrashedAt)))
21
+ .map(e => ({...project(e), trashedAt: e.data._toolTrashedAt}));
22
+ });
23
+ fastify.post(`${itemPath}/:id/restore`, may('update'), async (request, reply) => {
24
+ const entry = await own(request);
25
+ if (!entry) return reply.code(404).send({error: 'Item not found in Trash'});
26
+ const data = {...entry.data};
27
+ delete data._toolTrashedAt;
28
+ const restored = await updateEntry(slug, entry.id, data);
29
+ await onChange?.(uid(request));
30
+ return project(restored);
31
+ });
32
+ fastify.delete('/trash/:id', may('delete'), async (request, reply) => {
33
+ const entry = await own(request);
34
+ if (!entry) return reply.code(404).send({error: 'Item not found in Trash'});
35
+ await deleteEntry(slug, entry.id);
36
+ await onPurge?.(entry);
37
+ return {success: true};
38
+ });
39
+ }
@@ -19,7 +19,7 @@
19
19
  {{#if fontLink}}{{fontLink}}{{/if}}
20
20
 
21
21
  <!-- DommaJS CSS -->
22
- <link rel="stylesheet" href="/dist/domma/domma.css">
22
+ <link rel="stylesheet" href="/dist/domma/domma.css?v=20261010-fireworks">
23
23
  <link rel="stylesheet" href="/dist/domma/grid.css">
24
24
  <link rel="stylesheet" href="/dist/domma/elements.css">
25
25
  <link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
@@ -137,8 +137,8 @@
137
137
  </script>
138
138
 
139
139
  <!-- Site initialisation -->
140
- <script src="/public/js/site.js?v=20261009-more-classes" type="module"></script>
141
- <script src="/public/js/collection-context.js?v=20260927-ctx" type="module"></script>
140
+ <script src="/public/js/site.js?v=20261010-utf8-payloads" type="module"></script>
141
+ <script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
142
142
  {{ctxMenusModule}}
143
143
 
144
144
  <!-- Floating menu panels: collapse to a hamburger, drag to move, resize.
@@ -155,7 +155,7 @@
155
155
  <script src="/public/js/forms.js?v=20261009-form-classes" type="module"></script>
156
156
 
157
157
  <!-- Core effects runtime -->
158
- <script src="/public/js/effects.js?v=20261003-characters"></script>
158
+ <script src="/public/js/effects.js?v=20261010-motion-demos"></script>
159
159
 
160
160
  <!-- Plugin body-end injection -->
161
161
  {{bodyEndInject}}