domma-cms 0.54.2 → 0.66.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 +151 -77
- package/README.md +4 -4
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +3 -3
- 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/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/menu-editor.html +19 -25
- package/admin/js/templates/plugin-code.html +1 -1
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +27 -97
- package/admin/js/templates/theme.html +173 -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/menu-editor.js +13 -13
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +25 -23
- 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/bin/cli.js +11 -2
- package/bin/lib/smtp-defaults.js +53 -0
- package/bin/update.js +13 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/theme.json +18 -0
- package/package.json +12 -4
- 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 +4453 -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 +1002 -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/analytics/admin/css/index.css +1 -0
- package/plugins/analytics/admin/templates/analytics.html +22 -13
- package/plugins/analytics/plugin.json +3 -0
- package/plugins/blog/admin/css/index.css +1 -0
- package/plugins/blog/admin/templates/blog.html +30 -18
- package/plugins/blog/admin/templates/categories.html +2 -2
- package/plugins/blog/admin/templates/comments.html +2 -2
- package/plugins/blog/admin/templates/post-editor.html +34 -34
- package/plugins/blog/admin/templates/settings.html +6 -3
- package/plugins/blog/admin/views/blog.js +8 -5
- package/plugins/blog/admin/views/categories.js +5 -10
- package/plugins/blog/admin/views/comments.js +5 -5
- package/plugins/blog/admin/views/post-editor.js +39 -20
- package/plugins/blog/admin/views/settings.js +52 -50
- 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 +14 -13
- package/plugins/blog/plugin.js +36 -13
- package/plugins/blog/plugin.json +13 -5
- package/plugins/blog/plugin.public.js +312 -0
- package/plugins/contacts/admin/css/index.css +1 -0
- package/plugins/contacts/admin/templates/contacts.html +128 -0
- package/plugins/contacts/admin/views/contacts.js +237 -4
- package/plugins/contacts/collections/user-contacts/schema.json +108 -0
- package/plugins/contacts/plugin.js +214 -27
- package/plugins/contacts/plugin.json +4 -1
- package/plugins/invoice/admin/css/index.css +1 -0
- package/plugins/invoice/admin/templates/editor.html +140 -49
- package/plugins/invoice/admin/templates/index.html +153 -23
- package/plugins/invoice/admin/templates/issuers.html +2 -5
- package/plugins/invoice/admin/templates/receivers.html +2 -5
- package/plugins/invoice/admin/views/contacts-source.js +266 -0
- package/plugins/invoice/admin/views/editor.js +366 -199
- package/plugins/invoice/admin/views/export.js +199 -0
- package/plugins/invoice/admin/views/help-content.js +61 -0
- package/plugins/invoice/admin/views/index.js +582 -94
- package/plugins/invoice/admin/views/issuers.js +24 -17
- package/plugins/invoice/admin/views/media.js +172 -0
- package/plugins/invoice/admin/views/party-view.js +305 -67
- package/plugins/invoice/admin/views/payments.js +127 -0
- package/plugins/invoice/admin/views/print.js +130 -0
- package/plugins/invoice/admin/views/receivers.js +49 -16
- package/plugins/invoice/admin/views/send.js +212 -0
- package/plugins/invoice/admin/views/settings.js +594 -0
- package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
- package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
- package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
- package/plugins/invoice/collections/invoices/schema.json +19 -13
- package/plugins/invoice/config.js +27 -6
- package/plugins/invoice/pdf.js +164 -0
- package/plugins/invoice/plugin.js +1217 -44
- package/plugins/invoice/plugin.json +10 -9
- package/plugins/invoice/templates/_base.css +1 -0
- package/plugins/invoice/templates/classic-nologo.html +100 -0
- package/plugins/invoice/templates/classic.html +91 -0
- package/plugins/invoice/templates/invoice-print.html +24 -0
- package/plugins/invoice/templates/minimal.html +99 -0
- package/plugins/invoice/templates/modern-nologo.html +114 -0
- package/plugins/invoice/templates/modern.html +113 -0
- package/plugins/invoice/templates/templates.json +11 -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 +33 -0
- package/plugins/notes/admin/views/notes.js +1 -1
- package/plugins/notes/plugin.json +2 -2
- package/plugins/surveys/lib/audience.js +37 -0
- package/plugins/surveys/lib/campaigns.js +43 -0
- package/plugins/surveys/lib/ledger.js +110 -0
- package/plugins/surveys/lib/sending.js +106 -0
- package/plugins/surveys/lib/stats.js +62 -0
- package/plugins/surveys/lib/submit.js +95 -0
- package/plugins/surveys/lib/tokens.js +28 -0
- package/plugins/surveys/plugin.public.js +149 -0
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
- package/public/css/forms.css +1 -1
- package/public/css/menu-highlight.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- 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/menu-decor.mjs +1 -1
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +37 -3
- package/scripts/copy-domma.js +48 -0
- package/scripts/seed.js +1996 -0
- package/scripts/setup.js +8 -0
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/forms.js +42 -3
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugins.js +50 -6
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +74 -0
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/email.js +29 -3
- package/server/services/health.js +23 -2
- package/server/services/markdown.js +70 -9
- package/server/services/menuRender.js +28 -4
- package/server/services/menus.js +25 -1
- package/server/services/permissionRegistry.js +24 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/plugins.js +229 -6
- package/server/services/renderer.js +148 -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/themeSettings.js +541 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/notes/data/notes.json +0 -1
- 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/todo/data/todos.json +0 -1
|
@@ -18,7 +18,14 @@ export const postEditorView = {
|
|
|
18
18
|
templateUrl: '/plugins/blog/admin/templates/post-editor.html',
|
|
19
19
|
|
|
20
20
|
async onMount($container) {
|
|
21
|
-
|
|
21
|
+
// Domma's router hands onMount ONE argument and never the route params,
|
|
22
|
+
// and `R.current` is a METHOD - `R.current?.params` is undefined on the
|
|
23
|
+
// function object, so this view opened blank on every edit and Save
|
|
24
|
+
// created a duplicate. Read the slug off the hash, as every core editor
|
|
25
|
+
// view does.
|
|
26
|
+
const routeMatch = location.hash.match(/^#\/plugins\/blog\/posts\/([^?#]+)/);
|
|
27
|
+
const routeId = routeMatch ? decodeURIComponent(routeMatch[1]) : null;
|
|
28
|
+
const id = routeId && routeId !== 'new' ? routeId : null;
|
|
22
29
|
const isEdit = Boolean(id);
|
|
23
30
|
|
|
24
31
|
let post = null;
|
|
@@ -40,7 +47,7 @@ export const postEditorView = {
|
|
|
40
47
|
post = res.data ?? res;
|
|
41
48
|
} catch {
|
|
42
49
|
E.toast('Failed to load post.', { type: 'error' });
|
|
43
|
-
R.navigate('#/plugins/blog');
|
|
50
|
+
R.navigate('#/plugins/blog/posts');
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
}
|
|
@@ -87,11 +94,11 @@ export const postEditorView = {
|
|
|
87
94
|
const img = document.createElement('img');
|
|
88
95
|
img.src = featuredImageUrl;
|
|
89
96
|
img.alt = 'Featured image';
|
|
90
|
-
img.
|
|
97
|
+
img.className = 'blog-image-preview';
|
|
91
98
|
imagePreviewEl.appendChild(img);
|
|
92
|
-
if (clearImageBtn) clearImageBtn.
|
|
99
|
+
if (clearImageBtn) clearImageBtn.classList.remove('blog-hidden');
|
|
93
100
|
} else {
|
|
94
|
-
if (clearImageBtn) clearImageBtn.
|
|
101
|
+
if (clearImageBtn) clearImageBtn.classList.add('blog-hidden');
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -99,14 +106,13 @@ export const postEditorView = {
|
|
|
99
106
|
while (catsListEl.firstChild) catsListEl.removeChild(catsListEl.firstChild);
|
|
100
107
|
if (categories.length === 0) {
|
|
101
108
|
const p = document.createElement('p');
|
|
102
|
-
p.className = 'text-muted
|
|
109
|
+
p.className = 'text-muted text-sm';
|
|
103
110
|
p.textContent = 'No categories yet.';
|
|
104
111
|
catsListEl.appendChild(p);
|
|
105
112
|
} else {
|
|
106
113
|
categories.forEach((cat) => {
|
|
107
114
|
const label = document.createElement('label');
|
|
108
|
-
label.className = '
|
|
109
|
-
label.style.cursor = 'pointer';
|
|
115
|
+
label.className = 'flex items-center gap-2 mb-2 blog-clickable';
|
|
110
116
|
|
|
111
117
|
const cb = document.createElement('input');
|
|
112
118
|
cb.type = 'checkbox';
|
|
@@ -175,9 +181,9 @@ export const postEditorView = {
|
|
|
175
181
|
if (slugAutoEl) {
|
|
176
182
|
slugAutoEl.addEventListener('change', () => {
|
|
177
183
|
if (!slugAutoEl.checked && isEdit && post?.status === 'published') {
|
|
178
|
-
if (slugWarnEl) slugWarnEl.
|
|
184
|
+
if (slugWarnEl) slugWarnEl.classList.remove('blog-hidden');
|
|
179
185
|
} else {
|
|
180
|
-
if (slugWarnEl) slugWarnEl.
|
|
186
|
+
if (slugWarnEl) slugWarnEl.classList.add('blog-hidden');
|
|
181
187
|
}
|
|
182
188
|
if (slugAutoEl.checked && postTitleEl && postSlugEl) {
|
|
183
189
|
postSlugEl.value = slugify(postTitleEl.value);
|
|
@@ -189,7 +195,7 @@ export const postEditorView = {
|
|
|
189
195
|
const seoPanel = $container.find('#seo-panel').get(0);
|
|
190
196
|
if (seoToggle && seoPanel) {
|
|
191
197
|
seoToggle.addEventListener('click', () => {
|
|
192
|
-
seoPanel.
|
|
198
|
+
seoPanel.classList.toggle('blog-hidden');
|
|
193
199
|
});
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -223,15 +229,16 @@ export const postEditorView = {
|
|
|
223
229
|
|
|
224
230
|
const scheduleBtn = $container.find('#btn-schedule').get(0);
|
|
225
231
|
if (scheduleBtn) {
|
|
226
|
-
scheduleBtn.
|
|
232
|
+
scheduleBtn.classList.remove('blog-hidden');
|
|
227
233
|
scheduleBtn.addEventListener('click', async () => {
|
|
228
|
-
|
|
229
|
-
if (scheduleEl
|
|
234
|
+
// First click reveals the picker; the second one schedules.
|
|
235
|
+
if (scheduleEl) scheduleEl.classList.toggle('blog-hidden');
|
|
236
|
+
if (scheduleEl && !scheduleEl.classList.contains('blog-hidden')) return;
|
|
230
237
|
|
|
231
238
|
const publishedAt = schedAtEl?.value;
|
|
232
239
|
if (!publishedAt) {
|
|
233
240
|
E.toast('Please select a scheduled date/time.', { type: 'error' });
|
|
234
|
-
if (scheduleEl) scheduleEl.
|
|
241
|
+
if (scheduleEl) scheduleEl.classList.remove('blog-hidden');
|
|
235
242
|
return;
|
|
236
243
|
}
|
|
237
244
|
|
|
@@ -271,7 +278,7 @@ export const postEditorView = {
|
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
const grid = document.createElement('div');
|
|
274
|
-
grid.
|
|
281
|
+
grid.className = 'blog-media-grid';
|
|
275
282
|
|
|
276
283
|
if (!media.length) {
|
|
277
284
|
const p = document.createElement('p');
|
|
@@ -282,14 +289,14 @@ export const postEditorView = {
|
|
|
282
289
|
media.forEach((m) => {
|
|
283
290
|
const tile = document.createElement('button');
|
|
284
291
|
tile.type = 'button';
|
|
285
|
-
tile.
|
|
292
|
+
tile.className = 'blog-media-tile';
|
|
286
293
|
const img = document.createElement('img');
|
|
287
294
|
img.src = m.url;
|
|
288
295
|
img.alt = m.name;
|
|
289
|
-
|
|
296
|
+
|
|
290
297
|
const cap = document.createElement('span');
|
|
291
298
|
cap.textContent = m.name;
|
|
292
|
-
|
|
299
|
+
|
|
293
300
|
tile.appendChild(img);
|
|
294
301
|
tile.appendChild(cap);
|
|
295
302
|
tile.addEventListener('click', () => {
|
|
@@ -301,7 +308,19 @@ export const postEditorView = {
|
|
|
301
308
|
});
|
|
302
309
|
}
|
|
303
310
|
|
|
304
|
-
|
|
311
|
+
// close() only hides the panel and destroy() tears the component down
|
|
312
|
+
// without detaching its element, so every trip to the media library
|
|
313
|
+
// used to leave another slideover in the document.
|
|
314
|
+
const slideover = E.slideover({
|
|
315
|
+
title: 'Pick Featured Image',
|
|
316
|
+
size: 'lg',
|
|
317
|
+
position: 'right',
|
|
318
|
+
content: grid,
|
|
319
|
+
onClose: () => setTimeout(() => {
|
|
320
|
+
slideover.destroy();
|
|
321
|
+
slideover.element?.remove();
|
|
322
|
+
}, 350)
|
|
323
|
+
});
|
|
305
324
|
slideover.open();
|
|
306
325
|
}
|
|
307
326
|
|
|
@@ -37,61 +37,63 @@ export const settingsView = {
|
|
|
37
37
|
const formEl = $container.find('#settings-form').get(0);
|
|
38
38
|
if (!formEl) return;
|
|
39
39
|
|
|
40
|
-
F.create
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
// F.create takes a SCHEMA, initial data and options, and renders with
|
|
41
|
+
// .renderTo() - it does not take a container. Passing the element as the
|
|
42
|
+
// first argument made it the schema, so this form rendered nothing at
|
|
43
|
+
// all. See admin/js/views/plugins.js for the house pattern.
|
|
44
|
+
const form = F.create(
|
|
45
|
+
{
|
|
46
|
+
basePath: {
|
|
47
|
+
type: 'string', label: 'Base path', default: '/blog',
|
|
48
|
+
// placeholder and hint live under formConfig - Forma reads
|
|
49
|
+
// fieldDef.formConfig, and a top-level `placeholder` or
|
|
50
|
+
// `help` is silently ignored.
|
|
51
|
+
formConfig: {placeholder: '/blog', hint: 'URL prefix for all blog pages (e.g. /blog).'}
|
|
49
52
|
},
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
type: 'number',
|
|
54
|
-
placeholder: '10',
|
|
55
|
-
value: settings.postsPerPage ?? 10,
|
|
56
|
-
hint: 'Number of posts shown per listing page.'
|
|
53
|
+
postsPerPage: {
|
|
54
|
+
type: 'number', label: 'Posts per page', default: 10,
|
|
55
|
+
formConfig: {placeholder: '10', hint: 'Number of posts shown per listing page.'}
|
|
57
56
|
},
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
type: 'toggle',
|
|
62
|
-
value: settings.commentsEnabled ?? true,
|
|
63
|
-
hint: 'Allow readers to submit comments on posts.'
|
|
57
|
+
commentsEnabled: {
|
|
58
|
+
type: 'boolean', label: 'Enable comments',
|
|
59
|
+
formConfig: {hint: 'Allow readers to submit comments on posts.'}
|
|
64
60
|
},
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
type: 'toggle',
|
|
69
|
-
value: settings.commentModeration ?? true,
|
|
70
|
-
hint: 'Hold new comments for approval before they appear.'
|
|
61
|
+
commentModeration: {
|
|
62
|
+
type: 'boolean', label: 'Moderate comments',
|
|
63
|
+
formConfig: {hint: 'Hold new comments for approval before they appear.'}
|
|
71
64
|
},
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
type: 'number',
|
|
76
|
-
placeholder: '20',
|
|
77
|
-
value: settings.feedLength ?? 20,
|
|
78
|
-
hint: 'Maximum number of items in the RSS/Atom feed.'
|
|
79
|
-
}
|
|
80
|
-
],
|
|
81
|
-
onSubmit: async (values) => {
|
|
82
|
-
try {
|
|
83
|
-
const res = await H.put(`${BASE}/settings`, {
|
|
84
|
-
basePath: values.basePath,
|
|
85
|
-
postsPerPage: Number(values.postsPerPage) || 10,
|
|
86
|
-
commentsEnabled: Boolean(values.commentsEnabled),
|
|
87
|
-
commentModeration: Boolean(values.commentModeration),
|
|
88
|
-
feedLength: Number(values.feedLength) || 20
|
|
89
|
-
});
|
|
90
|
-
if (res.error) throw new Error(res.error);
|
|
91
|
-
E.toast('Settings saved.', { type: 'success' });
|
|
92
|
-
} catch (err) {
|
|
93
|
-
E.toast(err?.message ?? 'Failed to save settings.', { type: 'error' });
|
|
65
|
+
feedLength: {
|
|
66
|
+
type: 'number', label: 'RSS feed length', default: 20,
|
|
67
|
+
formConfig: {placeholder: '20', hint: 'Maximum number of items in the RSS/Atom feed.'}
|
|
94
68
|
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
basePath: settings.basePath ?? '/blog',
|
|
72
|
+
postsPerPage: settings.postsPerPage ?? 10,
|
|
73
|
+
commentsEnabled: settings.commentsEnabled ?? true,
|
|
74
|
+
commentModeration: settings.commentModeration ?? true,
|
|
75
|
+
feedLength: settings.feedLength ?? 20
|
|
76
|
+
},
|
|
77
|
+
{ showSubmitButton: false }
|
|
78
|
+
);
|
|
79
|
+
form.renderTo(formEl);
|
|
80
|
+
|
|
81
|
+
const saveBtn = $container.find('#btn-save-settings').get(0);
|
|
82
|
+
saveBtn?.addEventListener('click', async () => {
|
|
83
|
+
if (!form.validate()) return;
|
|
84
|
+
const values = form.getData();
|
|
85
|
+
try {
|
|
86
|
+
const res = await H.put(`${BASE}/settings`, {
|
|
87
|
+
basePath: values.basePath,
|
|
88
|
+
postsPerPage: Number(values.postsPerPage) || 10,
|
|
89
|
+
commentsEnabled: Boolean(values.commentsEnabled),
|
|
90
|
+
commentModeration: Boolean(values.commentModeration),
|
|
91
|
+
feedLength: Number(values.feedLength) || 20
|
|
92
|
+
});
|
|
93
|
+
if (res.error) throw new Error(res.error);
|
|
94
|
+
E.toast('Settings saved.', { type: 'success' });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
E.toast(err?.message ?? 'Failed to save settings.', { type: 'error' });
|
|
95
97
|
}
|
|
96
98
|
});
|
|
97
99
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "blog-categories",
|
|
3
|
-
"label": "Blog Categories",
|
|
4
2
|
"slug": "blog-categories",
|
|
3
|
+
"title": "Blog Categories",
|
|
4
|
+
"description": "Post categories managed by the Blog plugin.",
|
|
5
|
+
"plugin": "blog",
|
|
5
6
|
"storage": { "adapter": "file" },
|
|
6
7
|
"fields": [
|
|
7
|
-
{ "
|
|
8
|
-
{ "
|
|
9
|
-
{ "
|
|
10
|
-
{ "
|
|
8
|
+
{ "name": "name", "label": "Name", "type": "text", "required": true },
|
|
9
|
+
{ "name": "slug", "label": "Slug", "type": "text", "required": true },
|
|
10
|
+
{ "name": "description", "label": "Description", "type": "textarea", "required": false },
|
|
11
|
+
{ "name": "parentId", "label": "Parent", "type": "text", "required": false }
|
|
11
12
|
]
|
|
12
13
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "blog-comments",
|
|
3
|
-
"label": "Blog Comments",
|
|
4
2
|
"slug": "blog-comments",
|
|
3
|
+
"title": "Blog Comments",
|
|
4
|
+
"description": "Post comments managed by the Blog plugin.",
|
|
5
|
+
"plugin": "blog",
|
|
5
6
|
"storage": { "adapter": "file" },
|
|
6
7
|
"fields": [
|
|
7
|
-
{ "
|
|
8
|
-
{ "
|
|
9
|
-
{ "
|
|
10
|
-
{ "
|
|
11
|
-
{ "
|
|
12
|
-
{ "
|
|
13
|
-
{ "
|
|
14
|
-
{ "
|
|
8
|
+
{ "name": "postId", "label": "Post", "type": "text", "required": true },
|
|
9
|
+
{ "name": "parentId", "label": "Parent", "type": "text", "required": false },
|
|
10
|
+
{ "name": "authorName", "label": "Author Name", "type": "text", "required": true },
|
|
11
|
+
{ "name": "authorEmail", "label": "Author Email", "type": "text", "required": true },
|
|
12
|
+
{ "name": "authorUrl", "label": "Author URL", "type": "text", "required": false },
|
|
13
|
+
{ "name": "body", "label": "Body", "type": "textarea", "required": true },
|
|
14
|
+
{ "name": "status", "label": "Status", "type": "select", "required": true, "options": ["pending", "approved", "rejected", "spam"] },
|
|
15
|
+
{ "name": "createdAt", "label": "Created At", "type": "datetime", "required": false }
|
|
15
16
|
]
|
|
16
17
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "blog-posts",
|
|
3
|
-
"label": "Blog Posts",
|
|
4
2
|
"slug": "blog-posts",
|
|
3
|
+
"title": "Blog Posts",
|
|
4
|
+
"description": "Posts managed by the Blog plugin.",
|
|
5
|
+
"plugin": "blog",
|
|
5
6
|
"storage": { "adapter": "file" },
|
|
6
7
|
"fields": [
|
|
7
|
-
{ "
|
|
8
|
-
{ "
|
|
9
|
-
{ "
|
|
10
|
-
{ "
|
|
11
|
-
{ "
|
|
12
|
-
{ "
|
|
13
|
-
{ "
|
|
14
|
-
{ "
|
|
15
|
-
{ "
|
|
16
|
-
{ "
|
|
17
|
-
{ "
|
|
8
|
+
{ "name": "title", "label": "Title", "type": "text", "required": true },
|
|
9
|
+
{ "name": "slug", "label": "Slug", "type": "text", "required": true },
|
|
10
|
+
{ "name": "excerpt", "label": "Excerpt", "type": "textarea", "required": false },
|
|
11
|
+
{ "name": "content", "label": "Content", "type": "markdown", "required": false },
|
|
12
|
+
{ "name": "featuredImage", "label": "Featured Image", "type": "text", "required": false },
|
|
13
|
+
{ "name": "categories", "label": "Categories", "type": "array", "required": false },
|
|
14
|
+
{ "name": "tags", "label": "Tags", "type": "array", "required": false },
|
|
15
|
+
{ "name": "authorId", "label": "Author", "type": "text", "required": true },
|
|
16
|
+
{ "name": "status", "label": "Status", "type": "select", "required": true, "options": ["draft", "scheduled", "published"] },
|
|
17
|
+
{ "name": "publishedAt", "label": "Published At", "type": "datetime", "required": false },
|
|
18
|
+
{ "name": "seo", "label": "SEO", "type": "object", "required": false }
|
|
18
19
|
]
|
|
19
20
|
}
|
package/plugins/blog/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import defaultConfig from './config.js';
|
|
2
2
|
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
3
|
-
import {
|
|
3
|
+
import {getPluginStates, savePluginState} from '../../server/services/plugins.js';
|
|
4
4
|
|
|
5
5
|
const POSTS_SLUG = 'blog-posts';
|
|
6
6
|
const CATEGORIES_SLUG = 'blog-categories';
|
|
@@ -217,12 +217,19 @@ export default async function blogPlugin(fastify, options) {
|
|
|
217
217
|
categories, tags, status, slug
|
|
218
218
|
} = body;
|
|
219
219
|
|
|
220
|
+
// The schema marks title, slug, authorId and status required, so an
|
|
221
|
+
// empty title reaches createEntry and throws - a 500 where the
|
|
222
|
+
// categories and comments routes both answer 400. Say so here instead.
|
|
223
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
224
|
+
return reply.code(400).send({ error: 'title is required' });
|
|
225
|
+
}
|
|
226
|
+
|
|
220
227
|
const derivedSlug = slug
|
|
221
228
|
? String(slug).trim()
|
|
222
|
-
: slugify(title
|
|
229
|
+
: slugify(title);
|
|
223
230
|
|
|
224
231
|
const entry = await createEntry(POSTS_SLUG, {
|
|
225
|
-
title:
|
|
232
|
+
title: title.trim(),
|
|
226
233
|
slug: derivedSlug,
|
|
227
234
|
content: typeof content === 'string' ? content : '',
|
|
228
235
|
excerpt: typeof excerpt === 'string' ? excerpt.trim() : '',
|
|
@@ -465,21 +472,37 @@ export default async function blogPlugin(fastify, options) {
|
|
|
465
472
|
// Settings
|
|
466
473
|
// -------------------------------------------------------------------------
|
|
467
474
|
|
|
475
|
+
// Plugin settings live in config/plugins.json under blog.settings, and the
|
|
476
|
+
// way to reach them is getPluginStates() / savePluginState(). These used to
|
|
477
|
+
// call getConfig() and saveConfig() from server/config.js, which take a FILE
|
|
478
|
+
// NAME: getConfig() with no argument read config/undefined.json and threw,
|
|
479
|
+
// so this endpoint answered 500 every time, and saveConfig(cfg) passed the
|
|
480
|
+
// whole config object as the name.
|
|
481
|
+
|
|
468
482
|
/** GET /api/plugins/blog/settings */
|
|
469
483
|
fastify.get('/settings', { preHandler: [authenticate] }, async (request, reply) => {
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
return reply.send({ ...defaultConfig, ...overrides });
|
|
484
|
+
const stored = getPluginStates()?.blog?.settings ?? {};
|
|
485
|
+
return reply.send({ ...defaultConfig, ...stored });
|
|
473
486
|
});
|
|
474
487
|
|
|
475
488
|
/** PUT /api/plugins/blog/settings */
|
|
476
|
-
|
|
489
|
+
// requireAdmin reads request.user, which only `authenticate` populates -
|
|
490
|
+
// on its own it answers 401 to everyone, admin included.
|
|
491
|
+
fastify.put('/settings', { preHandler: [authenticate, requireAdmin] }, async (request, reply) => {
|
|
477
492
|
const body = request.body ?? {};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
493
|
+
|
|
494
|
+
// Whitelist rather than merge: every key here is read back by the
|
|
495
|
+
// public routes, and storing whatever was posted invites surprises.
|
|
496
|
+
const merged = {
|
|
497
|
+
...defaultConfig,
|
|
498
|
+
...(body.basePath !== undefined && { basePath: String(body.basePath) }),
|
|
499
|
+
...(body.postsPerPage !== undefined && { postsPerPage: Number(body.postsPerPage) || defaultConfig.postsPerPage }),
|
|
500
|
+
...(body.commentsEnabled !== undefined && { commentsEnabled: Boolean(body.commentsEnabled) }),
|
|
501
|
+
...(body.commentModeration !== undefined && { commentModeration: Boolean(body.commentModeration) }),
|
|
502
|
+
...(body.feedLength !== undefined && { feedLength: Number(body.feedLength) || defaultConfig.feedLength })
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
savePluginState('blog', { settings: merged });
|
|
506
|
+
return reply.send({ ok: true, settings: merged });
|
|
484
507
|
});
|
|
485
508
|
}
|
package/plugins/blog/plugin.json
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
"date": "2026-04-07",
|
|
8
8
|
"icon": "file-text",
|
|
9
9
|
"admin": {
|
|
10
|
+
"css": [
|
|
11
|
+
"admin/css/index.css"
|
|
12
|
+
],
|
|
10
13
|
"sidebar": [
|
|
11
14
|
{
|
|
12
15
|
"id": "blog",
|
|
@@ -39,6 +42,11 @@
|
|
|
39
42
|
}
|
|
40
43
|
],
|
|
41
44
|
"routes": [
|
|
45
|
+
{
|
|
46
|
+
"path": "/plugins/blog",
|
|
47
|
+
"view": "plugin-blog-posts",
|
|
48
|
+
"title": "Blog - Domma CMS"
|
|
49
|
+
},
|
|
42
50
|
{
|
|
43
51
|
"path": "/plugins/blog/posts",
|
|
44
52
|
"view": "plugin-blog-posts",
|
|
@@ -72,23 +80,23 @@
|
|
|
72
80
|
],
|
|
73
81
|
"views": {
|
|
74
82
|
"plugin-blog-posts": {
|
|
75
|
-
"entry": "blog/admin/views/blog.js?v=
|
|
83
|
+
"entry": "blog/admin/views/blog.js?v=01f8b54e",
|
|
76
84
|
"exportName": "blogView"
|
|
77
85
|
},
|
|
78
86
|
"plugin-blog-post-editor": {
|
|
79
|
-
"entry": "blog/admin/views/post-editor.js?v=
|
|
87
|
+
"entry": "blog/admin/views/post-editor.js?v=58c616ec",
|
|
80
88
|
"exportName": "postEditorView"
|
|
81
89
|
},
|
|
82
90
|
"plugin-blog-categories": {
|
|
83
|
-
"entry": "blog/admin/views/categories.js?v=
|
|
91
|
+
"entry": "blog/admin/views/categories.js?v=17f4038c",
|
|
84
92
|
"exportName": "categoriesView"
|
|
85
93
|
},
|
|
86
94
|
"plugin-blog-comments": {
|
|
87
|
-
"entry": "blog/admin/views/comments.js?v=
|
|
95
|
+
"entry": "blog/admin/views/comments.js?v=55dd8ad9",
|
|
88
96
|
"exportName": "commentsView"
|
|
89
97
|
},
|
|
90
98
|
"plugin-blog-settings": {
|
|
91
|
-
"entry": "blog/admin/views/settings.js?v=
|
|
99
|
+
"entry": "blog/admin/views/settings.js?v=bd2310f3",
|
|
92
100
|
"exportName": "settingsView"
|
|
93
101
|
}
|
|
94
102
|
}
|