domma-cms 1.6.56 → 1.6.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/css/calculator.css +1 -1
- package/admin/js/lib/analytics-shape.js +1 -1
- package/admin/js/lib/calculator-engine.js +1 -1
- package/admin/js/lib/calculator.js +22 -10
- package/admin/js/lib/form-editor-arrange.js +1 -1
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/notes-arrange.js +1 -1
- package/admin/js/lib/search-settings-arrange.js +1 -1
- package/admin/js/lib/seo-arrange.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -1
- package/admin/js/lib/tool-kit.js +1 -1
- package/admin/js/lib/tool-recovery.js +4 -4
- package/admin/js/templates/analytics.html +26 -14
- package/admin/js/templates/contacts.html +26 -5
- package/admin/js/templates/form-editor.html +2 -0
- package/admin/js/templates/notes.html +11 -8
- package/admin/js/templates/search.html +9 -3
- package/admin/js/templates/seo.html +23 -17
- package/admin/js/templates/todo.html +18 -14
- package/admin/js/views/analytics.js +10 -10
- package/admin/js/views/contacts.js +47 -47
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/forms.js +5 -5
- package/admin/js/views/notes.js +6 -6
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +43 -43
- package/admin/js/views/todo.js +8 -8
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +4 -1
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/reader-kit.js +364 -0
- package/plugins/_lib/admin/mail/reader-view.js +1009 -161
- package/plugins/_lib/admin/mail/when-closed.js +33 -3
- package/plugins/_lib/mail/readRoutes.js +48 -10
- package/plugins/_lib/mail/unreadBadge.js +4 -1
- package/plugins/blog/CLAUDE.md +14 -0
- package/plugins/blog/admin/css/index.css +5 -0
- package/plugins/blog/admin/lib/drafts.js +95 -0
- package/plugins/blog/admin/templates/blog.html +1 -1
- package/plugins/blog/admin/templates/categories.html +1 -1
- package/plugins/blog/admin/templates/comments.html +1 -1
- package/plugins/blog/admin/templates/post-editor.html +5 -1
- package/plugins/blog/admin/views/blog.js +44 -19
- package/plugins/blog/admin/views/categories.js +138 -20
- package/plugins/blog/admin/views/comments.js +32 -23
- package/plugins/blog/admin/views/kit.js +232 -22
- package/plugins/blog/admin/views/post-editor.js +348 -178
- package/plugins/blog/admin/views/settings.js +17 -3
- package/plugins/blog/plugin.js +1 -1
- package/plugins/blog/plugin.json +7 -7
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/drafts.test.js +50 -0
- package/plugins/booking/CLAUDE.md +13 -0
- package/plugins/booking/admin/lib/drafts.js +139 -0
- package/plugins/booking/admin/lib/kit.js +246 -25
- package/plugins/booking/admin/views/booking.js +11 -2
- package/plugins/booking/admin/views/bookings.js +7 -2
- package/plugins/booking/admin/views/diary.js +13 -6
- package/plugins/booking/admin/views/panels.js +85 -12
- package/plugins/booking/admin/views/people.js +41 -2
- package/plugins/booking/admin/views/services.js +45 -3
- package/plugins/booking/admin/views/settings.js +22 -2
- package/plugins/booking/plugin.json +2 -2
- package/plugins/booking/tests/admin-drafts.test.js +67 -0
- package/plugins/feedback/CLAUDE.md +12 -0
- package/plugins/feedback/admin/css/index.css +5 -0
- package/plugins/feedback/admin/lib/drafts.js +145 -0
- package/plugins/feedback/admin/lib/kit.js +161 -5
- package/plugins/feedback/admin/lib/settings.js +46 -28
- package/plugins/feedback/admin/views/feedback.js +1 -1
- package/plugins/feedback/admin/views/inbox.js +110 -46
- package/plugins/feedback/admin/views/send.js +174 -83
- package/plugins/feedback/plugin.json +3 -3
- package/plugins/feedback/tests/drafts.test.js +93 -0
- package/plugins/free-tier.lock.json +75 -63
- package/plugins/security/CLAUDE.md +9 -0
- package/plugins/security/admin/lib/screen-state.js +120 -0
- package/plugins/security/admin/templates/security.html +15 -10
- package/plugins/security/admin/views/security.js +270 -132
- package/plugins/security/plugin.json +2 -2
- package/plugins/security/tests/screen-state.test.js +69 -0
- package/plugins/shopping-cart/CLAUDE.md +13 -0
- package/plugins/shopping-cart/admin/css/index.css +21 -5
- package/plugins/shopping-cart/admin/lib/drafts.js +174 -0
- package/plugins/shopping-cart/admin/lib/kit.js +136 -28
- package/plugins/shopping-cart/admin/views/orders.js +117 -36
- package/plugins/shopping-cart/admin/views/overview.js +38 -8
- package/plugins/shopping-cart/admin/views/products.js +317 -68
- package/plugins/shopping-cart/admin/views/sales.js +21 -3
- package/plugins/shopping-cart/admin/views/settings.js +62 -3
- package/plugins/shopping-cart/admin/views/shop.js +18 -3
- package/plugins/shopping-cart/plugin.json +3 -3
- package/plugins/shopping-cart/tests/accounts.test.js +11 -0
- package/plugins/shopping-cart/tests/drafts.test.js +118 -0
- package/plugins/support/CLAUDE.md +14 -0
- package/plugins/support/admin/css/index.css +10 -0
- package/plugins/support/admin/lib/drafts.js +162 -0
- package/plugins/support/admin/lib/kit.js +156 -16
- package/plugins/support/admin/lib/settings.js +52 -29
- package/plugins/support/admin/views/desk.js +114 -33
- package/plugins/support/admin/views/inbox.js +85 -26
- package/plugins/support/admin/views/support.js +1 -1
- package/plugins/support/plugin.json +3 -3
- package/plugins/support/tests/drafts.test.js +92 -0
- package/server/routes/api/analytics.js +5 -1
- package/server/routes/api/contacts.js +42 -0
- package/server/routes/api/forms.js +8 -0
- package/server/routes/api/seo.js +4 -1
- package/server/routes/api/todo.js +50 -10
- package/server/server.js +1 -1
|
@@ -69,7 +69,11 @@ export const categoriesView = {
|
|
|
69
69
|
const kit = await import(`./kit.js?v=${ASSET_VERSION}`);
|
|
70
70
|
const {BASE} = kit;
|
|
71
71
|
const add = kit.viewScope('categories', /^#\/plugins\/blog\/categories\b/);
|
|
72
|
+
const here = add.here;
|
|
72
73
|
const root = $container.get(0);
|
|
74
|
+
const slot = root.querySelector('.blog-loading');
|
|
75
|
+
const {draftState, restoreQuestion, versionOf} = kit.draftLib;
|
|
76
|
+
const CAT_KEYS = ['name', 'slug', 'description'];
|
|
73
77
|
|
|
74
78
|
// ---- State ----
|
|
75
79
|
const categories = M.observable([]);
|
|
@@ -104,20 +108,27 @@ export const categoriesView = {
|
|
|
104
108
|
// ---- Server ----
|
|
105
109
|
const byId = (id) => categories.peek().find(c => c.id === id);
|
|
106
110
|
|
|
111
|
+
// The first load draws a skeleton; a reload keeps the rows on screen.
|
|
112
|
+
let loaded = false;
|
|
107
113
|
async function load() {
|
|
108
|
-
|
|
114
|
+
const work = Promise.all([
|
|
115
|
+
H.get(`${BASE}/categories`),
|
|
116
|
+
H.get(`${BASE}/posts`).catch(() => []),
|
|
117
|
+
kit.sitePath()
|
|
118
|
+
]);
|
|
109
119
|
try {
|
|
110
|
-
const [catsRes, postsRes] = await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
site = await kit.sitePath();
|
|
120
|
+
const [catsRes, postsRes, base] = loaded ? await work : await kit.withSkeleton(slot, work);
|
|
121
|
+
if (!here()) return false;
|
|
122
|
+
site = base;
|
|
123
|
+
loaded = true;
|
|
115
124
|
categories.value = catsRes?.data ?? (Array.isArray(catsRes) ? catsRes : []);
|
|
116
125
|
posts.value = postsRes?.data ?? (Array.isArray(postsRes) ? postsRes : []);
|
|
126
|
+
return true;
|
|
117
127
|
} catch {
|
|
118
|
-
E.toast('Failed to load categories.', {type: 'error'});
|
|
128
|
+
if (here()) E.toast('Failed to load categories.', {type: 'error'});
|
|
129
|
+
return false;
|
|
119
130
|
} finally {
|
|
120
|
-
loading.value = false;
|
|
131
|
+
if (here()) loading.value = false;
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
|
|
@@ -126,13 +137,14 @@ export const categoriesView = {
|
|
|
126
137
|
const ok = await E.confirm(n
|
|
127
138
|
? `Delete this category? ${n} post${n === 1 ? ' uses' : 's use'} it and will simply lose it.`
|
|
128
139
|
: 'Delete this category?');
|
|
129
|
-
if (!ok) return;
|
|
140
|
+
if (!ok || !here()) return;
|
|
130
141
|
try {
|
|
131
142
|
await H.delete(`${BASE}/categories/${id}`);
|
|
143
|
+
if (!here()) return;
|
|
132
144
|
categories.value = categories.peek().filter(c => c.id !== id);
|
|
133
145
|
E.toast('Category deleted.', {type: 'success'});
|
|
134
146
|
} catch {
|
|
135
|
-
E.toast('Failed to delete category.', {type: 'error'});
|
|
147
|
+
if (here()) E.toast('Failed to delete category.', {type: 'error'});
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
|
|
@@ -142,13 +154,55 @@ export const categoriesView = {
|
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
// ---- Editor: a slideover ----
|
|
145
|
-
|
|
157
|
+
// One at a time. Closing it with changes asks; leaving the screen keeps
|
|
158
|
+
// them as a draft (per category, or one for a new category), offered
|
|
159
|
+
// back the next time that editor opens. It closes after a save, so it
|
|
160
|
+
// is locked while saving - typing then would go unsent.
|
|
161
|
+
const draftKey = (cid) => `category.${cid || 'new'}`;
|
|
162
|
+
|
|
163
|
+
async function openEditor(cat = null, {startFrom = null} = {}) {
|
|
164
|
+
const release = kit.claim('category-editor');
|
|
165
|
+
if (!release) return;
|
|
166
|
+
try {
|
|
167
|
+
await editor(cat, startFrom, release);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
release();
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function editor(cat, startFrom, release) {
|
|
175
|
+
// A draft from last time for this same category: asked about BEFORE
|
|
176
|
+
// the panel opens, so the Escape that keeps it cannot also close the
|
|
177
|
+
// panel. Removed only once answered - leaving while asked keeps it.
|
|
178
|
+
let picked = startFrom;
|
|
179
|
+
let pickedWas = '';
|
|
180
|
+
if (!picked) {
|
|
181
|
+
const draft = kit.drafts.read(draftKey(cat?.id));
|
|
182
|
+
if (draft) {
|
|
183
|
+
const state = draftState({...draft, id: cat?.id ?? null}, cat, CAT_KEYS);
|
|
184
|
+
const throwAway = await kit.askThrowAway(restoreQuestion({what: 'category', name: cat?.name ?? '', state, isNew: !cat}));
|
|
185
|
+
if (!here()) { release(); return; }
|
|
186
|
+
kit.drafts.drop(draftKey(cat?.id));
|
|
187
|
+
if (!throwAway) {
|
|
188
|
+
picked = draft.values ?? {};
|
|
189
|
+
// A picked-up draft keeps its age: its values are still the older edits.
|
|
190
|
+
pickedWas = draft.was ?? '';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
146
194
|
const so = E.slideover({title: cat ? 'Edit category' : 'New category', size: 'sm', position: 'right'});
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
195
|
+
const original = {name: cat?.name ?? '', slug: cat?.slug ?? '', description: cat?.description ?? ''};
|
|
196
|
+
const name = M.observable(original.name);
|
|
197
|
+
const slug = M.observable(original.slug);
|
|
198
|
+
const description = M.observable(original.description);
|
|
150
199
|
const saving = M.observable(false);
|
|
151
200
|
const focusName = M.observable(false);
|
|
201
|
+
let open = true;
|
|
202
|
+
let was = cat ? versionOf(cat, CAT_KEYS) : '';
|
|
203
|
+
let baseline = JSON.stringify(original);
|
|
204
|
+
const values = () => ({name: name.peek(), slug: slug.peek(), description: description.peek()});
|
|
205
|
+
const isDirty = () => JSON.stringify(values()) !== baseline;
|
|
152
206
|
|
|
153
207
|
// A new category's slug follows its name until someone edits the slug.
|
|
154
208
|
let slugTouched = Boolean(cat);
|
|
@@ -164,24 +218,29 @@ export const categoriesView = {
|
|
|
164
218
|
methods: {
|
|
165
219
|
cancel: () => so.close(),
|
|
166
220
|
async save() {
|
|
221
|
+
if (saving.peek()) return;
|
|
167
222
|
const body = {name: name.peek().trim(), slug: slug.peek().trim(), description: description.peek().trim()};
|
|
168
223
|
if (!body.name) {
|
|
169
224
|
E.toast('Name is required.', {type: 'error'});
|
|
170
225
|
return;
|
|
171
226
|
}
|
|
172
227
|
saving.value = true;
|
|
228
|
+
const unlock = kit.lockWhileSaving(el);
|
|
173
229
|
try {
|
|
174
230
|
const res = cat
|
|
175
231
|
? await H.put(`${BASE}/categories/${cat.id}`, body)
|
|
176
232
|
: await H.post(`${BASE}/categories`, body);
|
|
177
233
|
if (res?.error) throw new Error(res.error);
|
|
234
|
+
kit.drafts.drop(draftKey(cat?.id));
|
|
235
|
+
baseline = JSON.stringify(values());
|
|
178
236
|
E.toast(cat ? 'Category updated.' : 'Category created.', {type: 'success'});
|
|
179
|
-
|
|
180
|
-
await load();
|
|
237
|
+
guard.close();
|
|
238
|
+
if (here()) await load();
|
|
181
239
|
} catch (err) {
|
|
182
240
|
E.toast(err?.message ?? 'Failed to save category.', {type: 'error'});
|
|
183
241
|
} finally {
|
|
184
242
|
saving.value = false;
|
|
243
|
+
unlock();
|
|
185
244
|
}
|
|
186
245
|
}
|
|
187
246
|
}
|
|
@@ -190,16 +249,74 @@ export const categoriesView = {
|
|
|
190
249
|
if (e.key === 'Enter' && e.target.tagName === 'INPUT') { e.preventDefault(); el.querySelector('.btn-primary').click(); }
|
|
191
250
|
});
|
|
192
251
|
so.element.appendChild(el);
|
|
193
|
-
|
|
252
|
+
let unhook = () => {};
|
|
253
|
+
kit.onceClosed(so, () => { open = false; unhook(); handle.dispose(); disposeHelp(); stopFollow(); release(); });
|
|
254
|
+
const guard = kit.guardSlideover(so, {dirty: () => !saving.peek() && isDirty()});
|
|
255
|
+
const keep = () => {
|
|
256
|
+
if (!isDirty()) return false;
|
|
257
|
+
kit.drafts.keep(draftKey(cat?.id), {id: cat?.id ?? null, name: original.name, values: values(), was});
|
|
258
|
+
return true;
|
|
259
|
+
};
|
|
260
|
+
unhook = kit.closeOnLeave(so, () => {
|
|
261
|
+
if (keep()) E.toast('Your unsaved category is kept - open it again to pick it up.', {type: 'info'});
|
|
262
|
+
guard.close();
|
|
263
|
+
});
|
|
264
|
+
const onUnload = (e) => { if (open && isDirty()) { keep(); e.preventDefault(); e.returnValue = ''; } };
|
|
265
|
+
window.addEventListener('beforeunload', onUnload);
|
|
266
|
+
const unhookLeave = unhook;
|
|
267
|
+
unhook = () => { unhookLeave(); window.removeEventListener('beforeunload', onUnload); };
|
|
268
|
+
|
|
269
|
+
const put = (v) => {
|
|
270
|
+
slugTouched = true;
|
|
271
|
+
name.value = v.name ?? '';
|
|
272
|
+
slug.value = v.slug ?? '';
|
|
273
|
+
description.value = v.description ?? '';
|
|
274
|
+
};
|
|
194
275
|
if (typeof I !== 'undefined' && I.scan) I.scan(el);
|
|
195
276
|
so.open();
|
|
196
|
-
|
|
277
|
+
|
|
278
|
+
// A kept draft's changes - or a deleted category's, started again as a new one.
|
|
279
|
+
if (picked) {
|
|
280
|
+
put(picked);
|
|
281
|
+
if (pickedWas) was = pickedWas;
|
|
282
|
+
}
|
|
283
|
+
if (open) setTimeout(() => { focusName.value = true; }, 60);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Drafts kept for categories deleted since: offered once the list is in.
|
|
288
|
+
* "Keep them" starts the category again as a new one (asking first if
|
|
289
|
+
* that would replace another unsaved new category).
|
|
290
|
+
*/
|
|
291
|
+
async function offerOrphans() {
|
|
292
|
+
let keys = [];
|
|
293
|
+
try { keys = Object.keys(localStorage).filter(k => k.includes('blog.draft.category.') && !k.endsWith('.new')); } catch { return; }
|
|
294
|
+
for (const k of keys) {
|
|
295
|
+
const cid = k.slice(k.indexOf('blog.draft.category.') + 'blog.draft.category.'.length);
|
|
296
|
+
if (byId(cid)) continue;
|
|
297
|
+
const draft = kit.drafts.read(draftKey(cid));
|
|
298
|
+
if (!draft) continue;
|
|
299
|
+
const throwAway = await kit.askThrowAway(restoreQuestion({what: 'category', name: draft.name || draft.values?.name || '', state: 'deleted'}));
|
|
300
|
+
if (!here()) return;
|
|
301
|
+
if (!throwAway && kit.drafts.read(draftKey(null))) {
|
|
302
|
+
const replace = await E.confirm('You also have an unsaved new category. Starting this one again replaces it. Replace it?',
|
|
303
|
+
{confirmText: 'Replace it', cancelText: 'Keep both for now'});
|
|
304
|
+
if (!here()) return;
|
|
305
|
+
if (!replace) continue; // both drafts stay
|
|
306
|
+
kit.drafts.drop(draftKey(null));
|
|
307
|
+
}
|
|
308
|
+
kit.drafts.drop(draftKey(cid));
|
|
309
|
+
if (!throwAway) { await openEditor(null, {startFrom: draft.values ?? {}}); return; }
|
|
310
|
+
}
|
|
197
311
|
}
|
|
198
312
|
|
|
199
313
|
// ---- Bindings ----
|
|
200
314
|
const list = root.querySelector('.blog-list');
|
|
201
315
|
root.querySelector('.blog-rows').innerHTML = ROW_TEMPLATE;
|
|
202
|
-
|
|
316
|
+
// Overtaken while it loaded: register nothing - the lifetime is the new mount's now.
|
|
317
|
+
const offHelp = await kit.scanHelp(root);
|
|
318
|
+
if (!here()) { offHelp(); return; }
|
|
319
|
+
add(offHelp);
|
|
203
320
|
const handle = M.applyBindings(data, root, {methods: {create: () => openEditor(null)}});
|
|
204
321
|
add(() => handle.dispose());
|
|
205
322
|
|
|
@@ -235,8 +352,9 @@ export const categoriesView = {
|
|
|
235
352
|
const row = e.target.closest?.('.blog-row');
|
|
236
353
|
if (row && e.key === 'Enter' && e.target === row) openEditor(byId(row.dataset.id));
|
|
237
354
|
});
|
|
355
|
+
add(kit.menuKeys(list, '.blog-row'));
|
|
238
356
|
|
|
239
357
|
if (typeof I !== 'undefined' && I.scan) I.scan(root);
|
|
240
|
-
await load();
|
|
358
|
+
if (await load() && here()) await offerOrphans();
|
|
241
359
|
}
|
|
242
360
|
};
|
|
@@ -55,7 +55,9 @@ export const commentsView = {
|
|
|
55
55
|
const kit = await import(`./kit.js?v=${ASSET_VERSION}`);
|
|
56
56
|
const {BASE} = kit;
|
|
57
57
|
const add = kit.viewScope('comments', /^#\/plugins\/blog\/comments\b/);
|
|
58
|
+
const here = add.here;
|
|
58
59
|
const root = $container.get(0);
|
|
60
|
+
const slot = root.querySelector('.blog-loading');
|
|
59
61
|
|
|
60
62
|
// ---- State ----
|
|
61
63
|
const comments = M.observable([]);
|
|
@@ -67,16 +69,6 @@ export const commentsView = {
|
|
|
67
69
|
|
|
68
70
|
const postTitle = (id) => posts.peek().find(p => p.id === id)?.title || 'a deleted post';
|
|
69
71
|
|
|
70
|
-
// Moderating moves the "waiting for approval" badge on Blog in the sidebar. An
|
|
71
|
-
// event rather than the admin's sidebarCount(): a plugin must not
|
|
72
|
-
// import admin/ statically.
|
|
73
|
-
add(M.effect(() => {
|
|
74
|
-
comments.value;
|
|
75
|
-
if (!loading.peek()) {
|
|
76
|
-
window.dispatchEvent(new CustomEvent('dm:sidebar-count', {detail: {url: '#/plugins/blog'}}));
|
|
77
|
-
}
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
72
|
const rows = M.observable([]);
|
|
81
73
|
const chips = M.observable([]);
|
|
82
74
|
add(M.effect(() => {
|
|
@@ -126,44 +118,51 @@ export const commentsView = {
|
|
|
126
118
|
const byId = (id) => comments.peek().find(c => c.id === id);
|
|
127
119
|
|
|
128
120
|
async function load() {
|
|
129
|
-
loading.value = true;
|
|
130
121
|
try {
|
|
131
|
-
const [commentsRes, postsRes, settingsRes] = await Promise.all([
|
|
122
|
+
const [commentsRes, postsRes, settingsRes] = await kit.withSkeleton(slot, Promise.all([
|
|
132
123
|
H.get(`${BASE}/comments`),
|
|
133
124
|
H.get(`${BASE}/posts`).catch(() => []),
|
|
134
125
|
H.get(`${BASE}/settings`).catch(() => ({}))
|
|
135
|
-
]);
|
|
126
|
+
]));
|
|
127
|
+
if (!here()) return;
|
|
136
128
|
comments.value = commentsRes?.data ?? (Array.isArray(commentsRes) ? commentsRes : []);
|
|
137
129
|
posts.value = postsRes?.data ?? (Array.isArray(postsRes) ? postsRes : []);
|
|
138
130
|
settings.value = settingsRes?.data ?? settingsRes ?? {};
|
|
139
131
|
} catch {
|
|
140
|
-
E.toast('Failed to load comments.', {type: 'error'});
|
|
132
|
+
if (here()) E.toast('Failed to load comments.', {type: 'error'});
|
|
141
133
|
} finally {
|
|
142
|
-
loading.value = false;
|
|
134
|
+
if (here()) loading.value = false;
|
|
143
135
|
}
|
|
144
136
|
}
|
|
145
137
|
|
|
146
|
-
async function setStatus(id, next) {
|
|
138
|
+
async function setStatus(id, next, {quiet = false} = {}) {
|
|
147
139
|
try {
|
|
148
140
|
const res = await H.put(`${BASE}/comments/${id}`, {status: next});
|
|
149
141
|
if (res?.error) throw new Error(res.error);
|
|
142
|
+
// Moderating moves the "waiting for approval" badge on Blog in the sidebar.
|
|
143
|
+
kit.pokeSidebar();
|
|
144
|
+
if (!here()) return true;
|
|
150
145
|
const updated = res?.data ?? res;
|
|
151
146
|
comments.value = comments.peek().map(c => (c.id === id ? {...c, ...(updated?.id ? updated : {status: next})} : c));
|
|
152
|
-
E.toast(`Comment marked ${next}.`, {type: 'success'});
|
|
147
|
+
if (!quiet) E.toast(`Comment marked ${next}.`, {type: 'success'});
|
|
148
|
+
return true;
|
|
153
149
|
} catch {
|
|
154
|
-
E.toast('Failed to update comment.', {type: 'error'});
|
|
150
|
+
if (here()) E.toast('Failed to update comment.', {type: 'error'});
|
|
151
|
+
return false;
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
154
|
|
|
158
155
|
async function remove(id) {
|
|
159
156
|
const ok = await E.confirm('Delete this comment? This cannot be undone.');
|
|
160
|
-
if (!ok) return;
|
|
157
|
+
if (!ok || !here()) return;
|
|
161
158
|
try {
|
|
162
159
|
await H.delete(`${BASE}/comments/${id}`);
|
|
160
|
+
kit.pokeSidebar();
|
|
161
|
+
if (!here()) return;
|
|
163
162
|
comments.value = comments.peek().filter(c => c.id !== id);
|
|
164
163
|
E.toast('Comment deleted.', {type: 'success'});
|
|
165
164
|
} catch {
|
|
166
|
-
E.toast('Failed to delete comment.', {type: 'error'});
|
|
165
|
+
if (here()) E.toast('Failed to delete comment.', {type: 'error'});
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
168
|
|
|
@@ -171,7 +170,10 @@ export const commentsView = {
|
|
|
171
170
|
const list = root.querySelector('.blog-list');
|
|
172
171
|
root.querySelector('.blog-rows').innerHTML = ROW_TEMPLATE;
|
|
173
172
|
root.querySelector('.blog-chips').innerHTML = CHIP_TEMPLATE;
|
|
174
|
-
|
|
173
|
+
// Overtaken while it loaded: register nothing - the lifetime is the new mount's now.
|
|
174
|
+
const offHelp = await kit.scanHelp(root);
|
|
175
|
+
if (!here()) { offHelp(); return; }
|
|
176
|
+
add(offHelp);
|
|
175
177
|
const handle = M.applyBindings(data, root, {
|
|
176
178
|
methods: {
|
|
177
179
|
settings: () => kit.openSettings().then(() => {}),
|
|
@@ -179,8 +181,13 @@ export const commentsView = {
|
|
|
179
181
|
const pending = comments.peek().filter(c => c.status === 'pending');
|
|
180
182
|
if (!pending.length) return;
|
|
181
183
|
const ok = await E.confirm(`Approve all ${pending.length} pending comment${pending.length === 1 ? '' : 's'}?`);
|
|
182
|
-
if (!ok) return;
|
|
183
|
-
|
|
184
|
+
if (!ok || !here()) return;
|
|
185
|
+
let done = 0;
|
|
186
|
+
for (const c of pending) {
|
|
187
|
+
if (await setStatus(c.id, 'approved', {quiet: true})) done++;
|
|
188
|
+
if (!here()) return;
|
|
189
|
+
}
|
|
190
|
+
E.toast(`Approved ${done} comment${done === 1 ? '' : 's'}.`, {type: done === pending.length ? 'success' : 'warning'});
|
|
184
191
|
}
|
|
185
192
|
}
|
|
186
193
|
});
|
|
@@ -229,9 +236,11 @@ export const commentsView = {
|
|
|
229
236
|
const row = e.target.closest?.('.blog-row');
|
|
230
237
|
if (row && e.key === 'Enter' && e.target === row) row.classList.toggle('is-open');
|
|
231
238
|
});
|
|
239
|
+
add(kit.menuKeys(list, '.blog-row'));
|
|
232
240
|
|
|
233
241
|
if (typeof I !== 'undefined' && I.scan) I.scan(root);
|
|
234
242
|
await load();
|
|
243
|
+
if (!here()) return;
|
|
235
244
|
// Landing with nothing pending shows everything, rather than an empty screen.
|
|
236
245
|
if (!comments.peek().some(c => c.status === 'pending') && comments.peek().length) status.value = 'all';
|
|
237
246
|
}
|