fix(editor): refresh the icon preview on outside icon writes

A10: the editor's icon preview is painted by a closure nothing outside can
reach, so a favicon fetched from Settings (or an import overwrite) updated
the cards and left the open editor showing the old initials. soIconField
publishes its painter; saveEntryIcon — the choke point for every icon write
— routes through editorAbsorbChange like tag/folder already do.

Also whitelists .toast and #busyOverlay in the editor's click-outside: both
can sit under the cursor during a long operation, and clicking one counted
as clicking away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-23 21:19:21 +01:00
parent 47c29bd04e
commit 04bc809c71
2 changed files with 16 additions and 0 deletions
+4
View File
@@ -52,7 +52,11 @@ async function saveEntryIcon(entryId, dataUri) {
try { const b = await r.json(); if (b && b.error) msg = b.error; }
catch (_) {}
toast('Icon NOT saved: ' + msg, 'error');
return;
}
// Choke point for every icon write (favicon backfill, upload, remove,
// import overwrite) — refresh the editor if it's open on this entry.
editorAbsorbChange(entryId, { icon: dataUri || '' });
} catch (e) {
toast('Icon save failed: ' + (e && e.message || e), 'error');
}
+12
View File
@@ -3508,6 +3508,7 @@ function editorAbsorbChange(id, change) {
const sel = $('#soFolder');
if (sel) sel.value = change.folder;
}
if (change.icon !== undefined && soIconRepaint) soIconRepaint(change.icon || null);
if (change.tag && !soState.tags.includes(change.tag)) {
soState.tags.push(change.tag);
soState.original.tags = soState.original.tags
@@ -4271,6 +4272,11 @@ function soEditableField(label, id, value) {
// came from.
const ICON_MAX_BYTES = 256 * 1024; // matches server-side cap (256 KB)
// Repaint hook for the editor's icon preview, published by soIconField so an
// icon written from OUTSIDE the editor (favicon backfill, import) can refresh
// it. Cleared on close — the preview node dies with the panel.
let soIconRepaint = null;
function soIconField(entry) {
const wrap = el('div', { class: 'slideover-field so-icon-field' });
const preview = el('div', { class: 'so-icon-preview' });
@@ -4289,6 +4295,7 @@ function soIconField(entry) {
}
}
paintPreview(entry.icon_b64 || null);
soIconRepaint = paintPreview;
const actions = el('div', { class: 'so-icon-actions' });
@@ -4953,6 +4960,7 @@ function closeSlideOver() {
const ae = document.activeElement;
if (ae && $('#slideover').contains(ae) && typeof ae.blur === 'function') ae.blur();
$('#slideover').classList.remove('is-open');
soIconRepaint = null;
state.selectedId = null;
renderGrid();
}
@@ -7332,6 +7340,10 @@ async function init() {
// themselves what happens to the editor.
if (e.target.closest('.batch-bar')) return;
if (e.target.closest('.content-header')) return;
// A toast or the busy spinner can land under the cursor mid-operation;
// clicking one must not count as "clicked away from the editor".
if (e.target.closest('.toast')) return;
if (e.target.closest('#busyOverlay')) return;
if (e.target.closest('.cols-menu')) return;
if (e.target.closest('.filters-menu')) return;
if (e.target.closest('.user-menu')) return;