mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 01:09:46 +00:00
New bottom button and badges (#8603)
- "Enter Subgraph" "Show advanced inputs" and a new "show node Errors" button now use a combined button design at the bottom of the node. - A new "Errors" tab is added to the right side panel - After a failed queue, the label of an invalid widget is now red. - Badges other than price are now displayed on the bottom of the node. - Price badge will now truncate from the first space, prioritizing the sizing of the node title - An indicator for the node resize handle is now displayed while mousing over the node. <img width="669" height="233" alt="image" src="https://github.com/user-attachments/assets/53b3b59c-830b-474d-8f20-07f557124af7" />  --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -14,11 +14,13 @@ import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TabError from './TabError.vue'
|
||||
import TabInfo from './info/TabInfo.vue'
|
||||
import TabGlobalParameters from './parameters/TabGlobalParameters.vue'
|
||||
import TabNodes from './parameters/TabNodes.vue'
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
import SubgraphEditor from './subgraph/SubgraphEditor.vue'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
@@ -87,10 +90,25 @@ function closePanel() {
|
||||
type RightSidePanelTabList = Array<{
|
||||
label: () => string
|
||||
value: RightSidePanelTab
|
||||
icon?: string
|
||||
}>
|
||||
|
||||
//FIXME all errors if nothing selected?
|
||||
const selectedNodeErrors = computed(() =>
|
||||
selectedNodes.value
|
||||
.map((node) => executionStore.getNodeErrors(`${node.id}`))
|
||||
.filter((nodeError) => !!nodeError)
|
||||
)
|
||||
|
||||
const tabs = computed<RightSidePanelTabList>(() => {
|
||||
const list: RightSidePanelTabList = []
|
||||
if (selectedNodeErrors.value.length) {
|
||||
list.push({
|
||||
label: () => t('g.error'),
|
||||
value: 'error',
|
||||
icon: 'icon-[lucide--octagon-alert] bg-node-stroke-error ml-1'
|
||||
})
|
||||
}
|
||||
|
||||
list.push({
|
||||
label: () =>
|
||||
@@ -271,6 +289,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
|
||||
:value="tab.value"
|
||||
>
|
||||
{{ tab.label() }}
|
||||
<i v-if="tab.icon" :class="cn(tab.icon, 'size-4')" />
|
||||
</Tab>
|
||||
</TabList>
|
||||
</nav>
|
||||
@@ -288,6 +307,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
|
||||
:node="selectedSingleNode"
|
||||
/>
|
||||
<template v-else>
|
||||
<TabError v-if="activeTab === 'error'" :errors="selectedNodeErrors" />
|
||||
<TabSubgraphInputs
|
||||
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
|
||||
:node="selectedSingleNode as SubgraphNode"
|
||||
|
||||
30
src/components/rightSidePanel/TabError.vue
Normal file
30
src/components/rightSidePanel/TabError.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
errors: NodeError[]
|
||||
}>()
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
</script>
|
||||
<template>
|
||||
<div class="m-4">
|
||||
<Button class="w-full" @click="copyToClipboard(JSON.stringify(errors))">
|
||||
{{ t('g.copy') }}
|
||||
<i class="icon-[lucide--copy] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(error, index) in errors.flatMap((ne) => ne.errors)"
|
||||
:key="index"
|
||||
class="px-2"
|
||||
>
|
||||
<h3 class="text-error" v-text="error.message" />
|
||||
<div class="text-muted-foreground" v-text="error.details" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,7 +36,9 @@ export const usePriceBadge = () => {
|
||||
return badges
|
||||
}
|
||||
|
||||
function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
|
||||
function isCreditsBadge(
|
||||
badge: Partial<LGraphBadge> | (() => Partial<LGraphBadge>)
|
||||
): boolean {
|
||||
const badgeInstance = typeof badge === 'function' ? badge() : badge
|
||||
return badgeInstance.icon?.image === componentIconSvg
|
||||
}
|
||||
@@ -61,6 +63,7 @@ export const usePriceBadge = () => {
|
||||
}
|
||||
return {
|
||||
getCreditsBadge,
|
||||
isCreditsBadge,
|
||||
updateSubgraphCredits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"icon": "Icon",
|
||||
"color": "Color",
|
||||
"error": "Error",
|
||||
"enterSubgraph": "Enter Subgraph",
|
||||
"resizeFromBottomRight": "Resize from bottom-right corner",
|
||||
"resizeFromTopRight": "Resize from top-right corner",
|
||||
"resizeFromBottomLeft": "Resize from bottom-left corner",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
:class="
|
||||
cn(
|
||||
'-translate-x-1/2 w-3',
|
||||
hasSlotError &&
|
||||
hasError &&
|
||||
'before:ring-2 before:ring-error before:ring-offset-0 before:size-4 before:absolute before:rounded-full before:pointer-events-none'
|
||||
)
|
||||
"
|
||||
@@ -40,7 +40,7 @@
|
||||
:class="
|
||||
cn(
|
||||
'truncate text-node-component-slot-text',
|
||||
hasSlotError && 'text-error font-medium'
|
||||
hasError && 'text-error font-medium'
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -65,19 +65,19 @@ import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
|
||||
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import SlotConnectionDot from './SlotConnectionDot.vue'
|
||||
|
||||
interface InputSlotProps {
|
||||
slotData: INodeSlot
|
||||
compatible?: boolean
|
||||
connected?: boolean
|
||||
dotOnly?: boolean
|
||||
hasError?: boolean
|
||||
index: number
|
||||
nodeType?: string
|
||||
nodeId?: string
|
||||
slotData: INodeSlot
|
||||
index: number
|
||||
connected?: boolean
|
||||
compatible?: boolean
|
||||
dotOnly?: boolean
|
||||
socketless?: boolean
|
||||
}
|
||||
|
||||
@@ -91,18 +91,6 @@ const hasNoLabel = computed(
|
||||
)
|
||||
const dotOnly = computed(() => props.dotOnly || hasNoLabel.value)
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
const hasSlotError = computed(() => {
|
||||
const nodeErrors = executionStore.lastNodeErrors?.[props.nodeId ?? '']
|
||||
if (!nodeErrors) return false
|
||||
|
||||
const slotName = props.slotData.name
|
||||
return nodeErrors.errors.some(
|
||||
(error) => error.extra_info?.input_name === slotName
|
||||
)
|
||||
})
|
||||
|
||||
const renderError = ref<string | null>(null)
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LGraphNode as LGLGraphNode,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
|
||||
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
const mockApp: { rootGraph?: Partial<LGraph> } = vi.hoisted(() => ({}))
|
||||
@@ -56,7 +56,7 @@ vi.mock('@/i18n', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
describe('NodeHeader - Subgraph Functionality', () => {
|
||||
describe('Vue Node - Subgraph Functionality', () => {
|
||||
// Helper to setup common mocks
|
||||
const setupMocks = async (isSubgraph = true, hasGraph = true) => {
|
||||
if (hasGraph) mockApp.rootGraph = {}
|
||||
@@ -64,7 +64,7 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
||||
|
||||
vi.mocked(getNodeByLocatorId).mockReturnValue({
|
||||
isSubgraphNode: (): this is SubgraphNode => isSubgraph
|
||||
} as LGraphNode)
|
||||
} as LGLGraphNode)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -89,8 +89,8 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
||||
flags: {}
|
||||
})
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
return mount(NodeHeader, {
|
||||
const createWrapper = (props: { nodeData: VueNodeData }) => {
|
||||
return mount(LGraphNode, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn })],
|
||||
@@ -106,8 +106,7 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
nodeData: createMockNodeData('test-node-1')
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -120,8 +119,7 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
||||
await setupMocks(false) // isSubgraph = false
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
nodeData: createMockNodeData('test-node-1')
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -130,29 +128,11 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
||||
expect(subgraphButton.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should emit enter-subgraph event when button is clicked', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
await subgraphButton.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('enter-subgraph')).toBeTruthy()
|
||||
expect(wrapper.emitted('enter-subgraph')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should handle subgraph context correctly', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1', 'subgraph-id'),
|
||||
readonly: false
|
||||
nodeData: createMockNodeData('test-node-1', 'subgraph-id')
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -167,26 +147,11 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
||||
expect(subgraphButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle missing graph gracefully', async () => {
|
||||
await setupMocks(true, false) // isSubgraph = true, hasGraph = false
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
expect(subgraphButton.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should prevent event propagation on double click', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
nodeData: createMockNodeData('test-node-1')
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -9,7 +9,7 @@
|
||||
:data-node-id="nodeData.id"
|
||||
:class="
|
||||
cn(
|
||||
'bg-component-node-background lg-node absolute text-sm',
|
||||
'group/node bg-node-component-header-surface lg-node absolute text-sm',
|
||||
'contain-style contain-layout min-w-[225px] min-h-(--node-height) w-(--node-width)',
|
||||
shapeClass,
|
||||
'touch-none flex flex-col',
|
||||
@@ -28,11 +28,9 @@
|
||||
muted,
|
||||
'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver
|
||||
},
|
||||
|
||||
shouldHandleNodePointerEvents && !nodeData.flags?.ghost
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none',
|
||||
!isCollapsed && ' pb-1'
|
||||
: 'pointer-events-none'
|
||||
)
|
||||
"
|
||||
:style="[
|
||||
@@ -40,7 +38,8 @@
|
||||
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex,
|
||||
opacity: nodeOpacity,
|
||||
'--component-node-background': applyLightThemeColor(nodeData.bgcolor)
|
||||
'--component-node-background': applyLightThemeColor(nodeData.bgcolor),
|
||||
backgroundColor: applyLightThemeColor(nodeData?.color)
|
||||
}
|
||||
]"
|
||||
v-bind="remainingPointerHandlers"
|
||||
@@ -71,9 +70,9 @@
|
||||
<NodeHeader
|
||||
:node-data="nodeData"
|
||||
:collapsed="isCollapsed"
|
||||
:price-badges="badges.pricing"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleHeaderTitleUpdate"
|
||||
@enter-subgraph="handleEnterSubgraph"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +88,7 @@
|
||||
/>
|
||||
|
||||
<template v-if="!isCollapsed">
|
||||
<div class="relative mb-1">
|
||||
<div class="relative">
|
||||
<!-- Progress bar for executing state -->
|
||||
<div
|
||||
v-if="executing && progress !== undefined"
|
||||
@@ -105,7 +104,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-1 flex-col gap-1 pb-2"
|
||||
class="flex flex-1 flex-col gap-1 pt-1 pb-3 bg-component-node-background rounded-b-2xl"
|
||||
:data-testid="`node-body-${nodeData.id}`"
|
||||
>
|
||||
<NodeSlots :node-data="nodeData" />
|
||||
@@ -120,42 +119,75 @@
|
||||
v-if="shouldShowPreviewImg"
|
||||
:image-url="latestPreviewUrl"
|
||||
/>
|
||||
|
||||
<!-- Show advanced inputs button for subgraph nodes -->
|
||||
<div v-if="showAdvancedInputsButton" class="flex justify-center px-3">
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
WidgetInputBaseClass,
|
||||
'w-full h-7 flex justify-center items-center gap-2 text-sm px-3 outline-0 ring-0 truncate',
|
||||
'transition-all cursor-pointer hover:bg-accent-background duration-150 active:scale-95'
|
||||
)
|
||||
"
|
||||
@click.stop="showAdvancedState = !showAdvancedState"
|
||||
>
|
||||
<template v-if="showAdvancedState">
|
||||
<i class="icon-[lucide--chevron-up] size-4" />
|
||||
<span>{{ t('rightSidePanel.hideAdvancedInputsButton') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="icon-[lucide--settings-2] size-4" />
|
||||
<span>{{ t('rightSidePanel.showAdvancedInputsButton') }} </span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<NodeBadges v-bind="badges" :pricing="undefined" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Button
|
||||
variant="textonly"
|
||||
:class="
|
||||
cn(
|
||||
'w-full h-12 rounded-b-2xl -mt-5 pt-7 pb-2 -z-1 text-xs',
|
||||
hasAnyError && 'hover:bg-destructive-background-hover'
|
||||
)
|
||||
"
|
||||
as-child
|
||||
>
|
||||
<button
|
||||
v-if="hasAnyError"
|
||||
@click.stop="useRightSidePanelStore().openPanel('error')"
|
||||
>
|
||||
<span>{{ t('g.error') }}</span>
|
||||
<i class="icon-[lucide--info] size-4" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="lgraphNode?.isSubgraphNode()"
|
||||
data-testid="subgraph-enter-button"
|
||||
@click.stop="handleEnterSubgraph"
|
||||
>
|
||||
<span>{{ t('g.enterSubgraph') }}</span>
|
||||
<i class="icon-[comfy--workflow] size-4" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="showAdvancedState || showAdvancedInputsButton"
|
||||
@click.stop="showAdvancedState = !showAdvancedState"
|
||||
>
|
||||
<template v-if="showAdvancedState">
|
||||
<span>{{ t('rightSidePanel.hideAdvancedInputsButton') }}</span>
|
||||
<i class="icon-[lucide--chevron-up] size-4" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ t('rightSidePanel.showAdvancedInputsButton') }} </span>
|
||||
<i class="icon-[lucide--settings-2] size-4" />
|
||||
</template>
|
||||
</button>
|
||||
</Button>
|
||||
<!-- Resize handle (bottom-right only) -->
|
||||
<div
|
||||
v-if="!isCollapsed && nodeData.resizable !== false"
|
||||
role="button"
|
||||
:aria-label="t('g.resizeFromBottomRight')"
|
||||
:class="
|
||||
cn(baseResizeHandleClasses, '-right-1 -bottom-1 cursor-se-resize')
|
||||
cn(
|
||||
baseResizeHandleClasses,
|
||||
'-right-1 -bottom-1 cursor-se-resize group-hover/node:opacity-100'
|
||||
)
|
||||
"
|
||||
@pointerdown.stop="handleResizePointerDown"
|
||||
/>
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
class="w-2/5 h-2/5 top-1 left-1 absolute"
|
||||
>
|
||||
<path
|
||||
d="M11 1L1 11M11 6L6 11"
|
||||
stroke="var(--color-muted-foreground)"
|
||||
stroke-width="0.975"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -172,6 +204,7 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
@@ -189,10 +222,12 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import NodeBadges from '@/renderer/extensions/vueNodes/components/NodeBadges.vue'
|
||||
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import { usePartitionedBadges } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges'
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
|
||||
@@ -212,7 +247,6 @@ import {
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useNodeResize } from '../interactions/resize/useNodeResize'
|
||||
import { WidgetInputBaseClass } from '../widgets/components/layout'
|
||||
import LivePreview from './LivePreview.vue'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
@@ -299,6 +333,7 @@ const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
|
||||
const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)
|
||||
const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
|
||||
const { startDrag } = useNodeDrag()
|
||||
const badges = usePartitionedBadges(nodeData)
|
||||
|
||||
async function nodeOnPointerdown(event: PointerEvent) {
|
||||
if (event.altKey && lgraphNode.value) {
|
||||
@@ -405,7 +440,7 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
)
|
||||
|
||||
const borderClass = computed(() => {
|
||||
if (hasAnyError.value) return 'border-node-stroke-error'
|
||||
if (hasAnyError.value) return 'border-node-stroke-error bg-error'
|
||||
//FIXME need a better way to detecting transparency
|
||||
if (
|
||||
!displayHeader.value &&
|
||||
|
||||
55
src/renderer/extensions/vueNodes/components/NodeBadges.vue
Normal file
55
src/renderer/extensions/vueNodes/components/NodeBadges.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import NodeBadge from '@/renderer/extensions/vueNodes/components/NodeBadge.vue'
|
||||
import type { NodeBadgeProps } from '@/renderer/extensions/vueNodes/components/NodeBadge.vue'
|
||||
|
||||
defineProps<{
|
||||
hasComfyBadge: boolean
|
||||
core: NodeBadgeProps[]
|
||||
extension: NodeBadgeProps[]
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="hasComfyBadge || core.length || extension.length"
|
||||
class="flex h-5 w-full gap-2 px-2 text-muted-foreground"
|
||||
>
|
||||
<div
|
||||
v-if="hasComfyBadge"
|
||||
class="rounded-full bg-component-node-widget-background size-6 flex justify-center items-center"
|
||||
>
|
||||
<i class="icon-[comfy--comfy-c] size-3" />
|
||||
</div>
|
||||
<div
|
||||
v-if="core.length"
|
||||
class="rounded-full bg-component-node-widget-background h-6 flex justify-center items-center overflow-clip"
|
||||
>
|
||||
<template v-for="(badge, index) of core" :key="badge.text">
|
||||
<div
|
||||
v-if="index !== 0"
|
||||
class="border-muted-foreground border-r h-4 mr-0.5 pr-0.5"
|
||||
/>
|
||||
<NodeBadge
|
||||
bg-color="transparent"
|
||||
v-bind="badge"
|
||||
class="h-6 first:pl-2 last:pr-2"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="extension.length"
|
||||
class="rounded-full bg-component-node-widget-background h-6 flex justify-center items-center overflow-clip"
|
||||
>
|
||||
<template v-for="(badge, index) of extension" :key="badge.text">
|
||||
<div
|
||||
v-if="index !== 0"
|
||||
class="border-muted-foreground border-r h-4 mr-0.5 pr-0.5"
|
||||
/>
|
||||
<NodeBadge
|
||||
bg-color="transparent"
|
||||
v-bind="badge"
|
||||
class="h-6 first:pl-2 last:pr-2"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,20 +7,16 @@
|
||||
:class="
|
||||
cn(
|
||||
'lg-node-header text-sm py-2 pl-2 pr-3 w-full min-w-0',
|
||||
'text-node-component-header bg-node-component-header-surface',
|
||||
'text-node-component-header',
|
||||
headerShapeClass
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
backgroundColor: applyLightThemeColor(nodeData?.color),
|
||||
opacity: useSettingStore().get('Comfy.Node.Opacity') ?? 1
|
||||
}"
|
||||
:data-testid="`node-header-${nodeData?.id || ''}`"
|
||||
@dblclick="handleDoubleClick"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2.5 min-w-0">
|
||||
<!-- Collapse/Expand Button -->
|
||||
<div class="relative grow-1 flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<div class="relative flex items-center gap-2.5 min-w-0 shrink-1 mr-auto">
|
||||
<div class="flex shrink-0 items-center px-0.5">
|
||||
<Button
|
||||
size="icon-sm"
|
||||
@@ -41,17 +37,13 @@
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
|
||||
<div v-if="isApiNode" class="icon-[lucide--component] size-4" />
|
||||
|
||||
<!-- Node Title -->
|
||||
<div
|
||||
v-tooltip.top="tooltipConfig"
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
data-testid="node-title"
|
||||
>
|
||||
<div class="truncate min-w-0 flex-1">
|
||||
<div class="truncate flex-1">
|
||||
<EditableText
|
||||
:model-value="displayTitle"
|
||||
:is-editing="isEditing"
|
||||
@@ -63,56 +55,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center justify-between gap-2">
|
||||
<NodeBadge
|
||||
v-for="badge of nodeBadges"
|
||||
:key="badge.text"
|
||||
v-bind="badge"
|
||||
/>
|
||||
<NodeBadge v-if="statusBadge" v-bind="statusBadge" />
|
||||
<i-comfy:pin
|
||||
v-if="isPinned"
|
||||
class="size-5"
|
||||
data-testid="node-pin-indicator"
|
||||
/>
|
||||
<Button
|
||||
v-if="isSubgraphNode"
|
||||
v-tooltip.top="enterSubgraphTooltipConfig"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
data-testid="subgraph-enter-button"
|
||||
class="text-node-component-header h-5 px-0.5"
|
||||
@click.stop="handleEnterSubgraph"
|
||||
@dblclick.stop
|
||||
<template v-for="badge in priceBadges ?? []" :key="badge.required">
|
||||
<span
|
||||
:class="
|
||||
cn(
|
||||
'flex h-5 bg-component-node-widget-background p-1 items-center text-xs shrink-0',
|
||||
badge.rest ? 'rounded-l-full pr-1' : 'rounded-full'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span>{{ $t('g.edit') }}</span>
|
||||
<i class="icon-[lucide--scaling] size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<i class="h-full icon-[lucide--component] bg-amber-400" />
|
||||
<span class="truncate" v-text="badge.required" />
|
||||
</span>
|
||||
<span
|
||||
v-if="badge.rest"
|
||||
class="truncate -ml-2.5 grow-1 basis-0 bg-component-node-widget-background rounded-r-full max-w-max min-w-0"
|
||||
>
|
||||
<span class="pr-2" v-text="badge.rest" />
|
||||
</span>
|
||||
</template>
|
||||
<NodeBadge v-if="statusBadge" v-bind="statusBadge" />
|
||||
<i
|
||||
v-if="isPinned"
|
||||
class="size-5 icon-[comfy--pin]"
|
||||
data-testid="node-pin-indicator"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onErrorCaptured, ref, toValue, watch } from 'vue'
|
||||
import { computed, onErrorCaptured, ref, watch } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import { LGraphEventMode, RenderShape } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import NodeBadge from '@/renderer/extensions/vueNodes/components/NodeBadge.vue'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
getNodeByLocatorId
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -121,6 +103,7 @@ import type { NodeBadgeProps } from './NodeBadge.vue'
|
||||
interface NodeHeaderProps {
|
||||
nodeData?: VueNodeData
|
||||
collapsed?: boolean
|
||||
priceBadges?: { required: string; rest?: string }[]
|
||||
}
|
||||
|
||||
const { nodeData, collapsed } = defineProps<NodeHeaderProps>()
|
||||
@@ -128,7 +111,6 @@ const { nodeData, collapsed } = defineProps<NodeHeaderProps>()
|
||||
const emit = defineEmits<{
|
||||
collapse: []
|
||||
'update:title': [newTitle: string]
|
||||
'enter-subgraph': []
|
||||
}>()
|
||||
|
||||
// Error boundary implementation
|
||||
@@ -156,10 +138,6 @@ const tooltipConfig = computed(() => {
|
||||
return createTooltipConfig(description)
|
||||
})
|
||||
|
||||
const enterSubgraphTooltipConfig = computed(() => {
|
||||
return createTooltipConfig(st('enterSubgraph', 'Enter Subgraph'))
|
||||
})
|
||||
|
||||
const resolveTitle = (info: VueNodeData | undefined) => {
|
||||
const untitledLabel = st('g.untitled', 'Untitled')
|
||||
return resolveNodeDisplayName(info ?? null, {
|
||||
@@ -185,71 +163,7 @@ const statusBadge = computed((): NodeBadgeProps | undefined =>
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Use per-node pricing revision to re-compute badges only when this node's pricing updates
|
||||
const {
|
||||
getRelevantWidgetNames,
|
||||
hasDynamicPricing,
|
||||
getInputGroupPrefixes,
|
||||
getInputNames,
|
||||
getNodeRevisionRef
|
||||
} = useNodePricing()
|
||||
// Cache pricing metadata (won't change during node lifetime)
|
||||
const isDynamicPricing = computed(() =>
|
||||
nodeData?.apiNode ? hasDynamicPricing(nodeData.type) : false
|
||||
)
|
||||
const relevantPricingWidgets = computed(() =>
|
||||
nodeData?.apiNode ? getRelevantWidgetNames(nodeData.type) : []
|
||||
)
|
||||
const inputGroupPrefixes = computed(() =>
|
||||
nodeData?.apiNode ? getInputGroupPrefixes(nodeData.type) : []
|
||||
)
|
||||
const relevantInputNames = computed(() =>
|
||||
nodeData?.apiNode ? getInputNames(nodeData.type) : []
|
||||
)
|
||||
const nodeBadges = computed<NodeBadgeProps[]>(() => {
|
||||
// For ALL API nodes: access per-node revision ref to detect when async pricing evaluation completes
|
||||
// This is needed even for static pricing because JSONata 2.x evaluation is async
|
||||
if (nodeData?.apiNode && nodeData?.id != null) {
|
||||
// Access per-node revision ref to establish dependency (each node has its own ref)
|
||||
void getNodeRevisionRef(nodeData.id).value
|
||||
|
||||
// For dynamic pricing, also track widget values and input connections
|
||||
if (isDynamicPricing.value) {
|
||||
// Access only the widget values that affect pricing (from widgetValueStore)
|
||||
const relevantNames = relevantPricingWidgets.value
|
||||
const widgetStore = useWidgetValueStore()
|
||||
if (relevantNames.length > 0 && nodeData?.id != null) {
|
||||
for (const name of relevantNames) {
|
||||
// Access value from store to create reactive dependency
|
||||
void widgetStore.getWidget(nodeData.id, name)?.value
|
||||
}
|
||||
}
|
||||
// Access input connections for regular inputs
|
||||
const inputNames = relevantInputNames.value
|
||||
if (inputNames.length > 0) {
|
||||
nodeData?.inputs?.forEach((inp) => {
|
||||
if (inp.name && inputNames.includes(inp.name)) {
|
||||
void inp.link // Access link to create reactive dependency
|
||||
}
|
||||
})
|
||||
}
|
||||
// Access input connections for input_groups (e.g., autogrow inputs)
|
||||
const groupPrefixes = inputGroupPrefixes.value
|
||||
if (groupPrefixes.length > 0) {
|
||||
nodeData?.inputs?.forEach((inp) => {
|
||||
if (
|
||||
groupPrefixes.some((prefix) => inp.name?.startsWith(prefix + '.'))
|
||||
) {
|
||||
void inp.link // Access link to create reactive dependency
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...(nodeData?.badges ?? [])].map(toValue)
|
||||
})
|
||||
const isPinned = computed(() => Boolean(nodeData?.flags?.pinned))
|
||||
const isApiNode = computed(() => Boolean(nodeData?.apiNode))
|
||||
|
||||
const headerShapeClass = computed(() => {
|
||||
if (collapsed) {
|
||||
@@ -272,22 +186,6 @@ const headerShapeClass = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Subgraph detection
|
||||
const isSubgraphNode = computed(() => {
|
||||
if (!nodeData?.id) return false
|
||||
|
||||
// Get the underlying LiteGraph node
|
||||
const graph = app.rootGraph
|
||||
if (!graph) return false
|
||||
|
||||
const locatorId = getLocatorIdFromNodeData(nodeData)
|
||||
|
||||
const litegraphNode = getNodeByLocatorId(graph, locatorId)
|
||||
|
||||
// Use the official type guard method
|
||||
return litegraphNode?.isSubgraphNode() ?? false
|
||||
})
|
||||
|
||||
// Watch for external changes to the node title or type
|
||||
watch(
|
||||
() => [nodeData?.title, nodeData?.type] as const,
|
||||
@@ -320,8 +218,4 @@ const handleTitleEdit = (newTitle: string) => {
|
||||
const handleTitleCancel = () => {
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const handleEnterSubgraph = () => {
|
||||
emit('enter-subgraph')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
boundingRect: [0, 0, 0, 0]
|
||||
}"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:has-error="widget.hasError"
|
||||
:index="widget.slotMetadata.index"
|
||||
:socketless="widget.simplified.spec?.socketless"
|
||||
dot-only
|
||||
@@ -60,7 +61,12 @@
|
||||
:widget="widget.simplified"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:node-type="nodeType"
|
||||
class="col-span-2"
|
||||
:class="
|
||||
cn(
|
||||
'col-span-2',
|
||||
widget.hasError && 'text-node-stroke-error font-bold'
|
||||
)
|
||||
"
|
||||
@update:model-value="widget.updateHandler"
|
||||
/>
|
||||
</div>
|
||||
@@ -95,6 +101,7 @@ import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -109,6 +116,7 @@ const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||
useCanvasInteractions()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
function handleWidgetPointerEvent(event: PointerEvent) {
|
||||
if (shouldHandleNodePointerEvents.value) return
|
||||
@@ -146,21 +154,23 @@ const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
|
||||
interface ProcessedWidget {
|
||||
name: string
|
||||
type: string
|
||||
vueComponent: Component
|
||||
simplified: SimplifiedWidget
|
||||
value: WidgetValue
|
||||
updateHandler: (value: WidgetValue) => void
|
||||
tooltipConfig: TooltipOptions
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
hidden: boolean
|
||||
advanced: boolean
|
||||
hasLayoutSize: boolean
|
||||
hasError: boolean
|
||||
hidden: boolean
|
||||
name: string
|
||||
simplified: SimplifiedWidget
|
||||
tooltipConfig: TooltipOptions
|
||||
type: string
|
||||
updateHandler: (value: WidgetValue) => void
|
||||
value: WidgetValue
|
||||
vueComponent: Component
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
}
|
||||
|
||||
const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
if (!nodeData?.widgets) return []
|
||||
const nodeErrors = executionStore.lastNodeErrors?.[nodeData.id ?? '']
|
||||
|
||||
const nodeId = nodeData.id
|
||||
const { widgets } = nodeData
|
||||
@@ -220,6 +230,13 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
const tooltipConfig = createTooltipConfig(tooltipText)
|
||||
|
||||
result.push({
|
||||
advanced: widget.options?.advanced ?? false,
|
||||
hasLayoutSize: widget.hasLayoutSize ?? false,
|
||||
hasError:
|
||||
nodeErrors?.errors?.some(
|
||||
(error) => error.extra_info?.input_name === widget.name
|
||||
) ?? false,
|
||||
hidden: widget.options?.hidden ?? false,
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
vueComponent,
|
||||
@@ -227,10 +244,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
value,
|
||||
updateHandler,
|
||||
tooltipConfig,
|
||||
slotMetadata,
|
||||
hidden: widget.options?.hidden ?? false,
|
||||
advanced: widget.options?.advanced ?? false,
|
||||
hasLayoutSize: widget.hasLayoutSize ?? false
|
||||
slotMetadata
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { trim } from 'es-toolkit'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeBadgeProps } from '@/renderer/extensions/vueNodes/components/NodeBadge.vue'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
function splitAroundFirstSpace(text: string): [string, string | undefined] {
|
||||
const index = text.indexOf(' ')
|
||||
if (index === -1) return [text, undefined]
|
||||
return [text.slice(0, index), text.slice(index + 1)]
|
||||
}
|
||||
|
||||
export function usePartitionedBadges(nodeData: VueNodeData) {
|
||||
// Use per-node pricing revision to re-compute badges only when this node's pricing updates
|
||||
const {
|
||||
getRelevantWidgetNames,
|
||||
hasDynamicPricing,
|
||||
getInputGroupPrefixes,
|
||||
getInputNames,
|
||||
getNodeRevisionRef
|
||||
} = useNodePricing()
|
||||
|
||||
const { isCreditsBadge } = usePriceBadge()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// Cache pricing metadata (won't change during node lifetime)
|
||||
const isDynamicPricing = computed(() =>
|
||||
nodeData?.apiNode ? hasDynamicPricing(nodeData.type) : false
|
||||
)
|
||||
const relevantPricingWidgets = computed(() =>
|
||||
nodeData?.apiNode ? getRelevantWidgetNames(nodeData.type) : []
|
||||
)
|
||||
const inputGroupPrefixes = computed(() =>
|
||||
nodeData?.apiNode ? getInputGroupPrefixes(nodeData.type) : []
|
||||
)
|
||||
const relevantInputNames = computed(() =>
|
||||
nodeData?.apiNode ? getInputNames(nodeData.type) : []
|
||||
)
|
||||
const unpartitionedBadges = computed<NodeBadgeProps[]>(() => {
|
||||
// For ALL API nodes: access per-node revision ref to detect when async pricing evaluation completes
|
||||
// This is needed even for static pricing because JSONata 2.x evaluation is async
|
||||
if (nodeData?.apiNode && nodeData?.id != null) {
|
||||
// Access per-node revision ref to establish dependency (each node has its own ref)
|
||||
void getNodeRevisionRef(nodeData.id).value
|
||||
|
||||
// For dynamic pricing, also track widget values and input connections
|
||||
if (isDynamicPricing.value) {
|
||||
// Access only the widget values that affect pricing (from widgetValueStore)
|
||||
const relevantNames = relevantPricingWidgets.value
|
||||
const widgetStore = useWidgetValueStore()
|
||||
if (relevantNames.length > 0 && nodeData?.id != null) {
|
||||
for (const name of relevantNames) {
|
||||
// Access value from store to create reactive dependency
|
||||
void widgetStore.getWidget(nodeData.id, name)?.value
|
||||
}
|
||||
}
|
||||
// Access input connections for regular inputs
|
||||
const inputNames = relevantInputNames.value
|
||||
if (inputNames.length > 0) {
|
||||
nodeData?.inputs?.forEach((inp) => {
|
||||
if (inp.name && inputNames.includes(inp.name)) {
|
||||
void inp.link // Access link to create reactive dependency
|
||||
}
|
||||
})
|
||||
}
|
||||
// Access input connections for input_groups (e.g., autogrow inputs)
|
||||
const groupPrefixes = inputGroupPrefixes.value
|
||||
if (groupPrefixes.length > 0) {
|
||||
nodeData?.inputs?.forEach((inp) => {
|
||||
if (
|
||||
groupPrefixes.some((prefix) => inp.name?.startsWith(prefix + '.'))
|
||||
) {
|
||||
void inp.link // Access link to create reactive dependency
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...(nodeData?.badges ?? [])].map(toValue)
|
||||
})
|
||||
const nodeDef = useNodeDefStore().nodeDefsByName[nodeData.type]
|
||||
return computed(() => {
|
||||
const displaySource = settingStore.get(
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode'
|
||||
)
|
||||
const isCoreNode =
|
||||
nodeDef?.isCoreNode && displaySource === NodeBadgeMode.ShowAll
|
||||
const core: NodeBadgeProps[] = []
|
||||
const extension: NodeBadgeProps[] = []
|
||||
const pricing: { required: string; rest?: string }[] = []
|
||||
if (
|
||||
settingStore.get('Comfy.NodeBadge.NodeLifeCycleBadgeMode') !==
|
||||
NodeBadgeMode.None
|
||||
) {
|
||||
const lifecycleText = nodeDef?.nodeLifeCycleBadgeText ?? ''
|
||||
const trimmed = trim(lifecycleText, ['[', ']'])
|
||||
if (trimmed) core.push({ text: trimmed })
|
||||
}
|
||||
if (
|
||||
settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') !== NodeBadgeMode.None
|
||||
)
|
||||
core.push({ text: `#${nodeData.id}` })
|
||||
const sourceText = nodeDef?.nodeSource?.badgeText
|
||||
if (
|
||||
!nodeDef?.isCoreNode &&
|
||||
displaySource !== NodeBadgeMode.None &&
|
||||
sourceText
|
||||
)
|
||||
core.push({ text: sourceText })
|
||||
|
||||
for (const badge of unpartitionedBadges.value.slice(1)) {
|
||||
if (!badge.text) continue
|
||||
|
||||
if (isCreditsBadge(badge)) {
|
||||
const [required, rest] = splitAroundFirstSpace(badge.text)
|
||||
pricing.push({ required, rest })
|
||||
continue
|
||||
}
|
||||
extension.push(badge)
|
||||
}
|
||||
|
||||
return {
|
||||
hasComfyBadge: isCoreNode && pricing.length === 0,
|
||||
core,
|
||||
extension,
|
||||
pricing
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
export type RightSidePanelTab =
|
||||
| 'error'
|
||||
| 'parameters'
|
||||
| 'nodes'
|
||||
| 'settings'
|
||||
|
||||
Reference in New Issue
Block a user