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.
- package/admin/js/lib/tool-kit.js +1 -1
- package/admin/js/lib/tool-recovery.js +12 -0
- package/admin/js/templates/analytics.html +5 -0
- package/admin/js/templates/contacts.html +4 -0
- package/admin/js/templates/docs/usage-tools.html +12 -0
- package/admin/js/templates/notes.html +4 -0
- package/admin/js/templates/todo.html +4 -0
- package/admin/js/views/analytics.js +7 -7
- package/admin/js/views/contacts.js +22 -22
- package/admin/js/views/notes.js +8 -8
- package/admin/js/views/todo.js +9 -9
- package/package.json +1 -1
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -1
- package/public/js/effects.js +1 -1
- package/public/js/site.js +1 -1
- package/public/js/utf8-base64.mjs +1 -0
- package/server/routes/api/contacts.js +12 -14
- package/server/routes/api/notes.js +7 -6
- package/server/routes/api/todo.js +7 -5
- package/server/services/collections.js +11 -2
- package/server/services/markdown.js +10 -10
- package/server/services/renderer.js +2 -2
- package/server/services/tool-trash.js +39 -0
- package/server/templates/page.html +4 -4
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Todo API - /api/todo
|
|
3
3
|
*
|
|
4
|
-
* Was the `todo` plugin until 0.67.0.
|
|
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,
|
|
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 =>
|
|
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
|
|
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
|
-
|
|
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
|
|
1824
|
-
if (
|
|
1825
|
-
|
|
1826
|
-
if (
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
141
|
-
<script src="/public/js/collection-context.js?v=
|
|
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=
|
|
158
|
+
<script src="/public/js/effects.js?v=20261010-motion-demos"></script>
|
|
159
159
|
|
|
160
160
|
<!-- Plugin body-end injection -->
|
|
161
161
|
{{bodyEndInject}}
|