Compare commits

...

3 Commits

Author SHA1 Message Date
Glary-Bot
ad3ad327b7 fix(litegraphService): route Bypass labels through vue-i18n
Addresses CodeRabbit review: per AGENTS.md, all user-facing strings
must use vue-i18n. The Vue `getBypassOption` already uses
`t('contextMenu.Bypass')` and `t('contextMenu.Remove Bypass')` —
reuse the same keys so the legacy LiteGraph entry stays exact-label
identical to the Vue entry (which is how the menu deduplicator
collapses them) in every locale.
2026-05-15 20:06:26 +00:00
Glary-Bot
d82c7ab74d fix(litegraphService): use selection-aware predicate for Bypass label
Address Oracle review on FE-720 fix: the label was derived from the
right-clicked node's mode (`this.mode`), but the click action operates
on the entire selection via `toggleSelectedNodesMode`. On a mixed
multi-selection (some nodes bypassed, some not), right-clicking a
bypassed node would show 'Remove Bypass' even though clicking it
bypasses the rest of the selection.

Extract the predicate as `areAllSelectedNodesInMode` on the
`useSelectedLiteGraphItems` composable so the label and the action
share one source of truth, and have the legacy bypass menu entry use
it. Adds a unit test covering all-bypassed, mixed, and empty cases.
2026-05-15 19:43:50 +00:00
Glary-Bot
26daf1e0df fix: dedupe Bypass context-menu items by making legacy entry state-aware
Right-clicking a bypassed node showed two bypass items in the Vue
context menu: a plain 'Bypass' from the legacy LiteGraph
`getExtraMenuOptions` hook in litegraphService and 'Remove Bypass'
from the Vue `getBypassOption` composable. The Vue menu's exact-label
deduplicator collapses the unbypassed case (both emit 'Bypass') but
not the bypassed case ('Bypass' vs 'Remove Bypass').

Make the legacy hook emit a stateful label that matches the Vue label,
so existing exact-label dedupe (which prefers the Vue source) handles
both states uniformly. The legacy LiteGraph menu (Comfy.UseNewMenu:
Disabled) now also shows the correct conditional label, which was
previously stuck on 'Bypass' regardless of node state.

Fixes FE-720
2026-05-15 19:29:56 +00:00
4 changed files with 67 additions and 12 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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' },

View File

@@ -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<LGraphNode, IBaseWidget>
@@ -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)