domma-cms 0.66.0 → 0.69.4
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 +64 -1
- package/README.md +14 -10
- package/admin/css/admin.css +1 -1
- 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/plugin-accent.js +1 -1
- package/admin/js/lib/plugin-chrome.js +1 -1
- 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 +3 -3
- 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/notes.html +137 -0
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/admin/js/templates/theme.html +38 -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/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 +24 -22
- package/admin/js/views/theme.js +1 -1
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +4 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +14 -0
- package/config/menus/admin-sidebar.json +1 -1
- package/config/server.json +3 -1
- package/package.json +3 -7
- package/plugins/_lib/admin/mail/reader-view.js +20 -0
- package/plugins/_lib/mail/store.js +21 -1
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -1
- package/plugins/blog/admin/templates/blog.html +35 -53
- 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 +36 -13
- package/plugins/blog/admin/templates/settings.html +2 -11
- package/plugins/blog/admin/views/blog.js +288 -162
- package/plugins/blog/admin/views/categories.js +212 -201
- 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 +269 -21
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -86
- 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/posts/schema.json +3 -1
- 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 +225 -15
- package/plugins/blog/plugin.json +21 -81
- package/plugins/blog/plugin.public.js +115 -210
- 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/plugin.json +2 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +41 -2
- package/scripts/run-tests.mjs +94 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
- package/server/routes/api/dashboard.js +8 -2
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +148 -3
- package/server/routes/api/todo.js +178 -0
- package/server/server.js +61 -1
- package/server/services/collections.js +13 -3
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +54 -1
- package/server/services/permissionRegistry.js +50 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +117 -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 +83 -3
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +310 -0
- package/server/services/themeSettings.js +39 -0
- package/config/theme.json +0 -18
- package/plugins/analytics/admin/css/index.css +0 -1
- package/plugins/analytics/admin/templates/analytics.html +0 -70
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -60
- 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 -254
- package/plugins/contacts/admin/views/contacts.js +0 -943
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -179
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/plugin.json +0 -36
- 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/css/index.css +0 -1
- package/plugins/invoice/admin/templates/editor.html +0 -220
- package/plugins/invoice/admin/templates/index.html +0 -173
- package/plugins/invoice/admin/templates/issuers.html +0 -2
- package/plugins/invoice/admin/templates/receivers.html +0 -2
- package/plugins/invoice/admin/views/contacts-source.js +0 -266
- package/plugins/invoice/admin/views/editor.js +0 -434
- package/plugins/invoice/admin/views/export.js +0 -199
- package/plugins/invoice/admin/views/help-content.js +0 -61
- package/plugins/invoice/admin/views/index.js +0 -643
- package/plugins/invoice/admin/views/issuers.js +0 -30
- package/plugins/invoice/admin/views/media.js +0 -172
- package/plugins/invoice/admin/views/party-view.js +0 -386
- package/plugins/invoice/admin/views/payments.js +0 -127
- package/plugins/invoice/admin/views/print.js +0 -130
- package/plugins/invoice/admin/views/receivers.js +0 -55
- package/plugins/invoice/admin/views/send.js +0 -212
- package/plugins/invoice/admin/views/settings.js +0 -594
- package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
- package/plugins/invoice/collections/invoices/schema.json +0 -33
- package/plugins/invoice/config.js +0 -37
- package/plugins/invoice/pdf.js +0 -164
- package/plugins/invoice/plugin.js +0 -1456
- package/plugins/invoice/plugin.json +0 -86
- package/plugins/invoice/templates/_base.css +0 -1
- package/plugins/invoice/templates/classic-nologo.html +0 -100
- package/plugins/invoice/templates/classic.html +0 -91
- package/plugins/invoice/templates/invoice-print.html +0 -237
- package/plugins/invoice/templates/minimal.html +0 -99
- package/plugins/invoice/templates/modern-nologo.html +0 -114
- package/plugins/invoice/templates/modern.html +0 -113
- package/plugins/invoice/templates/templates.json +0 -11
- 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/plugin.json +0 -33
- 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/lib/audience.js +0 -37
- package/plugins/surveys/lib/campaigns.js +0 -43
- package/plugins/surveys/lib/ledger.js +0 -110
- package/plugins/surveys/lib/sending.js +0 -106
- package/plugins/surveys/lib/stats.js +0 -62
- package/plugins/surveys/lib/submit.js +0 -95
- package/plugins/surveys/lib/tokens.js +0 -28
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/plugin.public.js +0 -149
- 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/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
- /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
|
@@ -1,187 +1,236 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Blog comments
|
|
2
|
+
* Blog comments, on the Tool upgrade path.
|
|
3
|
+
*
|
|
4
|
+
* Every comment is loaded once and filtered in the browser, so each status
|
|
5
|
+
* carries its count - "Pending 3" is the whole point of a moderation screen,
|
|
6
|
+
* and the old tabs could not say it (nor reach Rejected at all). Rows show the
|
|
7
|
+
* post's title rather than its id; a click expands the full comment in place;
|
|
8
|
+
* moderation is on the row's context menu (right-click, Shift+F10, or ⋮).
|
|
3
9
|
*
|
|
4
10
|
* @module blog/admin/views/comments
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
// The loader's cache-busting token, passed on so helpers are refetched with this view.
|
|
14
|
+
const ASSET_VERSION = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
15
|
+
|
|
16
|
+
const STATUSES = [
|
|
17
|
+
{key: 'pending', label: 'Pending'},
|
|
18
|
+
{key: 'approved', label: 'Approved'},
|
|
19
|
+
{key: 'rejected', label: 'Rejected'},
|
|
20
|
+
{key: 'spam', label: 'Spam'},
|
|
21
|
+
{key: 'all', label: 'All'}
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** One row. In JS, not the template: the router's _.render would eat {{…}}. */
|
|
25
|
+
const ROW_TEMPLATE = `
|
|
26
|
+
<div class="blog-row blog-comment" data-id="{{id}}" tabindex="0">
|
|
27
|
+
<span class="blog-row-main">
|
|
28
|
+
<strong class="blog-row-title">{{author}} <small class="blog-row-sub-inline">{{email}}</small></strong>
|
|
29
|
+
<span class="blog-comment-text">{{body}}</span>
|
|
30
|
+
<small class="blog-row-sub">on {{post}}</small>
|
|
31
|
+
</span>
|
|
32
|
+
<span class="badge {{badge}}">{{status}}</span>
|
|
33
|
+
<time class="blog-row-date" title="{{dateTitle}}">{{date}}</time>
|
|
34
|
+
<button type="button" class="blog-kebab" aria-label="Actions" title="Actions">⋮</button>
|
|
35
|
+
</div>`;
|
|
36
|
+
|
|
37
|
+
const CHIP_TEMPLATE = `<button type="button" class="blog-chip" data-status="{{key}}">{{label}} <b>{{count}}</b></button>`;
|
|
38
|
+
|
|
39
|
+
const BADGE = {pending: 'badge-warning', approved: 'badge-success', rejected: 'badge-secondary', spam: 'badge-danger'};
|
|
12
40
|
|
|
13
41
|
export const commentsView = {
|
|
14
42
|
templateUrl: '/plugins/blog/admin/templates/comments.html',
|
|
15
43
|
|
|
16
44
|
/**
|
|
17
|
-
* Mount the blog comments view.
|
|
18
|
-
*
|
|
19
45
|
* @param {object} $container - Domma-wrapped container element
|
|
20
46
|
* @returns {Promise<void>}
|
|
21
47
|
*/
|
|
22
48
|
async onMount($container) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
// Not awaited: the cog is chrome, and the screen should not wait on it.
|
|
50
|
+
import(`./settings-cog.js?v=${ASSET_VERSION}`)
|
|
51
|
+
.then(m => m.addSettingsCog($container, '.blog-bar'))
|
|
52
|
+
.catch(() => { /* no cog is better than no screen */ });
|
|
53
|
+
|
|
54
|
+
const kit = await import(`./kit.js?v=${ASSET_VERSION}`);
|
|
55
|
+
const {BASE} = kit;
|
|
56
|
+
const add = kit.viewScope('comments', /^#\/plugins\/blog\/comments\b/);
|
|
57
|
+
const root = $container.get(0);
|
|
58
|
+
|
|
59
|
+
// ---- State ----
|
|
60
|
+
const comments = M.observable([]);
|
|
61
|
+
const posts = M.observable([]);
|
|
62
|
+
const status = M.observable('pending');
|
|
63
|
+
const query = M.observable('');
|
|
64
|
+
const loading = M.observable(true);
|
|
65
|
+
const settings = M.observable({});
|
|
66
|
+
|
|
67
|
+
const postTitle = (id) => posts.peek().find(p => p.id === id)?.title || 'a deleted post';
|
|
68
|
+
|
|
69
|
+
// Moderating moves the "waiting for approval" badge on Blog in the sidebar. An
|
|
70
|
+
// event rather than the admin's sidebarCount(): a plugin must not
|
|
71
|
+
// import admin/ statically.
|
|
72
|
+
add(M.effect(() => {
|
|
73
|
+
comments.value;
|
|
74
|
+
if (!loading.peek()) {
|
|
75
|
+
window.dispatchEvent(new CustomEvent('dm:sidebar-count', {detail: {url: '#/plugins/blog'}}));
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const rows = M.observable([]);
|
|
80
|
+
const chips = M.observable([]);
|
|
81
|
+
add(M.effect(() => {
|
|
82
|
+
const term = query.value.trim().toLowerCase();
|
|
83
|
+
posts.value;
|
|
84
|
+
rows.value = comments.value
|
|
85
|
+
.filter(c => (status.value === 'all' || c.status === status.value)
|
|
86
|
+
&& (!term || [c.authorName, c.authorEmail, c.body].some(v => String(v ?? '').toLowerCase().includes(term))))
|
|
87
|
+
.map(c => ({
|
|
88
|
+
id: c.id,
|
|
89
|
+
author: c.authorName || 'Anonymous',
|
|
90
|
+
email: c.authorEmail || '',
|
|
91
|
+
body: c.body || '',
|
|
92
|
+
post: postTitle(c.postId),
|
|
93
|
+
status: c.status || 'pending',
|
|
94
|
+
badge: BADGE[c.status] || 'badge-secondary',
|
|
95
|
+
date: c.createdAt ? D(c.createdAt).fromNow() : '',
|
|
96
|
+
dateTitle: c.createdAt ? D(c.createdAt).format('DD MMM YYYY HH:mm') : ''
|
|
97
|
+
}));
|
|
98
|
+
}));
|
|
99
|
+
add(M.effect(() => {
|
|
100
|
+
const all = comments.value;
|
|
101
|
+
chips.value = STATUSES.map(s => ({
|
|
102
|
+
...s, count: s.key === 'all' ? all.length : all.filter(c => c.status === s.key).length
|
|
103
|
+
}));
|
|
104
|
+
}));
|
|
105
|
+
add(M.effect(() => {
|
|
106
|
+
const active = status.value;
|
|
107
|
+
chips.value;
|
|
108
|
+
queueMicrotask(() => root.querySelectorAll('.blog-chip').forEach(c =>
|
|
109
|
+
c.classList.toggle('is-active', c.dataset.status === active)));
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const data = {
|
|
113
|
+
rows, chips, query, loading,
|
|
114
|
+
pendingCount: M.computed(() => comments.value.filter(c => c.status === 'pending').length),
|
|
115
|
+
isEmpty: M.computed(() => !loading.value && rows.value.length === 0),
|
|
116
|
+
emptyText: M.computed(() => query.value.trim()
|
|
117
|
+
? 'No comments match the search.'
|
|
118
|
+
: status.value === 'pending' ? 'Nothing waiting for moderation.' : `No ${status.value === 'all' ? '' : status.value + ' '}comments.`),
|
|
119
|
+
commentsOff: M.computed(() => settings.value.commentsEnabled === false),
|
|
120
|
+
moderationText: M.computed(() => settings.value.commentModeration === false
|
|
121
|
+
? 'New comments appear straight away.' : 'New comments wait here for approval.')
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ---- Server ----
|
|
125
|
+
const byId = (id) => comments.peek().find(c => c.id === id);
|
|
126
|
+
|
|
127
|
+
async function load() {
|
|
128
|
+
loading.value = true;
|
|
32
129
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
130
|
+
const [commentsRes, postsRes, settingsRes] = await Promise.all([
|
|
131
|
+
H.get(`${BASE}/comments`),
|
|
132
|
+
H.get(`${BASE}/posts`).catch(() => []),
|
|
133
|
+
H.get(`${BASE}/settings`).catch(() => ({}))
|
|
134
|
+
]);
|
|
135
|
+
comments.value = commentsRes?.data ?? (Array.isArray(commentsRes) ? commentsRes : []);
|
|
136
|
+
posts.value = postsRes?.data ?? (Array.isArray(postsRes) ? postsRes : []);
|
|
137
|
+
settings.value = settingsRes?.data ?? settingsRes ?? {};
|
|
36
138
|
} catch {
|
|
37
|
-
comments
|
|
38
|
-
|
|
139
|
+
E.toast('Failed to load comments.', {type: 'error'});
|
|
140
|
+
} finally {
|
|
141
|
+
loading.value = false;
|
|
39
142
|
}
|
|
40
|
-
buildTable();
|
|
41
143
|
}
|
|
42
144
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
commentsTable = T.create(tableEl, {
|
|
54
|
-
data: comments,
|
|
55
|
-
columns: [
|
|
56
|
-
{
|
|
57
|
-
key: 'authorName',
|
|
58
|
-
title: 'Author',
|
|
59
|
-
sortable: true,
|
|
60
|
-
render: (v) => `<strong>${escapeHtml(v)}</strong>`
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
key: 'authorEmail',
|
|
64
|
-
title: 'Email',
|
|
65
|
-
sortable: false,
|
|
66
|
-
render: (v) => v
|
|
67
|
-
? `<a href="mailto:${escapeHtml(v)}">${escapeHtml(v)}</a>`
|
|
68
|
-
: '<span class="blog-empty-cell">-</span>'
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
key: 'body',
|
|
72
|
-
title: 'Comment',
|
|
73
|
-
sortable: false,
|
|
74
|
-
render: (v) => {
|
|
75
|
-
const preview = String(v ?? '').slice(0, 80);
|
|
76
|
-
const ellipsis = String(v ?? '').length > 80 ? '\u2026' : '';
|
|
77
|
-
return `<span class="blog-comment-body">${escapeHtml(preview)}${ellipsis}</span>`;
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
key: 'postId',
|
|
82
|
-
title: 'Post',
|
|
83
|
-
sortable: false,
|
|
84
|
-
render: (v) => v
|
|
85
|
-
? `<a href="#/plugins/blog/posts/${escapeHtml(v)}">${escapeHtml(v)}</a>`
|
|
86
|
-
: '<span class="blog-empty-cell">-</span>'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
key: 'createdAt',
|
|
90
|
-
title: 'Date',
|
|
91
|
-
sortable: true,
|
|
92
|
-
render: (v) => v
|
|
93
|
-
? `<span title="${escapeHtml(D(v).format('DD MMM YYYY HH:mm'))}">${escapeHtml(D(v).fromNow())}</span>`
|
|
94
|
-
: '<span class="blog-empty-cell">-</span>'
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
key: 'id',
|
|
98
|
-
title: 'Actions',
|
|
99
|
-
sortable: false,
|
|
100
|
-
render: (id) => {
|
|
101
|
-
const btns = [];
|
|
102
|
-
if (currentTab !== 'approved') {
|
|
103
|
-
btns.push(`<button class="btn btn-sm btn-success comment-approve-btn" data-id="${escapeHtml(id)}">Approve</button>`);
|
|
104
|
-
}
|
|
105
|
-
if (currentTab !== 'spam') {
|
|
106
|
-
btns.push(`<button class="btn btn-sm btn-warning comment-spam-btn" data-id="${escapeHtml(id)}">Spam</button>`);
|
|
107
|
-
}
|
|
108
|
-
if (currentTab !== 'rejected') {
|
|
109
|
-
btns.push(`<button class="btn btn-sm btn-outline comment-reject-btn" data-id="${escapeHtml(id)}">Reject</button>`);
|
|
110
|
-
}
|
|
111
|
-
btns.push(`<button class="btn btn-sm btn-danger comment-delete-btn" data-id="${escapeHtml(id)}">` +
|
|
112
|
-
`<span data-icon="trash" data-icon-size="14"></span></button>`);
|
|
113
|
-
return `<div class="blog-moderation-actions">${btns.join('')}</div>`;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
],
|
|
117
|
-
emptyMessage: `No ${currentTab} comments.`
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Event delegation - attach once
|
|
121
|
-
if (!tableEl.dataset.listenerAttached) {
|
|
122
|
-
tableEl.dataset.listenerAttached = '1';
|
|
123
|
-
tableEl.addEventListener('click', async (e) => {
|
|
124
|
-
const approveBtn = e.target.closest('.comment-approve-btn');
|
|
125
|
-
const rejectBtn = e.target.closest('.comment-reject-btn');
|
|
126
|
-
const spamBtn = e.target.closest('.comment-spam-btn');
|
|
127
|
-
const deleteBtn = e.target.closest('.comment-delete-btn');
|
|
128
|
-
|
|
129
|
-
if (approveBtn) await updateStatus(approveBtn.dataset.id, 'approved');
|
|
130
|
-
if (rejectBtn) await updateStatus(rejectBtn.dataset.id, 'rejected');
|
|
131
|
-
if (spamBtn) await updateStatus(spamBtn.dataset.id, 'spam');
|
|
132
|
-
|
|
133
|
-
if (deleteBtn) {
|
|
134
|
-
const ok = await E.confirm('Delete this comment?');
|
|
135
|
-
if (!ok) return;
|
|
136
|
-
try {
|
|
137
|
-
await H.delete(`${BASE}/comments/${deleteBtn.dataset.id}`);
|
|
138
|
-
E.toast('Comment deleted.', { type: 'success' });
|
|
139
|
-
await loadComments();
|
|
140
|
-
} catch {
|
|
141
|
-
E.toast('Failed to delete comment.', { type: 'error' });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
+
async function setStatus(id, next) {
|
|
146
|
+
try {
|
|
147
|
+
const res = await H.put(`${BASE}/comments/${id}`, {status: next});
|
|
148
|
+
if (res?.error) throw new Error(res.error);
|
|
149
|
+
const updated = res?.data ?? res;
|
|
150
|
+
comments.value = comments.peek().map(c => (c.id === id ? {...c, ...(updated?.id ? updated : {status: next})} : c));
|
|
151
|
+
E.toast(`Comment marked ${next}.`, {type: 'success'});
|
|
152
|
+
} catch {
|
|
153
|
+
E.toast('Failed to update comment.', {type: 'error'});
|
|
145
154
|
}
|
|
146
|
-
|
|
147
|
-
Domma.icons.scan();
|
|
148
155
|
}
|
|
149
156
|
|
|
150
|
-
async function
|
|
157
|
+
async function remove(id) {
|
|
158
|
+
const ok = await E.confirm('Delete this comment? This cannot be undone.');
|
|
159
|
+
if (!ok) return;
|
|
151
160
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
E.toast(
|
|
155
|
-
await loadComments();
|
|
161
|
+
await H.delete(`${BASE}/comments/${id}`);
|
|
162
|
+
comments.value = comments.peek().filter(c => c.id !== id);
|
|
163
|
+
E.toast('Comment deleted.', {type: 'success'});
|
|
156
164
|
} catch {
|
|
157
|
-
E.toast('Failed to
|
|
165
|
+
E.toast('Failed to delete comment.', {type: 'error'});
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
169
|
+
// ---- Bindings ----
|
|
170
|
+
const list = root.querySelector('.blog-list');
|
|
171
|
+
root.querySelector('.blog-rows').innerHTML = ROW_TEMPLATE;
|
|
172
|
+
root.querySelector('.blog-chips').innerHTML = CHIP_TEMPLATE;
|
|
173
|
+
add(await kit.scanHelp(root));
|
|
174
|
+
const handle = M.applyBindings(data, root, {
|
|
175
|
+
methods: {
|
|
176
|
+
settings: () => kit.openSettings().then(() => {}),
|
|
177
|
+
async approveAll() {
|
|
178
|
+
const pending = comments.peek().filter(c => c.status === 'pending');
|
|
179
|
+
if (!pending.length) return;
|
|
180
|
+
const ok = await E.confirm(`Approve all ${pending.length} pending comment${pending.length === 1 ? '' : 's'}?`);
|
|
181
|
+
if (!ok) return;
|
|
182
|
+
for (const c of pending) await setStatus(c.id, 'approved');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
add(() => handle.dispose());
|
|
187
|
+
|
|
188
|
+
root.querySelector('.blog-chips').addEventListener('click', (e) => {
|
|
189
|
+
const chip = e.target.closest('.blog-chip');
|
|
190
|
+
if (chip) status.value = chip.dataset.status;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const menu = E.contextMenu(list, {
|
|
194
|
+
match: '.blog-row',
|
|
195
|
+
inherit: false,
|
|
196
|
+
density: 'compact',
|
|
197
|
+
items: (row) => {
|
|
198
|
+
const c = byId(row.dataset.id);
|
|
199
|
+
if (!c) return [];
|
|
200
|
+
const s = c.status;
|
|
201
|
+
return kit.tidy([
|
|
202
|
+
{label: 'Approve', icon: 'check-circle', visible: s !== 'approved', action: () => setStatus(c.id, 'approved')},
|
|
203
|
+
{label: 'Back to pending', icon: 'clock', visible: s !== 'pending', action: () => setStatus(c.id, 'pending')},
|
|
204
|
+
{label: 'Reject', icon: 'x-circle', visible: s !== 'rejected', action: () => setStatus(c.id, 'rejected')},
|
|
205
|
+
{label: 'Mark as spam', icon: 'alert-triangle', visible: s !== 'spam', action: () => setStatus(c.id, 'spam')},
|
|
206
|
+
{type: 'divider'},
|
|
207
|
+
{label: 'Open the post', icon: 'edit', visible: Boolean(c.postId) && posts.peek().some(p => p.id === c.postId),
|
|
208
|
+
action: () => R.navigate(`#/plugins/blog/posts/${c.postId}`)},
|
|
209
|
+
{label: 'Reply by email', icon: 'mail', visible: Boolean(c.authorEmail),
|
|
210
|
+
action: () => { location.href = `mailto:${encodeURIComponent(c.authorEmail)}`; }},
|
|
211
|
+
{type: 'divider'},
|
|
212
|
+
{label: 'Delete', icon: 'trash', danger: true, action: () => remove(c.id)}
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
add(() => menu?.destroy?.());
|
|
217
|
+
|
|
218
|
+
// A click expands the comment in place; ⋮ opens the menu.
|
|
219
|
+
list.addEventListener('click', (e) => {
|
|
220
|
+
const row = e.target.closest('.blog-row');
|
|
221
|
+
if (!row) return;
|
|
222
|
+
const kebab = e.target.closest('.blog-kebab');
|
|
223
|
+
if (kebab) return kit.openMenuFrom(kebab);
|
|
224
|
+
row.classList.toggle('is-open');
|
|
225
|
+
});
|
|
226
|
+
list.addEventListener('keydown', (e) => {
|
|
227
|
+
const row = e.target.closest?.('.blog-row');
|
|
228
|
+
if (row && e.key === 'Enter' && e.target === row) row.classList.toggle('is-open');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (typeof I !== 'undefined' && I.scan) I.scan(root);
|
|
232
|
+
await load();
|
|
233
|
+
// Landing with nothing pending shows everything, rather than an empty screen.
|
|
234
|
+
if (!comments.peek().some(c => c.status === 'pending') && comments.peek().length) status.value = 'all';
|
|
186
235
|
}
|
|
187
236
|
};
|