diff --git a/src/composables/canvas/useSelectedLiteGraphItems.test.ts b/src/composables/canvas/useSelectedLiteGraphItems.test.ts index 45178088a02..b973d66aaa2 100644 --- a/src/composables/canvas/useSelectedLiteGraphItems.test.ts +++ b/src/composables/canvas/useSelectedLiteGraphItems.test.ts @@ -245,6 +245,22 @@ describe('useSelectedLiteGraphItems', () => { expect(node2.mode).toBe(LGraphEventMode.NEVER) }) + it('areAllSelectedNodesInMode returns true only when every selected node matches', () => { + const { areAllSelectedNodesInMode } = useSelectedLiteGraphItems() + const bypassed1 = { id: 1, mode: LGraphEventMode.BYPASS } as LGraphNode + const bypassed2 = { id: 2, mode: LGraphEventMode.BYPASS } as LGraphNode + const active = { id: 3, mode: LGraphEventMode.ALWAYS } as LGraphNode + + app.canvas.selected_nodes = { '0': bypassed1, '1': bypassed2 } + expect(areAllSelectedNodesInMode(LGraphEventMode.BYPASS)).toBe(true) + + app.canvas.selected_nodes = { '0': bypassed1, '1': active } + expect(areAllSelectedNodesInMode(LGraphEventMode.BYPASS)).toBe(false) + + app.canvas.selected_nodes = {} + expect(areAllSelectedNodesInMode(LGraphEventMode.BYPASS)).toBe(false) + }) + it('toggleSelectedNodesMode should set mode to ALWAYS when already in target mode', () => { const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() const node = { id: 1, mode: LGraphEventMode.BYPASS } as LGraphNode diff --git a/src/composables/canvas/useSelectedLiteGraphItems.ts b/src/composables/canvas/useSelectedLiteGraphItems.ts index c7777d7afbd..747391da0e6 100644 --- a/src/composables/canvas/useSelectedLiteGraphItems.ts +++ b/src/composables/canvas/useSelectedLiteGraphItems.ts @@ -93,6 +93,19 @@ export function useSelectedLiteGraphItems() { return collectFromNodes(nodeArray) } + /** + * True iff every selected (top-level) node is already in the given mode. + * Mirrors the predicate inside {@link toggleSelectedNodesMode} so callers + * (e.g. context-menu labels) can preview what the toggle will do. + */ + const areAllSelectedNodesInMode = (mode: LGraphEventMode): boolean => { + const selectedNodes = app.canvas.selected_nodes + if (!selectedNodes) return false + const selectedNodeArray = Object.values(selectedNodes) + if (selectedNodeArray.length === 0) return false + return selectedNodeArray.every((node) => node.mode === mode) + } + /** * Toggle the execution mode of all selected nodes * @@ -105,15 +118,10 @@ export function useSelectedLiteGraphItems() { const selectedNodes = app.canvas.selected_nodes if (!selectedNodes) return - // Convert selected_nodes object to array - const selectedNodeArray: LGraphNode[] = [] - for (const i in selectedNodes) { - selectedNodeArray.push(selectedNodes[i]) - } - const allNodesMatch = !selectedNodeArray.some( - (selectedNode) => selectedNode.mode !== mode - ) - const newModeForSelectedNode = allNodesMatch ? LGraphEventMode.ALWAYS : mode + const selectedNodeArray = Object.values(selectedNodes) + const newModeForSelectedNode = areAllSelectedNodesInMode(mode) + ? LGraphEventMode.ALWAYS + : mode for (const selectedNode of selectedNodeArray) selectedNode.mode = newModeForSelectedNode @@ -126,6 +134,7 @@ export function useSelectedLiteGraphItems() { hasSelectableItems, hasMultipleSelectableItems, getSelectedNodes, - toggleSelectedNodesMode + toggleSelectedNodesMode, + areAllSelectedNodesInMode } } diff --git a/src/composables/graph/contextMenuConverter.test.ts b/src/composables/graph/contextMenuConverter.test.ts index c2bf5de6e62..980701ee0d5 100644 --- a/src/composables/graph/contextMenuConverter.test.ts +++ b/src/composables/graph/contextMenuConverter.test.ts @@ -135,6 +135,33 @@ describe('contextMenuConverter', () => { expect(getIndex('Node Info')).toBeLessThanOrEqual(getIndex('Color')) }) + it('should collapse legacy and Vue Remove Bypass to the Vue item when a node is bypassed', () => { + const options: MenuOption[] = [ + { label: 'Remove Bypass', action: () => {}, source: 'litegraph' }, + { label: 'Remove Bypass', action: () => {}, source: 'vue' } + ] + + const result = buildStructuredMenu(options) + + const removeBypassItems = result.filter( + (opt) => opt.label === 'Remove Bypass' + ) + expect(removeBypassItems).toHaveLength(1) + expect(removeBypassItems[0].source).toBe('vue') + }) + + it('should not treat Bypass and Remove Bypass as equivalent labels', () => { + const options: MenuOption[] = [ + { label: 'Bypass', action: () => {}, source: 'litegraph' }, + { label: 'Remove Bypass', action: () => {}, source: 'vue' } + ] + + const result = buildStructuredMenu(options) + + expect(result.find((opt) => opt.label === 'Bypass')).toBeDefined() + expect(result.find((opt) => opt.label === 'Remove Bypass')).toBeDefined() + }) + it('should recognize Frame Nodes as a core menu item', () => { const options: MenuOption[] = [ { label: 'Rename', source: 'vue' }, diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 617a8c2ca7a..7bee220763e 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -176,7 +176,8 @@ export const useLitegraphService = () => { const toastStore = useToastStore() const widgetStore = useWidgetStore() const canvasStore = useCanvasStore() - const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() + const { toggleSelectedNodesMode, areAllSelectedNodesInMode } = + useSelectedLiteGraphItems() const subgraphPseudoWidgetCache = new WeakMap< SubgraphNode, SubgraphPseudoWidgetCache @@ -719,7 +720,9 @@ export const useLitegraphService = () => { } options.push({ - content: 'Bypass', + content: areAllSelectedNodesInMode(LGraphEventMode.BYPASS) + ? t('contextMenu.Remove Bypass') + : t('contextMenu.Bypass'), callback: () => { toggleSelectedNodesMode(LGraphEventMode.BYPASS) canvas.setDirty(true, true)