mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
Compare commits
9 Commits
version-bu
...
glary/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b96e455765 | ||
|
|
245e447618 | ||
|
|
da5ec439af | ||
|
|
0ed05f474f | ||
|
|
8efdd44ae2 | ||
|
|
5b691a4ba5 | ||
|
|
7110645400 | ||
|
|
774e094f5d | ||
|
|
ee50f6ad46 |
202
browser_tests/tests/propertiesPanel/subgraphSearchState.spec.ts
Normal file
202
browser_tests/tests/propertiesPanel/subgraphSearchState.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
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 when switching tabs', 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).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)
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,7 @@ import { getActiveGraphNodeIds } from '@/utils/graphTraversalUtil'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
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,14 @@ const isSingleSubgraphNode = computed(() => {
|
||||
return selectedSingleNode.value instanceof SubgraphNode
|
||||
})
|
||||
|
||||
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 workflowKey = computed(() => workflowStore.activeWorkflow?.path ?? '')
|
||||
|
||||
function closePanel() {
|
||||
rightSidePanelStore.closePanel()
|
||||
}
|
||||
@@ -374,20 +384,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="`${workflowKey}:${selectedSingleNode!.id}`"
|
||||
:node="selectedSingleNode"
|
||||
/>
|
||||
<template v-else>
|
||||
<TabSubgraphInputs
|
||||
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
|
||||
:key="`${workflowKey}:${selectedSingleNode!.id}`"
|
||||
: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,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useMounted, watchDebounced } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
@@ -15,14 +14,12 @@ import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
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 draggableList = ref<DraggableList | undefined>(undefined)
|
||||
|
||||
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,6 +1,5 @@
|
||||
<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'
|
||||
|
||||
@@ -8,7 +7,6 @@ import type { LGraphNode, NodeId } 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 { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
import type { NodeWidgetsListList } from '../shared'
|
||||
@@ -23,8 +21,7 @@ const nodes = computed((): LGraphNode[] => {
|
||||
return (canvasStore.canvas?.graph?.nodes ?? []) as LGraphNode[]
|
||||
})
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const { widgetsSectionDataList } = computedSectionDataList(nodes)
|
||||
|
||||
@@ -35,15 +32,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, NodeId } 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 { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
import type { NodeWidgetsListList } from '../shared'
|
||||
@@ -19,10 +16,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
|
||||
@@ -58,16 +52,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
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
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 { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import TabSubgraphInputs from './TabSubgraphInputs.vue'
|
||||
|
||||
vi.mock('@/core/graph/subgraph/promotedWidgetTypes', () => ({
|
||||
isPromotedWidgetView: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
vi.mock('@/core/graph/subgraph/promotionUtils', () => ({
|
||||
getWidgetName: vi.fn((widget) => widget.name)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/promotionStore', () => ({
|
||||
usePromotionStore: () => ({
|
||||
getPromotions: vi.fn(() => [
|
||||
{
|
||||
sourceNodeId: '11',
|
||||
sourceWidgetName: 'alpha-widget',
|
||||
disambiguatingSourceNodeId: undefined
|
||||
},
|
||||
{
|
||||
sourceNodeId: '22',
|
||||
sourceWidgetName: 'beta-widget',
|
||||
disambiguatingSourceNodeId: undefined
|
||||
}
|
||||
]),
|
||||
isPromoted: vi.fn(() => false),
|
||||
movePromotion: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
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>
|
||||
<span
|
||||
v-for="item in widgets"
|
||||
:key="item.widget.name"
|
||||
data-testid="widget-name"
|
||||
>
|
||||
{{ item.widget.name }}
|
||||
</span>
|
||||
<slot v-if="widgets.length === 0" name="empty" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
rightSidePanel: {
|
||||
inputs: 'Inputs',
|
||||
inputsNone: 'No inputs',
|
||||
advancedInputs: 'Advanced Inputs',
|
||||
noneSearchDesc: 'No results found',
|
||||
inputsNoneTooltip: 'No inputs available'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const subgraphNode = fromAny<SubgraphNode, unknown>({
|
||||
id: 100,
|
||||
rootGraph: { id: 'root-1' },
|
||||
widgets: [
|
||||
{
|
||||
name: 'alpha-widget',
|
||||
type: 'text',
|
||||
value: 'alpha',
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
name: 'beta-widget',
|
||||
type: 'text',
|
||||
value: 'beta',
|
||||
options: {}
|
||||
}
|
||||
],
|
||||
subgraph: {
|
||||
nodes: [
|
||||
{
|
||||
id: 11,
|
||||
title: 'Interior 1',
|
||||
widgets: [
|
||||
{
|
||||
name: 'interior-widget',
|
||||
type: 'text',
|
||||
value: 'interior',
|
||||
computedDisabled: false,
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
describe('TabSubgraphInputs', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ createSpy: vi.fn, stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with an empty search and filters promoted widgets', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(TabSubgraphInputs, {
|
||||
props: {
|
||||
node: subgraphNode
|
||||
},
|
||||
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 key-based remount', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const Wrapper = defineComponent({
|
||||
components: { TabSubgraphInputs },
|
||||
props: {
|
||||
k: { type: String, required: true }
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
node: subgraphNode
|
||||
}
|
||||
},
|
||||
template: '<TabSubgraphInputs :key="k" :node="node" />'
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -35,7 +35,8 @@ const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const { focusedSection } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const advancedInputsCollapsed = ref(true)
|
||||
const firstSectionCollapsed = ref(false)
|
||||
|
||||
@@ -5,21 +5,21 @@ import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import { flatAndCategorizeSelectedItems, searchWidgets } from './shared'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
describe('searchWidgets', () => {
|
||||
const createWidget = (
|
||||
name: string,
|
||||
type: string,
|
||||
value?: string,
|
||||
label?: string
|
||||
): { widget: IBaseWidget } => ({
|
||||
widget: {
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
label
|
||||
} as IBaseWidget
|
||||
})
|
||||
const createWidget = (
|
||||
name: string,
|
||||
type: string,
|
||||
value?: string,
|
||||
label?: string
|
||||
): { widget: IBaseWidget } => ({
|
||||
widget: {
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
label
|
||||
} as IBaseWidget
|
||||
})
|
||||
|
||||
describe('searchWidgets', () => {
|
||||
it('should return all widgets when query is empty', () => {
|
||||
const widgets = [
|
||||
createWidget('width', 'number', '100'),
|
||||
@@ -71,6 +71,22 @@ describe('searchWidgets', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('searchWidgets — context independence', () => {
|
||||
it('returns empty when query from previous context has no matches in new context', () => {
|
||||
const newWidgets = [createWidget('strength', 'slider', '0.8')]
|
||||
const result = searchWidgets(newWidgets, 'seed')
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns all widgets when fresh empty query is applied', () => {
|
||||
const widgets = [
|
||||
createWidget('strength', 'slider', '0.8'),
|
||||
createWidget('steps', 'number', '20')
|
||||
]
|
||||
expect(searchWidgets(widgets, '')).toEqual(widgets)
|
||||
})
|
||||
})
|
||||
|
||||
describe('flatAndCategorizeSelectedItems', () => {
|
||||
let testGroup1: LGraphGroup
|
||||
let testGroup2: LGraphGroup
|
||||
|
||||
224
src/components/rightSidePanel/subgraph/SubgraphEditor.test.ts
Normal file
224
src/components/rightSidePanel/subgraph/SubgraphEditor.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
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, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { SubgraphNode as RuntimeSubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import SubgraphEditor from './SubgraphEditor.vue'
|
||||
|
||||
const mockSelectedItems = ref<unknown[]>([])
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
canvas: { setDirty: vi.fn() },
|
||||
get selectedItems() {
|
||||
return mockSelectedItems.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/promotionStore', () => ({
|
||||
usePromotionStore: () => ({
|
||||
getPromotions: vi.fn(() => []),
|
||||
setPromotions: vi.fn(),
|
||||
isPromoted: vi.fn(() => false),
|
||||
movePromotion: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: () => ({
|
||||
updatePreviews: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/core/graph/subgraph/promotedWidgetTypes', () => ({
|
||||
isPromotedWidgetView: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
vi.mock('@/core/graph/subgraph/promotionUtils', () => ({
|
||||
demoteWidget: vi.fn(),
|
||||
getPromotableWidgets: vi.fn((node: { widgets?: unknown[] }) =>
|
||||
Array.isArray(node?.widgets) ? node.widgets : []
|
||||
),
|
||||
getSourceNodeId: vi.fn(() => undefined),
|
||||
getWidgetName: vi.fn((widget: { name: string }) => widget.name),
|
||||
isLinkedPromotion: vi.fn(() => false),
|
||||
isRecommendedWidget: vi.fn(() => false),
|
||||
promoteWidget: vi.fn(),
|
||||
pruneDisconnected: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/litegraph/src/subgraph/SubgraphNode', () => {
|
||||
class SubgraphNode {}
|
||||
return { SubgraphNode }
|
||||
})
|
||||
|
||||
const AsyncSearchInputStub = defineComponent({
|
||||
props: {
|
||||
modelValue: { type: String, default: '' }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(_, { emit }) {
|
||||
function onInput(event: Event) {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
return { onInput }
|
||||
},
|
||||
template:
|
||||
'<input data-testid="search-input" :value="modelValue" @input="onInput" />'
|
||||
})
|
||||
|
||||
const SubgraphNodeWidgetStub = defineComponent({
|
||||
props: {
|
||||
nodeTitle: { type: String, default: '' },
|
||||
widgetName: { type: String, default: '' }
|
||||
},
|
||||
template:
|
||||
'<div data-testid="subgraph-widget">{{ nodeTitle }}::{{ widgetName }}</div>'
|
||||
})
|
||||
|
||||
const DraggableListStub = defineComponent({
|
||||
template: '<div><slot :dragClass="\'\'" /></div>'
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
rightSidePanel: {
|
||||
noneSearchDesc: 'No results found'
|
||||
},
|
||||
subgraphStore: {
|
||||
linked: 'Linked',
|
||||
shown: 'Shown',
|
||||
hidden: 'Hidden',
|
||||
showAll: 'Show all',
|
||||
hideAll: 'Hide all',
|
||||
showRecommended: 'Show recommended'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function makeSubgraphNode(): SubgraphNode {
|
||||
const interiorAlpha = fromAny({
|
||||
id: 11,
|
||||
title: 'Alpha Interior',
|
||||
widgets: [
|
||||
{
|
||||
name: 'alpha-widget',
|
||||
type: 'text',
|
||||
value: 'alpha',
|
||||
computedDisabled: false,
|
||||
options: {}
|
||||
}
|
||||
],
|
||||
updateComputedDisabled: () => undefined
|
||||
})
|
||||
const interiorBeta = fromAny({
|
||||
id: 22,
|
||||
title: 'Beta Interior',
|
||||
widgets: [
|
||||
{
|
||||
name: 'beta-widget',
|
||||
type: 'text',
|
||||
value: 'beta',
|
||||
computedDisabled: false,
|
||||
options: {}
|
||||
}
|
||||
],
|
||||
updateComputedDisabled: () => undefined
|
||||
})
|
||||
|
||||
const NodeCtor = RuntimeSubgraphNode as unknown as new () => SubgraphNode
|
||||
return fromAny<SubgraphNode, unknown>(
|
||||
Object.assign(new NodeCtor(), {
|
||||
id: 100,
|
||||
rootGraph: { id: 'root-1' },
|
||||
widgets: [],
|
||||
subgraph: {
|
||||
_nodes_by_id: { '11': interiorAlpha, '22': interiorBeta },
|
||||
nodes: [interiorAlpha, interiorBeta]
|
||||
},
|
||||
computeSize: vi.fn(),
|
||||
setDirtyCanvas: vi.fn()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const renderOptions = {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
AsyncSearchInput: AsyncSearchInputStub,
|
||||
SubgraphNodeWidget: SubgraphNodeWidgetStub,
|
||||
DraggableList: DraggableListStub,
|
||||
Button: { template: '<button v-bind="$attrs"><slot /></button>' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('SubgraphEditor', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
mockSelectedItems.value = [makeSubgraphNode()]
|
||||
})
|
||||
|
||||
it('starts with an empty search query and renders candidate widgets', () => {
|
||||
render(SubgraphEditor, renderOptions)
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('Alpha Interior::alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('Beta Interior::beta-widget')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters candidate widgets when the user types into the search', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(SubgraphEditor, renderOptions)
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'alpha')
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('alpha')
|
||||
expect(screen.getByText('Alpha Interior::alpha-widget')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('Beta Interior::beta-widget')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resets local search query after a key-based remount', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const Wrapper = defineComponent({
|
||||
components: { SubgraphEditor },
|
||||
props: {
|
||||
k: { type: String, required: true }
|
||||
},
|
||||
template: '<SubgraphEditor :key="k" />'
|
||||
})
|
||||
|
||||
const { rerender } = render(Wrapper, {
|
||||
props: { k: 'ctx-1' },
|
||||
...renderOptions
|
||||
})
|
||||
|
||||
await user.type(screen.getByTestId('search-input'), 'alpha')
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('alpha')
|
||||
expect(
|
||||
screen.queryByText('Beta Interior::beta-widget')
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
await rerender({ k: 'ctx-2' })
|
||||
|
||||
expect(screen.getByTestId('search-input')).toHaveValue('')
|
||||
expect(screen.getByText('Alpha Interior::alpha-widget')).toBeInTheDocument()
|
||||
expect(screen.getByText('Beta Interior::beta-widget')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
@@ -23,7 +22,6 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
|
||||
@@ -31,8 +29,7 @@ import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const promotionEntries = computed(() => {
|
||||
const node = activeNode.value
|
||||
|
||||
@@ -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