diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs
index b85307a..f2e72f9 100644
--- a/tests/test_downloads_tab_workflow_route.mjs
+++ b/tests/test_downloads_tab_workflow_route.mjs
@@ -2416,6 +2416,96 @@ test('workflow analysis and loaded model caches stay independent and cloned', ()
assert.equal(dialog.workflowAnalysisCaches.get(dialog.workflowKey).data.missing_models.length, 1);
});
+test('Missing Models filter toggles persist state and rerender once per change', () => {
+ const bindMissingFilterToggle = eval(
+ `(${extractMethod(missingBrowserMethodsSource, 'bindMissingFilterToggle')})`
+ );
+ const wireMissingModelsBrowser = eval(
+ `(${extractMethod(missingBrowserMethodsSource, 'wireMissingModelsBrowser')})`
+ );
+ const window = new Window();
+ const previousDocument = globalThis.document;
+ const previousElement = globalThis.Element;
+ const previousHTMLElement = globalThis.HTMLElement;
+ const previousLocalStorage = globalThis.localStorage;
+ const localStorageValues = new Map();
+ const container = window.document.createElement('div');
+ container.innerHTML = `
+
+
+
+
+
+ `;
+ const renderOptions = [];
+ const dialog = {
+ showResolvedModels: false,
+ showAutoDownloadModels: true,
+ showInactiveModels: false,
+ showResolvedModelsStorageKey: 'test_show_resolved_models',
+ showAutoDownloadModelsStorageKey: 'test_show_auto_download_models',
+ showInactiveModelsStorageKey: 'test_show_inactive_models',
+ missingModelsTypeFilterMenuOpen: false,
+ bindMissingFilterToggle,
+ wireMissingBrowserSplitter() {},
+ wireVisibleMissingModelRows() {},
+ setupMissingModelsVirtualizer() {},
+ displayMissingModels(_container, _data, options) {
+ renderOptions.push(options);
+ },
+ };
+
+ globalThis.document = window.document;
+ globalThis.Element = window.Element;
+ globalThis.HTMLElement = window.HTMLElement;
+ globalThis.localStorage = {
+ getItem(key) {
+ return localStorageValues.has(key) ? localStorageValues.get(key) : null;
+ },
+ setItem(key, value) {
+ localStorageValues.set(key, String(value));
+ },
+ removeItem(key) {
+ localStorageValues.delete(key);
+ },
+ };
+
+ try {
+ wireMissingModelsBrowser.call(dialog, container, {}, []);
+ wireMissingModelsBrowser.call(dialog, container, {}, []);
+
+ const resolvedToggle = container.querySelector('#mr-show-resolved-models');
+ const autoDownloadToggle = container.querySelector('#mr-show-auto-download-models');
+ const inactiveToggle = container.querySelector('#mr-show-inactive-models');
+ resolvedToggle.checked = true;
+ autoDownloadToggle.checked = false;
+ inactiveToggle.checked = true;
+ resolvedToggle.dispatchEvent(new window.Event('change'));
+ autoDownloadToggle.dispatchEvent(new window.Event('change'));
+ inactiveToggle.dispatchEvent(new window.Event('change'));
+
+ assert.equal(dialog.showResolvedModels, true);
+ assert.equal(dialog.showAutoDownloadModels, false);
+ assert.equal(dialog.showInactiveModels, true);
+ assert.equal(safeStorage.getItem(dialog.showResolvedModelsStorageKey), '1');
+ assert.equal(safeStorage.getItem(dialog.showAutoDownloadModelsStorageKey), '0');
+ assert.equal(safeStorage.getItem(dialog.showInactiveModelsStorageKey), '1');
+ assert.deepEqual(renderOptions, [
+ { preserveBrowser: true },
+ { preserveBrowser: true },
+ { preserveBrowser: true },
+ ]);
+ } finally {
+ safeStorage.removeItem(dialog.showResolvedModelsStorageKey);
+ safeStorage.removeItem(dialog.showAutoDownloadModelsStorageKey);
+ safeStorage.removeItem(dialog.showInactiveModelsStorageKey);
+ globalThis.document = previousDocument;
+ globalThis.Element = previousElement;
+ globalThis.HTMLElement = previousHTMLElement;
+ globalThis.localStorage = previousLocalStorage;
+ }
+});
+
test('node widget changes request a content-preserving Missing Models refresh', async () => {
const log = { debug() {} };
const refreshForActiveWorkflowChange = eval(
diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js
index acc7ffe..d185474 100644
--- a/web/resolver/views/missing_browser_methods.js
+++ b/web/resolver/views/missing_browser_methods.js
@@ -1083,6 +1083,19 @@ export const missingBrowserMethods = {
});
},
+ bindMissingFilterToggle(toggle, { boundKey, storageKey, setValue, container, data }) {
+ if (!toggle || toggle.dataset[boundKey] === 'true') return;
+ toggle.dataset[boundKey] = 'true';
+ toggle.addEventListener('change', () => {
+ const value = Boolean(toggle.checked);
+ setValue(value);
+ try {
+ safeStorage.setItem(storageKey, value ? '1' : '0');
+ } catch (_e) {}
+ this.displayMissingModels(container, data, { preserveBrowser: true });
+ });
+ },
+
wireMissingModelsBrowser(container, data, sortedMissingModels) {
this.wireMissingBrowserSplitter(container);
@@ -1161,40 +1174,31 @@ export const missingBrowserMethods = {
}
const showResolvedToggle = container.querySelector('#mr-show-resolved-models');
- if (showResolvedToggle && showResolvedToggle.dataset.mlResolvedBound !== 'true') {
- showResolvedToggle.dataset.mlResolvedBound = 'true';
- showResolvedToggle.addEventListener('change', () => {
- this.showResolvedModels = Boolean(showResolvedToggle.checked);
- try {
- safeStorage.setItem(this.showResolvedModelsStorageKey, this.showResolvedModels ? '1' : '0');
- } catch (_e) {}
- this.displayMissingModels(container, data, { preserveBrowser: true });
- });
- }
+ this.bindMissingFilterToggle(showResolvedToggle, {
+ boundKey: 'mlResolvedBound',
+ storageKey: this.showResolvedModelsStorageKey,
+ setValue: value => { this.showResolvedModels = value; },
+ container,
+ data,
+ });
const showAutoDownloadToggle = container.querySelector('#mr-show-auto-download-models');
- if (showAutoDownloadToggle && showAutoDownloadToggle.dataset.mlAutoDownloadBound !== 'true') {
- showAutoDownloadToggle.dataset.mlAutoDownloadBound = 'true';
- showAutoDownloadToggle.addEventListener('change', () => {
- this.showAutoDownloadModels = Boolean(showAutoDownloadToggle.checked);
- try {
- safeStorage.setItem(this.showAutoDownloadModelsStorageKey, this.showAutoDownloadModels ? '1' : '0');
- } catch (_e) {}
- this.displayMissingModels(container, data, { preserveBrowser: true });
- });
- }
+ this.bindMissingFilterToggle(showAutoDownloadToggle, {
+ boundKey: 'mlAutoDownloadBound',
+ storageKey: this.showAutoDownloadModelsStorageKey,
+ setValue: value => { this.showAutoDownloadModels = value; },
+ container,
+ data,
+ });
const showInactiveToggle = container.querySelector('#mr-show-inactive-models');
- if (showInactiveToggle && showInactiveToggle.dataset.mlInactiveBound !== 'true') {
- showInactiveToggle.dataset.mlInactiveBound = 'true';
- showInactiveToggle.addEventListener('change', () => {
- this.showInactiveModels = Boolean(showInactiveToggle.checked);
- try {
- safeStorage.setItem(this.showInactiveModelsStorageKey, this.showInactiveModels ? '1' : '0');
- } catch (_e) {}
- this.displayMissingModels(container, data, { preserveBrowser: true });
- });
- }
+ this.bindMissingFilterToggle(showInactiveToggle, {
+ boundKey: 'mlInactiveBound',
+ storageKey: this.showInactiveModelsStorageKey,
+ setValue: value => { this.showInactiveModels = value; },
+ container,
+ data,
+ });
const getCurrentMissingModels = () => (
Array.isArray(this.missingModels)