mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: reconcile asset selection
This commit is contained in:
@@ -264,6 +264,7 @@ const focusAssetInSidebar = async (item: JobListItem) => {
|
|||||||
throw new Error('Asset not found in media assets panel')
|
throw new Error('Asset not found in media assets panel')
|
||||||
}
|
}
|
||||||
assetSelectionStore.setSelection([assetId])
|
assetSelectionStore.setSelection([assetId])
|
||||||
|
assetSelectionStore.setLastSelectedAssetId(assetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inspectJobAsset = wrapWithErrorHandlingAsync(
|
const inspectJobAsset = wrapWithErrorHandlingAsync(
|
||||||
|
|||||||
89
src/platform/assets/composables/useAssetSelection.test.ts
Normal file
89
src/platform/assets/composables/useAssetSelection.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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)
|
||||||
|
store.setLastSelectedAssetId('b')
|
||||||
|
|
||||||
|
selection.reconcileSelection([assets[1]])
|
||||||
|
|
||||||
|
expect(Array.from(store.selectedAssetIds)).toEqual(['b'])
|
||||||
|
expect(store.lastSelectedIndex).toBe(0)
|
||||||
|
expect(store.lastSelectedAssetId).toBe('b')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears selection when no visible assets remain', () => {
|
||||||
|
const selection = useAssetSelection()
|
||||||
|
const store = useAssetSelectionStore()
|
||||||
|
|
||||||
|
store.setSelection(['a'])
|
||||||
|
store.setLastSelectedIndex(0)
|
||||||
|
store.setLastSelectedAssetId('a')
|
||||||
|
|
||||||
|
selection.reconcileSelection([])
|
||||||
|
|
||||||
|
expect(store.selectedAssetIds.size).toBe(0)
|
||||||
|
expect(store.lastSelectedIndex).toBe(-1)
|
||||||
|
expect(store.lastSelectedAssetId).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('recomputes the anchor index when assets reorder', () => {
|
||||||
|
const selection = useAssetSelection()
|
||||||
|
const store = useAssetSelectionStore()
|
||||||
|
const assets: AssetItem[] = [
|
||||||
|
{ id: 'a', name: 'a.png', tags: [] },
|
||||||
|
{ id: 'b', name: 'b.png', tags: [] }
|
||||||
|
]
|
||||||
|
|
||||||
|
store.setSelection(['a'])
|
||||||
|
store.setLastSelectedIndex(0)
|
||||||
|
store.setLastSelectedAssetId('a')
|
||||||
|
|
||||||
|
selection.reconcileSelection([assets[1], assets[0]])
|
||||||
|
|
||||||
|
expect(store.lastSelectedIndex).toBe(1)
|
||||||
|
expect(store.lastSelectedAssetId).toBe('a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears anchor when the anchored asset is no longer visible', () => {
|
||||||
|
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(0)
|
||||||
|
store.setLastSelectedAssetId('a')
|
||||||
|
|
||||||
|
selection.reconcileSelection([assets[1]])
|
||||||
|
|
||||||
|
expect(Array.from(store.selectedAssetIds)).toEqual(['b'])
|
||||||
|
expect(store.lastSelectedIndex).toBe(-1)
|
||||||
|
expect(store.lastSelectedAssetId).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -21,6 +21,25 @@ export function useAssetSelection() {
|
|||||||
const metaKey = computed(() => isActive.value && metaKeyRaw.value)
|
const metaKey = computed(() => isActive.value && metaKeyRaw.value)
|
||||||
const cmdOrCtrlKey = computed(() => ctrlKey.value || metaKey.value)
|
const cmdOrCtrlKey = computed(() => ctrlKey.value || metaKey.value)
|
||||||
|
|
||||||
|
function setAnchor(index: number, assetId: string | null) {
|
||||||
|
selectionStore.setLastSelectedIndex(index)
|
||||||
|
selectionStore.setLastSelectedAssetId(assetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAnchorFromAssets(assets: AssetItem[]) {
|
||||||
|
const anchorId = selectionStore.lastSelectedAssetId
|
||||||
|
const anchorIndex = anchorId
|
||||||
|
? assets.findIndex((asset) => asset.id === anchorId)
|
||||||
|
: -1
|
||||||
|
|
||||||
|
if (anchorIndex !== -1) {
|
||||||
|
selectionStore.setLastSelectedIndex(anchorIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnchor(-1, null)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle asset click with modifier keys for selection
|
* Handle asset click with modifier keys for selection
|
||||||
* @param asset The clicked asset
|
* @param asset The clicked asset
|
||||||
@@ -60,14 +79,14 @@ export function useAssetSelection() {
|
|||||||
// Ctrl/Cmd + Click: Toggle individual selection
|
// Ctrl/Cmd + Click: Toggle individual selection
|
||||||
if (cmdOrCtrlKey.value) {
|
if (cmdOrCtrlKey.value) {
|
||||||
selectionStore.toggleSelection(assetId)
|
selectionStore.toggleSelection(assetId)
|
||||||
selectionStore.setLastSelectedIndex(index)
|
setAnchor(index, assetId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal Click: Single selection
|
// Normal Click: Single selection
|
||||||
selectionStore.clearSelection()
|
selectionStore.clearSelection()
|
||||||
selectionStore.addToSelection(assetId)
|
selectionStore.addToSelection(assetId)
|
||||||
selectionStore.setLastSelectedIndex(index)
|
setAnchor(index, assetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +96,8 @@ export function useAssetSelection() {
|
|||||||
const allIds = allAssets.map((a) => a.id)
|
const allIds = allAssets.map((a) => a.id)
|
||||||
selectionStore.setSelection(allIds)
|
selectionStore.setSelection(allIds)
|
||||||
if (allAssets.length > 0) {
|
if (allAssets.length > 0) {
|
||||||
selectionStore.setLastSelectedIndex(allAssets.length - 1)
|
const lastIndex = allAssets.length - 1
|
||||||
|
setAnchor(lastIndex, allAssets[lastIndex].id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +108,39 @@ export function useAssetSelection() {
|
|||||||
return allAssets.filter((asset) => selectionStore.isSelected(asset.id))
|
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) {
|
||||||
|
syncAnchorFromAssets(assets)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSelectedIds.length === 0) {
|
||||||
|
selectionStore.clearSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionStore.setSelection(nextSelectedIds)
|
||||||
|
syncAnchorFromAssets(assets)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the output count for a single asset
|
* Get the output count for a single asset
|
||||||
* Same logic as in AssetsSidebarTab.vue
|
* Same logic as in AssetsSidebarTab.vue
|
||||||
@@ -117,7 +170,7 @@ export function useAssetSelection() {
|
|||||||
function deactivate() {
|
function deactivate() {
|
||||||
isActive.value = false
|
isActive.value = false
|
||||||
// Reset selection state to ensure clean state when deactivated
|
// Reset selection state to ensure clean state when deactivated
|
||||||
selectionStore.reset()
|
selectionStore.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -132,10 +185,9 @@ export function useAssetSelection() {
|
|||||||
selectAll,
|
selectAll,
|
||||||
clearSelection: () => selectionStore.clearSelection(),
|
clearSelection: () => selectionStore.clearSelection(),
|
||||||
getSelectedAssets,
|
getSelectedAssets,
|
||||||
|
reconcileSelection,
|
||||||
getOutputCount,
|
getOutputCount,
|
||||||
getTotalOutputCount,
|
getTotalOutputCount,
|
||||||
reset: () => selectionStore.reset(),
|
|
||||||
|
|
||||||
// Lifecycle management
|
// Lifecycle management
|
||||||
activate,
|
activate,
|
||||||
deactivate,
|
deactivate,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const useAssetSelectionStore = defineStore('assetSelection', () => {
|
|||||||
// State
|
// State
|
||||||
const selectedAssetIds = ref<Set<string>>(new Set())
|
const selectedAssetIds = ref<Set<string>>(new Set())
|
||||||
const lastSelectedIndex = ref<number>(-1)
|
const lastSelectedIndex = ref<number>(-1)
|
||||||
|
const lastSelectedAssetId = ref<string | null>(null)
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
const selectedCount = computed(() => selectedAssetIds.value.size)
|
const selectedCount = computed(() => selectedAssetIds.value.size)
|
||||||
@@ -34,6 +35,7 @@ export const useAssetSelectionStore = defineStore('assetSelection', () => {
|
|||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selectedAssetIds.value.clear()
|
selectedAssetIds.value.clear()
|
||||||
lastSelectedIndex.value = -1
|
lastSelectedIndex.value = -1
|
||||||
|
lastSelectedAssetId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelection(assetId: string) {
|
function toggleSelection(assetId: string) {
|
||||||
@@ -52,16 +54,15 @@ export const useAssetSelectionStore = defineStore('assetSelection', () => {
|
|||||||
lastSelectedIndex.value = index
|
lastSelectedIndex.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset function for cleanup
|
function setLastSelectedAssetId(assetId: string | null) {
|
||||||
function reset() {
|
lastSelectedAssetId.value = assetId
|
||||||
selectedAssetIds.value.clear()
|
|
||||||
lastSelectedIndex.value = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
selectedAssetIds: computed(() => selectedAssetIds.value),
|
selectedAssetIds: computed(() => selectedAssetIds.value),
|
||||||
lastSelectedIndex: computed(() => lastSelectedIndex.value),
|
lastSelectedIndex: computed(() => lastSelectedIndex.value),
|
||||||
|
lastSelectedAssetId: computed(() => lastSelectedAssetId.value),
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
selectedCount,
|
selectedCount,
|
||||||
@@ -76,6 +77,6 @@ export const useAssetSelectionStore = defineStore('assetSelection', () => {
|
|||||||
toggleSelection,
|
toggleSelection,
|
||||||
isSelected,
|
isSelected,
|
||||||
setLastSelectedIndex,
|
setLastSelectedIndex,
|
||||||
reset
|
setLastSelectedAssetId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user