mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: MVP mark advanced input features
This commit is contained in:
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TooltipOptions } from 'primevue'
|
import type { TooltipOptions } from 'primevue'
|
||||||
import { computed, onErrorCaptured, ref, toValue } from 'vue'
|
import { computed, onErrorCaptured, provide, ref, toValue } from 'vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -77,6 +77,7 @@ import type {
|
|||||||
} from '@/composables/graph/useGraphNodeManager'
|
} from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||||
@@ -88,7 +89,12 @@ import {
|
|||||||
shouldExpand,
|
shouldExpand,
|
||||||
shouldRenderAsVue
|
shouldRenderAsVue
|
||||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||||
|
import {
|
||||||
|
getLocatorIdFromNodeData,
|
||||||
|
getNodeByLocatorId
|
||||||
|
} from '@/utils/graphTraversalUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import InputSlot from './InputSlot.vue'
|
import InputSlot from './InputSlot.vue'
|
||||||
@@ -103,6 +109,18 @@ const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
|||||||
useCanvasInteractions()
|
useCanvasInteractions()
|
||||||
const { bringNodeToFront } = useNodeZIndex()
|
const { bringNodeToFront } = useNodeZIndex()
|
||||||
|
|
||||||
|
// Get the actual LGraphNode for providing to child components
|
||||||
|
const lgraphNode = computed((): LGraphNode | null => {
|
||||||
|
if (!nodeData) return null
|
||||||
|
const locatorId = getLocatorIdFromNodeData(nodeData)
|
||||||
|
const graph = app.rootGraph
|
||||||
|
if (!graph) return null
|
||||||
|
return getNodeByLocatorId(graph, locatorId) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Provide node to child components for accessing advanced overrides
|
||||||
|
provide<LGraphNode | null>('node', lgraphNode.value)
|
||||||
|
|
||||||
function handleWidgetPointerEvent(event: PointerEvent) {
|
function handleWidgetPointerEvent(event: PointerEvent) {
|
||||||
if (shouldHandleNodePointerEvents.value) return
|
if (shouldHandleNodePointerEvents.value) return
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -174,7 +192,9 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
label: widget.label,
|
label: widget.label,
|
||||||
nodeType: widget.nodeType,
|
nodeType: widget.nodeType,
|
||||||
options: widgetOptions,
|
options: widgetOptions,
|
||||||
spec: widget.spec
|
spec: widget.spec,
|
||||||
|
advanced: widget.options?.advanced,
|
||||||
|
hidden: widget.options?.hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHandler(value: WidgetValue) {
|
function updateHandler(value: WidgetValue) {
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
|
|
||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { useAdvancedWidgetOverridesStore } from '@/stores/workspace/advancedWidgetOverridesStore'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
defineProps<{
|
const { widget } = defineProps<{
|
||||||
widget: Pick<
|
widget: Pick<
|
||||||
SimplifiedWidget<string | number | undefined>,
|
SimplifiedWidget<string | number | undefined>,
|
||||||
'name' | 'label' | 'borderStyle'
|
'name' | 'label' | 'borderStyle' | 'advanced'
|
||||||
>
|
>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-subgrid min-w-0 justify-between gap-1 text-node-component-slot-text"
|
class="grid grid-cols-subgrid min-w-0 justify-between gap-1 text-node-component-slot-text"
|
||||||
>
|
>
|
||||||
<div v-if="!hideLayoutField" class="truncate content-center-safe">
|
<div
|
||||||
|
v-if="!hideLayoutField"
|
||||||
|
class="truncate content-center-safe flex items-center gap-2"
|
||||||
|
>
|
||||||
<template v-if="widget.name">
|
<template v-if="widget.name">
|
||||||
{{ widget.label || widget.name }}
|
{{ widget.label || widget.name }}
|
||||||
</template>
|
</template>
|
||||||
@@ -39,5 +60,15 @@ const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
235
src/stores/workspace/advancedWidgetOverridesStore.ts
Normal file
235
src/stores/workspace/advancedWidgetOverridesStore.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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 { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||||
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier for a widget's advanced override.
|
||||||
|
*/
|
||||||
|
interface AdvancedWidgetOverrideId {
|
||||||
|
nodeLocatorId: NodeLocatorId
|
||||||
|
widgetName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage format for persisted advanced widget overrides.
|
||||||
|
* Stored in workflow.extra.advancedWidgetOverrides.
|
||||||
|
*/
|
||||||
|
interface AdvancedWidgetOverridesStorage {
|
||||||
|
overrides: AdvancedWidgetOverrideId[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store for managing advanced widget status overrides.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export const useAdvancedWidgetOverridesStore = defineStore(
|
||||||
|
'advancedWidgetOverrides',
|
||||||
|
() => {
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const overriddenIds = ref<string[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique string key for an override ID.
|
||||||
|
*/
|
||||||
|
function getOverrideKey(id: AdvancedWidgetOverrideId): string {
|
||||||
|
return JSON.stringify([id.nodeLocatorId, id.widgetName])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an override key back into an AdvancedWidgetOverrideId.
|
||||||
|
*/
|
||||||
|
function parseOverrideKey(key: string): AdvancedWidgetOverrideId | null {
|
||||||
|
try {
|
||||||
|
const [nodeLocatorId, widgetName] = JSON.parse(key) as [string, string]
|
||||||
|
if (!nodeLocatorId || !widgetName) return null
|
||||||
|
return { nodeLocatorId, widgetName }
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOverrideId(
|
||||||
|
node: LGraphNode,
|
||||||
|
widgetName: string
|
||||||
|
): AdvancedWidgetOverrideId {
|
||||||
|
return {
|
||||||
|
nodeLocatorId: workflowStore.nodeToNodeLocatorId(node),
|
||||||
|
widgetName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load overrides from the current workflow's extra data.
|
||||||
|
*/
|
||||||
|
function loadFromWorkflow() {
|
||||||
|
const graph = app.rootGraph
|
||||||
|
if (!graph) {
|
||||||
|
overriddenIds.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedData = graph.extra?.advancedWidgetOverrides as
|
||||||
|
| AdvancedWidgetOverridesStorage
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
if (storedData?.overrides) {
|
||||||
|
const keys = storedData.overrides
|
||||||
|
.filter((override) => override.nodeLocatorId && override.widgetName)
|
||||||
|
.map((override) => getOverrideKey(override))
|
||||||
|
overriddenIds.value = keys
|
||||||
|
} else {
|
||||||
|
overriddenIds.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Failed to load advanced widget overrides from workflow:',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
overriddenIds.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 }
|
||||||
|
|
||||||
|
graph.extra ??= {}
|
||||||
|
graph.extra.advancedWidgetOverrides = data
|
||||||
|
|
||||||
|
canvasStore.canvas?.setDirty(true, true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Failed to save advanced widget overrides to workflow:',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resolved advanced state for a widget.
|
||||||
|
* Returns: user override if exists, otherwise backend value.
|
||||||
|
*/
|
||||||
|
function getAdvancedState(node: LGraphNode, widget: IBaseWidget): boolean {
|
||||||
|
const key = getOverrideKey(createOverrideId(node, widget.name))
|
||||||
|
const isOverridden = overriddenIds.value.includes(key)
|
||||||
|
|
||||||
|
if (isOverridden) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToWorkflow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a widget has an active override (user marked as advanced).
|
||||||
|
*/
|
||||||
|
function isOverridden(node: LGraphNode, widgetName: string): boolean {
|
||||||
|
const key = getOverrideKey(createOverrideId(node, widgetName))
|
||||||
|
return overriddenIds.value.includes(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all overrides for the current workflow.
|
||||||
|
*/
|
||||||
|
function clearOverrides() {
|
||||||
|
overriddenIds.value = []
|
||||||
|
saveToWorkflow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove invalid overrides (where node or widget no longer exists).
|
||||||
|
*/
|
||||||
|
function pruneInvalidOverrides() {
|
||||||
|
const graph = app.rootGraph
|
||||||
|
if (!graph) return
|
||||||
|
|
||||||
|
const validKeys: Set<string> = new Set()
|
||||||
|
|
||||||
|
graph.nodes?.forEach((node) => {
|
||||||
|
node.widgets?.forEach((widget) => {
|
||||||
|
const id = createOverrideId(node as LGraphNode, widget.name)
|
||||||
|
const key = getOverrideKey(id)
|
||||||
|
validKeys.add(key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredIds = overriddenIds.value.filter((key) =>
|
||||||
|
validKeys.has(key)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filteredIds.length !== overriddenIds.value.length) {
|
||||||
|
overriddenIds.value = filteredIds
|
||||||
|
saveToWorkflow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => workflowStore.activeWorkflow?.path,
|
||||||
|
() => {
|
||||||
|
loadFromWorkflow()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
overriddenIds: computed(() => overriddenIds.value),
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
getAdvancedState,
|
||||||
|
toggleAdvanced,
|
||||||
|
isOverridden,
|
||||||
|
clearOverrides,
|
||||||
|
pruneInvalidOverrides,
|
||||||
|
loadFromWorkflow,
|
||||||
|
saveToWorkflow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -52,6 +52,12 @@ export interface SimplifiedWidget<
|
|||||||
|
|
||||||
borderStyle?: string
|
borderStyle?: string
|
||||||
|
|
||||||
|
/** Whether this is an advanced widget */
|
||||||
|
advanced?: boolean
|
||||||
|
|
||||||
|
/** Whether this widget is hidden */
|
||||||
|
hidden?: boolean
|
||||||
|
|
||||||
/** Callback fired when value changes */
|
/** Callback fired when value changes */
|
||||||
callback?: (value: T) => void
|
callback?: (value: T) => void
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user