domma-cms 0.55.1 → 0.69.3
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 +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- 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/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -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/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- 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/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- 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 +4473 -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 +1022 -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/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- 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 +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -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 +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- 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/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -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/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- 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/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -1,93 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client for outbound calls from domma-cms → domma-cms-manager.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* A MANAGED site is one the manager forked, and the manager tells it so in its
|
|
5
|
+
* environment: `MANAGER_URL` (where), `SITE_SLUG` (who) and `FLEET_TOKEN` (the
|
|
6
|
+
* proof, minted per fork and never written down). An unmanaged site has none of
|
|
7
|
+
* these, every call here returns null/[], and the Marketplace screen points at
|
|
8
|
+
* Install from File instead - which is the whole of that customer's procedure.
|
|
9
|
+
*
|
|
10
|
+
* There used to be a `registerInstance()` handshake: HMAC a fingerprint with a
|
|
11
|
+
* shared INSTANCE_SECRET, get a licence token back. It never had a caller, and
|
|
12
|
+
* it was solving a problem this fleet does not have - the manager starts every
|
|
13
|
+
* child itself, so it already knows which site is on the other end. Replacing
|
|
14
|
+
* it with a fork-time token removed a shared secret, a distribution problem and
|
|
15
|
+
* a round trip.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here throws. A manager that is down, slow or mid-restart must leave
|
|
18
|
+
* the site serving pages, so every failure is a warning and an empty answer.
|
|
17
19
|
*/
|
|
18
|
-
async function getCmsVersion() {
|
|
19
|
-
try {
|
|
20
|
-
const pkgPath = join(__dirname, '../../package.json');
|
|
21
|
-
const raw = await readFile(pkgPath, 'utf-8');
|
|
22
|
-
return JSON.parse(raw).version ?? '0.0.0';
|
|
23
|
-
} catch {
|
|
24
|
-
return '0.0.0';
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
20
|
|
|
28
21
|
/**
|
|
29
|
-
*
|
|
22
|
+
* True when this site was started by a manager and can talk to it.
|
|
30
23
|
*
|
|
31
|
-
* @
|
|
32
|
-
* @returns {Promise<{licenseToken: string, instanceId: string}|null>}
|
|
24
|
+
* @returns {boolean}
|
|
33
25
|
*/
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!MANAGER_URL) return null;
|
|
39
|
-
if (!INSTANCE_SECRET) {
|
|
40
|
-
console.warn('[managerClient] INSTANCE_SECRET not set - skipping registerInstance');
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const cmsVersion = await getCmsVersion();
|
|
46
|
-
const hostname = process.env.HOSTNAME ?? 'unknown';
|
|
47
|
-
|
|
48
|
-
const token = createHmac('sha256', INSTANCE_SECRET)
|
|
49
|
-
.update(fingerprint)
|
|
50
|
-
.digest('hex');
|
|
51
|
-
|
|
52
|
-
const res = await fetch(`${MANAGER_URL}/api/instances/register`, {
|
|
53
|
-
signal: AbortSignal.timeout(10_000),
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers: {
|
|
56
|
-
'Content-Type': 'application/json',
|
|
57
|
-
'X-Instance-Token': token,
|
|
58
|
-
},
|
|
59
|
-
body: JSON.stringify({ fingerprint, cmsVersion, hostname }),
|
|
60
|
-
});
|
|
26
|
+
export function isManaged() {
|
|
27
|
+
return Boolean(process.env.MANAGER_URL && process.env.FLEET_TOKEN);
|
|
28
|
+
}
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
30
|
+
/** The slug this site is known by in its fleet, or null when unmanaged. */
|
|
31
|
+
export function siteSlug() {
|
|
32
|
+
return process.env.SITE_SLUG || null;
|
|
33
|
+
}
|
|
66
34
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
35
|
+
/** Bearer the fork-time token. Read per call: a restart mints a new one. */
|
|
36
|
+
function fleetHeaders() {
|
|
37
|
+
return {Authorization: `Bearer ${process.env.FLEET_TOKEN}`};
|
|
72
38
|
}
|
|
73
39
|
|
|
74
40
|
/**
|
|
75
41
|
* Fetch the plugin catalogue from domma-cms-manager.
|
|
76
42
|
*
|
|
77
|
-
* @
|
|
78
|
-
*
|
|
43
|
+
* @returns {Promise<Array<{slug: string, version: string, displayName: string,
|
|
44
|
+
* description: string, price: number|null, entitled: boolean, licenceStatus: string|null}>>}
|
|
79
45
|
*/
|
|
80
|
-
export async function fetchCatalogue(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!MANAGER_URL) return [];
|
|
46
|
+
export async function fetchCatalogue() {
|
|
47
|
+
if (!isManaged()) return [];
|
|
84
48
|
|
|
85
49
|
try {
|
|
86
|
-
const res = await fetch(`${MANAGER_URL}/api/plugins/catalogue`, {
|
|
50
|
+
const res = await fetch(`${process.env.MANAGER_URL}/api/plugins/catalogue`, {
|
|
87
51
|
signal: AbortSignal.timeout(10_000),
|
|
88
|
-
headers:
|
|
89
|
-
Authorization: `Bearer ${licenseToken}`,
|
|
90
|
-
},
|
|
52
|
+
headers: fleetHeaders(),
|
|
91
53
|
});
|
|
92
54
|
|
|
93
55
|
if (!res.ok) {
|
|
@@ -103,80 +65,54 @@ export async function fetchCatalogue(licenseToken) {
|
|
|
103
65
|
}
|
|
104
66
|
|
|
105
67
|
/**
|
|
106
|
-
* Fetch a plugin bundle
|
|
68
|
+
* Fetch a plugin bundle from domma-cms-manager.
|
|
69
|
+
*
|
|
70
|
+
* Returns the triple `installPlugin()` takes, plus the entitlement the manager
|
|
71
|
+
* minted for THIS site. The entitlement travels with the bytes it licenses
|
|
72
|
+
* rather than arriving separately, because a paid plugin installed without one
|
|
73
|
+
* is a plugin that will not load - a failure that looks like a broken install
|
|
74
|
+
* rather than a licensing answer.
|
|
107
75
|
*
|
|
108
|
-
* @param
|
|
109
|
-
* @param
|
|
110
|
-
* @
|
|
111
|
-
*
|
|
76
|
+
* @param {string} slug
|
|
77
|
+
* @param {string} version
|
|
78
|
+
* @returns {Promise<{tarball: Buffer, signature: string, publicKeyId: string,
|
|
79
|
+
* entitlement: object|null, version: string}|null>} null when unmanaged or unreachable
|
|
80
|
+
* @throws {Error} when the manager refuses - an unlicensed plugin is an answer
|
|
81
|
+
* worth showing, not a silent empty result
|
|
112
82
|
*/
|
|
113
|
-
export async function fetchPluginBundle(slug, version
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!MANAGER_URL) return null;
|
|
83
|
+
export async function fetchPluginBundle(slug, version) {
|
|
84
|
+
if (!isManaged()) return null;
|
|
117
85
|
|
|
86
|
+
let res;
|
|
118
87
|
try {
|
|
119
|
-
|
|
120
|
-
signal: AbortSignal.timeout(
|
|
121
|
-
headers:
|
|
122
|
-
Authorization: `Bearer ${licenseToken}`,
|
|
123
|
-
},
|
|
88
|
+
res = await fetch(`${process.env.MANAGER_URL}/api/plugins/install/${slug}/${version}`, {
|
|
89
|
+
signal: AbortSignal.timeout(30_000),
|
|
90
|
+
headers: fleetHeaders(),
|
|
124
91
|
});
|
|
125
|
-
|
|
126
|
-
if (!res.ok) {
|
|
127
|
-
console.warn(`[managerClient] fetchPluginBundle failed: ${res.status} ${res.statusText}`);
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const json = await res.json();
|
|
132
|
-
|
|
133
|
-
if (typeof json.tarball !== 'string' || json.tarball.length > 100 * 1024 * 1024) {
|
|
134
|
-
console.warn('[managerClient] fetchPluginBundle: tarball too large or invalid, refusing decode');
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
tarball: Buffer.from(json.tarball, 'base64'),
|
|
140
|
-
signature: json.signature,
|
|
141
|
-
publicKeyId: json.publicKeyId,
|
|
142
|
-
};
|
|
143
92
|
} catch (err) {
|
|
144
93
|
console.warn('[managerClient] fetchPluginBundle error:', err.message);
|
|
145
94
|
return null;
|
|
146
95
|
}
|
|
147
|
-
}
|
|
148
96
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
export async function heartbeat(licenseToken, installedSlugs) {
|
|
157
|
-
const MANAGER_URL = process.env.MANAGER_URL;
|
|
158
|
-
|
|
159
|
-
if (!MANAGER_URL) return false;
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const res = await fetch(`${MANAGER_URL}/api/instances/heartbeat`, {
|
|
163
|
-
signal: AbortSignal.timeout(10_000),
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: {
|
|
166
|
-
'Content-Type': 'application/json',
|
|
167
|
-
Authorization: `Bearer ${licenseToken}`,
|
|
168
|
-
},
|
|
169
|
-
body: JSON.stringify({ installedSlugs, versions: {} }),
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (!res.ok) {
|
|
173
|
-
console.warn(`[managerClient] heartbeat failed: ${res.status} ${res.statusText}`);
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
// 403 is "you have no licence for this", 409 "your catalogue is stale".
|
|
99
|
+
// Both are things the admin can act on, so they are raised rather than
|
|
100
|
+
// logged and swallowed the way an unreachable manager is.
|
|
101
|
+
const body = await res.json().catch(() => ({}));
|
|
102
|
+
throw new Error(body.error || `The manager refused that download (${res.status}).`);
|
|
103
|
+
}
|
|
176
104
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
console.warn('[managerClient]
|
|
180
|
-
return
|
|
105
|
+
const json = await res.json();
|
|
106
|
+
if (typeof json.tarball !== 'string' || json.tarball.length > 100 * 1024 * 1024) {
|
|
107
|
+
console.warn('[managerClient] fetchPluginBundle: tarball too large or invalid, refusing decode');
|
|
108
|
+
return null;
|
|
181
109
|
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
tarball: Buffer.from(json.tarball, 'base64'),
|
|
113
|
+
signature: json.signature,
|
|
114
|
+
publicKeyId: json.publicKeyId,
|
|
115
|
+
entitlement: json.entitlement ?? null,
|
|
116
|
+
version: json.version ?? version,
|
|
117
|
+
};
|
|
182
118
|
}
|
|
@@ -28,6 +28,7 @@ const BUILTIN_SHORTCODES = new Set([
|
|
|
28
28
|
'banner', 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
29
29
|
'reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle',
|
|
30
30
|
'ticker-tape', 'butterflies', 'strobe', 'animate', 'ambient', 'list-group', 'menu',
|
|
31
|
+
'demo',
|
|
31
32
|
]);
|
|
32
33
|
|
|
33
34
|
// Configure marked for safe output
|
|
@@ -97,12 +98,44 @@ function buildShortcodeFilter(attrs) {
|
|
|
97
98
|
* Load a block template by name from content/blocks/{name}.html.
|
|
98
99
|
* Throws if the file does not exist.
|
|
99
100
|
*
|
|
101
|
+
* The template's HTML comments are DROPPED here, before anything looks at it.
|
|
102
|
+
* Every block ships with a header comment documenting its parameters and
|
|
103
|
+
* showing an example call - documentation for whoever opens the file, which
|
|
104
|
+
* has no business on a public page and, worse, is still live markup by the
|
|
105
|
+
* time the shortcode processor sees it.
|
|
106
|
+
*
|
|
107
|
+
* `plugin-card` is what that costs: its example reads
|
|
108
|
+
* `[collection slug="plugins" display="block" block="plugin-card" …]` with no
|
|
109
|
+
* `/]`, so the processor took it for an unclosed tag and swallowed the rest of
|
|
110
|
+
* the render. Four plugin cards came out as one empty `<div>`, and the block's
|
|
111
|
+
* stylesheet came out as prose. The card that renders documentation for itself
|
|
112
|
+
* is not a rare shape - every bundled block has that comment - so this is
|
|
113
|
+
* fixed at the load point rather than in one template.
|
|
114
|
+
*
|
|
100
115
|
* @param {string} blockName
|
|
101
116
|
* @returns {Promise<string>}
|
|
102
117
|
*/
|
|
103
118
|
async function loadBlockTemplate(blockName) {
|
|
104
119
|
const safe = path.basename(blockName).replace(/[^a-z0-9-]/g, '');
|
|
105
|
-
|
|
120
|
+
const raw = await readFile(path.join(BLOCKS_DIR, `${safe}.html`), 'utf8');
|
|
121
|
+
return stripTemplateComments(raw);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove HTML comments from a block template.
|
|
126
|
+
*
|
|
127
|
+
* Unterminated (`<!--` with no `-->`) is treated as comment-to-end: a truncated
|
|
128
|
+
* comment leaking its remainder onto the page is the failure this exists to
|
|
129
|
+
* prevent, so the safe reading is that everything after it was meant to be one.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} tpl
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function stripTemplateComments(tpl) {
|
|
135
|
+
return String(tpl || '')
|
|
136
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
137
|
+
.replace(/<!--[\s\S]*$/, '')
|
|
138
|
+
.trim();
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
/**
|
|
@@ -258,9 +291,23 @@ function blockTemplateFields(tpl) {
|
|
|
258
291
|
return [...names].map(n => ({name: n, label: n}));
|
|
259
292
|
}
|
|
260
293
|
|
|
261
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Wrap a block's CSS in a `<style>` scoped to that block.
|
|
296
|
+
*
|
|
297
|
+
* BLANK LINES ARE REMOVED, and that is the whole of the second bug here. A
|
|
298
|
+
* block rendered inside another shortcode's wrapper - `[reveal]`, a `[col]`,
|
|
299
|
+
* anything - arrives mid-HTML-block as far as Markdown is concerned, and a
|
|
300
|
+
* blank line ENDS an HTML block: everything after the first one in the
|
|
301
|
+
* stylesheet came back wrapped in `<p>` and was printed on the page as text.
|
|
302
|
+
* Readable CSS is not worth a stylesheet in the body copy, so the blank lines
|
|
303
|
+
* go and the line breaks stay.
|
|
304
|
+
*/
|
|
305
|
+
export function buildBlockStyleTag(blockName, css) {
|
|
262
306
|
if (!css || !css.trim()) return '';
|
|
263
|
-
const body = String(css)
|
|
307
|
+
const body = String(css)
|
|
308
|
+
.replace(/<\/style/gi, '<\\/style')
|
|
309
|
+
.replace(/\n\s*\n+/g, '\n')
|
|
310
|
+
.trim();
|
|
264
311
|
return `<style>[data-block="${escapeAttr(blockName)}"] {\n${body}\n}</style>\n`;
|
|
265
312
|
}
|
|
266
313
|
|
|
@@ -1940,8 +1987,14 @@ export async function refreshComponentTagAllowlist() {
|
|
|
1940
1987
|
export function collectAndRewriteComponents(source) {
|
|
1941
1988
|
const used = new Set();
|
|
1942
1989
|
|
|
1990
|
+
// Scrub first: a `[component …/]` or a `<dm-x>` tag written inside a fenced
|
|
1991
|
+
// block is documentation showing the syntax, not a component to mount.
|
|
1992
|
+
// Rewriting it turns the example into the thing it was explaining, and
|
|
1993
|
+
// counting it pulls that component's script onto a page that never uses it.
|
|
1994
|
+
const {scrubbed, restore} = scrubCodeRegions(source);
|
|
1995
|
+
|
|
1943
1996
|
// Rewrite shortcode form → raw tag form.
|
|
1944
|
-
const rewritten =
|
|
1997
|
+
const rewritten = scrubbed.replace(
|
|
1945
1998
|
/\[component\s+([^\]]+?)\s*\/?\]/g,
|
|
1946
1999
|
(_, attrs) => {
|
|
1947
2000
|
const nameMatch = attrs.match(/name\s*=\s*["']([a-z][a-z0-9-]*)["']/);
|
|
@@ -1957,7 +2010,7 @@ export function collectAndRewriteComponents(source) {
|
|
|
1957
2010
|
used.add(m[1]);
|
|
1958
2011
|
}
|
|
1959
2012
|
|
|
1960
|
-
return {output: rewritten, used};
|
|
2013
|
+
return {output: restore(rewritten), used};
|
|
1961
2014
|
}
|
|
1962
2015
|
|
|
1963
2016
|
export function escapeAttr(str) {
|
|
@@ -3371,6 +3424,51 @@ function processIconBlocks(markdown) {
|
|
|
3371
3424
|
));
|
|
3372
3425
|
}
|
|
3373
3426
|
|
|
3427
|
+
// ─── [demo] - embed a local demo page ──────────────────────────────────────
|
|
3428
|
+
|
|
3429
|
+
/** Where [demo src="..."] looks for its pages, unless the tag says otherwise. */
|
|
3430
|
+
const DEMO_BASE_PATH = '/public/demos';
|
|
3431
|
+
const DEMO_DEFAULT_HEIGHT = '500px';
|
|
3432
|
+
|
|
3433
|
+
/**
|
|
3434
|
+
* [demo src="elements/accordion" /]
|
|
3435
|
+
* [demo src="effects/ambient" height="600px" title="Ambient Effect" base="/public/x" /]
|
|
3436
|
+
*
|
|
3437
|
+
* Was the `demo-viewer` plugin until 2026-09-22. It is a shortcode and nothing
|
|
3438
|
+
* else - no admin screen, no routes, no data - so it belongs here rather than
|
|
3439
|
+
* behind an install.
|
|
3440
|
+
*
|
|
3441
|
+
* The src is a SAME-ORIGIN path, which is why `allowIframeRelativeUrls` matters
|
|
3442
|
+
* in the sanitizer below. `base` exists because the plugin had a configurable
|
|
3443
|
+
* basePath; an attribute serves the one site that set it without inventing a
|
|
3444
|
+
* config key nobody else would ever touch.
|
|
3445
|
+
*
|
|
3446
|
+
* @param {string} markdown
|
|
3447
|
+
* @returns {string}
|
|
3448
|
+
*/
|
|
3449
|
+
function processDemoBlocks(markdown) {
|
|
3450
|
+
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
3451
|
+
return restore(scrubbed.replace(
|
|
3452
|
+
/\[demo([^\]]*?)\/\]/gi,
|
|
3453
|
+
(_, attrStr) => {
|
|
3454
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
3455
|
+
const src = typeof attrs.src === 'string' ? attrs.src.trim() : '';
|
|
3456
|
+
if (!src) return '';
|
|
3457
|
+
// A src is a path fragment under base, never a URL of its own: anything
|
|
3458
|
+
// carrying a scheme or a protocol-relative prefix would escape the site.
|
|
3459
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith('//')) return '';
|
|
3460
|
+
const base = (typeof attrs.base === 'string' && attrs.base.startsWith('/'))
|
|
3461
|
+
? attrs.base.replace(/\/+$/, '')
|
|
3462
|
+
: DEMO_BASE_PATH;
|
|
3463
|
+
const height = attrs.height || DEMO_DEFAULT_HEIGHT;
|
|
3464
|
+
const title = attrs.title || '';
|
|
3465
|
+
const iframeSrc = `${base}/${src.replace(/^\/+/, '')}.html`;
|
|
3466
|
+
const titleBar = title ? `<div class="demo-viewer-title">${escapeAttr(title)}</div>` : '';
|
|
3467
|
+
return `<div class="demo-viewer">${titleBar}<iframe src="${escapeAttr(iframeSrc)}" style="width:100%;height:${escapeAttr(height)};border:none;border-radius:var(--dm-radius, 8px);background:var(--dm-surface, #1a1a2e);" loading="lazy" allowfullscreen></iframe></div>`;
|
|
3468
|
+
}
|
|
3469
|
+
));
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3374
3472
|
// ─── [embed] - video / iframe embeds ───────────────────────────────────────
|
|
3375
3473
|
|
|
3376
3474
|
/**
|
|
@@ -3652,7 +3750,12 @@ async function processFormBlocks(markdown, tagSet) {
|
|
|
3652
3750
|
const raw = await readFile(filePath, 'utf8');
|
|
3653
3751
|
const form = JSON.parse(raw);
|
|
3654
3752
|
await expandReferenceFields(form);
|
|
3655
|
-
|
|
3753
|
+
// `actions` holds the notification email recipients and the webhook URL.
|
|
3754
|
+
// The embed is base64 in the page source, which is not a secret keeping
|
|
3755
|
+
// measure, so strip them exactly as GET /forms/:slug/public does - the
|
|
3756
|
+
// browser never reads them, the submit route resolves them server-side.
|
|
3757
|
+
const {actions: _actions, ...publicForm} = form;
|
|
3758
|
+
const encoded = Buffer.from(JSON.stringify(publicForm)).toString('base64');
|
|
3656
3759
|
const extraClass = attrs.class ? ` ${escapeAttr(attrs.class)}` : '';
|
|
3657
3760
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
3658
3761
|
replacement = `<div class="cms-form-embed${extraClass}" data-form-inline="${escapeAttr(encoded)}"${idAttr}></div>`;
|
|
@@ -3868,11 +3971,15 @@ function processSlideoverBlocks(markdown) {
|
|
|
3868
3971
|
* @returns {Promise<string>}
|
|
3869
3972
|
*/
|
|
3870
3973
|
async function processMenuBlocks(markdown, user, ctx = {}) {
|
|
3974
|
+
// Scrub first, or a `[menu …/]` written inside a fenced block as an example
|
|
3975
|
+
// of the syntax is rendered into a real nav and then escaped into the code
|
|
3976
|
+
// block - the docs page shows the output instead of the shortcode.
|
|
3977
|
+
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
3871
3978
|
const re = /\[menu(\s+[^\]]*?)?\s*\/\]/gi;
|
|
3872
|
-
const matches = [...
|
|
3979
|
+
const matches = [...scrubbed.matchAll(re)];
|
|
3873
3980
|
if (!matches.length) return markdown;
|
|
3874
3981
|
|
|
3875
|
-
let out =
|
|
3982
|
+
let out = scrubbed;
|
|
3876
3983
|
for (const m of matches) {
|
|
3877
3984
|
const attrs = parseShortcodeAttrs(m[1] || '');
|
|
3878
3985
|
// A bare `slug` / `location` with no value parses as the boolean `true`
|
|
@@ -3925,7 +4032,7 @@ async function processMenuBlocks(markdown, user, ctx = {}) {
|
|
|
3925
4032
|
});
|
|
3926
4033
|
out = out.replace(m[0], html);
|
|
3927
4034
|
}
|
|
3928
|
-
return out;
|
|
4035
|
+
return restore(out);
|
|
3929
4036
|
}
|
|
3930
4037
|
|
|
3931
4038
|
/** Visible diagnostic for an unusable [menu] shortcode - mirrors [form]'s. */
|
|
@@ -4074,7 +4181,8 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4074
4181
|
const withSpacer = processSpacerBlocks(withListGroup);
|
|
4075
4182
|
const withCenter = processCenterBlocks(withSpacer);
|
|
4076
4183
|
const withIcon = processIconBlocks(withCenter);
|
|
4077
|
-
const
|
|
4184
|
+
const withDemo = processDemoBlocks(withIcon);
|
|
4185
|
+
const withEmbed = processEmbedBlocks(withDemo);
|
|
4078
4186
|
const withForm = await processFormBlocks(withEmbed, tagSet);
|
|
4079
4187
|
const withHero = processHeroBlocks(withForm);
|
|
4080
4188
|
const withTable = processTableBlocks(withHero);
|
|
@@ -4102,6 +4210,12 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4102
4210
|
'player.vimeo.com',
|
|
4103
4211
|
...getEmbedAllowedHosts(),
|
|
4104
4212
|
],
|
|
4213
|
+
// sanitize-html drops a RELATIVE iframe src whenever allowedIframeHostnames
|
|
4214
|
+
// is set, because it cannot match a hostname it has not been given. That
|
|
4215
|
+
// silently emptied every [demo] iframe. A relative src is same-origin by
|
|
4216
|
+
// definition, so the hostname gate has nothing to say about it: the gate
|
|
4217
|
+
// exists to stop embedding somebody else's site, not your own page.
|
|
4218
|
+
allowIframeRelativeUrls: true,
|
|
4105
4219
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
|
|
4106
4220
|
'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
4107
4221
|
'form', 'input', 'textarea', 'select', 'option', 'optgroup',
|
|
@@ -87,6 +87,19 @@ export const REGISTRY = [
|
|
|
87
87
|
{key: 'delete', label: 'Delete', description: 'Delete menus'}
|
|
88
88
|
]
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
key: 'context-menus',
|
|
92
|
+
label: 'Context Menus',
|
|
93
|
+
description: 'Author right-click menus and the regions they bind to.',
|
|
94
|
+
icon: 'menu',
|
|
95
|
+
group: 'Structure',
|
|
96
|
+
actions: [
|
|
97
|
+
{key: 'read', label: 'View', description: 'View context menus'},
|
|
98
|
+
{key: 'create', label: 'Create', description: 'Create new context menus'},
|
|
99
|
+
{key: 'update', label: 'Edit', description: 'Edit context menus'},
|
|
100
|
+
{key: 'delete', label: 'Delete', description: 'Delete context menus'}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
90
103
|
{
|
|
91
104
|
key: 'projects',
|
|
92
105
|
label: 'Projects',
|
|
@@ -165,6 +178,17 @@ export const REGISTRY = [
|
|
|
165
178
|
{key: 'delete', label: 'Delete', description: 'Delete users'}
|
|
166
179
|
]
|
|
167
180
|
},
|
|
181
|
+
{
|
|
182
|
+
key: 'theme',
|
|
183
|
+
label: 'Theme',
|
|
184
|
+
description: 'Choose the site and admin themes, fonts, and per-element theme overrides.',
|
|
185
|
+
icon: 'droplet',
|
|
186
|
+
group: 'Configuration',
|
|
187
|
+
actions: [
|
|
188
|
+
{key: 'read', label: 'View', description: 'View theme settings and overrides'},
|
|
189
|
+
{key: 'update', label: 'Edit', description: 'Change themes, fonts and overrides'}
|
|
190
|
+
]
|
|
191
|
+
},
|
|
168
192
|
{
|
|
169
193
|
key: 'settings',
|
|
170
194
|
label: 'Settings',
|
|
@@ -178,6 +202,56 @@ export const REGISTRY = [
|
|
|
178
202
|
{key: 'delete', label: 'Delete', description: 'Remove settings'}
|
|
179
203
|
]
|
|
180
204
|
},
|
|
205
|
+
{
|
|
206
|
+
key: 'contacts',
|
|
207
|
+
label: 'Contacts',
|
|
208
|
+
description: 'Address book with groups, favourites and import/export. Each user sees only their own.',
|
|
209
|
+
icon: 'users',
|
|
210
|
+
group: 'Tools',
|
|
211
|
+
actions: [
|
|
212
|
+
{key: 'read', label: 'View', description: 'View own contacts'},
|
|
213
|
+
{key: 'create', label: 'Create', description: 'Add contacts and groups'},
|
|
214
|
+
{key: 'update', label: 'Edit', description: 'Edit own contacts'},
|
|
215
|
+
{key: 'delete', label: 'Delete', description: 'Delete own contacts'}
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
key: 'analytics',
|
|
220
|
+
label: 'Analytics',
|
|
221
|
+
description: 'Page-view analytics: trends, top pages, journeys and export.',
|
|
222
|
+
icon: 'chart-bar',
|
|
223
|
+
group: 'Tools',
|
|
224
|
+
actions: [
|
|
225
|
+
{key: 'read', label: 'View', description: 'View analytics'},
|
|
226
|
+
{key: 'delete', label: 'Reset', description: 'Reset all counters'}
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: 'notes',
|
|
231
|
+
label: 'Notes',
|
|
232
|
+
description: 'Personal notes. Each user sees only their own.',
|
|
233
|
+
icon: 'edit-3',
|
|
234
|
+
group: 'Tools',
|
|
235
|
+
actions: [
|
|
236
|
+
{key: 'read', label: 'View', description: 'View own notes'},
|
|
237
|
+
{key: 'create', label: 'Create', description: 'Write a note'},
|
|
238
|
+
{key: 'update', label: 'Edit', description: 'Edit own notes'},
|
|
239
|
+
{key: 'delete', label: 'Delete', description: 'Delete own notes'}
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: 'todo',
|
|
244
|
+
label: 'Todo',
|
|
245
|
+
description: 'Personal task list. Each user sees only their own.',
|
|
246
|
+
icon: 'check-square',
|
|
247
|
+
group: 'Tools',
|
|
248
|
+
actions: [
|
|
249
|
+
{key: 'read', label: 'View', description: 'View own tasks'},
|
|
250
|
+
{key: 'create', label: 'Create', description: 'Add a task'},
|
|
251
|
+
{key: 'update', label: 'Edit', description: 'Edit own tasks'},
|
|
252
|
+
{key: 'delete', label: 'Delete', description: 'Delete own tasks'}
|
|
253
|
+
]
|
|
254
|
+
},
|
|
181
255
|
{
|
|
182
256
|
key: 'plugins',
|
|
183
257
|
label: 'Plugins',
|