Compare commits

...

2 Commits

Author SHA1 Message Date
Rizumu Ayaka
57c26b22ec fix: use merged store options for widget hidden state 2026-03-21 23:09:31 +08:00
Rizumu Ayaka
83ee120f3d feat: user customizable advanced widgets
Users can customize which widgets are shown as "advanced" on a per-workflow
basis, integrating with the existing backend-defined advanced flag system.

Per widget, three states:
- No override — uses backend value (widget.options.advanced)
- Override to advanced — user hides a basic widget into the advanced section
- Override to non-advanced — user pins an advanced widget to always show

Storage: Map<string, boolean> keyed by JSON.stringify([nodeLocatorId, widgetName]).
Persisted in workflow.extra.advancedWidgetOverrides.
2026-03-21 01:40:57 +08:00
8 changed files with 590 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
})
})
})

View 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
}
}
)

View File

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