domma-cms 1.6.55 → 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/public/js/site.js +1 -1
- 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
- package/server/templates/page.html +1 -1
|
@@ -82,7 +82,14 @@ export function viewScope(name, route) {
|
|
|
82
82
|
lifetimes.set(name, life);
|
|
83
83
|
}
|
|
84
84
|
life.reset();
|
|
85
|
-
|
|
85
|
+
// This mount's own flag: the next mount's reset (or leaving) turns it off,
|
|
86
|
+
// so work finishing after an await can ask whether its screen is still there.
|
|
87
|
+
let alive = true;
|
|
88
|
+
life.add(() => { alive = false; });
|
|
89
|
+
const add = (fn) => { if (typeof fn === 'function') life.add(fn); };
|
|
90
|
+
/** Is this mount still the one on screen? Check it after EVERY await. */
|
|
91
|
+
add.here = () => alive;
|
|
92
|
+
return add;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
function localLifetime(pattern) {
|
|
@@ -176,6 +183,141 @@ export function takeHandOff(key) {
|
|
|
176
183
|
}
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/** Tell the sidebar the Blog badge may have changed (it re-asks the countUrl). */
|
|
187
|
+
export function pokeSidebar() {
|
|
188
|
+
const url = '#/plugins/blog';
|
|
189
|
+
if (core?.sidebarCount) return core.sidebarCount(url);
|
|
190
|
+
try { window.dispatchEvent(new CustomEvent('dm:sidebar-count', {detail: {url}})); } catch { /* no sidebar */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Shift+F10 on a focused row opens its context menu, as a right-click would.
|
|
195
|
+
* (The ContextMenu key is left alone: Chrome on Windows opens its own menu on
|
|
196
|
+
* key-up as well.)
|
|
197
|
+
*
|
|
198
|
+
* @param {HTMLElement} list
|
|
199
|
+
* @param {string} rowSel
|
|
200
|
+
* @returns {() => void} unhook
|
|
201
|
+
*/
|
|
202
|
+
export function menuKeys(list, rowSel) {
|
|
203
|
+
const onKey = (e) => {
|
|
204
|
+
const row = e.target.closest?.(rowSel);
|
|
205
|
+
if (!row || e.target !== row || !(e.shiftKey && e.key === 'F10')) return;
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
openMenuFrom(row);
|
|
208
|
+
};
|
|
209
|
+
list.addEventListener('keydown', onKey);
|
|
210
|
+
return () => list.removeEventListener('keydown', onKey);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A skeleton in `slot` while `promise` runs; the data back once it has gone
|
|
215
|
+
* (rethrows). No slot, or an admin without loading.js: just the promise. The
|
|
216
|
+
* slot must be a BLOCK element.
|
|
217
|
+
*/
|
|
218
|
+
const loadingLib = await adminLib('loading.js');
|
|
219
|
+
export function withSkeleton(slot, promise, shape = 'rows') {
|
|
220
|
+
if (!slot || !loadingLib?.whileLoading) return promise;
|
|
221
|
+
return loadingLib.whileLoading(slot, promise, loadingLib.SHAPES?.[shape] ?? loadingLib.SHAPES?.rows);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Unsaved work: kept drafts, the questions, saving without losing typing
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
/** The pure half (versions, how a draft stands, the wording) - admin/lib/drafts.js. */
|
|
229
|
+
const KIT_V = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
230
|
+
export const draftLib = await import(`../lib/drafts.js?v=${KIT_V}`);
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* One kept draft per key - never a map of them, so a late writer cannot put
|
|
234
|
+
* back a stale copy of the others. Never holds a secret (Blog has none).
|
|
235
|
+
*/
|
|
236
|
+
export const drafts = {
|
|
237
|
+
read(key) { try { const d = S.get(`blog.draft.${key}`); return d && d.values ? d : null; } catch { return null; } },
|
|
238
|
+
keep(key, draft) { try { S.set(`blog.draft.${key}`, {...draft, at: new Date().toISOString()}); } catch { /* storage blocked */ } },
|
|
239
|
+
drop(key) { try { if (S.remove) S.remove(`blog.draft.${key}`); else S.set(`blog.draft.${key}`, null); } catch { /* storage blocked */ } }
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Ask whether to throw away a kept draft. OK ("Throw away") discards;
|
|
244
|
+
* Cancel, Escape and the backdrop all KEEP - Domma's confirm cannot tell
|
|
245
|
+
* them apart, so the safe answer has to be the one dismissing gives.
|
|
246
|
+
* `message` is plain text (escaped here).
|
|
247
|
+
*
|
|
248
|
+
* @param {string} message
|
|
249
|
+
* @param {{confirmText?: string, cancelText?: string}} [labels]
|
|
250
|
+
* @returns {Promise<boolean>} true = throw away
|
|
251
|
+
*/
|
|
252
|
+
export async function askThrowAway(message, labels = draftLib.RESTORE_ANSWERS) {
|
|
253
|
+
return Boolean(await E.confirm(escapeHtml(message), {...draftLib.RESTORE_ANSWERS, ...labels}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Ask before a slideover with unsaved changes closes - Cancel, ×, Escape and
|
|
258
|
+
* the backdrop all go through close(). Wrap AFTER onceClosed. `close()` on
|
|
259
|
+
* the handle closes without asking (after a save, or when the screen is left
|
|
260
|
+
* and a draft has been kept).
|
|
261
|
+
*
|
|
262
|
+
* @param {object} so
|
|
263
|
+
* @param {{dirty: () => boolean, question?: string}} opts
|
|
264
|
+
* @returns {{close: () => void}}
|
|
265
|
+
*/
|
|
266
|
+
export function guardSlideover(so, {dirty, question = 'Discard your changes? They have not been saved.'}) {
|
|
267
|
+
const inner = so.close;
|
|
268
|
+
let asking = false;
|
|
269
|
+
let free = false;
|
|
270
|
+
so.close = (...args) => {
|
|
271
|
+
if (free || !dirty()) return inner(...args);
|
|
272
|
+
if (asking) return undefined;
|
|
273
|
+
asking = true;
|
|
274
|
+
Promise.resolve(E.confirm(question, {confirmText: 'Discard', cancelText: 'Keep editing'})).then((ok) => {
|
|
275
|
+
asking = false;
|
|
276
|
+
if (ok) { free = true; inner(...args); }
|
|
277
|
+
});
|
|
278
|
+
return undefined;
|
|
279
|
+
};
|
|
280
|
+
return {close() { free = true; try { inner(); } catch { /* already gone */ } }};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Lock a panel while it saves when it closes afterwards - anything typed
|
|
285
|
+
* meanwhile would go unsent. Returns the unlock (focus back where it was).
|
|
286
|
+
*/
|
|
287
|
+
export function lockWhileSaving(panel) {
|
|
288
|
+
const had = document.activeElement;
|
|
289
|
+
panel.inert = true;
|
|
290
|
+
return () => {
|
|
291
|
+
panel.inert = false;
|
|
292
|
+
if (had && panel.contains(had) && had.isConnected) had.focus();
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Close a slideover the moment the address changes - a panel over the next
|
|
298
|
+
* screen catches its clicks. `close` is how (a guarded panel passes its
|
|
299
|
+
* guard's close, which does not ask). Returns the unhook.
|
|
300
|
+
*/
|
|
301
|
+
export function closeOnLeave(so, close = () => so.close()) {
|
|
302
|
+
const at = location.hash;
|
|
303
|
+
const onHash = () => { if (location.hash !== at) { try { close(); } catch { /* gone */ } } };
|
|
304
|
+
window.addEventListener('hashchange', onHash);
|
|
305
|
+
return () => window.removeEventListener('hashchange', onHash);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* One panel of a kind at a time. `claim(name)` is false while it is open;
|
|
310
|
+
* the release must run if opening throws, or it could never open again
|
|
311
|
+
* until a reload.
|
|
312
|
+
*/
|
|
313
|
+
const openPanels = new Set();
|
|
314
|
+
export function claim(name) {
|
|
315
|
+
if (openPanels.has(name)) return null;
|
|
316
|
+
openPanels.add(name);
|
|
317
|
+
let done = false;
|
|
318
|
+
return () => { if (!done) { done = true; openPanels.delete(name); } };
|
|
319
|
+
}
|
|
320
|
+
|
|
179
321
|
// ---------------------------------------------------------------------------
|
|
180
322
|
// Settings
|
|
181
323
|
// ---------------------------------------------------------------------------
|
|
@@ -262,26 +404,33 @@ const SETTINGS_PANEL = `
|
|
|
262
404
|
</div>
|
|
263
405
|
</div>`;
|
|
264
406
|
|
|
407
|
+
/** The settings fields a draft is aged against. */
|
|
408
|
+
const SETTING_KEYS = ['basePath', 'postsPerPage', 'feedLength', 'commentsEnabled', 'commentModeration',
|
|
409
|
+
'postLayout', 'listDisplay', 'displaySwitcher', 'interactive', 'relatedCount'];
|
|
410
|
+
|
|
265
411
|
/**
|
|
266
412
|
* The settings form as an element, bound to its own observables. Used by the
|
|
267
413
|
* banner cog's slideover and by the old #/plugins/blog/settings page, so the
|
|
268
414
|
* two can never drift apart.
|
|
269
415
|
*
|
|
416
|
+
* A failed load leaves the form unable to save: saving defaults over settings
|
|
417
|
+
* that could not be read would wipe them.
|
|
418
|
+
*
|
|
270
419
|
* @param {{onClose?: Function}} [opts] - with onClose, a Cancel button closes the host
|
|
271
|
-
* @returns {Promise<{el: HTMLElement, dispose: Function
|
|
420
|
+
* @returns {Promise<{el: HTMLElement, dispose: Function, isDirty: () => boolean,
|
|
421
|
+
* values: () => object, restore: (v: object) => void, was: string, failed: boolean}>}
|
|
272
422
|
*/
|
|
273
423
|
export async function settingsPanel({onClose = null} = {}) {
|
|
274
424
|
let stored = {};
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
425
|
+
let failed = false;
|
|
426
|
+
const [settingsRes, layoutsRes] = await Promise.allSettled([H.get(`${BASE}/settings`), H.get(`${BASE}/layouts`)]);
|
|
427
|
+
if (settingsRes.status === 'fulfilled' && settingsRes.value && !settingsRes.value.error) {
|
|
428
|
+
stored = settingsRes.value?.data ?? settingsRes.value ?? {};
|
|
429
|
+
} else {
|
|
430
|
+
failed = true;
|
|
280
431
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
layouts = await H.get(`${BASE}/layouts`);
|
|
284
|
-
} catch { /* the selects fall back to the stored value alone */ }
|
|
432
|
+
// The selects fall back to the stored value alone.
|
|
433
|
+
const layouts = layoutsRes.status === 'fulfilled' && layoutsRes.value ? layoutsRes.value : {postLayouts: [], listDisplays: []};
|
|
285
434
|
const initial = {
|
|
286
435
|
basePath: stored.basePath ?? '/blog',
|
|
287
436
|
postsPerPage: Number(stored.postsPerPage ?? 10),
|
|
@@ -300,7 +449,7 @@ export async function settingsPanel({onClose = null} = {}) {
|
|
|
300
449
|
|
|
301
450
|
const f = Object.fromEntries(Object.entries(initial).map(([k, v]) => [k, M.observable(v)]));
|
|
302
451
|
const saving = M.observable(false);
|
|
303
|
-
const status = M.observable('');
|
|
452
|
+
const status = M.observable(failed ? 'The settings could not be loaded - close this and try again.' : '');
|
|
304
453
|
const current = () => ({
|
|
305
454
|
basePath: String(f.basePath.value).trim() || '/blog',
|
|
306
455
|
postsPerPage: Number(f.postsPerPage.value) || 10,
|
|
@@ -313,13 +462,15 @@ export async function settingsPanel({onClose = null} = {}) {
|
|
|
313
462
|
interactive: Boolean(f.interactive.value),
|
|
314
463
|
relatedCount: Math.max(0, Math.min(6, Number(f.relatedCount.value) || 0))
|
|
315
464
|
});
|
|
465
|
+
const isDirty = () => !failed && JSON.stringify(current()) !== JSON.stringify(baseline.peek());
|
|
316
466
|
|
|
317
467
|
const data = {
|
|
318
468
|
...f, saving, status,
|
|
319
469
|
closable: Boolean(onClose),
|
|
470
|
+
failed,
|
|
320
471
|
layoutOptions: M.observable(withCurrent(layouts.postLayouts ?? [], initial.postLayout)),
|
|
321
472
|
displayOptions: M.observable(withCurrent(layouts.listDisplays ?? [], initial.listDisplay)),
|
|
322
|
-
dirty: M.computed(() => JSON.stringify(current()) !== JSON.stringify(baseline.value))
|
|
473
|
+
dirty: M.computed(() => !failed && JSON.stringify(current()) !== JSON.stringify(baseline.value))
|
|
323
474
|
};
|
|
324
475
|
|
|
325
476
|
const el = fragment(SETTINGS_PANEL);
|
|
@@ -332,6 +483,7 @@ export async function settingsPanel({onClose = null} = {}) {
|
|
|
332
483
|
R.navigate('#/blocks');
|
|
333
484
|
},
|
|
334
485
|
async save() {
|
|
486
|
+
if (failed) return;
|
|
335
487
|
const body = current();
|
|
336
488
|
if (!body.basePath.startsWith('/')) {
|
|
337
489
|
E.toast('Base path must start with /.', {type: 'error'});
|
|
@@ -341,9 +493,12 @@ export async function settingsPanel({onClose = null} = {}) {
|
|
|
341
493
|
try {
|
|
342
494
|
const res = await H.put(`${BASE}/settings`, body);
|
|
343
495
|
if (res?.error) throw new Error(res.error);
|
|
344
|
-
// The server tidies the path ("/news/" is saved as "/news"); show what it
|
|
496
|
+
// The server tidies the path ("/news/" is saved as "/news"); show what it
|
|
497
|
+
// kept - unless it was typed over while saving.
|
|
345
498
|
const savedBase = (res?.settings ?? res?.data?.settings)?.basePath;
|
|
346
|
-
if (savedBase
|
|
499
|
+
if (savedBase && String(f.basePath.peek()).trim() === body.basePath) f.basePath.value = savedBase;
|
|
500
|
+
if (savedBase) body.basePath = savedBase;
|
|
501
|
+
// What was SENT is the new baseline: anything typed meanwhile stays unsaved.
|
|
347
502
|
baseline.value = {...body};
|
|
348
503
|
basePathCache = null;
|
|
349
504
|
status.value = 'Saved.';
|
|
@@ -357,14 +512,69 @@ export async function settingsPanel({onClose = null} = {}) {
|
|
|
357
512
|
}
|
|
358
513
|
});
|
|
359
514
|
if (typeof I !== 'undefined' && I.scan) I.scan(el);
|
|
360
|
-
return {
|
|
515
|
+
return {
|
|
516
|
+
el,
|
|
517
|
+
failed,
|
|
518
|
+
was: failed ? '' : draftLib.versionOf(initial, SETTING_KEYS),
|
|
519
|
+
isDirty,
|
|
520
|
+
values: current,
|
|
521
|
+
restore(v) {
|
|
522
|
+
for (const k of SETTING_KEYS) if (k in (v ?? {})) f[k].value = v[k];
|
|
523
|
+
},
|
|
524
|
+
dispose: () => { handle.dispose(); disposeHelp(); }
|
|
525
|
+
};
|
|
361
526
|
}
|
|
362
527
|
|
|
363
|
-
/**
|
|
528
|
+
/**
|
|
529
|
+
* A kept settings draft, offered back when the settings open: "Throw away"
|
|
530
|
+
* drops it; Cancel/Escape put the changes back in the form. Left alone when
|
|
531
|
+
* `stillHere()` turns false while asking - it is there for next time.
|
|
532
|
+
*/
|
|
533
|
+
export async function offerSettingsDraft(panel, stillHere) {
|
|
534
|
+
const draft = drafts.read('settings');
|
|
535
|
+
if (!draft || panel.failed) return;
|
|
536
|
+
const state = draft.was && draft.was !== panel.was ? 'changed' : 'same';
|
|
537
|
+
const throwAway = await askThrowAway(draftLib.restoreQuestion({what: 'settings', state}));
|
|
538
|
+
if (!stillHere()) return;
|
|
539
|
+
drafts.drop('settings');
|
|
540
|
+
if (!throwAway) panel.restore(draft.values);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Keep the settings form's unsaved changes for next time. */
|
|
544
|
+
export function keepSettingsDraft(panel) {
|
|
545
|
+
if (!panel.isDirty()) return false;
|
|
546
|
+
drafts.keep('settings', {id: 'settings', name: '', values: panel.values(), was: panel.was});
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The banner cog's slideover. Resizable, like every admin slideover. One at a
|
|
552
|
+
* time; closing with unsaved changes asks; leaving the screen keeps them as a
|
|
553
|
+
* draft, offered back next time.
|
|
554
|
+
*/
|
|
364
555
|
export async function openSettings() {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
so
|
|
368
|
-
|
|
369
|
-
|
|
556
|
+
const release = claim('settings');
|
|
557
|
+
if (!release) return;
|
|
558
|
+
let so = null;
|
|
559
|
+
try {
|
|
560
|
+
const panel = await settingsPanel({onClose: () => so?.close()});
|
|
561
|
+
// A kept draft is asked about BEFORE the panel opens, so the Escape
|
|
562
|
+
// that keeps it cannot also close the panel. Left while asked: kept.
|
|
563
|
+
const at = location.hash;
|
|
564
|
+
await offerSettingsDraft(panel, () => location.hash === at);
|
|
565
|
+
if (location.hash !== at) { panel.dispose(); release(); return; }
|
|
566
|
+
so = E.slideover({title: 'Blog settings', size: 'sm', position: 'right'});
|
|
567
|
+
so.element.appendChild(panel.el);
|
|
568
|
+
let unhook = () => {};
|
|
569
|
+
onceClosed(so, () => { unhook(); panel.dispose(); release(); });
|
|
570
|
+
const guard = guardSlideover(so, {dirty: panel.isDirty});
|
|
571
|
+
unhook = closeOnLeave(so, () => {
|
|
572
|
+
if (keepSettingsDraft(panel)) E.toast('Your unsaved settings are kept - open Blog settings again to pick them up.', {type: 'info'});
|
|
573
|
+
guard.close();
|
|
574
|
+
});
|
|
575
|
+
so.open();
|
|
576
|
+
} catch (err) {
|
|
577
|
+
release();
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
370
580
|
}
|