diff --git a/src/composables/graph/useSelectionState.ts b/src/composables/graph/useSelectionState.ts index f717d00bb..0e713eba2 100644 --- a/src/composables/graph/useSelectionState.ts +++ b/src/composables/graph/useSelectionState.ts @@ -1,14 +1,8 @@ import { storeToRefs } from 'pinia' import { computed } from 'vue' -import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab' -import { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/stores/graphStore' -import { useNodeDefStore } from '@/stores/nodeDefStore' -import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore' -import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' -import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil' -import { filterOutputNodes } from '@/utils/nodeFilterUtil' +import { isLGraphNode } from '@/utils/litegraphUtil' /** * Centralized computed selection state + shared helper actions to avoid duplication @@ -16,93 +10,20 @@ import { filterOutputNodes } from '@/utils/nodeFilterUtil' */ export function useSelectionState() { const canvasStore = useCanvasStore() - const nodeDefStore = useNodeDefStore() - const sidebarTabStore = useSidebarTabStore() - const nodeHelpStore = useNodeHelpStore() - const { id: nodeLibraryTabId } = useNodeLibrarySidebarTab() - const { selectedItems } = storeToRefs(canvasStore) const selectedNodes = computed(() => { return selectedItems.value.filter((i) => isLGraphNode(i)) }) - const nodeDef = computed(() => { - if (selectedNodes.value.length !== 1) return null - return nodeDefStore.fromLGraphNode(selectedNodes.value[0]) - }) - const hasAnySelection = computed(() => selectedItems.value.length > 0) - const hasSingleSelection = computed(() => selectedItems.value.length === 1) - const hasMultipleSelection = computed(() => selectedItems.value.length > 1) - - const isSingleNode = computed( - () => hasSingleSelection.value && isLGraphNode(selectedItems.value[0]) - ) - const isSingleSubgraph = computed( - () => - isSingleNode.value && - (selectedItems.value[0] as LGraphNode)?.isSubgraphNode?.() - ) - const isSingleImageNode = computed( - () => - isSingleNode.value && isImageNode(selectedItems.value[0] as LGraphNode) - ) - - const hasSubgraphs = computed(() => - selectedItems.value.some((i) => (i as LGraphNode)?.isSubgraphNode?.()) - ) - const hasImageNode = computed(() => isSingleImageNode.value) - const hasOutputNodesSelected = computed( - () => filterOutputNodes(selectedNodes.value).length > 0 - ) - /** Toggle node help sidebar/panel for the single selected node (if any). */ - const showNodeHelp = () => { - const def = nodeDef.value - if (!def) return - - const isSidebarActive = - sidebarTabStore.activeSidebarTabId === nodeLibraryTabId - const currentHelpNode: any = nodeHelpStore.currentHelpNode - const isSameNodeHelpOpen = - isSidebarActive && - nodeHelpStore.isHelpOpen && - currentHelpNode && - currentHelpNode.nodePath === def.nodePath - - if (isSameNodeHelpOpen) { - nodeHelpStore.closeHelp() - sidebarTabStore.toggleSidebarTab(nodeLibraryTabId) - return - } - - if (!isSidebarActive) sidebarTabStore.toggleSidebarTab(nodeLibraryTabId) - nodeHelpStore.openHelp(def) - } - - const isRemovableItem = (item: unknown): item is Positionable => - item != null && typeof item === 'object' && 'removable' in item - const isDeletable = computed(() => - selectedItems.value - .filter(isRemovableItem) - .some((x) => x.removable !== false) + selectedItems.value.some((x) => x.removable) ) - return { selectedItems, selectedNodes, - nodeDef, - showNodeHelp, hasAnySelection, - hasSingleSelection, - hasMultipleSelection, - isSingleNode, - isSingleSubgraph, - isSingleImageNode, - hasSubgraphs, - hasImageNode, - hasOutputNodesSelected, isDeletable } } diff --git a/tests-ui/tests/composables/graph/useSelectionState.test.ts b/tests-ui/tests/composables/graph/useSelectionState.test.ts index 5ee4c95c4..65e6c2dd1 100644 --- a/tests-ui/tests/composables/graph/useSelectionState.test.ts +++ b/tests-ui/tests/composables/graph/useSelectionState.test.ts @@ -1,6 +1,6 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { type Ref, nextTick, ref } from 'vue' +import { type Ref, ref } from 'vue' import { useSelectionState } from '@/composables/graph/useSelectionState' import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab' @@ -73,17 +73,6 @@ const createTestNode = (config: TestNodeConfig = {}): TestNode => { } } -const createTestSubgraphNode = (config: TestNodeConfig = {}): TestNode => { - return { - type: 'SubgraphNode', - mode: config.mode || LGraphEventMode.ALWAYS, - flags: config.flags, - pinned: config.pinned, - removable: config.removable, - isSubgraphNode: () => true - } -} - // Mock comment/connection objects const mockComment = { type: 'comment', isNode: false } const mockConnection = { type: 'connection', isNode: false } @@ -206,21 +195,6 @@ describe('useSelectionState', () => { const { hasAnySelection } = useSelectionState() expect(hasAnySelection.value).toBe(true) }) - - test('should distinguish single vs multiple selections', () => { - const node = createTestNode() - mockSelectedItems.value = [node] - - const { hasSingleSelection, hasMultipleSelection } = useSelectionState() - expect(hasSingleSelection.value).toBe(true) - expect(hasMultipleSelection.value).toBe(false) - - // Test multiple selection with new instance - mockSelectedItems.value = [node, createTestNode()] - const multipleState = useSelectionState() - expect(multipleState.hasSingleSelection.value).toBe(false) - expect(multipleState.hasMultipleSelection.value).toBe(true) - }) }) describe('Node Type Filtering', () => { @@ -232,15 +206,6 @@ describe('useSelectionState', () => { expect(selectedNodes.value).toHaveLength(1) expect(selectedNodes.value[0]).toEqual(graphNode) }) - - test('should detect subgraphs in selection', () => { - const subgraph = createTestSubgraphNode() - mockSelectedItems.value = [subgraph] - - const { hasSubgraphs, isSingleSubgraph } = useSelectionState() - expect(hasSubgraphs.value).toBe(true) - expect(isSingleSubgraph.value).toBe(true) - }) }) describe('Node State Computation', () => { @@ -297,192 +262,4 @@ describe('useSelectionState', () => { expect(newIsPinned).toBe(false) }) }) - - describe('Data Integrity', () => { - test('should handle missing removable property', () => { - const node = createTestNode() - delete node.removable - mockSelectedItems.value = [node] - - const { selectedItems } = useSelectionState() - expect(selectedItems.value[0].removable).toBeUndefined() - }) - - test('should return default states for empty selection', () => { - const { selectedNodes } = useSelectionState() - const isPinned = selectedNodes.value.some((n) => n.pinned === true) - const isCollapsed = selectedNodes.value.some( - (n) => n.flags?.collapsed === true - ) - const isBypassed = selectedNodes.value.some( - (n) => n.mode === LGraphEventMode.BYPASS - ) - expect(isPinned).toBe(false) - expect(isCollapsed).toBe(false) - expect(isBypassed).toBe(false) - }) - }) - - describe('Help Integration', () => { - test('should show help for single node', async () => { - const node = createTestNode({ type: 'TestNode' }) - mockSelectedItems.value = [node] - - const { showNodeHelp } = useSelectionState() - const sidebarStore = useSidebarTabStore() - const nodeHelpStore = useNodeHelpStore() - - showNodeHelp() - await nextTick() - - expect(sidebarStore.toggleSidebarTab).toHaveBeenCalledWith( - 'node-library-tab' - ) - expect(nodeHelpStore.openHelp).toHaveBeenCalledWith({ - nodePath: 'test.TestNode', - name: 'TestNode' - }) - }) - - test('should ignore help request for multiple nodes', () => { - const node1 = createTestNode() - const node2 = createTestNode() - mockSelectedItems.value = [node1, node2] - - const { showNodeHelp } = useSelectionState() - const sidebarStore = useSidebarTabStore() - const nodeHelpStore = useNodeHelpStore() - - showNodeHelp() - - expect(sidebarStore.toggleSidebarTab).not.toHaveBeenCalled() - expect(nodeHelpStore.openHelp).not.toHaveBeenCalled() - }) - - test('should toggle help when same node help is already open', async () => { - const node = createTestNode({ type: 'TestNode' }) - mockSelectedItems.value = [node] - - // Update the mock stores to have the right state - const sidebarStore = useSidebarTabStore() - const nodeHelpStore = useNodeHelpStore() - - vi.mocked(useSidebarTabStore).mockReturnValue({ - ...sidebarStore, - activeSidebarTabId: 'node-library-tab' - } as any) - - vi.mocked(useNodeHelpStore).mockReturnValue({ - ...nodeHelpStore, - isHelpOpen: true, - currentHelpNode: { nodePath: 'test.TestNode' } - } as any) - - const { showNodeHelp } = useSelectionState() - showNodeHelp() - await nextTick() - - expect(nodeHelpStore.closeHelp).toHaveBeenCalled() - expect(sidebarStore.toggleSidebarTab).toHaveBeenCalledWith( - 'node-library-tab' - ) - }) - }) - - describe('Button Pattern Consistency', () => { - test('should provide consistent state for all buttons', () => { - const node = createTestNode({ - mode: LGraphEventMode.BYPASS, - pinned: true - }) - mockSelectedItems.value = [node] - - const state1 = useSelectionState() - const state2 = useSelectionState() - - const isBypassed1 = state1.selectedNodes.value.some( - (n) => n.mode === LGraphEventMode.BYPASS - ) - const isBypassed2 = state2.selectedNodes.value.some( - (n) => n.mode === LGraphEventMode.BYPASS - ) - const isPinned1 = state1.selectedNodes.value.some( - (n) => n.pinned === true - ) - const isPinned2 = state2.selectedNodes.value.some( - (n) => n.pinned === true - ) - - expect(isBypassed1).toBe(true) - expect(isBypassed2).toBe(true) - expect(isPinned1).toBe(true) - expect(isPinned2).toBe(true) - - // Test with empty selection using new instances - mockSelectedItems.value = [] - const emptyState1 = useSelectionState() - const emptyState2 = useSelectionState() - - expect(emptyState1.hasAnySelection.value).toBe(false) - expect(emptyState2.hasAnySelection.value).toBe(false) - }) - - test('should support standardized deletability check', () => { - const deletableNode = createTestNode({ removable: true }) - const nonDeletableNode = createTestNode({ removable: false }) - - mockSelectedItems.value = [deletableNode] - const { isDeletable } = useSelectionState() - expect(isDeletable.value).toBe(true) - - mockSelectedItems.value = [nonDeletableNode] - const { isDeletable: isDeletable2 } = useSelectionState() - expect(isDeletable2.value).toBe(false) - - mockSelectedItems.value = [deletableNode, nonDeletableNode] - const { isDeletable: isDeletable3 } = useSelectionState() - // When there's a mix of deletable and non-deletable items, - // isDeletable returns true because SOME items can be deleted - expect(isDeletable3.value).toBe(true) - }) - }) - - describe('Special Node Types', () => { - test('should detect image nodes', () => { - const imageNode = createTestNode({ type: 'ImageNode' }) - mockSelectedItems.value = [imageNode] - - const { isSingleImageNode, hasImageNode } = useSelectionState() - expect(isSingleImageNode.value).toBe(true) - expect(hasImageNode.value).toBe(true) - }) - - test('should detect output nodes', () => { - const outputNode = createTestNode({ type: 'OutputNode' }) - mockSelectedItems.value = [outputNode] - - const { hasOutputNodesSelected } = useSelectionState() - expect(hasOutputNodesSelected.value).toBe(true) - }) - - test('should return correct nodeDef for single node', () => { - const node = createTestNode({ type: 'TestNode' }) - mockSelectedItems.value = [node] - - const { nodeDef } = useSelectionState() - expect(nodeDef.value).toEqual({ - nodePath: 'test.TestNode', - name: 'TestNode' - }) - }) - - test('should return null nodeDef for multiple nodes', () => { - const node1 = createTestNode() - const node2 = createTestNode() - mockSelectedItems.value = [node1, node2] - - const { nodeDef } = useSelectionState() - expect(nodeDef.value).toBeNull() - }) - }) })