domma-cms 1.6.36 → 1.6.38
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/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/compose-window.js +4 -0
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +15 -6
- package/plugins/_lib/mail/compose.js +7 -2
- package/plugins/_lib/mail/message-action.js +56 -0
- package/plugins/_lib/mail/scheduler.js +2 -0
- package/plugins/_lib/mail/send.js +12 -3
- package/plugins/_lib/mail/store.js +16 -6
- package/plugins/_lib/outreach/index.js +1 -1
- package/plugins/_lib/outreach/ledger.js +10 -3
- package/plugins/_lib/outreach/sending.js +9 -3
- package/plugins/_lib/outreach/suppression.js +14 -3
- package/plugins/_lib/outreach/unsubscribe.js +13 -0
- package/plugins/blog/CLAUDE.md +11 -1
- package/plugins/blog/lib/cache.js +43 -0
- package/plugins/blog/lib/page.js +28 -9
- package/plugins/blog/plugin.js +14 -13
- package/plugins/blog/plugin.json +1 -1
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/cache.test.js +65 -0
- package/plugins/booking/CLAUDE.md +14 -2
- package/plugins/booking/admin/lib/diary-layout.js +18 -0
- package/plugins/booking/admin/views/diary.js +153 -152
- package/plugins/booking/admin/views/people.js +64 -66
- package/plugins/booking/admin/views/settings.js +31 -32
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/admin-diary.test.js +9 -1
- package/plugins/feedback/CLAUDE.md +55 -0
- package/plugins/feedback/admin/css/index.css +16 -1
- package/plugins/feedback/admin/lib/kit.js +106 -0
- package/plugins/feedback/admin/lib/settings.js +107 -0
- package/plugins/feedback/admin/views/feedback.js +4 -1
- package/plugins/feedback/admin/views/inbox.js +11 -2
- package/plugins/feedback/admin/views/send.js +74 -7
- package/plugins/feedback/docs/guide.md +9 -9
- package/plugins/feedback/lib/attachments.js +133 -0
- package/plugins/feedback/lib/cache.js +43 -0
- package/plugins/feedback/lib/receiver.js +36 -6
- package/plugins/feedback/lib/sender.js +70 -6
- package/plugins/feedback/lib/shape.js +111 -6
- package/plugins/feedback/plugin.js +171 -17
- package/plugins/feedback/plugin.json +2 -2
- package/plugins/feedback/tests/api.test.js +125 -3
- package/plugins/feedback/tests/settings.test.js +59 -0
- package/plugins/feedback/tests/shape.test.js +64 -0
- package/plugins/free-tier.lock.json +73 -61
- package/plugins/mail-reader/CLAUDE.md +15 -4
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/shopping-cart/CLAUDE.md +28 -2
- package/plugins/shopping-cart/admin/css/index.css +3 -0
- package/plugins/shopping-cart/admin/views/orders.js +42 -10
- package/plugins/shopping-cart/admin/views/shop.js +1 -1
- package/plugins/shopping-cart/docs/guide.md +7 -3
- package/plugins/shopping-cart/lib/payments.js +15 -2
- package/plugins/shopping-cart/lib/refunds.js +51 -0
- package/plugins/shopping-cart/lib/sales.js +15 -8
- package/plugins/shopping-cart/lib/shop.js +38 -3
- package/plugins/shopping-cart/plugin.js +47 -19
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/api.test.js +52 -0
- package/plugins/shopping-cart/tests/commission.test.js +1 -1
- package/plugins/shopping-cart/tests/refunds.test.js +42 -0
- package/plugins/support/CLAUDE.md +51 -0
- package/plugins/support/admin/css/index.css +16 -1
- package/plugins/support/admin/lib/kit.js +192 -8
- package/plugins/support/admin/lib/settings.js +123 -0
- package/plugins/support/admin/views/desk.js +55 -12
- package/plugins/support/admin/views/inbox.js +17 -4
- package/plugins/support/admin/views/support.js +4 -1
- package/plugins/support/docs/guide.md +7 -9
- package/plugins/support/lib/attachments.js +133 -0
- package/plugins/support/lib/cache.js +43 -0
- package/plugins/support/lib/receiver.js +45 -10
- package/plugins/support/lib/sender.js +81 -10
- package/plugins/support/lib/shape.js +153 -12
- package/plugins/support/plugin.js +159 -29
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/api.test.js +125 -3
- package/plugins/support/tests/settings.test.js +68 -0
- package/plugins/support/tests/shape.test.js +51 -1
- package/server/routes/outreach-public.js +54 -10
package/plugins/blog/lib/page.js
CHANGED
|
@@ -13,6 +13,13 @@ import defaultConfig from '../config.js';
|
|
|
13
13
|
import {fmtDate} from '../../_lib/admin/ui/dates.js';
|
|
14
14
|
import {buildToc, byNewest, cardVars, cleanBasePath, DEFAULT_POST_LAYOUT, DISPLAY_DEFAULTS, escapeHtml, interpolate, pickPostLayout, relatedPosts, safeImageUrl} from './layouts.js';
|
|
15
15
|
import {catalogue, loadBlock, renderCards, styleTag} from './render.js';
|
|
16
|
+
import {createCache} from './cache.js';
|
|
17
|
+
|
|
18
|
+
// Whole-collection reads and author names, kept a short while (lib/cache.js):
|
|
19
|
+
// every public page view and every badge poll read all posts, categories and
|
|
20
|
+
// comments. plugin.js calls forgetEntries() on every write route of this plugin.
|
|
21
|
+
const remembered = createCache();
|
|
22
|
+
export const forgetEntries = () => remembered.forget();
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Whether readers see a post. Blog Pro files posts under several blogs
|
|
@@ -69,12 +76,21 @@ export function liveBase(registered) {
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/** Always returns the actual array of entries, regardless of adapter shape. */
|
|
72
|
-
|
|
79
|
+
async function readEntries(slug) {
|
|
73
80
|
const result = await listEntries(slug, {limit: 100000, sort: 'createdAt', order: 'desc'});
|
|
74
81
|
const arr = Array.isArray(result) ? result : (Array.isArray(result?.entries) ? result.entries : []);
|
|
75
82
|
return arr.map((entry) => ({id: entry.id, ...(entry.data ?? entry)}));
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A collection's entries, newest first - from the cache when a read is fresh.
|
|
87
|
+
* Callers get the SAME array, so none may change it in place (they filter and
|
|
88
|
+
* sort copies, as before).
|
|
89
|
+
*/
|
|
90
|
+
export function loadEntries(slug) {
|
|
91
|
+
return remembered.get(`entries:${slug}`, () => readEntries(slug));
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
/** Render a post body through the full Markdown + shortcode pipeline. */
|
|
79
95
|
export async function renderPostBody(content) {
|
|
80
96
|
if (!content) return {html: '', usedComponents: []};
|
|
@@ -87,14 +103,17 @@ export async function renderPostBody(content) {
|
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
/** A user's display name - one lookup per author per cache window, not per card. */
|
|
107
|
+
export function resolveAuthorName(authorId) {
|
|
108
|
+
if (!authorId) return Promise.resolve('Unknown');
|
|
109
|
+
return remembered.get(`author:${authorId}`, async () => {
|
|
110
|
+
try {
|
|
111
|
+
const user = await getUserById(authorId);
|
|
112
|
+
return user?.name || user?.email || 'Unknown';
|
|
113
|
+
} catch {
|
|
114
|
+
return 'Unknown';
|
|
115
|
+
}
|
|
116
|
+
});
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
/** One lookup per author, however many posts they wrote. */
|
package/plugins/blog/plugin.js
CHANGED
|
@@ -5,7 +5,7 @@ import {getPluginStates, savePluginState} from '../../server/services/plugins.js
|
|
|
5
5
|
import {renderBlogPage} from '../../server/services/renderer.js';
|
|
6
6
|
import {cleanBasePath, DISPLAY_DEFAULTS, pickDisplaySettings, safeImageUrl} from './lib/layouts.js';
|
|
7
7
|
import {catalogue, seedBlocks} from './lib/render.js';
|
|
8
|
-
import {liveBase, liveSettings, loadEntries as loadAll, renderPostPage, slugHolder} from './lib/page.js';
|
|
8
|
+
import {forgetEntries, liveBase, liveSettings, loadEntries as loadAll, renderPostPage, slugHolder} from './lib/page.js';
|
|
9
9
|
import {addNavLink, moveNavLink, removeNavLink} from './lib/nav-link.js';
|
|
10
10
|
import {SAMPLE_CATEGORIES, samplePosts} from './lib/samples.js';
|
|
11
11
|
import {commentKey, commentNotice} from './lib/notices.js';
|
|
@@ -167,17 +167,9 @@ function isVisible(post) {
|
|
|
167
167
|
|| (post.status === 'scheduled' && post.publishedAt && new Date(post.publishedAt) <= new Date());
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return arr.map((e) => ({ id: e.id, ...(e.data ?? e) }));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function loadCategories() {
|
|
177
|
-
const result = await listEntries(CATEGORIES_SLUG, { limit: 10000 });
|
|
178
|
-
const arr = Array.isArray(result) ? result : (result?.entries || []);
|
|
179
|
-
return arr.map((e) => ({ id: e.id, ...(e.data ?? e) }));
|
|
180
|
-
}
|
|
170
|
+
// The shortcodes run on public page views: the same cached reads as the pages.
|
|
171
|
+
const loadPosts = () => loadAll(POSTS_SLUG);
|
|
172
|
+
const loadCategories = () => loadAll(CATEGORIES_SLUG);
|
|
181
173
|
|
|
182
174
|
function selectPosts(posts, { limit, categorySlug, categoryMap }) {
|
|
183
175
|
let filtered = posts.filter(isVisible);
|
|
@@ -337,6 +329,15 @@ export default async function blogPlugin(fastify, options) {
|
|
|
337
329
|
});
|
|
338
330
|
}
|
|
339
331
|
|
|
332
|
+
// Every write route of this plugin (posts, categories, comments - the public
|
|
333
|
+
// POST /comments too - samples) retires the cached reads behind the public
|
|
334
|
+
// pages, the shortcodes and the badge. onSend, not onResponse: the memory is
|
|
335
|
+
// gone BEFORE the writer's response leaves, so their very next read is fresh.
|
|
336
|
+
// The hook is encapsulated - only this plugin's routes reach it.
|
|
337
|
+
fastify.addHook('onSend', async (request) => {
|
|
338
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') forgetEntries();
|
|
339
|
+
});
|
|
340
|
+
|
|
340
341
|
// The layouts are blocks; put the shipped ones where the Blocks tool can
|
|
341
342
|
// edit them. Once each, never over a site's own copy (lib/render.js).
|
|
342
343
|
try {
|
|
@@ -392,7 +393,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
392
393
|
});
|
|
393
394
|
const html = await renderBlogPage(
|
|
394
395
|
fileURLToPath(new URL('./templates/post.html', import.meta.url)),
|
|
395
|
-
{ post: page.html, assetV: '1.9.
|
|
396
|
+
{ post: page.html, assetV: '1.9.7' },
|
|
396
397
|
{ title: post.title || 'Preview', description: post.excerpt || '', ogImage: safeImageUrl(post.featuredImage),
|
|
397
398
|
usedComponents: page.usedComponents, urlPath: `${basePath}/${post.slug}`, noindex: true }
|
|
398
399
|
);
|
package/plugins/blog/plugin.json
CHANGED
|
@@ -24,7 +24,7 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
|
24
24
|
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
25
25
|
|
|
26
26
|
/** Bumped with the plugin, so a release is never served a stale script. */
|
|
27
|
-
const ASSET_V = '1.9.
|
|
27
|
+
const ASSET_V = '1.9.7';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Above this many posts a listing pages on the server instead: the browser
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog - the short memory behind the public pages and the badge (lib/cache.js),
|
|
3
|
+
* and the rule that matters: a write is seen by the very next read.
|
|
4
|
+
*/
|
|
5
|
+
import {test} from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import Fastify from 'fastify';
|
|
8
|
+
import {createCache} from '../lib/cache.js';
|
|
9
|
+
|
|
10
|
+
test('a read is shared for the cache window, then read again', async () => {
|
|
11
|
+
let now = 0;
|
|
12
|
+
let reads = 0;
|
|
13
|
+
const c = createCache({ttl: 30_000, clock: () => now});
|
|
14
|
+
const load = async () => { reads++; return ['post']; };
|
|
15
|
+
const [a, b] = await Promise.all([c.get('posts', load), c.get('posts', load)]);
|
|
16
|
+
assert.equal(reads, 1, 'concurrent misses share one read');
|
|
17
|
+
assert.equal(a, b, 'the same array - callers must not change it in place');
|
|
18
|
+
now = 29_999;
|
|
19
|
+
await c.get('posts', load);
|
|
20
|
+
assert.equal(reads, 1);
|
|
21
|
+
now = 30_000;
|
|
22
|
+
await c.get('posts', load);
|
|
23
|
+
assert.equal(reads, 2, 'the window passed: read again');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('forget() retires every key, and a read in flight when the write landed is not kept', async () => {
|
|
27
|
+
const c = createCache({ttl: 60_000});
|
|
28
|
+
let value = 'old';
|
|
29
|
+
let release;
|
|
30
|
+
const slow = () => new Promise(r => { release = r; });
|
|
31
|
+
const inFlight = c.get('posts', slow);
|
|
32
|
+
await c.get('categories', async () => ['cat']);
|
|
33
|
+
assert.equal(c.size, 2);
|
|
34
|
+
// The write finishes while the read is still on its way.
|
|
35
|
+
value = 'new';
|
|
36
|
+
c.forget();
|
|
37
|
+
release('old');
|
|
38
|
+
assert.equal(await inFlight, 'old', 'the caller who started before the write still gets what they read');
|
|
39
|
+
assert.equal(await c.get('posts', async () => value), 'new', 'the next read is fresh, not the stale one');
|
|
40
|
+
assert.deepEqual(await c.get('categories', async () => ['fresh']), ['fresh']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a failed read is not remembered', async () => {
|
|
44
|
+
const c = createCache();
|
|
45
|
+
await assert.rejects(() => c.get('posts', async () => { throw new Error('adapter down'); }), /adapter down/);
|
|
46
|
+
assert.equal(c.size, 0);
|
|
47
|
+
assert.deepEqual(await c.get('posts', async () => ['ok']), ['ok']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the plugin forgets on every write route, before the writer\'s response leaves', async () => {
|
|
51
|
+
// The hook as plugin.js registers it, on a stand-in app: the point under test
|
|
52
|
+
// is the method rule and that it fires in onSend (the writer's next read is fresh).
|
|
53
|
+
const c = createCache();
|
|
54
|
+
const app = Fastify();
|
|
55
|
+
app.addHook('onSend', async (request) => { if (request.method !== 'GET' && request.method !== 'HEAD') c.forget(); });
|
|
56
|
+
app.get('/posts', async () => c.get('posts', async () => ({at: Date.now()})));
|
|
57
|
+
app.put('/posts/1', async () => ({ok: true}));
|
|
58
|
+
await c.get('posts', async () => 'stale');
|
|
59
|
+
assert.equal(c.size, 1);
|
|
60
|
+
await app.inject({method: 'GET', url: '/posts'});
|
|
61
|
+
assert.equal(c.size, 1, 'a GET keeps the memory');
|
|
62
|
+
await app.inject({method: 'PUT', url: '/posts/1', payload: {}});
|
|
63
|
+
assert.equal(c.size, 0, 'a write retires it');
|
|
64
|
+
await app.close();
|
|
65
|
+
});
|
|
@@ -17,8 +17,8 @@ Calendar events block their times (read from `calendar-occurrences` / `calendar-
|
|
|
17
17
|
service - no import of the Calendar, which is a separate paid Tool).
|
|
18
18
|
|
|
19
19
|
`tier: free`, ships with the engine, disabled on arrival. Plan and phases: `dcms-marketplace/docs/booking-plan.md`.
|
|
20
|
-
Booking Pro (
|
|
21
|
-
pattern - it does not supersede it.
|
|
20
|
+
Booking Pro (marketplace `booking-pro`, 0.4.x) is an ADD-ON that registers into this one (`lib/extensions.js`), the
|
|
21
|
+
Cart / Cart Pro pattern - it does not supersede it. Neither takes payment yet.
|
|
22
22
|
|
|
23
23
|
1.6.0 (2026-09-30): "Add people" in an online booking's call (core with `server/services/callInvites.js` and
|
|
24
24
|
`_lib/call/invite.js`; both imported dynamically - lib/call.js by top-level `await import`, plugin.js beside
|
|
@@ -168,3 +168,15 @@ only to roles that hold it.
|
|
|
168
168
|
the run (restored after) and forges a Host to prove it.
|
|
169
169
|
- The API tests create contacts in domma-cms's `content/collections/contacts-contacts/data.json` (tracked) - they
|
|
170
170
|
snapshot and restore it; a crashed run can leave it changed: `git checkout` it in domma-cms.
|
|
171
|
+
|
|
172
|
+
## 1.6.4 (2026-10-13 Tool sweep, upgrades)
|
|
173
|
+
- The Diary is domma-reactive end to end: heads, hour gutter and columns are keyed `data-each` lists (`HEAD`, `HOUR`,
|
|
174
|
+
`COL` in diary.js; each column's open bands and blocks are `{{#each}}` inside it). One `draw` effect works out every
|
|
175
|
+
measure as a CSS length (`blockRow`) and the markup only binds; the empty state, "add a service" tip and first-run
|
|
176
|
+
checklist (`STEP`) are bound too. Kebab / right-click / Shift+F10 still come from `wireRows(gridBox, {match: '.bk-block'})`
|
|
177
|
+
(delegated, so re-rendered blocks need no rewiring). `mergeWindows` moved to admin/lib/diary-layout.js (tested).
|
|
178
|
+
- Settings > Closed days (`CLOSURE_ROW`) and a person's opening hours / days off (`DAY_ROW` with a nested
|
|
179
|
+
`{{#each windows}}`, `OFF_ROW`) are keyed lists of per-field observables; `hoursOut` / `daysOffOut` / `closuresOut`
|
|
180
|
+
turn them back into plain data for the dirty snapshot and the save. A "from" after the "to" still pulls "to" along.
|
|
181
|
+
- Icons in rows added after binding are drawn by `I.scan` after a flush (`rescan`), because new rows arrive after
|
|
182
|
+
the panel's first scan.
|
|
@@ -138,3 +138,21 @@ export function columnItems(bookings, {bookableId = ''} = {}) {
|
|
|
138
138
|
}
|
|
139
139
|
return [...groups.values()].map(g => ({...g, session: g.bookings.length > 1}));
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Opening windows (minutes) merged where they touch or overlap - the week view's one band per day for everyone.
|
|
144
|
+
* Moved here from diary.js (1.6.4) so it is tested with the rest of the geometry.
|
|
145
|
+
*
|
|
146
|
+
* @param {{from: number, to: number}[]} list
|
|
147
|
+
* @returns {{from: number, to: number}[]} sorted, non-overlapping copies
|
|
148
|
+
*/
|
|
149
|
+
export function mergeWindows(list) {
|
|
150
|
+
const sorted = [...(list || [])].sort((a, b) => a.from - b.from);
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const w of sorted) {
|
|
153
|
+
const last = out[out.length - 1];
|
|
154
|
+
if (last && w.from <= last.to) last.to = Math.max(last.to, w.to);
|
|
155
|
+
else out.push({...w});
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|