mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 03:01:33 +00:00
Compare commits
2 Commits
coderabbit
...
rizumu/fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57c26b22ec | ||
|
|
83ee120f3d |
@@ -7,6 +7,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
@@ -22,6 +23,7 @@ const { t } = useI18n()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const { widgetsSectionDataList, includesAdvanced } = computedSectionDataList(
|
||||
@@ -38,7 +40,8 @@ const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
const advancedWidgets = widgets
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
|
||||
!(w.options?.canvasOnly || w.options?.hidden) &&
|
||||
advancedOverridesStore.getAdvancedState(node, w)
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
return { widgets: advancedWidgets, node }
|
||||
@@ -116,6 +119,14 @@ const advancedLabel = computed(() => {
|
||||
? t('rightSidePanel.advancedInputs')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
|
||||
const showAdvancedSection = computed(
|
||||
() =>
|
||||
advancedWidgetsSectionDataList.value.length > 0 &&
|
||||
!isSearching.value &&
|
||||
!isMultipleNodesSelected.value &&
|
||||
!mustShowNodeTitle
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -165,7 +176,7 @@ const advancedLabel = computed(() => {
|
||||
@update:collapse="setSectionCollapsed(String(node.id), $event)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
<template v-if="showAdvancedSection">
|
||||
<SectionWidgets
|
||||
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
||||
:key="`advanced-${node.id}`"
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
|
||||
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||
@@ -41,7 +43,9 @@ const emit = defineEmits<{
|
||||
const label = defineModel<string>('label', { required: true })
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const settingStore = useSettingStore()
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const { t } = useI18n()
|
||||
@@ -54,6 +58,18 @@ const isFavorited = computed(() =>
|
||||
favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)
|
||||
)
|
||||
|
||||
const isEffectivelyAdvanced = computed(() =>
|
||||
advancedOverridesStore.getAdvancedState(node, widget)
|
||||
)
|
||||
|
||||
const backendDefaultAdvanced = computed(() => !!widget.options?.advanced)
|
||||
|
||||
const showAdvancedToggle = computed(
|
||||
() =>
|
||||
!hasParents.value &&
|
||||
!settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
)
|
||||
|
||||
const inputSpec = computed(() =>
|
||||
nodeDefStore.getInputSpecForWidget(node, widget.name)
|
||||
)
|
||||
@@ -105,6 +121,17 @@ function handleToggleFavorite() {
|
||||
favoritedWidgetsStore.toggleFavorite(favoriteNode.value, widget.name)
|
||||
}
|
||||
|
||||
function handleToggleAdvanced() {
|
||||
const newEffective = !isEffectivelyAdvanced.value
|
||||
if (newEffective === backendDefaultAdvanced.value) {
|
||||
advancedOverridesStore.clearOverride(node, widget.name)
|
||||
} else {
|
||||
advancedOverridesStore.setAdvanced(node, widget.name, newEffective)
|
||||
}
|
||||
|
||||
node.expandToFitContent()
|
||||
}
|
||||
|
||||
function handleResetToDefault() {
|
||||
if (!hasDefault.value) return
|
||||
emit('resetToDefault', defaultValue.value)
|
||||
@@ -176,6 +203,28 @@ function handleResetToDefault() {
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="showAdvancedToggle"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
class="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm transition-all active:scale-95"
|
||||
@click="
|
||||
() => {
|
||||
handleToggleAdvanced()
|
||||
close()
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-if="isEffectivelyAdvanced">
|
||||
<i class="icon-[lucide--eye] size-4" />
|
||||
<span>{{ t('rightSidePanel.showInput') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="icon-[lucide--eye-off] size-4" />
|
||||
<span>{{ t('rightSidePanel.hideInput') }}</span>
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="hasDefault"
|
||||
variant="textonly"
|
||||
|
||||
@@ -7,8 +7,9 @@ import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
export const GetNodeParentGroupKey: InjectionKey<
|
||||
(node: LGraphNode) => LGraphGroup | null
|
||||
@@ -252,6 +253,7 @@ function repeatItems<T>(items: T[]): T[] {
|
||||
|
||||
export function computedSectionDataList(nodes: MaybeRefOrGetter<LGraphNode[]>) {
|
||||
const settingStore = useSettingStore()
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
|
||||
const includesAdvanced = computed(() =>
|
||||
settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
@@ -266,7 +268,8 @@ export function computedSectionDataList(nodes: MaybeRefOrGetter<LGraphNode[]>) {
|
||||
!(
|
||||
w.options?.canvasOnly ||
|
||||
w.options?.hidden ||
|
||||
(w.options?.advanced && !includesAdvanced.value)
|
||||
(advancedOverridesStore.getAdvancedState(node, w) &&
|
||||
!includesAdvanced.value)
|
||||
)
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
|
||||
@@ -162,7 +162,11 @@
|
||||
>
|
||||
<NodeSlots :node-data="nodeData" />
|
||||
|
||||
<NodeWidgets v-if="nodeData.widgets?.length" :node-data="nodeData" />
|
||||
<NodeWidgets
|
||||
v-if="nodeData.widgets?.length"
|
||||
:node-data="nodeData"
|
||||
:node="lgraphNode"
|
||||
/>
|
||||
|
||||
<div v-if="hasCustomContent" class="flex min-h-0 flex-1 flex-col">
|
||||
<NodeContent
|
||||
@@ -270,6 +274,7 @@ import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { hasUnpromotedWidgets } from '@/core/graph/subgraph/promotionUtils'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { st } from '@/i18n'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
@@ -728,21 +733,20 @@ const lgraphNode = computed(() => {
|
||||
// reaching through lgraphNode for promoted preview resolution.
|
||||
const { promotedPreviews } = usePromotedPreviews(lgraphNode)
|
||||
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
|
||||
const showAdvancedInputsButton = computed(() => {
|
||||
const node = lgraphNode.value
|
||||
if (!node) return false
|
||||
|
||||
// For subgraph nodes: check for unpromoted widgets
|
||||
if (node instanceof SubgraphNode) {
|
||||
return hasUnpromotedWidgets(node)
|
||||
}
|
||||
|
||||
// For regular nodes: show button if there are advanced widgets and they're currently hidden
|
||||
const hasAdvancedWidgets = nodeData.widgets?.some((w) => w.options?.advanced)
|
||||
const alwaysShowAdvanced = settingStore.get(
|
||||
'Comfy.Node.AlwaysShowAdvancedWidgets'
|
||||
)
|
||||
return hasAdvancedWidgets && !alwaysShowAdvanced
|
||||
return advancedOverridesStore.hasAnyAdvanced(node) && !alwaysShowAdvanced
|
||||
})
|
||||
|
||||
const showAdvancedState = customRef((track, trigger) => {
|
||||
@@ -772,6 +776,9 @@ const showAdvancedState = customRef((track, trigger) => {
|
||||
} else {
|
||||
node.showAdvanced = value
|
||||
internalState = value
|
||||
nextTick(() => {
|
||||
node.expandToFitContent()
|
||||
})
|
||||
}
|
||||
trigger()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
>
|
||||
<template v-for="widget in processedWidgets" :key="widget.renderKey">
|
||||
<div
|
||||
v-if="!widget.hidden && (!widget.advanced || showAdvanced)"
|
||||
v-if="
|
||||
!widget.simplified.hidden &&
|
||||
(!widget.simplified.advanced || showAdvanced)
|
||||
"
|
||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
||||
>
|
||||
<!-- Widget Input Slot Dot -->
|
||||
@@ -91,6 +94,7 @@ import { useAppMode } from '@/composables/useAppMode'
|
||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -108,6 +112,7 @@ import {
|
||||
shouldExpand,
|
||||
shouldRenderAsVue
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { nodeTypeValidForApp } from '@/stores/appModeStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
@@ -130,15 +135,17 @@ import InputSlot from './InputSlot.vue'
|
||||
|
||||
interface NodeWidgetsProps {
|
||||
nodeData?: VueNodeData
|
||||
node?: LGraphNode | null
|
||||
}
|
||||
|
||||
const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||
const { nodeData, node: lgraphNode = null } = defineProps<NodeWidgetsProps>()
|
||||
|
||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||
useCanvasInteractions()
|
||||
const { isSelectInputsMode } = useAppMode()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
@@ -177,10 +184,11 @@ const canSelectInputs = computed(
|
||||
)
|
||||
const nodeType = computed(() => nodeData?.type || '')
|
||||
const settingStore = useSettingStore()
|
||||
const alwaysShowAdvancedWidgets = computed(() =>
|
||||
settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
)
|
||||
const showAdvanced = computed(
|
||||
() =>
|
||||
nodeData?.showAdvanced ||
|
||||
settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
() => nodeData?.showAdvanced || alwaysShowAdvancedWidgets.value
|
||||
)
|
||||
const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
|
||||
nodeType.value
|
||||
@@ -405,6 +413,10 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
}
|
||||
: undefined
|
||||
|
||||
const resolvedAdvanced = lgraphNode
|
||||
? advancedOverridesStore.getAdvancedState(lgraphNode, widget)
|
||||
: !!widget.options?.advanced
|
||||
|
||||
const simplified: SimplifiedWidget = {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
@@ -415,7 +427,9 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
label: widgetState?.label,
|
||||
linkedUpstream,
|
||||
options: widgetOptions,
|
||||
spec: widget.spec
|
||||
spec: widget.spec,
|
||||
advanced: resolvedAdvanced,
|
||||
hidden: mergedOptions.hidden
|
||||
}
|
||||
|
||||
const updateHandler = createWidgetUpdateHandler(
|
||||
@@ -441,7 +455,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
}
|
||||
|
||||
result.push({
|
||||
advanced: mergedOptions.advanced ?? false,
|
||||
advanced: resolvedAdvanced,
|
||||
handleContextMenu,
|
||||
hasLayoutSize: widget.hasLayoutSize ?? false,
|
||||
hasError: hasWidgetError(widget, nodeExecId, nodeErrors),
|
||||
@@ -459,13 +473,33 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
})
|
||||
}
|
||||
|
||||
if (nodeData?.showAdvanced && !alwaysShowAdvancedWidgets.value) {
|
||||
const normal = result.filter((w) => !w.simplified.advanced)
|
||||
const advanced = result.filter((w) => w.simplified.advanced)
|
||||
return [...normal, ...advanced]
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
/**
|
||||
* Grid rows must follow the original `nodeData.widgets` order; the rendered list
|
||||
* may reorder widgets (e.g. grouping advanced widgets at the end).
|
||||
*/
|
||||
const gridTemplateRows = computed((): string => {
|
||||
// Use processedWidgets directly since it already has store-based hidden/advanced
|
||||
return toValue(processedWidgets)
|
||||
.filter((w) => !w.hidden && (!w.advanced || showAdvanced.value))
|
||||
if (!nodeData?.widgets) return ''
|
||||
const processedNames = new Set(toValue(processedWidgets).map((w) => w.name))
|
||||
return nodeData.widgets
|
||||
.filter((w) => {
|
||||
if (!processedNames.has(w.name)) return false
|
||||
if (w.options?.hidden) return false
|
||||
|
||||
const resolved = lgraphNode
|
||||
? advancedOverridesStore.getAdvancedState(lgraphNode, w)
|
||||
: !!w.options?.advanced
|
||||
|
||||
return !resolved || showAdvanced.value
|
||||
})
|
||||
.map((w) =>
|
||||
shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
|
||||
)
|
||||
|
||||
223
src/stores/workspace/advancedWidgetOverridesStore.test.ts
Normal file
223
src/stores/workspace/advancedWidgetOverridesStore.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
|
||||
const mockGraph = {
|
||||
extra: {} as Record<string, unknown>,
|
||||
nodes: [] as LGraphNode[]
|
||||
}
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
get rootGraph() {
|
||||
return mockGraph
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: { path: 'test-workflow' },
|
||||
nodeToNodeLocatorId: (node: { id: number; locatorId?: string }) =>
|
||||
node.locatorId ?? `node-${node.id}`
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
canvas: { setDirty: vi.fn() }
|
||||
})
|
||||
}))
|
||||
|
||||
function makeNode(
|
||||
id: number,
|
||||
widgets: Array<{ name: string; options?: IWidgetOptions<unknown> }>,
|
||||
locatorId?: string
|
||||
) {
|
||||
return { id, widgets, locatorId } as unknown as LGraphNode
|
||||
}
|
||||
|
||||
describe('useAdvancedWidgetOverridesStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockGraph.extra = {}
|
||||
mockGraph.nodes = []
|
||||
})
|
||||
|
||||
it('returns backend value when no override exists', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'steps', options: { advanced: true } }])
|
||||
|
||||
expect(store.getAdvancedState(node, node.widgets![0])).toBe(true)
|
||||
|
||||
const node2 = makeNode(2, [{ name: 'cfg' }])
|
||||
expect(store.getAdvancedState(node2, node2.widgets![0])).toBe(false)
|
||||
})
|
||||
|
||||
it('override to advanced takes precedence over backend', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'cfg' }])
|
||||
|
||||
store.setAdvanced(node, 'cfg', true)
|
||||
|
||||
expect(store.getAdvancedState(node, node.widgets![0])).toBe(true)
|
||||
expect(store.isOverridden(node, 'cfg')).toBe(true)
|
||||
})
|
||||
|
||||
it('override to non-advanced takes precedence over backend', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'steps', options: { advanced: true } }])
|
||||
|
||||
store.setAdvanced(node, 'steps', false)
|
||||
|
||||
expect(store.getAdvancedState(node, node.widgets![0])).toBe(false)
|
||||
expect(store.isOverridden(node, 'steps')).toBe(true)
|
||||
})
|
||||
|
||||
it('clearOverride reverts to backend default', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'steps', options: { advanced: true } }])
|
||||
|
||||
store.setAdvanced(node, 'steps', false)
|
||||
expect(store.getAdvancedState(node, node.widgets![0])).toBe(false)
|
||||
|
||||
store.clearOverride(node, 'steps')
|
||||
expect(store.getAdvancedState(node, node.widgets![0])).toBe(true)
|
||||
expect(store.isOverridden(node, 'steps')).toBe(false)
|
||||
})
|
||||
|
||||
it('hasAnyAdvanced reflects overrides', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'cfg' }, { name: 'steps' }])
|
||||
|
||||
expect(store.hasAnyAdvanced(node)).toBe(false)
|
||||
|
||||
store.setAdvanced(node, 'cfg', true)
|
||||
expect(store.hasAnyAdvanced(node)).toBe(true)
|
||||
})
|
||||
|
||||
it('persists overrides to workflow.extra', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'cfg' }])
|
||||
|
||||
store.setAdvanced(node, 'cfg', true)
|
||||
|
||||
const stored = mockGraph.extra.advancedWidgetOverrides as {
|
||||
overrides: Array<{
|
||||
nodeLocatorId: string
|
||||
widgetName: string
|
||||
advanced: boolean
|
||||
}>
|
||||
}
|
||||
expect(stored.overrides).toHaveLength(1)
|
||||
expect(stored.overrides[0]).toEqual({
|
||||
nodeLocatorId: 'node-1',
|
||||
widgetName: 'cfg',
|
||||
advanced: true
|
||||
})
|
||||
})
|
||||
|
||||
it('loads overrides from workflow.extra', async () => {
|
||||
mockGraph.extra = {
|
||||
advancedWidgetOverrides: {
|
||||
overrides: [
|
||||
{ nodeLocatorId: 'node-1', widgetName: 'cfg', advanced: true },
|
||||
{ nodeLocatorId: 'node-2', widgetName: 'steps', advanced: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
await nextTick()
|
||||
|
||||
const node1 = makeNode(1, [{ name: 'cfg' }])
|
||||
expect(store.getAdvancedState(node1, node1.widgets![0])).toBe(true)
|
||||
|
||||
const node2 = makeNode(2, [{ name: 'steps', options: { advanced: true } }])
|
||||
expect(store.getAdvancedState(node2, node2.widgets![0])).toBe(false)
|
||||
})
|
||||
|
||||
it('clearAllOverrides removes everything', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'cfg' }])
|
||||
|
||||
store.setAdvanced(node, 'cfg', true)
|
||||
expect(store.isOverridden(node, 'cfg')).toBe(true)
|
||||
|
||||
store.clearAllOverrides()
|
||||
expect(store.isOverridden(node, 'cfg')).toBe(false)
|
||||
|
||||
const stored = mockGraph.extra.advancedWidgetOverrides as {
|
||||
overrides: unknown[]
|
||||
}
|
||||
expect(stored.overrides).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('pruneInvalidOverrides removes stale entries', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
const node = makeNode(1, [{ name: 'cfg' }, { name: 'steps' }])
|
||||
|
||||
store.setAdvanced(node, 'cfg', true)
|
||||
store.setAdvanced(node, 'steps', true)
|
||||
expect(store.isOverridden(node, 'cfg')).toBe(true)
|
||||
expect(store.isOverridden(node, 'steps')).toBe(true)
|
||||
|
||||
// Simulate node having only 'cfg' widget now
|
||||
mockGraph.nodes = [makeNode(1, [{ name: 'cfg' }])]
|
||||
|
||||
store.pruneInvalidOverrides()
|
||||
expect(store.isOverridden(node, 'cfg')).toBe(true)
|
||||
expect(store.isOverridden(node, 'steps')).toBe(false)
|
||||
|
||||
const stored = mockGraph.extra.advancedWidgetOverrides as {
|
||||
overrides: Array<{
|
||||
nodeLocatorId: string
|
||||
widgetName: string
|
||||
advanced: boolean
|
||||
}>
|
||||
}
|
||||
expect(stored.overrides).toHaveLength(1)
|
||||
expect(stored.overrides[0]).toEqual({
|
||||
nodeLocatorId: 'node-1',
|
||||
widgetName: 'cfg',
|
||||
advanced: true
|
||||
})
|
||||
})
|
||||
|
||||
it('pruneInvalidOverrides preserves overrides for nodes inside subgraphs', () => {
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
|
||||
const innerNode = makeNode(1, [{ name: 'cfg' }], 'node-subgraph-10:node-1')
|
||||
store.setAdvanced(innerNode, 'cfg', true)
|
||||
|
||||
const subgraph = { nodes: [innerNode] } as unknown as Subgraph
|
||||
const subgraphNode = {
|
||||
id: 10,
|
||||
widgets: [],
|
||||
subgraph,
|
||||
isSubgraphNode: () => true
|
||||
} as unknown as LGraphNode
|
||||
|
||||
mockGraph.nodes = [subgraphNode]
|
||||
store.pruneInvalidOverrides()
|
||||
|
||||
expect(store.isOverridden(innerNode, 'cfg')).toBe(true)
|
||||
|
||||
const stored = mockGraph.extra.advancedWidgetOverrides as {
|
||||
overrides: Array<{
|
||||
nodeLocatorId: string
|
||||
widgetName: string
|
||||
advanced: boolean
|
||||
}>
|
||||
}
|
||||
expect(stored.overrides).toContainEqual({
|
||||
nodeLocatorId: 'node-subgraph-10:node-1',
|
||||
widgetName: 'cfg',
|
||||
advanced: true
|
||||
})
|
||||
})
|
||||
})
|
||||
238
src/stores/workspace/advancedWidgetOverridesStore.ts
Normal file
238
src/stores/workspace/advancedWidgetOverridesStore.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
interface AdvancedWidgetOverrideEntry {
|
||||
nodeLocatorId: NodeLocatorId
|
||||
widgetName: string
|
||||
/** true = force advanced, false = force non-advanced */
|
||||
advanced: boolean
|
||||
}
|
||||
|
||||
interface AdvancedWidgetOverridesStorage {
|
||||
overrides: AdvancedWidgetOverrideEntry[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages per-workflow user overrides for widget advanced status.
|
||||
*
|
||||
* Three-state model per widget:
|
||||
* - No override: uses backend value (widget.options.advanced)
|
||||
* - Override to true: widget is forced into the advanced section
|
||||
* - Override to false: widget is forced out of the advanced section
|
||||
*
|
||||
* Persisted in workflow.extra.advancedWidgetOverrides.
|
||||
*/
|
||||
export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
'advancedWidgetOverrides',
|
||||
() => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
/** Map of override key -> advanced boolean */
|
||||
const overrides = ref<Map<string, boolean>>(new Map())
|
||||
|
||||
function getOverrideKey(
|
||||
nodeLocatorId: NodeLocatorId,
|
||||
widgetName: string
|
||||
): string {
|
||||
return JSON.stringify([nodeLocatorId, widgetName])
|
||||
}
|
||||
|
||||
function parseOverrideKey(
|
||||
key: string
|
||||
): { nodeLocatorId: NodeLocatorId; widgetName: string } | null {
|
||||
try {
|
||||
const [nodeLocatorId, widgetName] = JSON.parse(key) as [string, string]
|
||||
if (!nodeLocatorId || !widgetName) return null
|
||||
return { nodeLocatorId, widgetName }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeLocatorId(node: LGraphNode): NodeLocatorId {
|
||||
return workflowStore.nodeToNodeLocatorId(node)
|
||||
}
|
||||
|
||||
function setOverrideKey(key: string, advanced: boolean) {
|
||||
const next = new Map(overrides.value)
|
||||
next.set(key, advanced)
|
||||
overrides.value = next
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
function deleteOverrideKey(key: string) {
|
||||
if (!overrides.value.has(key)) return
|
||||
const next = new Map(overrides.value)
|
||||
next.delete(key)
|
||||
overrides.value = next
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
function loadFromWorkflow() {
|
||||
const graph = app.rootGraph
|
||||
if (!graph) {
|
||||
overrides.value = new Map()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const storedData = graph.extra?.advancedWidgetOverrides as
|
||||
| AdvancedWidgetOverridesStorage
|
||||
| undefined
|
||||
|
||||
const newMap = new Map<string, boolean>()
|
||||
if (storedData?.overrides) {
|
||||
for (const entry of storedData.overrides) {
|
||||
if (!entry.nodeLocatorId || !entry.widgetName) continue
|
||||
const key = getOverrideKey(entry.nodeLocatorId, entry.widgetName)
|
||||
newMap.set(key, entry.advanced)
|
||||
}
|
||||
}
|
||||
overrides.value = newMap
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to load advanced widget overrides from workflow:',
|
||||
error
|
||||
)
|
||||
overrides.value = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
function saveToWorkflow() {
|
||||
const graph = app.rootGraph
|
||||
if (!graph) return
|
||||
|
||||
try {
|
||||
const entries: AdvancedWidgetOverrideEntry[] = []
|
||||
for (const [key, advanced] of overrides.value) {
|
||||
const parsed = parseOverrideKey(key)
|
||||
if (!parsed) continue
|
||||
entries.push({ ...parsed, advanced })
|
||||
}
|
||||
|
||||
const data: AdvancedWidgetOverridesStorage = { overrides: entries }
|
||||
graph.extra ??= {}
|
||||
graph.extra.advancedWidgetOverrides = data
|
||||
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to save advanced widget overrides to workflow:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved advanced state for a widget, considering user override.
|
||||
*/
|
||||
function getAdvancedState(
|
||||
node: LGraphNode,
|
||||
widget: { name: string; options?: IWidgetOptions<unknown> }
|
||||
): boolean {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widget.name)
|
||||
const override = overrides.value.get(key)
|
||||
if (override !== undefined) return override
|
||||
return !!widget.options?.advanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the advanced override for a widget.
|
||||
* Pass the desired advanced state (true/false).
|
||||
*/
|
||||
function setAdvanced(
|
||||
node: LGraphNode,
|
||||
widgetName: string,
|
||||
advanced: boolean
|
||||
) {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widgetName)
|
||||
setOverrideKey(key, advanced)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the override for a widget, reverting to backend default.
|
||||
*/
|
||||
function clearOverride(node: LGraphNode, widgetName: string) {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widgetName)
|
||||
deleteOverrideKey(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a widget has a user override.
|
||||
*/
|
||||
function isOverridden(node: LGraphNode, widgetName: string): boolean {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widgetName)
|
||||
return overrides.value.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a node has any widget that is effectively advanced
|
||||
* (after applying overrides).
|
||||
*/
|
||||
function hasAnyAdvanced(node: LGraphNode): boolean {
|
||||
const widgets = node.widgets
|
||||
if (!widgets?.length) return false
|
||||
return widgets.some((w) => getAdvancedState(node, w))
|
||||
}
|
||||
|
||||
function clearAllOverrides() {
|
||||
if (overrides.value.size === 0) return
|
||||
overrides.value = new Map()
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove overrides for nodes/widgets that no longer exist.
|
||||
*/
|
||||
function pruneInvalidOverrides() {
|
||||
const graph = app.rootGraph
|
||||
if (!graph) return
|
||||
|
||||
const validKeys = new Set<string>()
|
||||
forEachNode(graph, (node) => {
|
||||
for (const widget of node.widgets ?? []) {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widget.name)
|
||||
validKeys.add(key)
|
||||
}
|
||||
})
|
||||
|
||||
const next = new Map<string, boolean>()
|
||||
for (const [key, advanced] of overrides.value) {
|
||||
if (!validKeys.has(key)) continue
|
||||
next.set(key, advanced)
|
||||
}
|
||||
|
||||
if (next.size === overrides.value.size) return
|
||||
overrides.value = next
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
/** Keep overrides in sync with the currently active workflow. */
|
||||
watch(
|
||||
() => workflowStore.activeWorkflow,
|
||||
() => loadFromWorkflow(),
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
getAdvancedState,
|
||||
setAdvanced,
|
||||
clearOverride,
|
||||
isOverridden,
|
||||
hasAnyAdvanced,
|
||||
clearAllOverrides,
|
||||
pruneInvalidOverrides
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -58,6 +58,12 @@ export interface SimplifiedWidget<
|
||||
|
||||
borderStyle?: string
|
||||
|
||||
/** Whether this is an advanced widget */
|
||||
advanced?: boolean
|
||||
|
||||
/** Whether this widget is hidden */
|
||||
hidden?: boolean
|
||||
|
||||
/** Callback fired when value changes */
|
||||
callback?: (value: T) => void
|
||||
|
||||
|
||||
Reference in New Issue
Block a user