mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-19 02:06:38 +00:00
Compare commits
13 Commits
codex/fron
...
glary/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c3b2beb3b | ||
|
|
444fdc6bc5 | ||
|
|
37320bee25 | ||
|
|
541dff3b01 | ||
|
|
b96e455765 | ||
|
|
245e447618 | ||
|
|
da5ec439af | ||
|
|
0ed05f474f | ||
|
|
8efdd44ae2 | ||
|
|
5b691a4ba5 | ||
|
|
7110645400 | ||
|
|
774e094f5d | ||
|
|
ee50f6ad46 |
@@ -513,16 +513,22 @@ export class NodeReference {
|
||||
await this.comfyPage.contextMenu.clickMenuItem(optionText)
|
||||
}
|
||||
async convertToSubgraph() {
|
||||
const existingIds = new Set(
|
||||
(await this.comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph')).map(
|
||||
(node) => String(node.id)
|
||||
)
|
||||
)
|
||||
await this.clickContextMenuOption('Convert to Subgraph')
|
||||
await this.comfyPage.nextFrame()
|
||||
const nodes =
|
||||
await this.comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph')
|
||||
if (nodes.length !== 1) {
|
||||
throw new Error(
|
||||
`Did not find single subgraph node (found=${nodes.length})`
|
||||
|
||||
const findNewNodes = async () =>
|
||||
(await this.comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph')).filter(
|
||||
(node) => !existingIds.has(String(node.id))
|
||||
)
|
||||
}
|
||||
return nodes[0]
|
||||
|
||||
await expect.poll(async () => (await findNewNodes()).length).toBe(1)
|
||||
const created = await findNewNodes()
|
||||
return created[0]
|
||||
}
|
||||
async navigateIntoSubgraph() {
|
||||
const titleHeight = await this.comfyPage.page.evaluate(() => {
|
||||
|
||||
207
browser_tests/tests/propertiesPanel/subgraphSearchState.spec.ts
Normal file
207
browser_tests/tests/propertiesPanel/subgraphSearchState.spec.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
||||
|
||||
async function createTwoSubgraphs(comfyPage: ComfyPage) {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
|
||||
const ksampler = await comfyPage.nodeOps.getNodeRefById('3')
|
||||
await ksampler.click('title')
|
||||
const subgraphA = await ksampler.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const clipTextEncode = await comfyPage.nodeOps.getNodeRefById('6')
|
||||
await clipTextEncode.click('title')
|
||||
const subgraphB = await clipTextEncode.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
return { subgraphA, subgraphB }
|
||||
}
|
||||
|
||||
async function selectNodeById(comfyPage: ComfyPage, nodeId: string) {
|
||||
const nodeRef = await comfyPage.nodeOps.getNodeRefById(nodeId)
|
||||
await nodeRef.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
test.describe(
|
||||
'Properties panel - Search state scoping',
|
||||
{ tag: ['@subgraph'] },
|
||||
() => {
|
||||
let panel: PropertiesPanelHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
panel = new PropertiesPanelHelper(comfyPage.page)
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
await expect(panel.root).toBeVisible()
|
||||
})
|
||||
|
||||
test('search resets when selecting a different subgraph node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { subgraphA, subgraphB } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await subgraphB.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets in subgraph editor when selecting different subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { subgraphA, subgraphB } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.subgraphEditButton).toBeVisible()
|
||||
await panel.subgraphEditButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await subgraphB.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets when going from subgraph to normal node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { subgraphA } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await selectNodeById(comfyPage, '4')
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets when deselecting all nodes', async ({ comfyPage }) => {
|
||||
const { subgraphA } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await comfyPage.canvasOps.click({ x: 10, y: 10 })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets when going from no selection to node selection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await selectNodeById(comfyPage, '3')
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets after switching away and back to a tab', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.nextFrame()
|
||||
await selectNodeById(comfyPage, '3')
|
||||
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await panel.switchToTab('Settings')
|
||||
await expect(panel.searchBox).toBeHidden()
|
||||
|
||||
await panel.switchToTab('Parameters')
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets when toggling subgraph editor mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { subgraphA } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.subgraphEditButton).toBeVisible()
|
||||
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await panel.subgraphEditButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('search resets when switching between normal nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await selectNodeById(comfyPage, '3')
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await selectNodeById(comfyPage, '4')
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('widgets render immediately after subgraph switch without flicker', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { subgraphA, subgraphB } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await subgraphB.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
await expect(panel.contentArea).toBeVisible()
|
||||
await expect
|
||||
.poll(() => panel.contentArea.getByText('text').count())
|
||||
.toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('search works correctly in new subgraph after switching', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { subgraphA, subgraphB } = await createTwoSubgraphs(comfyPage)
|
||||
|
||||
await subgraphA.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
|
||||
await subgraphB.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(panel.searchBox).toHaveValue('')
|
||||
|
||||
await panel.searchWidgets('seed')
|
||||
await expect(panel.searchBox).toHaveValue('seed')
|
||||
await expect
|
||||
.poll(() =>
|
||||
panel.contentArea
|
||||
.getByText(/no .* match|no results|no items/i)
|
||||
.count()
|
||||
)
|
||||
.toBeGreaterThan(0)
|
||||
})
|
||||
}
|
||||
)
|
||||
371
src/components/rightSidePanel/RightSidePanel.test.ts
Normal file
371
src/components/rightSidePanel/RightSidePanel.test.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
import RightSidePanel from './RightSidePanel.vue'
|
||||
|
||||
const mockActiveWorkflow = ref<{ path: string } | null>({ path: 'wf-1' })
|
||||
const mockSidebarLocation = ref<'left' | 'right'>('right')
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', async () => {
|
||||
const { defineStore } = await import('pinia')
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
useCanvasStore: defineStore('canvasStoreMock', () => {
|
||||
const selectedItems = ref<unknown[]>([])
|
||||
const currentGraph = ref(undefined)
|
||||
const canvas = { setDirty: vi.fn() }
|
||||
return { canvas, currentGraph, selectedItems }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
get activeWorkflow() {
|
||||
return mockActiveWorkflow.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/rightSidePanelStore', async () => {
|
||||
const { defineStore } = await import('pinia')
|
||||
const { ref } = await import('vue')
|
||||
type ActiveTab =
|
||||
| 'errors'
|
||||
| 'parameters'
|
||||
| 'nodes'
|
||||
| 'info'
|
||||
| 'settings'
|
||||
| 'subgraph'
|
||||
return {
|
||||
useRightSidePanelStore: defineStore('rightSidePanelStoreMock', () => {
|
||||
const isOpen = ref(true)
|
||||
const activeTab = ref<ActiveTab>('parameters')
|
||||
const isEditingSubgraph = ref(false)
|
||||
const closePanel = vi.fn()
|
||||
const openPanel = vi.fn()
|
||||
return { isOpen, activeTab, isEditingSubgraph, closePanel, openPanel }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Sidebar.Location') return mockSidebarLocation.value
|
||||
if (key === 'Comfy.RightSidePanel.ShowErrorsTab') return false
|
||||
return undefined
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/executionErrorStore', async () => {
|
||||
const { defineStore } = await import('pinia')
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
useExecutionErrorStore: defineStore('executionErrorStoreMock', () => {
|
||||
const hasAnyError = ref(false)
|
||||
const allErrorExecutionIds = ref<string[]>([])
|
||||
const activeGraphErrorNodeIds = ref(new Set<string>())
|
||||
return {
|
||||
hasAnyError,
|
||||
allErrorExecutionIds,
|
||||
activeGraphErrorNodeIds,
|
||||
isContainerWithInternalError: vi.fn(() => false)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/missingModel/missingModelStore', async () => {
|
||||
const { defineStore } = await import('pinia')
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
useMissingModelStore: defineStore('missingModelStoreMock', () => {
|
||||
const activeMissingModelGraphIds = ref(new Set<string>())
|
||||
return { activeMissingModelGraphIds }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/missingMedia/missingMediaStore', async () => {
|
||||
const { defineStore } = await import('pinia')
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
useMissingMediaStore: defineStore('missingMediaStoreMock', () => {
|
||||
const activeMissingMediaGraphIds = ref(new Set<string>())
|
||||
return { activeMissingMediaGraphIds }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/nodeReplacement/missingNodesErrorStore', () => ({
|
||||
useMissingNodesErrorStore: () => ({
|
||||
missingAncestorExecutionIds: []
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useGraphHierarchy', () => ({
|
||||
useGraphHierarchy: () => ({
|
||||
findParentGroup: vi.fn(() => null)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
isGraphReady: false,
|
||||
rootGraph: { nodes: [] }
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
getActiveGraphNodeIds: vi.fn(() => new Set<string>())
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/nodeTitleUtil', () => ({
|
||||
resolveNodeDisplayName: vi.fn(() => 'Mock Node')
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/executableGroupNodeDto', () => ({
|
||||
isGroupNode: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isLGraphGroup: vi.fn(() => false),
|
||||
isLGraphNode: vi.fn(
|
||||
(item: unknown) =>
|
||||
typeof item === 'object' && item !== null && 'isSubgraphNode' in item
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: vi.fn((key: string) => key)
|
||||
}))
|
||||
|
||||
vi.mock(import('@/lib/litegraph/src/litegraph'), async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
class SubgraphNode {}
|
||||
return {
|
||||
...actual,
|
||||
SubgraphNode: SubgraphNode as unknown as typeof actual.SubgraphNode
|
||||
}
|
||||
})
|
||||
|
||||
const mountCounters = {
|
||||
tabNodes: 0,
|
||||
tabNormalInputs: 0,
|
||||
tabSubgraphInputs: 0,
|
||||
subgraphEditor: 0
|
||||
}
|
||||
|
||||
function makeMountTracker(key: keyof typeof mountCounters, testid: string) {
|
||||
return defineComponent({
|
||||
setup() {
|
||||
mountCounters[key]++
|
||||
const id = mountCounters[key]
|
||||
return { id }
|
||||
},
|
||||
template: `<div data-testid="${testid}" :data-mount-id="id" />`
|
||||
})
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: { settings: 'Settings' },
|
||||
rightSidePanel: {
|
||||
errors: 'Errors',
|
||||
nodes: 'Nodes',
|
||||
parameters: 'Parameters',
|
||||
info: 'Info',
|
||||
workflowOverview: 'Workflow',
|
||||
title: 'Selection ({count})',
|
||||
fallbackNodeTitle: 'Untitled',
|
||||
fallbackGroupTitle: 'Untitled Group',
|
||||
editTitle: 'Edit',
|
||||
editSubgraph: 'Edit subgraph',
|
||||
togglePanel: 'Toggle panel',
|
||||
globalSettings: { title: 'Global Settings' }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const renderOptions = {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
EditableText: { template: '<span><slot /></span>' },
|
||||
Tab: { template: '<div><slot /></div>' },
|
||||
TabList: {
|
||||
template: '<div><slot /></div>',
|
||||
emits: ['update:modelValue']
|
||||
},
|
||||
Button: { template: '<button><slot /></button>' },
|
||||
TabErrors: { template: '<div data-testid="tab-errors" />' },
|
||||
TabGlobalParameters: {
|
||||
template: '<div data-testid="tab-global-parameters" />'
|
||||
},
|
||||
TabNodes: makeMountTracker('tabNodes', 'tab-nodes'),
|
||||
TabGlobalSettings: {
|
||||
template: '<div data-testid="tab-global-settings" />'
|
||||
},
|
||||
TabSubgraphInputs: makeMountTracker(
|
||||
'tabSubgraphInputs',
|
||||
'tab-subgraph-inputs'
|
||||
),
|
||||
TabNormalInputs: makeMountTracker('tabNormalInputs', 'tab-normal-inputs'),
|
||||
TabInfo: { template: '<div data-testid="tab-info" />' },
|
||||
TabSettings: { template: '<div data-testid="tab-settings" />' },
|
||||
SubgraphEditor: makeMountTracker('subgraphEditor', 'subgraph-editor')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('RightSidePanel', () => {
|
||||
let canvasStore: ReturnType<typeof useCanvasStore>
|
||||
let rightSidePanelStore: ReturnType<typeof useRightSidePanelStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
canvasStore = useCanvasStore()
|
||||
rightSidePanelStore = useRightSidePanelStore()
|
||||
canvasStore.selectedItems = []
|
||||
mockActiveWorkflow.value = { path: 'wf-1' }
|
||||
rightSidePanelStore.activeTab = 'parameters'
|
||||
mountCounters.tabNodes = 0
|
||||
mountCounters.tabNormalInputs = 0
|
||||
mountCounters.tabSubgraphInputs = 0
|
||||
mountCounters.subgraphEditor = 0
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('remounts TabNodes when the active workflow path changes', async () => {
|
||||
rightSidePanelStore.activeTab = 'nodes'
|
||||
|
||||
render(RightSidePanel, renderOptions)
|
||||
await nextTick()
|
||||
|
||||
const initial = screen
|
||||
.getByTestId('tab-nodes')
|
||||
.getAttribute('data-mount-id')
|
||||
expect(initial).toBeTruthy()
|
||||
|
||||
mockActiveWorkflow.value = { path: 'wf-2' }
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
const remounted = screen
|
||||
.getByTestId('tab-nodes')
|
||||
.getAttribute('data-mount-id')
|
||||
expect(remounted).not.toBe(initial)
|
||||
})
|
||||
|
||||
it('does not remount TabNodes when the workflow path stays the same', async () => {
|
||||
rightSidePanelStore.activeTab = 'nodes'
|
||||
|
||||
render(RightSidePanel, renderOptions)
|
||||
await nextTick()
|
||||
|
||||
const initial = screen
|
||||
.getByTestId('tab-nodes')
|
||||
.getAttribute('data-mount-id')
|
||||
|
||||
mockActiveWorkflow.value = { path: 'wf-1' }
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getByTestId('tab-nodes').getAttribute('data-mount-id')).toBe(
|
||||
initial
|
||||
)
|
||||
})
|
||||
|
||||
it('uses an empty workflow key when no workflow is active', async () => {
|
||||
rightSidePanelStore.activeTab = 'nodes'
|
||||
mockActiveWorkflow.value = null
|
||||
|
||||
render(RightSidePanel, renderOptions)
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getByTestId('tab-nodes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('remounts TabNormalInputs when selection identity changes', async () => {
|
||||
const node1 = fromAny<LGraphNode, unknown>({
|
||||
id: 11,
|
||||
title: 'Node 1',
|
||||
isSubgraphNode: () => false,
|
||||
widgets: []
|
||||
})
|
||||
const node2 = fromAny<LGraphNode, unknown>({
|
||||
id: 22,
|
||||
title: 'Node 2',
|
||||
isSubgraphNode: () => false,
|
||||
widgets: []
|
||||
})
|
||||
|
||||
canvasStore.selectedItems = [node1]
|
||||
rightSidePanelStore.activeTab = 'parameters'
|
||||
|
||||
render(RightSidePanel, renderOptions)
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
const initial = screen
|
||||
.getByTestId('tab-normal-inputs')
|
||||
.getAttribute('data-mount-id')
|
||||
expect(initial).toBeTruthy()
|
||||
|
||||
canvasStore.selectedItems = [node2]
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
const remounted = screen
|
||||
.getByTestId('tab-normal-inputs')
|
||||
.getAttribute('data-mount-id')
|
||||
expect(remounted).not.toBe(initial)
|
||||
})
|
||||
|
||||
it('remounts TabNormalInputs when the workflow path changes (selectedNodesKey embeds workflowKey)', async () => {
|
||||
const node = fromAny<LGraphNode, unknown>({
|
||||
id: 11,
|
||||
title: 'Node 1',
|
||||
isSubgraphNode: () => false,
|
||||
widgets: []
|
||||
})
|
||||
|
||||
canvasStore.selectedItems = [node]
|
||||
rightSidePanelStore.activeTab = 'parameters'
|
||||
|
||||
render(RightSidePanel, renderOptions)
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
const initial = screen
|
||||
.getByTestId('tab-normal-inputs')
|
||||
.getAttribute('data-mount-id')
|
||||
|
||||
mockActiveWorkflow.value = { path: 'wf-2' }
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
const remounted = screen
|
||||
.getByTestId('tab-normal-inputs')
|
||||
.getAttribute('data-mount-id')
|
||||
expect(remounted).not.toBe(initial)
|
||||
})
|
||||
})
|
||||
@@ -15,6 +15,7 @@ import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
|
||||
@@ -40,6 +41,7 @@ import SubgraphEditor from './subgraph/SubgraphEditor.vue'
|
||||
import TabErrors from './errors/TabErrors.vue'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
const missingMediaStore = useMissingMediaStore()
|
||||
@@ -105,6 +107,20 @@ const isSingleSubgraphNode = computed(() => {
|
||||
return selectedSingleNode.value instanceof SubgraphNode
|
||||
})
|
||||
|
||||
const workflowKey = computed(() => workflowStore.activeWorkflow?.path ?? '')
|
||||
|
||||
const selectedNodesKey = computed(() => {
|
||||
const nodeIds = selectedNodes.value.map((n) => n.id).join(',')
|
||||
const groupIds = selectedGroups.value.map((g) => g.id).join(',')
|
||||
return `${workflowKey.value}|${nodeIds}|${groupIds}`
|
||||
})
|
||||
|
||||
const singleSubgraphKey = computed(() =>
|
||||
selectedSingleNode.value
|
||||
? `${workflowKey.value}:${selectedSingleNode.value.id}`
|
||||
: ''
|
||||
)
|
||||
|
||||
function closePanel() {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'right_side_panel_closed',
|
||||
@@ -378,20 +394,23 @@ function handleTitleCancel() {
|
||||
<TabErrors v-if="activeTab === 'errors'" />
|
||||
<template v-else-if="!hasSelection">
|
||||
<TabGlobalParameters v-if="activeTab === 'parameters'" />
|
||||
<TabNodes v-else-if="activeTab === 'nodes'" />
|
||||
<TabNodes v-else-if="activeTab === 'nodes'" :key="workflowKey" />
|
||||
<TabGlobalSettings v-else-if="activeTab === 'settings'" />
|
||||
</template>
|
||||
<SubgraphEditor
|
||||
v-else-if="isSingleSubgraphNode && isEditingSubgraph"
|
||||
:key="singleSubgraphKey"
|
||||
:node="selectedSingleNode"
|
||||
/>
|
||||
<template v-else>
|
||||
<TabSubgraphInputs
|
||||
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
|
||||
:key="singleSubgraphKey"
|
||||
:node="selectedSingleNode as SubgraphNode"
|
||||
/>
|
||||
<TabNormalInputs
|
||||
v-else-if="activeTab === 'parameters'"
|
||||
:key="selectedNodesKey"
|
||||
:nodes="selectedNodes"
|
||||
:must-show-node-title="selectedGroups.length > 0"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import TabGlobalParameters from './TabGlobalParameters.vue'
|
||||
|
||||
const mockValidFavoritedWidgets = ref([
|
||||
{
|
||||
node: { id: 1, title: 'Node A' },
|
||||
widget: {
|
||||
name: 'alpha-widget',
|
||||
type: 'text',
|
||||
value: 'alpha',
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
node: { id: 2, title: 'Node B' },
|
||||
widget: {
|
||||
name: 'beta-widget',
|
||||
type: 'text',
|
||||
value: 'beta',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const mockReorderFavorites = vi.fn()
|
||||
|
||||
vi.mock('@/stores/workspace/favoritedWidgetsStore', () => ({
|
||||
useFavoritedWidgetsStore: () => ({
|
||||
validFavoritedWidgets: mockValidFavoritedWidgets.value,
|
||||
reorderFavorites: mockReorderFavorites
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/ui/draggableList', () => ({
|
||||
DraggableList: vi.fn().mockImplementation(() => ({ dispose: vi.fn() }))
|
||||
}))
|
||||
|
||||
const AsyncSearchInputStub = defineComponent({
|
||||
props: {
|
||||
modelValue: { type: String, default: '' },
|
||||
searcher: { type: Function, default: undefined }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
function onInput(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
emit('update:modelValue', value)
|
||||
props.searcher?.(value)
|
||||
}
|
||||
|
||||
return { onInput }
|
||||
},
|
||||
template:
|
||||
'<input data-testid="search-input" :value="modelValue" @input="onInput" />'
|
||||
})
|
||||
|
||||
const SectionWidgetsStub = defineComponent({
|
||||
props: {
|
||||
widgets: { type: Array, default: () => [] }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in widgets"
|
||||
:key="item.widget.name"
|
||||
data-testid="widget-name"
|
||||
>
|
||||
{{ item.widget.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<slot v-if="widgets.length === 0" name="empty" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
rightSidePanel: {
|
||||
favorites: 'Favorites',
|
||||
favoritesNone: 'No favorites',
|
||||
noneSearchDesc: 'No results found',
|
||||
favoritesNoneDesc: 'Add favorites to get started',
|
||||
favoritesNoneHint: 'Use the <moreIcon/> menu to favorite widgets'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function renderComponent() {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(TabGlobalParameters, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SectionWidgets: SectionWidgetsStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { user, ...result }
|
||||
}
|
||||
|
||||
describe('TabGlobalParameters', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with empty search query and filters widgets as user types', async () => {
|
||||
const { user } = renderComponent()
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('beta-widget')).toBeInTheDocument()
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'alpha-widget')
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('alpha-widget')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.queryByText('beta-widget')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resets local search query after remount via key change', async () => {
|
||||
const user = userEvent.setup()
|
||||
const Wrapper = defineComponent({
|
||||
components: { TabGlobalParameters },
|
||||
props: {
|
||||
k: { type: String, required: true }
|
||||
},
|
||||
template: '<TabGlobalParameters :key="k" />'
|
||||
})
|
||||
|
||||
const { rerender } = render(Wrapper, {
|
||||
props: { k: 'ctx-1' },
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SectionWidgets: SectionWidgetsStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'beta-widget')
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('beta-widget')
|
||||
|
||||
await rerender({ k: 'ctx-2' })
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('beta-widget')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import type { ValidFavoritedWidget } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
import { searchWidgets } from '../shared'
|
||||
import SectionWidgets from './SectionWidgets.vue'
|
||||
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSearching = ref(false)
|
||||
|
||||
177
src/components/rightSidePanel/parameters/TabNodes.test.ts
Normal file
177
src/components/rightSidePanel/parameters/TabNodes.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import TabNodes from './TabNodes.vue'
|
||||
|
||||
const mockNodes = [
|
||||
fromAny<LGraphNode, unknown>({
|
||||
id: 11,
|
||||
title: 'Alpha Node',
|
||||
getTitle: () => 'Alpha Node',
|
||||
widgets: [
|
||||
{
|
||||
name: 'alpha-widget',
|
||||
type: 'text',
|
||||
value: 'alpha',
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
}),
|
||||
fromAny<LGraphNode, unknown>({
|
||||
id: 22,
|
||||
title: 'Beta Node',
|
||||
getTitle: () => 'Beta Node',
|
||||
widgets: [
|
||||
{
|
||||
name: 'beta-widget',
|
||||
type: 'text',
|
||||
value: 'beta',
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn(() => false)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
canvas: {
|
||||
graph: {
|
||||
nodes: mockNodes
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: { path: 'workflow-a' }
|
||||
})
|
||||
}))
|
||||
|
||||
const AsyncSearchInputStub = defineComponent({
|
||||
props: {
|
||||
modelValue: { type: String, default: '' },
|
||||
searcher: { type: Function, default: undefined }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
function onInput(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
emit('update:modelValue', value)
|
||||
props.searcher?.(value)
|
||||
}
|
||||
|
||||
return { onInput }
|
||||
},
|
||||
template:
|
||||
'<input data-testid="search-input" :value="modelValue" @input="onInput" />'
|
||||
})
|
||||
|
||||
const SectionWidgetsStub = defineComponent({
|
||||
props: {
|
||||
node: { type: Object, required: true },
|
||||
widgets: { type: Array, default: () => [] }
|
||||
},
|
||||
template: `
|
||||
<div data-testid="section-item">
|
||||
<span data-testid="node-title">{{ node.title }}</span>
|
||||
<span
|
||||
v-for="item in widgets"
|
||||
:key="item.widget.name"
|
||||
data-testid="widget-name"
|
||||
>
|
||||
{{ item.widget.name }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
rightSidePanel: {
|
||||
noneSearchDesc: 'No results found',
|
||||
inputsNoneTooltip: 'No inputs'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('TabNodes', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with an empty search and filters node widgets', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(TabNodes, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SectionWidgets: SectionWidgetsStub,
|
||||
CollapseToggleButton: { template: '<div />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('beta-widget')).toBeInTheDocument()
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'alpha-widget')
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('alpha-widget')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.queryByText('beta-widget')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('creates fresh local search state after key-driven remount', async () => {
|
||||
const user = userEvent.setup()
|
||||
const Wrapper = defineComponent({
|
||||
components: { TabNodes },
|
||||
props: {
|
||||
k: { type: String, required: true }
|
||||
},
|
||||
template: '<TabNodes :key="k" />'
|
||||
})
|
||||
|
||||
const { rerender } = render(Wrapper, {
|
||||
props: { k: 'ctx-1' },
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SectionWidgets: SectionWidgetsStub,
|
||||
CollapseToggleButton: { template: '<div />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'beta-widget')
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('beta-widget')
|
||||
|
||||
await rerender({ k: 'ctx-2' })
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('beta-widget')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, reactive, ref, shallowRef } from 'vue'
|
||||
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
@@ -16,16 +13,12 @@ import type { NodeWidgetsListList } from '../shared'
|
||||
import SectionWidgets from './SectionWidgets.vue'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const nodes = computed((): LGraphNode[] => {
|
||||
// Depend on activeWorkflow to trigger recomputation when workflow changes
|
||||
void workflowStore.activeWorkflow?.path
|
||||
return (canvasStore.canvas?.graph?.nodes ?? []) as LGraphNode[]
|
||||
})
|
||||
const nodes = computed(
|
||||
(): LGraphNode[] => (canvasStore.canvas?.graph?.nodes ?? []) as LGraphNode[]
|
||||
)
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const { widgetsSectionDataList } = computedSectionDataList(nodes)
|
||||
|
||||
@@ -36,15 +29,6 @@ const isSearching = ref(false)
|
||||
|
||||
const collapseMap = reactive<Record<string, boolean>>({})
|
||||
|
||||
watch(
|
||||
() => workflowStore.activeWorkflow?.path,
|
||||
() => {
|
||||
for (const key of Object.keys(collapseMap)) {
|
||||
delete collapseMap[key]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function isSectionCollapsed(nodeId: NodeId): boolean {
|
||||
// Defaults to collapsed when not explicitly set by the user
|
||||
return collapseMap[nodeId] ?? true
|
||||
|
||||
171
src/components/rightSidePanel/parameters/TabNormalInputs.test.ts
Normal file
171
src/components/rightSidePanel/parameters/TabNormalInputs.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import TabNormalInputs from './TabNormalInputs.vue'
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn(() => false)
|
||||
})
|
||||
}))
|
||||
|
||||
const AsyncSearchInputStub = defineComponent({
|
||||
props: {
|
||||
modelValue: { type: String, default: '' },
|
||||
searcher: { type: Function, default: undefined }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
function onInput(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
emit('update:modelValue', value)
|
||||
props.searcher?.(value)
|
||||
}
|
||||
|
||||
return { onInput }
|
||||
},
|
||||
template:
|
||||
'<input data-testid="search-input" :value="modelValue" @input="onInput" />'
|
||||
})
|
||||
|
||||
const SectionWidgetsStub = defineComponent({
|
||||
props: {
|
||||
node: { type: Object, default: undefined },
|
||||
widgets: { type: Array, default: () => [] }
|
||||
},
|
||||
template: `
|
||||
<div data-testid="section-item">
|
||||
<span v-if="node" data-testid="node-title">{{ node.title }}</span>
|
||||
<span
|
||||
v-for="item in widgets"
|
||||
:key="item.widget.name"
|
||||
data-testid="widget-name"
|
||||
>
|
||||
{{ item.widget.name }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
rightSidePanel: {
|
||||
noneSearchDesc: 'No results found',
|
||||
nodesNoneDesc: 'No nodes selected',
|
||||
inputs: 'Inputs',
|
||||
inputsNone: 'No inputs',
|
||||
advancedInputs: 'Advanced Inputs',
|
||||
inputsNoneTooltip: 'No inputs available'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function createNode(
|
||||
id: number,
|
||||
title: string,
|
||||
widgetName: string,
|
||||
options: { advanced?: boolean } = {}
|
||||
): LGraphNode {
|
||||
return fromAny<LGraphNode, unknown>({
|
||||
id,
|
||||
title,
|
||||
getTitle: () => title,
|
||||
widgets: [
|
||||
{
|
||||
name: widgetName,
|
||||
type: 'text',
|
||||
value: widgetName,
|
||||
options
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const testNodes = [
|
||||
createNode(1, 'Alpha Node', 'alpha-widget'),
|
||||
createNode(2, 'Beta Node', 'beta-widget')
|
||||
]
|
||||
|
||||
describe('TabNormalInputs', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with empty search query and filters widgets from provided nodes', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(TabNormalInputs, {
|
||||
props: {
|
||||
nodes: testNodes
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SectionWidgets: SectionWidgetsStub,
|
||||
CollapseToggleButton: { template: '<div />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('beta-widget')).toBeInTheDocument()
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'alpha-widget')
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('alpha-widget')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.queryByText('beta-widget')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resets local search query after remount with a new key', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const Wrapper = defineComponent({
|
||||
components: { TabNormalInputs },
|
||||
props: {
|
||||
k: { type: String, required: true }
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
nodes: testNodes
|
||||
}
|
||||
},
|
||||
template: '<TabNormalInputs :key="k" :nodes="nodes" />'
|
||||
})
|
||||
|
||||
const { rerender } = render(Wrapper, {
|
||||
props: { k: 'ctx-1' },
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SectionWidgets: SectionWidgetsStub,
|
||||
CollapseToggleButton: { template: '<div />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'beta-widget')
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('beta-widget')
|
||||
|
||||
await rerender({ k: 'ctx-2' })
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('beta-widget')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, reactive, ref, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
@@ -20,10 +17,7 @@ const { nodes, mustShowNodeTitle } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const { widgetsSectionDataList, includesAdvanced } = computedSectionDataList(
|
||||
() => nodes
|
||||
@@ -63,16 +57,6 @@ const isSearching = ref(false)
|
||||
const collapseMap = reactive<Record<string, boolean>>({})
|
||||
const advancedCollapsed = ref(true)
|
||||
|
||||
watch(
|
||||
() => workflowStore.activeWorkflow?.path,
|
||||
() => {
|
||||
for (const key of Object.keys(collapseMap)) {
|
||||
delete collapseMap[key]
|
||||
}
|
||||
advancedCollapsed.value = true
|
||||
}
|
||||
)
|
||||
|
||||
function isSectionCollapsed(nodeId: NodeId): boolean {
|
||||
// When not explicitly set, sections are collapsed if multiple nodes are selected
|
||||
return collapseMap[nodeId] ?? isMultipleNodesSelected.value
|
||||
|
||||
@@ -26,7 +26,8 @@ const { node } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const { focusedSection } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const advancedInputsCollapsed = ref(true)
|
||||
const firstSectionCollapsed = ref(false)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, shallowRef, watch } from 'vue'
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -31,7 +30,6 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { UNASSIGNED_NODE_ID } from '@/types/nodeId'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
@@ -53,8 +51,7 @@ type ActiveRow = PromotedRow | PreviewRow
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const previewExposureStore = usePreviewExposureStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
const activeNode = computed(() => {
|
||||
|
||||
@@ -39,7 +39,6 @@ export const useRightSidePanelStore = defineStore('rightSidePanel', () => {
|
||||
* cleared by TabErrors after expanding the relevant error group.
|
||||
*/
|
||||
const focusedErrorNodeId = ref<string | null>(null)
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Auto-close panel when switching to legacy menu mode
|
||||
watch(isLegacyMenu, (legacy) => {
|
||||
@@ -87,7 +86,6 @@ export const useRightSidePanelStore = defineStore('rightSidePanel', () => {
|
||||
isEditingSubgraph,
|
||||
focusedSection,
|
||||
focusedErrorNodeId,
|
||||
searchQuery,
|
||||
openPanel,
|
||||
closePanel,
|
||||
togglePanel,
|
||||
|
||||
Reference in New Issue
Block a user