diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue index 006718b67..0acf2473f 100644 --- a/src/components/sidebar/tabs/AssetsSidebarTab.vue +++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue @@ -310,6 +310,7 @@ const { hasSelection, clearSelection, getSelectedAssets, + reconcileSelection, getOutputCount, getTotalOutputCount, activate: activateSelection, @@ -406,6 +407,9 @@ const showEmptyState = computed( ) watch(visibleAssets, (newAssets) => { + // Alternative: keep hidden selections and surface them in UI; for now prune + // so selection stays consistent with what this view can act on. + reconcileSelection(newAssets) if (currentGalleryAssetId.value && galleryActiveIndex.value !== -1) { const newIndex = newAssets.findIndex( (asset) => asset.id === currentGalleryAssetId.value diff --git a/src/platform/assets/composables/useAssetSelection.test.ts b/src/platform/assets/composables/useAssetSelection.test.ts new file mode 100644 index 000000000..56627ae39 --- /dev/null +++ b/src/platform/assets/composables/useAssetSelection.test.ts @@ -0,0 +1,48 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' + +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' + +import { useAssetSelection } from './useAssetSelection' +import { useAssetSelectionStore } from './useAssetSelectionStore' + +vi.mock('@vueuse/core', () => ({ + useKeyModifier: vi.fn(() => ref(false)) +})) + +describe('useAssetSelection', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('prunes selection to visible assets', () => { + const selection = useAssetSelection() + const store = useAssetSelectionStore() + const assets: AssetItem[] = [ + { id: 'a', name: 'a.png', tags: [] }, + { id: 'b', name: 'b.png', tags: [] } + ] + + store.setSelection(['a', 'b']) + store.setLastSelectedIndex(1) + + selection.reconcileSelection([assets[1]]) + + expect(Array.from(store.selectedAssetIds)).toEqual(['b']) + expect(store.lastSelectedIndex).toBe(-1) + }) + + it('clears selection when no visible assets remain', () => { + const selection = useAssetSelection() + const store = useAssetSelectionStore() + + store.setSelection(['a']) + store.setLastSelectedIndex(0) + + selection.reconcileSelection([]) + + expect(store.selectedAssetIds.size).toBe(0) + expect(store.lastSelectedIndex).toBe(-1) + }) +}) diff --git a/src/platform/assets/composables/useAssetSelection.ts b/src/platform/assets/composables/useAssetSelection.ts index 290698d4d..ace5d3364 100644 --- a/src/platform/assets/composables/useAssetSelection.ts +++ b/src/platform/assets/composables/useAssetSelection.ts @@ -88,6 +88,38 @@ export function useAssetSelection() { return allAssets.filter((asset) => selectionStore.isSelected(asset.id)) } + function reconcileSelection(assets: AssetItem[]) { + if (selectionStore.selectedAssetIds.size === 0) { + return + } + + if (assets.length === 0) { + selectionStore.clearSelection() + return + } + + const visibleIds = new Set(assets.map((asset) => asset.id)) + const nextSelectedIds: string[] = [] + + for (const id of selectionStore.selectedAssetIds) { + if (visibleIds.has(id)) { + nextSelectedIds.push(id) + } + } + + if (nextSelectedIds.length === selectionStore.selectedAssetIds.size) { + return + } + + if (nextSelectedIds.length === 0) { + selectionStore.clearSelection() + return + } + + selectionStore.setSelection(nextSelectedIds) + selectionStore.setLastSelectedIndex(-1) + } + /** * Get the output count for a single asset * Same logic as in AssetsSidebarTab.vue @@ -132,6 +164,7 @@ export function useAssetSelection() { selectAll, clearSelection: () => selectionStore.clearSelection(), getSelectedAssets, + reconcileSelection, getOutputCount, getTotalOutputCount, reset: () => selectionStore.reset(),