From 11e48f8d0507fb69d2c8644d44a2d3e9fca52f1e Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:26:20 -0400 Subject: [PATCH] Fix: open a widget's settings from its own config, not the last one edited The config form keeps values typed into it across a re-render, so a view switch does not discard them. That cache was matched on widget type alone and outlived the editor, so opening a second widget of the same type filled its form from the first: a tile reading one source showed another as its source, and saving would have written it. The cache now belongs to one opening of the editor. Switching type tiles inside an open editor still keeps what was typed, which clearing the cache outright would have discarded. --- CHANGELOG.md | 3 +++ ui/admin/index.html | 2 +- ui/js/admin-app-form.js | 8 ++++---- ui/js/admin-auth.js | 2 +- ui/js/admin-color-control.js | 2 +- ui/js/admin-logic.js | 9 +++++++++ ui/js/admin-settings.js | 4 ++-- ui/js/admin-shared.js | 2 +- ui/js/admin-state.js | 2 ++ ui/js/admin-widget-form.js | 25 +++++++++++++++++++------ ui/js/admin.js | 15 ++++++++------- ui/js/widget-config-form.js | 6 +++--- ui/test/admin-logic.test.mjs | 15 +++++++++++++++ 13 files changed, 69 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b670c1..2cb610af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A widget's settings no longer open filled in from the last widget of the same + type you edited. + - A tile with only a fixed label set now shows that badge. - Saving settings no longer warns about deleting the stored password when diff --git a/ui/admin/index.html b/ui/admin/index.html index 4eec2f65..a5b14afe 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -303,6 +303,6 @@

General

- + diff --git a/ui/js/admin-app-form.js b/ui/js/admin-app-form.js index da34cb40..090de2bb 100644 --- a/ui/js/admin-app-form.js +++ b/ui/js/admin-app-form.js @@ -1,8 +1,8 @@ import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=b18c93ed'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { loadLocalIcons, resolveIcon, iconChain, cdnIconName } from '/js/icons.js?v=69c2b9bd'; -import { state } from '/js/admin-state.js?v=42393ee1'; -import { isDockBlocked, DOCK_MAX, clearsStoredSecret } from '/js/admin-logic.js?v=80a4637d'; +import { state } from '/js/admin-state.js?v=b7731aa4'; +import { isDockBlocked, DOCK_MAX, clearsStoredSecret } from '/js/admin-logic.js?v=f3f87abf'; import { t } from '/js/i18n.js?v=d056c9c5'; import { toast, @@ -13,8 +13,8 @@ import { initInlineEdit, setTogDisabled, wireChecklist, -} from '/js/admin-shared.js?v=9c6bb360'; -import { renderColorControl, BADGE_SWATCHES } from '/js/admin-color-control.js?v=c853fe21'; +} from '/js/admin-shared.js?v=3d2627a9'; +import { renderColorControl, BADGE_SWATCHES } from '/js/admin-color-control.js?v=3a3a1bad'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; export function buildFolderForm(body, item) { diff --git a/ui/js/admin-auth.js b/ui/js/admin-auth.js index 0090af30..1597716f 100644 --- a/ui/js/admin-auth.js +++ b/ui/js/admin-auth.js @@ -1,4 +1,4 @@ -import { ag, ap } from '/js/admin-shared.js?v=9c6bb360'; +import { ag, ap } from '/js/admin-shared.js?v=3d2627a9'; import { t } from '/js/i18n.js?v=d056c9c5'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; import { el, inp as inpById, qa } from '/js/utils.js?v=b18c93ed'; diff --git a/ui/js/admin-color-control.js b/ui/js/admin-color-control.js index f78f3fc9..bb7a5880 100644 --- a/ui/js/admin-color-control.js +++ b/ui/js/admin-color-control.js @@ -1,4 +1,4 @@ -import { PE_SVG, initInlineEdit } from '/js/admin-shared.js?v=9c6bb360'; +import { PE_SVG, initInlineEdit } from '/js/admin-shared.js?v=3d2627a9'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { qa, q } from '/js/utils.js?v=b18c93ed'; diff --git a/ui/js/admin-logic.js b/ui/js/admin-logic.js index 9be9a7ee..6f8df4c6 100644 --- a/ui/js/admin-logic.js +++ b/ui/js/admin-logic.js @@ -1,6 +1,15 @@ // @ts-check /* Keep this module free of the DOM and of imports. Tests load it directly. */ +/* Values typed into a widget's config form survive a re-render of that same + form, and nothing else. Opening the editor again is a new session, so the + saved config wins, or one widget's form would fill in from the last widget of + the same type that was open. */ +export function carriesTypedValues(cached, wtype, session) { + if (!cached || !cached.form) return false; + return cached.type === wtype && cached.session === session; +} + export function seedCarried(config, carryKeys) { const out = Object.create(null); for (const k of carryKeys || []) if (config && config[k] !== undefined) out[k] = config[k]; diff --git a/ui/js/admin-settings.js b/ui/js/admin-settings.js index ff71ce51..0546d40d 100644 --- a/ui/js/admin-settings.js +++ b/ui/js/admin-settings.js @@ -1,7 +1,7 @@ -import { toast, ag, ap } from '/js/admin-shared.js?v=9c6bb360'; +import { toast, ag, ap } from '/js/admin-shared.js?v=3d2627a9'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; import { t } from '/js/i18n.js?v=d056c9c5'; -import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=80a4637d'; +import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=f3f87abf'; import { el, inp, q, qa, setUserText } from '/js/utils.js?v=b18c93ed'; /* Mirrors the server's rule: auth cannot be switched on with no password. */ diff --git a/ui/js/admin-shared.js b/ui/js/admin-shared.js index a58fa6b5..ab025c53 100644 --- a/ui/js/admin-shared.js +++ b/ui/js/admin-shared.js @@ -1,5 +1,5 @@ /* Stateless helpers shared by the admin modules. Mutable state stays out. */ -import { nextActiveIndex, recoversSession, toastMs } from '/js/admin-logic.js?v=80a4637d'; +import { nextActiveIndex, recoversSession, toastMs } from '/js/admin-logic.js?v=f3f87abf'; import { el, qa, q } from '/js/utils.js?v=b18c93ed'; import { t } from '/js/i18n.js?v=d056c9c5'; diff --git a/ui/js/admin-state.js b/ui/js/admin-state.js index 10b8baf4..cc5e884b 100644 --- a/ui/js/admin-state.js +++ b/ui/js/admin-state.js @@ -28,4 +28,6 @@ export const state = { _wAutoCfg: {}, _autoForm: null, _autoFormType: null, + _autoFormSession: null, + _evSession: 0, }; diff --git a/ui/js/admin-widget-form.js b/ui/js/admin-widget-form.js index 186206e2..c26957f7 100644 --- a/ui/js/admin-widget-form.js +++ b/ui/js/admin-widget-form.js @@ -1,8 +1,14 @@ -import { state } from '/js/admin-state.js?v=42393ee1'; -import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=9c6bb360'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=e2dfacc3'; +import { state } from '/js/admin-state.js?v=b7731aa4'; +import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=3d2627a9'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=bf180b36'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { sizesForView, widgetConfigMode, rejectionLines, refusedNoticeKey } from '/js/admin-logic.js?v=80a4637d'; +import { + sizesForView, + widgetConfigMode, + rejectionLines, + refusedNoticeKey, + carriesTypedValues, +} from '/js/admin-logic.js?v=f3f87abf'; import { t } from '/js/i18n.js?v=d056c9c5'; import { q, qi, qa } from '/js/utils.js?v=b18c93ed'; @@ -35,8 +41,14 @@ export function buildWidgetForm(body, item) { } function _renderWidgetForm(body) { - /* Re-render of the same registry widget: keep typed values. */ - if (state._autoForm && state._autoFormType === state._wtype) { + /* Re-render of the same form in the same editing session: keep typed values. */ + if ( + carriesTypedValues( + { form: state._autoForm, type: state._autoFormType, session: state._autoFormSession }, + state._wtype, + state._evSession, + ) + ) { state._wAutoCfg = Object.assign({}, state._wAutoCfg, state._autoForm.getValues()); } state._autoForm = null; @@ -120,6 +132,7 @@ function _renderWidgetForm(body) { }, }); state._autoFormType = state._wtype; + state._autoFormSession = state._evSession; } else if (_mode === 'unavailable') _renderUnavailableConfig(body); else _renderCustomConfig(body); } diff --git a/ui/js/admin.js b/ui/js/admin.js index be893a92..00ab543a 100644 --- a/ui/js/admin.js +++ b/ui/js/admin.js @@ -1,7 +1,7 @@ -import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=70e2d15b'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=cb00465f'; +import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=f8fda76d'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=76be763e'; import { applyDrop, canJoinFolder } from '/js/admin-drag-logic.js?v=8fcf583a'; -import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=80a4637d'; +import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=f3f87abf'; import { buildAppItem, claimFolderChildren, @@ -10,10 +10,10 @@ import { snapshotItems, upsertItem, } from '/js/admin-save-logic.js?v=52a970d3'; -import { loadSettings, showBgFields } from '/js/admin-settings.js?v=263d3422'; -import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=9c6bb360'; -import { state } from '/js/admin-state.js?v=42393ee1'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=04ee8ab3'; +import { loadSettings, showBgFields } from '/js/admin-settings.js?v=e9bec962'; +import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=3d2627a9'; +import { state } from '/js/admin-state.js?v=b7731aa4'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=5292c6a0'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=d056c9c5'; import { iconChain, loadLocalIcons, resolveIcon } from '/js/icons.js?v=69c2b9bd'; @@ -695,6 +695,7 @@ function openModal(idx) { state._evItem = item; state._evIsEdit = isEdit; + state._evSession += 1; _renderEditBody(); showEditView(); diff --git a/ui/js/widget-config-form.js b/ui/js/widget-config-form.js index 55c8dce6..6a3f33ab 100644 --- a/ui/js/widget-config-form.js +++ b/ui/js/widget-config-form.js @@ -4,8 +4,8 @@ import { t } from '/js/i18n.js?v=d056c9c5'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { wireChecklist } from '/js/admin-shared.js?v=9c6bb360'; -import { renderColorControl } from '/js/admin-color-control.js?v=c853fe21'; +import { wireChecklist } from '/js/admin-shared.js?v=3d2627a9'; +import { renderColorControl } from '/js/admin-color-control.js?v=3a3a1bad'; import { seedCarried, applyOptionSet, @@ -13,7 +13,7 @@ import { requiredFieldMissing, groupBounds, visibleFieldFlags, -} from '/js/admin-logic.js?v=80a4637d'; +} from '/js/admin-logic.js?v=f3f87abf'; import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; import { qi } from '/js/utils.js?v=b18c93ed'; diff --git a/ui/test/admin-logic.test.mjs b/ui/test/admin-logic.test.mjs index eeba15dc..0096ff17 100644 --- a/ui/test/admin-logic.test.mjs +++ b/ui/test/admin-logic.test.mjs @@ -6,6 +6,7 @@ import { nextActiveIndex, groupBounds, visibleFieldKeys, + carriesTypedValues, visibleFieldFlags, collectFieldValues, clearsStoredSecret, @@ -176,6 +177,20 @@ test('visibleFieldKeys does not leak a field across a hidden branch default', () assert.deepEqual([...shown], ['source']); }); +/* Two tiles of one widget type each open the same form. Carrying values by type + alone filled the second tile's form from the first, so a Beszel tile opened + after a Glances one showed Glances as its source. */ +test('typed values are carried within one editing session, never into the next', () => { + const form = { getValues: () => ({}) }; + const opened = { form, type: 'system-summary', session: 4 }; + + assert.equal(carriesTypedValues(opened, 'system-summary', 4), true, 'a re-render of the same open form'); + assert.equal(carriesTypedValues(opened, 'system-summary', 5), false, 'another widget of the same type'); + assert.equal(carriesTypedValues(opened, 'dns', 4), false, 'a different widget type'); + assert.equal(carriesTypedValues({ form: null, type: 'system-summary', session: 4 }, 'system-summary', 4), false); + assert.equal(carriesTypedValues(null, 'system-summary', 4), false); +}); + test('only the visible copy of a repeated key is saved', () => { /* The system summary declares its slot list once per source. Both are built, one is on screen, and the saved config must carry that one alone. */