Skip to content

Commit 9aa7077

Browse files
committed
feat: preserve Missing Models browser during refresh instead of replacing it
1 parent df01b17 commit 9aa7077

3 files changed

Lines changed: 193 additions & 26 deletions

File tree

tests/test_downloads_tab_workflow_route.mjs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,7 @@ test('background Missing Models refresh keeps the current view until new data is
18391839
});
18401840
let progressRenderCount = 0;
18411841
let progressPollCount = 0;
1842+
let displayOptions = null;
18421843
const dialog = {
18431844
activeTab: 'missing',
18441845
contentElement,
@@ -1859,7 +1860,8 @@ test('background Missing Models refresh keeps the current view until new data is
18591860
applyResolvedSelectionAliasesToAnalysisData() {},
18601861
saveAnalysisCacheForActiveWorkflow() {},
18611862
ensureDownloadDirectoriesLoaded: async () => {},
1862-
displayMissingModels(container, data) {
1863+
displayMissingModels(container, data, options) {
1864+
displayOptions = options;
18631865
container.innerHTML = `<div>${data.resolved_models[0].strength}</div>`;
18641866
container.scrollTop = 0;
18651867
},
@@ -1888,6 +1890,47 @@ test('background Missing Models refresh keeps the current view until new data is
18881890

18891891
assert.equal(contentElement.innerHTML, '<div>0.75</div>');
18901892
assert.equal(contentElement.scrollTop, 36);
1893+
assert.deepEqual(displayOptions, { preserveBrowser: true });
1894+
});
1895+
1896+
test('content-preserving Missing Models refresh patches the browser instead of clearing it', () => {
1897+
const patchMissingModelsBrowserElement = extractMethod(
1898+
missingBrowserMethodsSource,
1899+
'patchMissingModelsBrowserElement'
1900+
);
1901+
const displayMissingModels = extractMethod(
1902+
missingBrowserMethodsSource,
1903+
'displayMissingModels'
1904+
);
1905+
1906+
assert.match(
1907+
patchMissingModelsBrowserElement,
1908+
/currentRow\?\.outerHTML === nextRow\.outerHTML/
1909+
);
1910+
assert.doesNotMatch(
1911+
patchMissingModelsBrowserElement,
1912+
/currentBrowser\.replaceWith\(nextBrowser\)/
1913+
);
1914+
assert.match(
1915+
patchMissingModelsBrowserElement,
1916+
/currentList\.appendChild\(row\)/
1917+
);
1918+
assert.match(
1919+
patchMissingModelsBrowserElement,
1920+
/currentDetail\.innerHTML = nextDetail\.innerHTML/
1921+
);
1922+
assert.doesNotMatch(
1923+
patchMissingModelsBrowserElement,
1924+
/currentDetail\.replaceWith\(nextDetail\)/
1925+
);
1926+
assert.match(
1927+
displayMissingModels,
1928+
/options\.preserveBrowser[\s\S]*?patchMissingModelsBrowserElement/
1929+
);
1930+
assert.match(
1931+
displayMissingModels,
1932+
/if \(!browserPatched\) \{\s*container\.innerHTML = browserHtml;/
1933+
);
18911934
});
18921935

18931936
test('node widget changes request a content-preserving Missing Models refresh', async () => {

web/resolver/shell/lifecycle_graph_methods.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,11 @@ export const lifecycleGraphMethods = {
167167
) {
168168
if (shouldRenderMissingModels()) {
169169
await this.ensureDownloadDirectoriesLoaded();
170-
this.displayMissingModels(this.contentElement, this.cachedAnalysisData);
170+
this.displayMissingModels(
171+
this.contentElement,
172+
this.cachedAnalysisData,
173+
{ preserveBrowser: preserveContent }
174+
);
171175
this.applyPendingWorkflowModelSelection?.(this.cachedAnalysisData);
172176
this.reconnectActiveDownloads();
173177
}
@@ -190,7 +194,11 @@ export const lifecycleGraphMethods = {
190194
}
191195
if (shouldRenderMissingModels()) {
192196
await this.ensureDownloadDirectoriesLoaded();
193-
this.displayMissingModels(this.contentElement, data);
197+
this.displayMissingModels(
198+
this.contentElement,
199+
data,
200+
{ preserveBrowser: preserveContent }
201+
);
194202
this.applyPendingWorkflowModelSelection?.(data);
195203
}
196204
return data;
@@ -231,7 +239,11 @@ export const lifecycleGraphMethods = {
231239
}
232240
if (shouldRenderMissingModels()) {
233241
await this.ensureDownloadDirectoriesLoaded();
234-
this.displayMissingModels(this.contentElement, data);
242+
this.displayMissingModels(
243+
this.contentElement,
244+
data,
245+
{ preserveBrowser: preserveContent }
246+
);
235247
this.applyPendingWorkflowModelSelection?.(data);
236248
if (preservedScrollTop !== null) {
237249
this.contentElement.scrollTop = preservedScrollTop;

web/resolver/views/missing_browser_methods.js

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -784,17 +784,99 @@ export const missingBrowserMethods = {
784784
return html;
785785
},
786786

787+
patchMissingModelsBrowserElement(container, html) {
788+
const currentBrowser = container?.querySelector?.('.mr-missing-browser');
789+
if (!currentBrowser || typeof document === 'undefined') return false;
790+
791+
const template = document.createElement('template');
792+
template.innerHTML = String(html || '').trim();
793+
const nextBrowser = template.content.firstElementChild;
794+
const currentList = currentBrowser.querySelector('.mr-missing-list');
795+
const nextList = nextBrowser?.querySelector?.('.mr-missing-list');
796+
if (!nextBrowser || !currentList || !nextList) return false;
797+
798+
currentBrowser.style.cssText = nextBrowser.style.cssText;
799+
const currentToolbar = currentBrowser.querySelector('.mr-missing-list-toolbar');
800+
const nextToolbar = nextBrowser.querySelector('.mr-missing-list-toolbar');
801+
if (currentToolbar && nextToolbar) {
802+
currentToolbar.replaceWith(nextToolbar);
803+
}
804+
805+
const currentHead = currentList.querySelector('.mr-missing-list-head');
806+
const nextHead = nextList.querySelector('.mr-missing-list-head');
807+
if (currentHead && nextHead) {
808+
currentHead.replaceWith(nextHead);
809+
}
810+
811+
const currentRows = new Map(
812+
Array.from(currentList.querySelectorAll('.mr-missing-list-row'))
813+
.map(row => [row.dataset.missingKey || '', row])
814+
.filter(([key]) => key)
815+
);
816+
const retainedRows = new Set();
817+
nextList.querySelectorAll('.mr-missing-list-row').forEach((nextRow) => {
818+
const currentRow = currentRows.get(nextRow.dataset.missingKey || '');
819+
const row = currentRow?.outerHTML === nextRow.outerHTML
820+
? currentRow
821+
: nextRow;
822+
retainedRows.add(row);
823+
currentList.appendChild(row);
824+
});
825+
currentRows.forEach((row) => {
826+
if (!retainedRows.has(row)) row.remove();
827+
});
828+
829+
const currentSplitter = currentBrowser.querySelector('.mr-missing-browser-splitter');
830+
const nextSplitter = nextBrowser.querySelector('.mr-missing-browser-splitter');
831+
if (currentSplitter && nextSplitter) {
832+
currentSplitter.replaceWith(nextSplitter);
833+
}
834+
835+
const currentDetail = currentBrowser.querySelector('.mr-missing-detail-pane');
836+
const nextDetail = nextBrowser.querySelector('.mr-missing-detail-pane');
837+
this._missingBrowserDetailPreserved = Boolean(
838+
currentDetail
839+
&& nextDetail
840+
&& currentDetail.outerHTML === nextDetail.outerHTML
841+
);
842+
if (currentDetail && nextDetail && !this._missingBrowserDetailPreserved) {
843+
currentDetail.innerHTML = nextDetail.innerHTML;
844+
}
845+
return true;
846+
},
847+
787848
wireMissingModelsBrowser(container, data, sortedMissingModels) {
788849
this.wireMissingBrowserSplitter(container);
789850

790851
const browser = container.querySelector('.mr-missing-browser');
852+
const getCurrentData = () => this.cachedAnalysisData || data;
853+
const getCurrentMissingModels = () => (
854+
Array.isArray(this.missingModels)
855+
? this.missingModels
856+
: sortedMissingModels
857+
);
858+
if (!(this._wiredMissingModelRows instanceof WeakSet)) {
859+
this._wiredMissingModelRows = new WeakSet();
860+
}
861+
if (!(this._wiredMissingLocateButtons instanceof WeakSet)) {
862+
this._wiredMissingLocateButtons = new WeakSet();
863+
}
864+
if (!(this._wiredMissingBrowsers instanceof WeakSet)) {
865+
this._wiredMissingBrowsers = new WeakSet();
866+
}
791867
const typeFilterToggle = browser?.querySelector('[data-missing-type-filter-toggle]');
792-
const typeFilterMenu = browser?.querySelector('.mr-missing-type-filter-menu');
793868
const setTypeFilterMenuOpen = (open) => {
794869
this.missingModelsTypeFilterMenuOpen = Boolean(open);
795-
if (typeFilterMenu) typeFilterMenu.hidden = !this.missingModelsTypeFilterMenuOpen;
796-
if (typeFilterToggle) {
797-
typeFilterToggle.setAttribute('aria-expanded', this.missingModelsTypeFilterMenuOpen ? 'true' : 'false');
870+
const activeMenu = browser?.querySelector('.mr-missing-type-filter-menu');
871+
const activeToggle = browser?.querySelector('[data-missing-type-filter-toggle]');
872+
if (activeMenu) {
873+
activeMenu.hidden = !this.missingModelsTypeFilterMenuOpen;
874+
}
875+
if (activeToggle) {
876+
activeToggle.setAttribute(
877+
'aria-expanded',
878+
this.missingModelsTypeFilterMenuOpen ? 'true' : 'false'
879+
);
798880
}
799881
};
800882

@@ -814,18 +896,31 @@ export const missingBrowserMethods = {
814896
});
815897
});
816898

817-
browser?.addEventListener('click', (event) => {
818-
if (!this.missingModelsTypeFilterMenuOpen) return;
819-
if (event.target instanceof Element && event.target.closest('.mr-missing-type-filter-wrap')) return;
820-
setTypeFilterMenuOpen(false);
821-
});
899+
if (browser && !this._wiredMissingBrowsers.has(browser)) {
900+
this._wiredMissingBrowsers.add(browser);
901+
browser.addEventListener('click', (event) => {
902+
if (!this.missingModelsTypeFilterMenuOpen) return;
903+
if (
904+
event.target instanceof Element
905+
&& event.target.closest('.mr-missing-type-filter-wrap')
906+
) {
907+
return;
908+
}
909+
setTypeFilterMenuOpen(false);
910+
});
822911

823-
browser?.addEventListener('keydown', (event) => {
824-
if (event.key !== 'Escape' || !this.missingModelsTypeFilterMenuOpen) return;
825-
event.preventDefault();
826-
setTypeFilterMenuOpen(false);
827-
typeFilterToggle?.focus();
828-
});
912+
browser.addEventListener('keydown', (event) => {
913+
if (
914+
event.key !== 'Escape'
915+
|| !this.missingModelsTypeFilterMenuOpen
916+
) {
917+
return;
918+
}
919+
event.preventDefault();
920+
setTypeFilterMenuOpen(false);
921+
browser.querySelector('[data-missing-type-filter-toggle]')?.focus();
922+
});
923+
}
829924

830925
const refreshBtn = container.querySelector('#mr-refresh-missing-analysis');
831926
if (refreshBtn && refreshBtn.dataset.mlRefreshBound !== 'true') {
@@ -861,7 +956,11 @@ export const missingBrowserMethods = {
861956
const key = row.dataset.missingKey;
862957
if (!key || key === this.selectedMissingModelKey) return;
863958
this.selectedMissingModelKey = key;
864-
this.displayMissingModels(container, data, { selectionOnly: true });
959+
this.displayMissingModels(
960+
container,
961+
getCurrentData(),
962+
{ selectionOnly: true }
963+
);
865964
};
866965

867966
const selectAllCheckbox = container.querySelector('.mr-missing-select-all-check');
@@ -873,7 +972,7 @@ export const missingBrowserMethods = {
873972
selectAllCheckbox.addEventListener('change', () => {
874973
const shouldSelectAll = selectAllCheckbox.checked;
875974
this.batchSelectedMissingKeys = shouldSelectAll
876-
? new Set((sortedMissingModels || []).map(missing => this.getMissingModelKey(missing)))
975+
? new Set(getCurrentMissingModels().map(missing => this.getMissingModelKey(missing)))
877976
: new Set();
878977
this.lastBatchSelectedMissingKey = null;
879978
this.refreshBatchSelectionUi();
@@ -882,21 +981,24 @@ export const missingBrowserMethods = {
882981
}
883982

884983
container.querySelectorAll('.mr-missing-list-row').forEach(row => {
984+
if (this._wiredMissingModelRows.has(row)) return;
985+
this._wiredMissingModelRows.add(row);
885986
const checkbox = row.querySelector('.mr-missing-row-check');
886987
if (checkbox) {
887988
checkbox.addEventListener('click', (event) => {
888989
event.stopPropagation();
889-
checkbox.dataset.shiftClick = event.shiftKey ? '1' : '0';
990+
checkbox._missingShiftClick = event.shiftKey;
890991
});
891992
checkbox.addEventListener('change', (event) => {
892993
const key = row.dataset.missingKey;
893994
if (!key) return;
894995
const selected = checkbox.checked;
895-
const isShiftRange = event.shiftKey || checkbox.dataset.shiftClick === '1';
996+
const isShiftRange = event.shiftKey || checkbox._missingShiftClick === true;
997+
const currentMissingModels = getCurrentMissingModels();
896998

897999
if (isShiftRange && this.lastBatchSelectedMissingKey) {
8981000
this.applyBatchSelectionRange(
899-
sortedMissingModels,
1001+
currentMissingModels,
9001002
this.lastBatchSelectedMissingKey,
9011003
key,
9021004
selected
@@ -925,6 +1027,8 @@ export const missingBrowserMethods = {
9251027
});
9261028

9271029
container.querySelectorAll('.mr-missing-row-locate').forEach(button => {
1030+
if (this._wiredMissingLocateButtons.has(button)) return;
1031+
this._wiredMissingLocateButtons.add(button);
9281032
button.addEventListener('click', (event) => {
9291033
event.preventDefault();
9301034
event.stopPropagation();
@@ -938,7 +1042,9 @@ export const missingBrowserMethods = {
9381042
});
9391043

9401044
const selectedMissing = sortedMissingModels.find(missing => this.getMissingModelKey(missing) === this.selectedMissingModelKey);
941-
if (!selectedMissing) return;
1045+
const detailPreserved = this._missingBrowserDetailPreserved === true;
1046+
this._missingBrowserDetailPreserved = false;
1047+
if (!selectedMissing || detailPreserved) return;
9421048

9431049
const selectedIndex = sortedMissingModels.indexOf(selectedMissing);
9441050
this.wireMissingModelDetail(container, selectedMissing, selectedIndex);
@@ -2244,7 +2350,7 @@ export const missingBrowserMethods = {
22442350
return;
22452351
}
22462352

2247-
container.innerHTML = this.renderMissingModelsBrowser(
2353+
const browserHtml = this.renderMissingModelsBrowser(
22482354
sortedMissingModels,
22492355
this.selectedMissingModelKey,
22502356
sortedMissingModels.length,
@@ -2261,6 +2367,12 @@ export const missingBrowserMethods = {
22612367
activeTypeFilter,
22622368
}
22632369
);
2370+
this._missingBrowserDetailPreserved = false;
2371+
const browserPatched = options.preserveBrowser
2372+
&& this.patchMissingModelsBrowserElement(container, browserHtml);
2373+
if (!browserPatched) {
2374+
container.innerHTML = browserHtml;
2375+
}
22642376
this.wireMissingModelsBrowser(container, data, sortedMissingModels);
22652377
this.restoreMissingListScroll(container, listScrollSnapshot);
22662378
this.scheduleInitialUrnLocalMatchRefresh(sortedMissingModels, container, data);

0 commit comments

Comments
 (0)