From 1bad09b0d59c2a3d14f421e8d3c48d98e8089d9b Mon Sep 17 00:00:00 2001 From: shiva Date: Sun, 1 Nov 2026 11:37:24 +0530 Subject: [PATCH] fix(data-browser): expand the panel of newly loaded data The panels were only opened when the set of patients or the presence of non-DICOM images changed, so loading another image or a new study for an existing patient left a collapsed panel collapsed. Open the panel whenever new images or volumes appear, without adding duplicate keys. --- src/components/DataBrowser.vue | 45 +++++++--- src/components/__tests__/DataBrowser.spec.ts | 91 ++++++++++++++++++++ tests/specs/pet-ct-sample.e2e.ts | 5 +- 3 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 src/components/__tests__/DataBrowser.spec.ts diff --git a/src/components/DataBrowser.vue b/src/components/DataBrowser.vue index 881db15d5..fd83713db 100644 --- a/src/components/DataBrowser.vue +++ b/src/components/DataBrowser.vue @@ -79,21 +79,40 @@ export default defineComponent({ { immediate: true } ); - watch( - [hasAnonymousImages, patients] as const, - ([showAnonymous, patients_]) => { - const showPatients = patients_.length > 0; - if (showAnonymous) { - panels.value.push(ANONYMOUS_DATA_KEY); - } - if (showPatients) { - panels.value.push(...patients_.map((patient) => patient.key)); - } - if (showAnonymous || showPatients) { - removeFromArray(panels.value, SAMPLE_DATA_KEY); - } + const openPanel = (key: string) => { + if (!panels.value.includes(key)) { + panels.value.push(key); } + removeFromArray(panels.value, SAMPLE_DATA_KEY); + }; + + // Expand the panel of newly loaded data, also when the panel already + // exists but was collapsed. + const anonymousImageIDs = computed(() => + imageStore.idList.filter((id) => isRegularImage(id)) ); + watch(anonymousImageIDs, (ids, oldIds) => { + if (ids.some((id) => !oldIds.includes(id))) { + openPanel(ANONYMOUS_DATA_KEY); + } + }); + + const patientVolumes = computed(() => + Object.fromEntries( + Object.entries(dicomStore.patientStudies).map(([patient, studies]) => [ + patient, + studies.flatMap((study) => dicomStore.studyVolumes[study] ?? []), + ]) + ) + ); + watch(patientVolumes, (volumes, oldVolumes) => { + Object.entries(volumes).forEach(([patient, ids]) => { + const oldIds = oldVolumes[patient] ?? []; + if (ids.some((id) => !oldIds.includes(id))) { + openPanel(patient); + } + }); + }); const deletePatient = (key: string) => { dicomStore.patientStudies[key] diff --git a/src/components/__tests__/DataBrowser.spec.ts b/src/components/__tests__/DataBrowser.spec.ts new file mode 100644 index 000000000..3f1049081 --- /dev/null +++ b/src/components/__tests__/DataBrowser.spec.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createPinia, setActivePinia } from 'pinia'; +import { nextTick } from 'vue'; +import { enableAutoUnmount, shallowMount } from '@vue/test-utils'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; + +import DataBrowser from '@/src/components/DataBrowser.vue'; +import { useDICOMStore } from '@/src/store/datasets-dicom'; +import { useImageStore } from '@/src/store/datasets-images'; + +enableAutoUnmount(afterEach); + +const mountPanels = () => { + const wrapper = shallowMount(DataBrowser); + const panels = wrapper.findComponent({ name: 'VExpansionPanels' }); + return { + open: () => panels.props('modelValue'), + collapse: async () => { + panels.vm.$emit('update:modelValue', []); + await nextTick(); + }, + }; +}; + +describe('DataBrowser panels', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it('reopens Anonymous for a new image, without duplicate keys or reopening on deletion', async () => { + const panels = mountPanels(); + const images = useImageStore(); + const addImage = (id: string) => + images.addVTKImageData(id, vtkImageData.newInstance(), { id }); + + addImage('first'); + await nextTick(); + expect(panels.open()).toEqual(['anonymousData']); + await panels.collapse(); + + addImage('second'); + await nextTick(); + expect(panels.open()).toEqual(['anonymousData']); + addImage('third'); + await nextTick(); + expect(panels.open()).toEqual(['anonymousData']); + + await panels.collapse(); + images.deleteData('first'); + await nextTick(); + expect(panels.open()).toEqual([]); + }); + + it.each(['new study', 'existing study'])( + 'reopens only the patient receiving a volume in a %s', + async (target) => { + const dicom = useDICOMStore(); + const panels = mountPanels(); + for (const patient of ['patient', 'other']) { + dicom.patientInfo[patient] = { + PatientID: patient, + PatientName: patient, + PatientBirthDate: '', + PatientSex: '', + }; + dicom.patientStudies[patient] = [patient]; + dicom.studyVolumes[patient] = [`${patient}-volume`]; + } + await nextTick(); + expect([...panels.open()].sort()).toEqual(['other', 'patient']); + await panels.collapse(); + + const study = target === 'new study' ? 'study-2' : 'patient'; + if (target === 'new study') { + dicom.patientStudies.patient.push(study); + dicom.studyVolumes[study] = []; + } + dicom.studyVolumes[study].push('volume-2'); + await nextTick(); + expect(panels.open()).toEqual(['patient']); + + dicom.studyVolumes[study].push('volume-3'); + await nextTick(); + expect(panels.open()).toEqual(['patient']); + await panels.collapse(); + dicom.studyVolumes[study].pop(); + await nextTick(); + expect(panels.open()).toEqual([]); + } + ); +}); diff --git a/tests/specs/pet-ct-sample.e2e.ts b/tests/specs/pet-ct-sample.e2e.ts index ed9e9fa09..82ad17552 100644 --- a/tests/specs/pet-ct-sample.e2e.ts +++ b/tests/specs/pet-ct-sample.e2e.ts @@ -171,7 +171,10 @@ async function reopenSample( await browser.saveScreenshot(path.join(TEMP_DIR, 'pet-ct-sample-reload.png')); await sample.waitForClickable(); await sample.click(); - await sample.$('.mdi-check').waitForExist(); + await expect( + $('.volume-card[html-title="PET WB Corrected"]') + ).toBeDisplayed(); + await waitForModuleTransitions(); await volViewPage.waitForViewCounts(1, false); await volViewPage.renderingModuleTab.click(); await expect($$('h4*=PET WB Corrected')).toBeElementsArrayOfSize(1);