mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
[backport cloud/1.38] fix: add Frame Nodes to core menu items for multi-selection context menu (#8554)
Backport of #8524 to `cloud/1.38` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8554-backport-cloud-1-38-fix-add-Frame-Nodes-to-core-menu-items-for-multi-selection-context-2fc6d73d365081c9b5c7cd6f6c4d5335) by [Unito](https://www.unito.io) Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -134,6 +134,27 @@ describe('contextMenuConverter', () => {
|
|||||||
// Node Info (section 4) should come before or with Color (section 4)
|
// Node Info (section 4) should come before or with Color (section 4)
|
||||||
expect(getIndex('Node Info')).toBeLessThanOrEqual(getIndex('Color'))
|
expect(getIndex('Node Info')).toBeLessThanOrEqual(getIndex('Color'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should recognize Frame Nodes as a core menu item', () => {
|
||||||
|
const options: MenuOption[] = [
|
||||||
|
{ label: 'Rename', source: 'vue' },
|
||||||
|
{ label: 'Frame Nodes', source: 'vue' },
|
||||||
|
{ label: 'Custom Extension', source: 'vue' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = buildStructuredMenu(options)
|
||||||
|
|
||||||
|
// Frame Nodes should appear in the core items section (before Extensions)
|
||||||
|
const frameNodesIndex = result.findIndex(
|
||||||
|
(opt) => opt.label === 'Frame Nodes'
|
||||||
|
)
|
||||||
|
const extensionsCategoryIndex = result.findIndex(
|
||||||
|
(opt) => opt.label === 'Extensions' && opt.type === 'category'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frame Nodes should come before Extensions category
|
||||||
|
expect(frameNodesIndex).toBeLessThan(extensionsCategoryIndex)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('convertContextMenuToOptions', () => {
|
describe('convertContextMenuToOptions', () => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const CORE_MENU_ITEMS = new Set([
|
|||||||
// Structure operations
|
// Structure operations
|
||||||
'Convert to Subgraph',
|
'Convert to Subgraph',
|
||||||
'Frame selection',
|
'Frame selection',
|
||||||
|
'Frame Nodes',
|
||||||
'Minimize Node',
|
'Minimize Node',
|
||||||
'Expand',
|
'Expand',
|
||||||
'Collapse',
|
'Collapse',
|
||||||
@@ -103,7 +104,8 @@ function isDuplicateItem(label: string, existingItems: MenuOption[]): boolean {
|
|||||||
shape: ['shape', 'shapes'],
|
shape: ['shape', 'shapes'],
|
||||||
pin: ['pin', 'unpin'],
|
pin: ['pin', 'unpin'],
|
||||||
delete: ['remove', 'delete'],
|
delete: ['remove', 'delete'],
|
||||||
duplicate: ['clone', 'duplicate']
|
duplicate: ['clone', 'duplicate'],
|
||||||
|
frame: ['frame selection', 'frame nodes']
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingItems.some((item) => {
|
return existingItems.some((item) => {
|
||||||
@@ -226,6 +228,7 @@ const MENU_ORDER: string[] = [
|
|||||||
// Section 3: Structure operations
|
// Section 3: Structure operations
|
||||||
'Convert to Subgraph',
|
'Convert to Subgraph',
|
||||||
'Frame selection',
|
'Frame selection',
|
||||||
|
'Frame Nodes',
|
||||||
'Minimize Node',
|
'Minimize Node',
|
||||||
'Expand',
|
'Expand',
|
||||||
'Collapse',
|
'Collapse',
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
import { useSelectionMenuOptions } from '@/composables/graph/useSelectionMenuOptions'
|
import { useSelectionMenuOptions } from '@/composables/graph/useSelectionMenuOptions'
|
||||||
|
|
||||||
const subgraphMocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
convertToSubgraph: vi.fn(),
|
convertToSubgraph: vi.fn(),
|
||||||
unpackSubgraph: vi.fn(),
|
unpackSubgraph: vi.fn(),
|
||||||
addSubgraphToLibrary: vi.fn(),
|
addSubgraphToLibrary: vi.fn(),
|
||||||
|
frameNodes: vi.fn(),
|
||||||
createI18nMock: vi.fn(() => ({
|
createI18nMock: vi.fn(() => ({
|
||||||
global: {
|
global: {
|
||||||
t: vi.fn(),
|
t: vi.fn(),
|
||||||
@@ -19,7 +20,7 @@ vi.mock('vue-i18n', () => ({
|
|||||||
useI18n: () => ({
|
useI18n: () => ({
|
||||||
t: (key: string) => key
|
t: (key: string) => key
|
||||||
}),
|
}),
|
||||||
createI18n: subgraphMocks.createI18nMock
|
createI18n: mocks.createI18nMock
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/composables/graph/useSelectionOperations', () => ({
|
vi.mock('@/composables/graph/useSelectionOperations', () => ({
|
||||||
@@ -42,18 +43,46 @@ vi.mock('@/composables/graph/useNodeArrangement', () => ({
|
|||||||
|
|
||||||
vi.mock('@/composables/graph/useSubgraphOperations', () => ({
|
vi.mock('@/composables/graph/useSubgraphOperations', () => ({
|
||||||
useSubgraphOperations: () => ({
|
useSubgraphOperations: () => ({
|
||||||
convertToSubgraph: subgraphMocks.convertToSubgraph,
|
convertToSubgraph: mocks.convertToSubgraph,
|
||||||
unpackSubgraph: subgraphMocks.unpackSubgraph,
|
unpackSubgraph: mocks.unpackSubgraph,
|
||||||
addSubgraphToLibrary: subgraphMocks.addSubgraphToLibrary
|
addSubgraphToLibrary: mocks.addSubgraphToLibrary
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/composables/graph/useFrameNodes', () => ({
|
vi.mock('@/composables/graph/useFrameNodes', () => ({
|
||||||
useFrameNodes: () => ({
|
useFrameNodes: () => ({
|
||||||
frameNodes: vi.fn()
|
frameNodes: mocks.frameNodes
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
describe('useSelectionMenuOptions - multiple nodes options', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns Frame Nodes option that invokes frameNodes when called', () => {
|
||||||
|
const { getMultipleNodesOptions } = useSelectionMenuOptions()
|
||||||
|
const options = getMultipleNodesOptions()
|
||||||
|
|
||||||
|
const frameOption = options.find((opt) => opt.label === 'g.frameNodes')
|
||||||
|
expect(frameOption).toBeDefined()
|
||||||
|
expect(frameOption?.action).toBeDefined()
|
||||||
|
|
||||||
|
frameOption?.action?.()
|
||||||
|
expect(mocks.frameNodes).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns Convert to Group Node option from getMultipleNodesOptions', () => {
|
||||||
|
const { getMultipleNodesOptions } = useSelectionMenuOptions()
|
||||||
|
const options = getMultipleNodesOptions()
|
||||||
|
|
||||||
|
const groupNodeOption = options.find(
|
||||||
|
(opt) => opt.label === 'contextMenu.Convert to Group Node'
|
||||||
|
)
|
||||||
|
expect(groupNodeOption).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('useSelectionMenuOptions - subgraph options', () => {
|
describe('useSelectionMenuOptions - subgraph options', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@@ -68,7 +97,7 @@ describe('useSelectionMenuOptions - subgraph options', () => {
|
|||||||
|
|
||||||
expect(options).toHaveLength(1)
|
expect(options).toHaveLength(1)
|
||||||
expect(options[0]?.label).toBe('contextMenu.Convert to Subgraph')
|
expect(options[0]?.label).toBe('contextMenu.Convert to Subgraph')
|
||||||
expect(options[0]?.action).toBe(subgraphMocks.convertToSubgraph)
|
expect(options[0]?.action).toBe(mocks.convertToSubgraph)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('includes convert, add to library, and unpack when subgraphs are selected', () => {
|
it('includes convert, add to library, and unpack when subgraphs are selected', () => {
|
||||||
@@ -86,7 +115,7 @@ describe('useSelectionMenuOptions - subgraph options', () => {
|
|||||||
const convertOption = options.find(
|
const convertOption = options.find(
|
||||||
(option) => option.label === 'contextMenu.Convert to Subgraph'
|
(option) => option.label === 'contextMenu.Convert to Subgraph'
|
||||||
)
|
)
|
||||||
expect(convertOption?.action).toBe(subgraphMocks.convertToSubgraph)
|
expect(convertOption?.action).toBe(mocks.convertToSubgraph)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('hides convert option when only a single subgraph is selected', () => {
|
it('hides convert option when only a single subgraph is selected', () => {
|
||||||
|
|||||||
@@ -87,6 +87,25 @@ describe('useSelectionState', () => {
|
|||||||
const { hasAnySelection } = useSelectionState()
|
const { hasAnySelection } = useSelectionState()
|
||||||
expect(hasAnySelection.value).toBe(true)
|
expect(hasAnySelection.value).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('hasMultipleSelection should be true when 2+ items selected', () => {
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
const node1 = createMockLGraphNode({ id: 1 })
|
||||||
|
const node2 = createMockLGraphNode({ id: 2 })
|
||||||
|
canvasStore.$state.selectedItems = [node1, node2]
|
||||||
|
|
||||||
|
const { hasMultipleSelection } = useSelectionState()
|
||||||
|
expect(hasMultipleSelection.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hasMultipleSelection should be false when only 1 item selected', () => {
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
const node1 = createMockLGraphNode({ id: 1 })
|
||||||
|
canvasStore.$state.selectedItems = [node1]
|
||||||
|
|
||||||
|
const { hasMultipleSelection } = useSelectionState()
|
||||||
|
expect(hasMultipleSelection.value).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Node Type Filtering', () => {
|
describe('Node Type Filtering', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user