mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
feat: user customizable advanced widgets
This commit is contained in:
@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
@@ -19,6 +20,7 @@ const { nodes, mustShowNodeTitle } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const { widgetsSectionDataList, includesAdvanced } = computedSectionDataList(
|
||||
@@ -35,7 +37,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 }
|
||||
@@ -110,7 +113,14 @@ const advancedLabel = computed(() => {
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
<template
|
||||
v-if="
|
||||
advancedWidgetsSectionDataList.length > 0 &&
|
||||
!isSearching &&
|
||||
!isMultipleNodesSelected &&
|
||||
!mustShowNodeTitle
|
||||
"
|
||||
>
|
||||
<SectionWidgets
|
||||
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
||||
:key="`advanced-${node.id}`"
|
||||
|
||||
@@ -12,8 +12,10 @@ 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 { useDialogService } from '@/services/dialogService'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
|
||||
const {
|
||||
@@ -31,7 +33,9 @@ const {
|
||||
const label = defineModel<string>('label', { required: true })
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const settingStore = useSettingStore()
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
const dialogService = useDialogService()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -43,6 +47,16 @@ const isFavorited = computed(() =>
|
||||
favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)
|
||||
)
|
||||
|
||||
const isEffectivelyAdvanced = computed(() =>
|
||||
advancedOverridesStore.getAdvancedState(node, widget)
|
||||
)
|
||||
|
||||
const showAdvancedToggle = computed(
|
||||
() =>
|
||||
!hasParents.value &&
|
||||
!settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
)
|
||||
|
||||
async function handleRename() {
|
||||
const newLabel = await dialogService.prompt({
|
||||
title: t('g.rename'),
|
||||
@@ -97,6 +111,15 @@ function handleToggleFavorite() {
|
||||
favoritedWidgetsStore.toggleFavorite(favoriteNode.value, widget.name)
|
||||
}
|
||||
|
||||
function handleToggleAdvanced() {
|
||||
advancedOverridesStore.setAdvanced(
|
||||
node,
|
||||
widget.name,
|
||||
!isEffectivelyAdvanced.value
|
||||
)
|
||||
node.expandToFitContent()
|
||||
}
|
||||
|
||||
const buttonClasses = cn([
|
||||
'border-none bg-transparent',
|
||||
'w-full flex items-center gap-2 rounded px-3 py-2 text-sm',
|
||||
@@ -144,6 +167,26 @@ const buttonClasses = cn([
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="showAdvancedToggle"
|
||||
:class="buttonClasses"
|
||||
@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
|
||||
:class="buttonClasses"
|
||||
@click="
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -200,6 +200,7 @@ import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeS
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { isTransparent } from '@/utils/colorUtil'
|
||||
import {
|
||||
@@ -496,6 +497,8 @@ const lgraphNode = computed(() => {
|
||||
return getNodeByLocatorId(app.rootGraph, locatorId)
|
||||
})
|
||||
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
|
||||
const showAdvancedInputsButton = computed(() => {
|
||||
const node = lgraphNode.value
|
||||
if (!node) return false
|
||||
@@ -507,12 +510,11 @@ const showAdvancedInputsButton = computed(() => {
|
||||
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
|
||||
}
|
||||
|
||||
// For regular nodes: show button if there are advanced widgets and they're currently hidden
|
||||
const hasAdvancedWidgets = nodeData.widgets?.some((w) => w.options?.advanced)
|
||||
// For regular nodes: show button if there are effectively advanced widgets
|
||||
const alwaysShowAdvanced = settingStore.get(
|
||||
'Comfy.Node.AlwaysShowAdvancedWidgets'
|
||||
)
|
||||
return hasAdvancedWidgets && !alwaysShowAdvanced
|
||||
return advancedOverridesStore.hasAnyAdvanced(node) && !alwaysShowAdvanced
|
||||
})
|
||||
|
||||
const showAdvancedState = customRef((track, trigger) => {
|
||||
@@ -543,6 +545,9 @@ const showAdvancedState = customRef((track, trigger) => {
|
||||
} else {
|
||||
node.showAdvanced = value
|
||||
internalState = value
|
||||
nextTick(() => {
|
||||
node.expandToFitContent()
|
||||
})
|
||||
}
|
||||
trigger()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div
|
||||
v-if="
|
||||
!widget.simplified.options?.hidden &&
|
||||
(!widget.simplified.options?.advanced || showAdvanced)
|
||||
(!widget.simplified.advanced || showAdvanced)
|
||||
"
|
||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
||||
>
|
||||
@@ -94,6 +94,7 @@ import {
|
||||
shouldRenderAsVue
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
@@ -122,8 +123,9 @@ const lgraphNode = computed((): LGraphNode | null => {
|
||||
return getNodeByLocatorId(graph, locatorId) ?? null
|
||||
})
|
||||
|
||||
// Provide node to child components for accessing advanced overrides
|
||||
provide<LGraphNode | null>('node', lgraphNode.value)
|
||||
provide('node', lgraphNode)
|
||||
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
|
||||
function handleWidgetPointerEvent(event: PointerEvent) {
|
||||
if (shouldHandleNodePointerEvents.value) return
|
||||
@@ -192,6 +194,10 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
? { ...options, disabled: true }
|
||||
: options
|
||||
|
||||
const resolvedAdvanced = lgraphNode.value
|
||||
? advancedOverridesStore.getAdvancedState(lgraphNode.value, widget)
|
||||
: !!widget.options?.advanced
|
||||
|
||||
const simplified: SimplifiedWidget = {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
@@ -203,7 +209,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
nodeType: widget.nodeType,
|
||||
options: widgetOptions,
|
||||
spec: widget.spec,
|
||||
advanced: widget.options?.advanced,
|
||||
advanced: resolvedAdvanced,
|
||||
hidden: widget.options?.hidden
|
||||
}
|
||||
|
||||
@@ -229,22 +235,37 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
})
|
||||
}
|
||||
|
||||
// When per-node showAdvanced is on, move advanced widgets to the end.
|
||||
// When global AlwaysShowAdvancedWidgets is on, keep original order.
|
||||
if (
|
||||
nodeData?.showAdvanced &&
|
||||
!settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
) {
|
||||
const normal = result.filter((w) => !w.simplified.advanced)
|
||||
const advanced = result.filter((w) => w.simplified.advanced)
|
||||
return [...normal, ...advanced]
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const gridTemplateRows = computed((): string => {
|
||||
if (!nodeData?.widgets) return ''
|
||||
const processedNames = new Set(toValue(processedWidgets).map((w) => w.name))
|
||||
return nodeData.widgets
|
||||
.filter(
|
||||
(w) =>
|
||||
processedNames.has(w.name) &&
|
||||
!w.options?.hidden &&
|
||||
(!w.options?.advanced || showAdvanced.value)
|
||||
)
|
||||
.map((w) =>
|
||||
shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
|
||||
)
|
||||
const processed = toValue(processedWidgets)
|
||||
const advancedByName = new Map(
|
||||
processed.map((w) => [w.name, w.simplified.advanced])
|
||||
)
|
||||
// Use processedWidgets order (which may have advanced sorted to end)
|
||||
const visible = processed.filter(
|
||||
(w) =>
|
||||
!w.simplified.hidden &&
|
||||
(!advancedByName.get(w.name) || showAdvanced.value)
|
||||
)
|
||||
return visible
|
||||
.map((w) => {
|
||||
const raw = nodeData.widgets?.find((rw) => rw.name === w.name)
|
||||
return shouldExpand(w.type) || raw?.hasLayoutSize ? 'auto' : 'min-content'
|
||||
})
|
||||
.join(' ')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { inject } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -14,22 +12,6 @@ const { widget } = defineProps<{
|
||||
}>()
|
||||
|
||||
const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
||||
const node = inject<LGraphNode | null>('node', null)
|
||||
|
||||
const advancedOverridesStore = useAdvancedWidgetOverridesStore()
|
||||
|
||||
const isAdvanced = computed(() => {
|
||||
if (!node) return !!widget.advanced
|
||||
return advancedOverridesStore.getAdvancedState(node, {
|
||||
name: widget.name,
|
||||
options: { advanced: widget.advanced }
|
||||
} as any)
|
||||
})
|
||||
|
||||
function toggleAdvanced() {
|
||||
if (!node) return
|
||||
advancedOverridesStore.toggleAdvanced(node, widget.name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,15 +42,5 @@ function toggleAdvanced() {
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
'absolute right-1 hover:scale-150 size-2 rounded-full p-0 m-0 ring-0 border-none z-10',
|
||||
isAdvanced ? 'bg-green-500' : 'bg-gray-500'
|
||||
)
|
||||
"
|
||||
@click.stop="toggleAdvanced"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
160
src/stores/workspace/advancedWidgetOverridesStore.test.ts
Normal file
160
src/stores/workspace/advancedWidgetOverridesStore.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||
|
||||
const mockGraph = {
|
||||
extra: {} as Record<string, unknown>,
|
||||
nodes: [] as Array<{ id: number; widgets: Array<{ name: string }> }>
|
||||
}
|
||||
|
||||
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 }) => `node-${node.id}`
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
canvas: { setDirty: vi.fn() }
|
||||
})
|
||||
}))
|
||||
|
||||
function makeNode(
|
||||
id: number,
|
||||
widgets: Array<{ name: string; options?: Record<string, unknown> }>
|
||||
) {
|
||||
return { id, widgets } as any
|
||||
}
|
||||
|
||||
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 any
|
||||
expect(stored.overrides).toHaveLength(1)
|
||||
expect(stored.overrides[0]).toEqual({
|
||||
nodeLocatorId: 'node-1',
|
||||
widgetName: 'cfg',
|
||||
advanced: true
|
||||
})
|
||||
})
|
||||
|
||||
it('loads overrides from workflow.extra', () => {
|
||||
mockGraph.extra = {
|
||||
advancedWidgetOverrides: {
|
||||
overrides: [
|
||||
{ nodeLocatorId: 'node-1', widgetName: 'cfg', advanced: true },
|
||||
{ nodeLocatorId: 'node-2', widgetName: 'steps', advanced: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const store = useAdvancedWidgetOverridesStore()
|
||||
store.loadFromWorkflow()
|
||||
|
||||
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.overrides.size).toBe(1)
|
||||
|
||||
store.clearAllOverrides()
|
||||
expect(store.overrides.size).toBe(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.overrides.size).toBe(2)
|
||||
|
||||
// Simulate node having only 'cfg' widget now
|
||||
mockGraph.nodes = [{ id: 1, widgets: [{ name: 'cfg' }] }] as any
|
||||
|
||||
store.pruneInvalidOverrides()
|
||||
expect(store.overrides.size).toBe(1)
|
||||
expect(store.isOverridden(node, 'cfg')).toBe(true)
|
||||
expect(store.isOverridden(node, 'steps')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,41 +1,33 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
/**
|
||||
* Unique identifier for a widget's advanced override.
|
||||
*/
|
||||
interface AdvancedWidgetOverrideId {
|
||||
interface AdvancedWidgetOverrideEntry {
|
||||
nodeLocatorId: NodeLocatorId
|
||||
widgetName: string
|
||||
/** true = force advanced, false = force non-advanced */
|
||||
advanced: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage format for persisted advanced widget overrides.
|
||||
* Stored in workflow.extra.advancedWidgetOverrides.
|
||||
*/
|
||||
interface AdvancedWidgetOverridesStorage {
|
||||
overrides: AdvancedWidgetOverrideId[]
|
||||
overrides: AdvancedWidgetOverrideEntry[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Store for managing advanced widget status overrides.
|
||||
* Manages per-workflow user overrides for widget advanced status.
|
||||
*
|
||||
* Users can manually mark/unmark widgets as advanced, and this preference
|
||||
* is stored per-workflow. This allows customization of which widgets
|
||||
* appear in the advanced section.
|
||||
* 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
|
||||
*
|
||||
* Design decisions:
|
||||
* - Scope: Per-workflow (not global user preference)
|
||||
* - Identifier: node locator ID + widget.name
|
||||
* - Persistence: Stored in workflow.extra.advancedWidgetOverrides
|
||||
* - Override logic: User override takes precedence over backend value
|
||||
* Persisted in workflow.extra.advancedWidgetOverrides.
|
||||
*/
|
||||
export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
'advancedWidgetOverrides',
|
||||
@@ -43,19 +35,19 @@ export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
const workflowStore = useWorkflowStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const overriddenIds = ref<string[]>([])
|
||||
/** Map of override key → advanced boolean */
|
||||
const overrides = ref<Map<string, boolean>>(new Map())
|
||||
|
||||
/**
|
||||
* Generate a unique string key for an override ID.
|
||||
*/
|
||||
function getOverrideKey(id: AdvancedWidgetOverrideId): string {
|
||||
return JSON.stringify([id.nodeLocatorId, id.widgetName])
|
||||
function getOverrideKey(
|
||||
nodeLocatorId: NodeLocatorId,
|
||||
widgetName: string
|
||||
): string {
|
||||
return JSON.stringify([nodeLocatorId, widgetName])
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an override key back into an AdvancedWidgetOverrideId.
|
||||
*/
|
||||
function parseOverrideKey(key: string): AdvancedWidgetOverrideId | null {
|
||||
function parseOverrideKey(
|
||||
key: string
|
||||
): { nodeLocatorId: NodeLocatorId; widgetName: string } | null {
|
||||
try {
|
||||
const [nodeLocatorId, widgetName] = JSON.parse(key) as [string, string]
|
||||
if (!nodeLocatorId || !widgetName) return null
|
||||
@@ -65,23 +57,14 @@ export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
function createOverrideId(
|
||||
node: LGraphNode,
|
||||
widgetName: string
|
||||
): AdvancedWidgetOverrideId {
|
||||
return {
|
||||
nodeLocatorId: workflowStore.nodeToNodeLocatorId(node),
|
||||
widgetName
|
||||
}
|
||||
function getNodeLocatorId(node: LGraphNode): NodeLocatorId {
|
||||
return workflowStore.nodeToNodeLocatorId(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load overrides from the current workflow's extra data.
|
||||
*/
|
||||
function loadFromWorkflow() {
|
||||
const graph = app.rootGraph
|
||||
if (!graph) {
|
||||
overriddenIds.value = []
|
||||
overrides.value = new Map()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,38 +73,37 @@ export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
| AdvancedWidgetOverridesStorage
|
||||
| undefined
|
||||
|
||||
const newMap = new Map<string, boolean>()
|
||||
if (storedData?.overrides) {
|
||||
const keys = storedData.overrides
|
||||
.filter((override) => override.nodeLocatorId && override.widgetName)
|
||||
.map((override) => getOverrideKey(override))
|
||||
overriddenIds.value = keys
|
||||
} else {
|
||||
overriddenIds.value = []
|
||||
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
|
||||
)
|
||||
overriddenIds.value = []
|
||||
overrides.value = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save overrides to the current workflow's extra data.
|
||||
* Marks the workflow as modified.
|
||||
*/
|
||||
function saveToWorkflow() {
|
||||
const graph = app.rootGraph
|
||||
if (!graph) return
|
||||
|
||||
try {
|
||||
const overrides: AdvancedWidgetOverrideId[] = overriddenIds.value
|
||||
.map(parseOverrideKey)
|
||||
.filter((id): id is AdvancedWidgetOverrideId => id !== null)
|
||||
|
||||
const data: AdvancedWidgetOverridesStorage = { overrides }
|
||||
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
|
||||
|
||||
@@ -135,77 +117,94 @@ export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved advanced state for a widget.
|
||||
* Returns: user override if exists, otherwise backend value.
|
||||
* Resolved advanced state for a widget, considering user override.
|
||||
*/
|
||||
function getAdvancedState(node: LGraphNode, widget: IBaseWidget): boolean {
|
||||
const key = getOverrideKey(createOverrideId(node, widget.name))
|
||||
const isOverridden = overriddenIds.value.includes(key)
|
||||
|
||||
if (isOverridden) {
|
||||
return true
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the advanced override for a widget.
|
||||
* If the widget is already marked as advanced (by backend or override),
|
||||
* toggling removes the override (falls back to backend).
|
||||
* If not marked as advanced, toggling adds it to the override list.
|
||||
* Set the advanced override for a widget.
|
||||
* Pass the desired advanced state (true/false).
|
||||
*/
|
||||
function toggleAdvanced(node: LGraphNode, widgetName: string) {
|
||||
const id = createOverrideId(node, widgetName)
|
||||
const key = getOverrideKey(id)
|
||||
|
||||
if (overriddenIds.value.includes(key)) {
|
||||
overriddenIds.value = overriddenIds.value.filter((k) => k !== key)
|
||||
} else {
|
||||
overriddenIds.value.push(key)
|
||||
}
|
||||
|
||||
function setAdvanced(
|
||||
node: LGraphNode,
|
||||
widgetName: string,
|
||||
advanced: boolean
|
||||
) {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widgetName)
|
||||
overrides.value.set(key, advanced)
|
||||
overrides.value = new Map(overrides.value)
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a widget has an active override (user marked as advanced).
|
||||
* Remove the override for a widget, reverting to backend default.
|
||||
*/
|
||||
function clearOverride(node: LGraphNode, widgetName: string) {
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widgetName)
|
||||
overrides.value.delete(key)
|
||||
overrides.value = new Map(overrides.value)
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a widget has a user override.
|
||||
*/
|
||||
function isOverridden(node: LGraphNode, widgetName: string): boolean {
|
||||
const key = getOverrideKey(createOverrideId(node, widgetName))
|
||||
return overriddenIds.value.includes(key)
|
||||
const key = getOverrideKey(getNodeLocatorId(node), widgetName)
|
||||
return overrides.value.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all overrides for the current workflow.
|
||||
* Whether a node has any widget that is effectively advanced
|
||||
* (after applying overrides).
|
||||
*/
|
||||
function clearOverrides() {
|
||||
overriddenIds.value = []
|
||||
function hasAnyAdvanced(node: LGraphNode): boolean {
|
||||
const widgets = node.widgets
|
||||
if (!widgets?.length) return false
|
||||
return widgets.some((w) => getAdvancedState(node, w))
|
||||
}
|
||||
|
||||
function clearAllOverrides() {
|
||||
overrides.value = new Map()
|
||||
saveToWorkflow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove invalid overrides (where node or widget no longer exists).
|
||||
* Remove overrides for nodes/widgets that no longer exist.
|
||||
*/
|
||||
function pruneInvalidOverrides() {
|
||||
const graph = app.rootGraph
|
||||
if (!graph) return
|
||||
|
||||
const validKeys: Set<string> = new Set()
|
||||
|
||||
const validKeys = new Set<string>()
|
||||
graph.nodes?.forEach((node) => {
|
||||
node.widgets?.forEach((widget) => {
|
||||
const id = createOverrideId(node as LGraphNode, widget.name)
|
||||
const key = getOverrideKey(id)
|
||||
const key = getOverrideKey(
|
||||
getNodeLocatorId(node as LGraphNode),
|
||||
widget.name
|
||||
)
|
||||
validKeys.add(key)
|
||||
})
|
||||
})
|
||||
|
||||
const filteredIds = overriddenIds.value.filter((key) =>
|
||||
validKeys.has(key)
|
||||
)
|
||||
let changed = false
|
||||
for (const key of overrides.value.keys()) {
|
||||
if (!validKeys.has(key)) {
|
||||
overrides.value.delete(key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredIds.length !== overriddenIds.value.length) {
|
||||
overriddenIds.value = filteredIds
|
||||
if (changed) {
|
||||
overrides.value = new Map(overrides.value)
|
||||
saveToWorkflow()
|
||||
}
|
||||
}
|
||||
@@ -219,14 +218,14 @@ export const useAdvancedWidgetOverridesStore = defineStore(
|
||||
)
|
||||
|
||||
return {
|
||||
// State
|
||||
overriddenIds: computed(() => overriddenIds.value),
|
||||
overrides: computed(() => overrides.value),
|
||||
|
||||
// Actions
|
||||
getAdvancedState,
|
||||
toggleAdvanced,
|
||||
setAdvanced,
|
||||
clearOverride,
|
||||
isOverridden,
|
||||
clearOverrides,
|
||||
hasAnyAdvanced,
|
||||
clearAllOverrides,
|
||||
pruneInvalidOverrides,
|
||||
loadFromWorkflow,
|
||||
saveToWorkflow
|
||||
|
||||
Reference in New Issue
Block a user