Merge remote-tracking branch 'origin/main' into feat/top-menu-active-jobs-label

This commit is contained in:
Benjamin Lu
2026-01-20 14:53:29 -08:00
31 changed files with 740 additions and 491 deletions

View File

@@ -96,15 +96,15 @@ const config: StorybookConfig = {
}
]
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
rolldownOptions: {
experimental: {
strictExecutionOrder: true
},
treeshake: false,
output: {
keepNames: true
},
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -64,7 +64,7 @@ export default defineConfig(() => {
})
],
build: {
minify: SHOULD_MINIFY ? ('esbuild' as const) : false,
minify: SHOULD_MINIFY,
target: 'es2022',
sourcemap: true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.38.6",
"version": "1.38.7",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -12,6 +12,7 @@ declare global {
const __ALGOLIA_API_KEY__: string
const __USE_PROD_CONFIG__: boolean
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
const __IS_NIGHTLY__: boolean
}
type GlobalWithDefines = typeof globalThis & {
@@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & {
__ALGOLIA_API_KEY__: string
__USE_PROD_CONFIG__: boolean
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
__IS_NIGHTLY__: boolean
window?: Record<string, unknown>
}
@@ -36,6 +38,7 @@ globalWithDefines.__ALGOLIA_APP_ID__ = ''
globalWithDefines.__ALGOLIA_API_KEY__ = ''
globalWithDefines.__USE_PROD_CONFIG__ = false
globalWithDefines.__DISTRIBUTION__ = 'localhost'
globalWithDefines.__IS_NIGHTLY__ = false
// Provide a minimal window shim for Node environment
// This is needed for code that checks window existence during imports

View File

@@ -28,7 +28,7 @@
/>
</div>
<div
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node" />
</div>

View File

@@ -25,13 +25,31 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => {
return nodes.map((node) => {
const { widgets = [] } = node
const shownWidgets = widgets
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
.filter(
(w) =>
!(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced)
)
.map((widget) => ({ node, widget }))
return { widgets: shownWidgets, node }
})
})
const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
return nodes
.map((node) => {
const { widgets = [] } = node
const advancedWidgets = widgets
.filter(
(w) =>
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
)
.map((widget) => ({ node, widget }))
return { widgets: advancedWidgets, node }
})
.filter(({ widgets }) => widgets.length > 0)
})
const isMultipleNodesSelected = computed(
() => widgetsSectionDataList.value.length > 1
)
@@ -56,6 +74,12 @@ const label = computed(() => {
: t('rightSidePanel.inputsNone')
: undefined // SectionWidgets display node titles by default
})
const advancedLabel = computed(() => {
return !mustShowNodeTitle && !isMultipleNodesSelected.value
? t('rightSidePanel.advancedInputs')
: undefined // SectionWidgets display node titles by default
})
</script>
<template>
@@ -93,4 +117,16 @@ const label = computed(() => {
class="border-b border-interface-stroke"
/>
</TransitionGroup>
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
<SectionWidgets
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
:key="`advanced-${node.id}`"
:collapse="true"
:node
:label="advancedLabel"
:widgets
:show-locate-button="isMultipleNodesSelected"
class="border-b border-interface-stroke"
/>
</template>
</template>

View File

@@ -368,7 +368,7 @@ export class GroupNodeConfig {
}
getNodeDef(
node: GroupNodeData
node: GroupNodeData | GroupNodeWorkflowData['nodes'][number]
): GroupNodeDef | ComfyNodeDef | null | undefined {
if (node.type) {
const def = globalDefs[node.type]
@@ -386,7 +386,8 @@ export class GroupNodeConfig {
let type: string | number | null = linksFrom[0]?.[0]?.[5] ?? null
if (type === 'COMBO') {
// Use the array items
const source = node.outputs?.[0]?.widget?.name
const output = node.outputs?.[0] as GroupNodeOutput | undefined
const source = output?.widget?.name
const nodeIdx = linksFrom[0]?.[0]?.[2]
if (source && nodeIdx != null) {
const fromTypeName = this.nodeData.nodes[Number(nodeIdx)]?.type

View File

@@ -1,9 +1,11 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import {
type LGraphNode,
type LGraphNodeConstructor,
LiteGraph
import type {
GroupNodeConfigEntry,
GroupNodeWorkflowData,
LGraphNode,
LGraphNodeConstructor
} from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { type ComfyApp, app } from '../../scripts/app'
@@ -15,18 +17,20 @@ import './groupNodeManage.css'
const ORDER: symbol = Symbol()
// @ts-expect-error fixme ts strict error
function merge(target, source) {
if (typeof target === 'object' && typeof source === 'object') {
for (const key in source) {
const sv = source[key]
if (typeof sv === 'object') {
let tv = target[key]
if (!tv) tv = target[key] = {}
merge(tv, source[key])
} else {
target[key] = sv
function merge(
target: Record<string, unknown>,
source: Record<string, unknown>
): Record<string, unknown> {
for (const key in source) {
const sv = source[key]
if (typeof sv === 'object' && sv !== null) {
let tv = target[key] as Record<string, unknown> | undefined
if (!tv) {
tv = target[key] = {}
}
merge(tv, sv as Record<string, unknown>)
} else {
target[key] = sv
}
}
@@ -34,8 +38,7 @@ function merge(target, source) {
}
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
// @ts-expect-error fixme ts strict error
tabs: Record<
tabs!: Record<
'Inputs' | 'Outputs' | 'Widgets',
{ tab: HTMLAnchorElement; page: HTMLElement }
>
@@ -52,31 +55,26 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
>
>
> = {}
// @ts-expect-error fixme ts strict error
nodeItems: any[]
nodeItems!: HTMLLIElement[]
app: ComfyApp
// @ts-expect-error fixme ts strict error
groupNodeType: LGraphNodeConstructor<LGraphNode>
groupNodeDef: any
groupData: any
groupNodeType!: LGraphNodeConstructor<LGraphNode>
groupData!: GroupNodeConfig
// @ts-expect-error fixme ts strict error
innerNodesList: HTMLUListElement
// @ts-expect-error fixme ts strict error
widgetsPage: HTMLElement
// @ts-expect-error fixme ts strict error
inputsPage: HTMLElement
// @ts-expect-error fixme ts strict error
outputsPage: HTMLElement
draggable: any
innerNodesList!: HTMLUListElement
widgetsPage!: HTMLElement
inputsPage!: HTMLElement
outputsPage!: HTMLElement
draggable: DraggableList | undefined
get selectedNodeInnerIndex() {
// @ts-expect-error fixme ts strict error
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
get selectedNodeInnerIndex(): number {
const index = this.selectedNodeIndex
if (index == null) throw new Error('No node selected')
const item = this.nodeItems[index]
if (!item?.dataset.nodeindex) throw new Error('Invalid node item')
return +item.dataset.nodeindex
}
// @ts-expect-error fixme ts strict error
constructor(app) {
constructor(app: ComfyApp) {
super()
this.app = app
this.element = $el('dialog.comfy-group-manage', {
@@ -84,19 +82,15 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}) as HTMLDialogElement
}
// @ts-expect-error fixme ts strict error
changeTab(tab) {
changeTab(tab: keyof ManageGroupDialog['tabs']): void {
this.tabs[this.selectedTab].tab.classList.remove('active')
this.tabs[this.selectedTab].page.classList.remove('active')
// @ts-expect-error fixme ts strict error
this.tabs[tab].tab.classList.add('active')
// @ts-expect-error fixme ts strict error
this.tabs[tab].page.classList.add('active')
this.selectedTab = tab
}
// @ts-expect-error fixme ts strict error
changeNode(index, force?) {
changeNode(index: number, force?: boolean): void {
if (!force && this.selectedNodeIndex === index) return
if (this.selectedNodeIndex != null) {
@@ -122,43 +116,41 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.groupNodeType = LiteGraph.registered_node_types[
`${PREFIX}${SEPARATOR}` + this.selectedGroup
] as unknown as LGraphNodeConstructor<LGraphNode>
this.groupNodeDef = this.groupNodeType.nodeData
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)!
}
// @ts-expect-error fixme ts strict error
changeGroup(group, reset = true) {
changeGroup(group: string, reset = true): void {
this.selectedGroup = group
this.getGroupData()
const nodes = this.groupData.nodeData.nodes
// @ts-expect-error fixme ts strict error
this.nodeItems = nodes.map((n, i) =>
$el(
'li.draggable-item',
{
dataset: {
nodeindex: n.index + ''
},
onclick: () => {
this.changeNode(i)
}
},
[
$el('span.drag-handle'),
$el(
'div',
{
textContent: n.title ?? n.type
this.nodeItems = nodes.map(
(n, i) =>
$el(
'li.draggable-item',
{
dataset: {
nodeindex: n.index + ''
},
n.title
? $el('span', {
textContent: n.type
})
: []
)
]
)
onclick: () => {
this.changeNode(i)
}
},
[
$el('span.drag-handle'),
$el(
'div',
{
textContent: n.title ?? n.type
},
n.title
? $el('span', {
textContent: n.type
})
: []
)
]
) as HTMLLIElement
)
this.innerNodesList.replaceChildren(...this.nodeItems)
@@ -167,47 +159,46 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.selectedNodeIndex = null
this.changeNode(0)
} else {
const items = this.draggable.getAllItems()
// @ts-expect-error fixme ts strict error
let index = items.findIndex((item) => item.classList.contains('selected'))
if (index === -1) index = this.selectedNodeIndex
const items = this.draggable!.getAllItems()
let index = items.findIndex((item: Element) =>
item.classList.contains('selected')
)
if (index === -1) index = this.selectedNodeIndex!
this.changeNode(index, true)
}
const ordered = [...nodes]
this.draggable?.dispose()
this.draggable = new DraggableList(this.innerNodesList, 'li')
this.draggable.addEventListener(
'dragend',
// @ts-expect-error fixme ts strict error
({ detail: { oldPosition, newPosition } }) => {
if (oldPosition === newPosition) return
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
for (let i = 0; i < ordered.length; i++) {
this.storeModification({
nodeIndex: ordered[i].index,
section: ORDER,
prop: 'order',
value: i
})
}
this.draggable.addEventListener('dragend', (e: Event) => {
const { oldPosition, newPosition } = (e as CustomEvent).detail
if (oldPosition === newPosition) return
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
for (let i = 0; i < ordered.length; i++) {
this.storeModification({
nodeIndex: ordered[i].index,
section: ORDER,
prop: 'order',
value: i
})
}
)
})
}
storeModification(props: {
nodeIndex?: number
section: symbol
section: string | symbol
prop: string
value: any
value: unknown
}) {
const { nodeIndex, section, prop, value } = props
// @ts-expect-error fixme ts strict error
const groupMod = (this.modifications[this.selectedGroup] ??= {})
const nodesMod = (groupMod.nodes ??= {})
const groupKey = this.selectedGroup!
const groupMod = (this.modifications[groupKey] ??= {})
const nodesMod = ((groupMod as Record<string, unknown>).nodes ??=
{}) as Record<string, Record<symbol | string, Record<string, unknown>>>
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {})
const typeMod = (nodeMod[section] ??= {})
if (typeof value === 'object') {
if (typeof value === 'object' && value !== null) {
const objMod = (typeMod[prop] ??= {})
Object.assign(objMod, value)
} else {
@@ -215,35 +206,45 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}
}
// @ts-expect-error fixme ts strict error
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
if (value === placeholder) value = ''
getEditElement(
section: string,
prop: string | number,
value: unknown,
placeholder: string,
checked: boolean,
checkable = true
): HTMLDivElement {
let displayValue = value === placeholder ? '' : value
const mods =
// @ts-expect-error fixme ts strict error
this.modifications[this.selectedGroup]?.nodes?.[
this.selectedNodeInnerIndex
]?.[section]?.[prop]
if (mods) {
if (mods.name != null) {
value = mods.name
const groupKey = this.selectedGroup!
const mods = (
this.modifications[groupKey] as Record<string, unknown> | undefined
)?.nodes as
| Record<
number,
Record<string, Record<string, { name?: string; visible?: boolean }>>
>
| undefined
const modEntry = mods?.[this.selectedNodeInnerIndex]?.[section]?.[prop]
if (modEntry) {
if (modEntry.name != null) {
displayValue = modEntry.name
}
if (mods.visible != null) {
checked = mods.visible
if (modEntry.visible != null) {
checked = modEntry.visible
}
}
return $el('div', [
$el('input', {
value,
value: displayValue as string,
placeholder,
type: 'text',
// @ts-expect-error fixme ts strict error
onchange: (e) => {
onchange: (e: Event) => {
this.storeModification({
section,
prop,
value: { name: e.target.value }
prop: String(prop),
value: { name: (e.target as HTMLInputElement).value }
})
}
}),
@@ -252,25 +253,23 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
type: 'checkbox',
checked,
disabled: !checkable,
// @ts-expect-error fixme ts strict error
onchange: (e) => {
onchange: (e: Event) => {
this.storeModification({
section,
prop,
value: { visible: !!e.target.checked }
prop: String(prop),
value: { visible: !!(e.target as HTMLInputElement).checked }
})
}
})
])
])
]) as HTMLDivElement
}
buildWidgetsPage() {
const widgets =
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
const items = Object.keys(widgets ?? {})
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
const config = type.config?.[this.selectedNodeInnerIndex]?.input
this.widgetsPage.replaceChildren(
...items.map((oldName) => {
@@ -289,28 +288,25 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
buildInputsPage() {
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]
const items = Object.keys(inputs ?? {})
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
const config = type.config?.[this.selectedNodeInnerIndex]?.input
this.inputsPage.replaceChildren(
// @ts-expect-error fixme ts strict error
...items
.map((oldName) => {
let value = inputs[oldName]
if (!value) {
return
}
const elements = items
.map((oldName) => {
const value = inputs[oldName]
if (!value) {
return null
}
return this.getEditElement(
'input',
oldName,
value,
oldName,
config?.[oldName]?.visible !== false
)
})
.filter(Boolean)
)
return this.getEditElement(
'input',
oldName,
value,
oldName,
config?.[oldName]?.visible !== false
)
})
.filter((el): el is HTMLDivElement => el !== null)
this.inputsPage.replaceChildren(...elements)
return !!items.length
}
@@ -323,38 +319,35 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
const groupOutputs =
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
const config = type.config?.[this.selectedNodeInnerIndex]?.output
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]
const checkable = node.type !== 'PrimitiveNode'
this.outputsPage.replaceChildren(
...outputs
// @ts-expect-error fixme ts strict error
.map((type, slot) => {
const groupOutputIndex = groupOutputs?.[slot]
const oldName = innerNodeDef.output_name?.[slot] ?? type
let value = config?.[slot]?.name
const visible = config?.[slot]?.visible || groupOutputIndex != null
if (!value || value === oldName) {
value = ''
}
return this.getEditElement(
'output',
slot,
value,
oldName,
visible,
checkable
)
})
.filter(Boolean)
)
const elements = outputs.map((outputType: unknown, slot: number) => {
const groupOutputIndex = groupOutputs?.[slot]
const oldName = innerNodeDef?.output_name?.[slot] ?? String(outputType)
let value = config?.[slot]?.name
const visible = config?.[slot]?.visible || groupOutputIndex != null
if (!value || value === oldName) {
value = ''
}
return this.getEditElement(
'output',
slot,
value,
oldName,
visible,
checkable
)
})
this.outputsPage.replaceChildren(...elements)
return !!outputs.length
}
// @ts-expect-error fixme ts strict error
show(type?) {
override show(groupNodeType?: string | HTMLElement | HTMLElement[]): void {
// Extract string type - this method repurposes the show signature
const nodeType =
typeof groupNodeType === 'string' ? groupNodeType : undefined
const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort(
(a, b) => a.localeCompare(b)
)
@@ -371,24 +364,27 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.outputsPage
])
this.tabs = [
type TabName = 'Inputs' | 'Widgets' | 'Outputs'
const tabEntries: [TabName, HTMLElement][] = [
['Inputs', this.inputsPage],
['Widgets', this.widgetsPage],
['Outputs', this.outputsPage]
// @ts-expect-error fixme ts strict error
].reduce((p, [name, page]: [string, HTMLElement]) => {
// @ts-expect-error fixme ts strict error
p[name] = {
tab: $el('a', {
onclick: () => {
this.changeTab(name)
},
textContent: name
}),
page
}
return p
}, {}) as any
]
this.tabs = tabEntries.reduce(
(p, [name, page]) => {
p[name] = {
tab: $el('a', {
onclick: () => {
this.changeTab(name)
},
textContent: name
}) as HTMLAnchorElement,
page
}
return p
},
{} as ManageGroupDialog['tabs']
)
const outer = $el('div.comfy-group-manage-outer', [
$el('header', [
@@ -396,15 +392,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
$el(
'select',
{
// @ts-expect-error fixme ts strict error
onchange: (e) => {
this.changeGroup(e.target.value)
onchange: (e: Event) => {
this.changeGroup((e.target as HTMLSelectElement).value)
}
},
groupNodes.map((g) =>
$el('option', {
textContent: g,
selected: `${PREFIX}${SEPARATOR}${g}` === type,
selected: `${PREFIX}${SEPARATOR}${g}` === nodeType,
value: g
})
)
@@ -439,8 +434,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
`Are you sure you want to remove the node: "${this.selectedGroup}"`
)
) {
// @ts-expect-error fixme ts strict error
delete app.rootGraph.extra.groupNodes[this.selectedGroup]
delete app.rootGraph.extra.groupNodes![this.selectedGroup!]
LiteGraph.unregisterNodeType(
`${PREFIX}${SEPARATOR}` + this.selectedGroup
)
@@ -454,97 +448,106 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
'button.comfy-btn',
{
onclick: async () => {
let nodesByType
let recreateNodes = []
const types = {}
type NodesByType = Record<string, LGraphNode[]>
let nodesByType: NodesByType | undefined
const recreateNodes: LGraphNode[] = []
const types: Record<string, GroupNodeWorkflowData> = {}
for (const g in this.modifications) {
// @ts-expect-error fixme ts strict error
const type = app.rootGraph.extra.groupNodes[g]
let config = (type.config ??= {})
const groupNodeData = app.rootGraph.extra.groupNodes![g]!
let config = (groupNodeData.config ??= {})
let nodeMods = this.modifications[g]?.nodes
type NodeMods = Record<
string,
Record<symbol | string, Record<string, unknown>>
>
let nodeMods = this.modifications[g]?.nodes as
| NodeMods
| undefined
if (nodeMods) {
const keys = Object.keys(nodeMods)
// @ts-expect-error fixme ts strict error
if (nodeMods[keys[0]][ORDER]) {
if (nodeMods[keys[0]]?.[ORDER]) {
// If any node is reordered, they will all need sequencing
const orderedNodes = []
const orderedMods = {}
const orderedConfig = {}
const orderedNodes: GroupNodeWorkflowData['nodes'] = []
const orderedMods: NodeMods = {}
const orderedConfig: Record<number, GroupNodeConfigEntry> =
{}
for (const n of keys) {
// @ts-expect-error fixme ts strict error
const order = nodeMods[n][ORDER].order
orderedNodes[order] = type.nodes[+n]
// @ts-expect-error fixme ts strict error
const order = (nodeMods[n][ORDER] as { order: number })
.order
orderedNodes[order] = groupNodeData.nodes[+n]
orderedMods[order] = nodeMods[n]
orderedNodes[order].index = order
}
// Rewrite links
for (const l of type.links) {
// @ts-expect-error l[0]/l[2] used as node index
if (l[0] != null) l[0] = type.nodes[l[0]].index
// @ts-expect-error l[0]/l[2] used as node index
if (l[2] != null) l[2] = type.nodes[l[2]].index
const nodesLen = groupNodeData.nodes.length
for (const l of groupNodeData.links) {
const srcIdx = l[0] as number
const dstIdx = l[2] as number
if (srcIdx != null && srcIdx < nodesLen)
l[0] = groupNodeData.nodes[srcIdx].index!
if (dstIdx != null && dstIdx < nodesLen)
l[2] = groupNodeData.nodes[dstIdx].index!
}
// Rewrite externals
if (type.external) {
for (const ext of type.external) {
if (ext[0] != null) {
// @ts-expect-error ext[0] used as node index
ext[0] = type.nodes[ext[0]].index
if (groupNodeData.external) {
for (const ext of groupNodeData.external) {
const extIdx = ext[0] as number
if (extIdx != null && extIdx < nodesLen) {
ext[0] = groupNodeData.nodes[extIdx].index!
}
}
}
// Rewrite modifications
for (const id of keys) {
// @ts-expect-error id used as node index
if (config[id]) {
// @ts-expect-error fixme ts strict error
orderedConfig[type.nodes[id].index] = config[id]
if (config[+id]) {
orderedConfig[groupNodeData.nodes[+id].index!] =
config[+id]
}
// @ts-expect-error id used as config key
delete config[id]
delete config[+id]
}
type.nodes = orderedNodes
groupNodeData.nodes = orderedNodes
nodeMods = orderedMods
type.config = config = orderedConfig
groupNodeData.config = config = orderedConfig
}
merge(config, nodeMods)
merge(
config as Record<string, unknown>,
nodeMods as Record<string, unknown>
)
}
// @ts-expect-error fixme ts strict error
types[g] = type
types[g] = groupNodeData
if (!nodesByType) {
nodesByType = app.rootGraph.nodes.reduce((p, n) => {
// @ts-expect-error fixme ts strict error
p[n.type] ??= []
// @ts-expect-error fixme ts strict error
p[n.type].push(n)
return p
}, {})
nodesByType = app.rootGraph.nodes.reduce<NodesByType>(
(p, n) => {
const nodeType = n.type ?? ''
p[nodeType] ??= []
p[nodeType].push(n)
return p
},
{}
)
}
// @ts-expect-error fixme ts strict error
const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
if (nodes) recreateNodes.push(...nodes)
const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
if (groupTypeNodes) recreateNodes.push(...groupTypeNodes)
}
await GroupNodeConfig.registerFromWorkflow(types, [])
for (const node of recreateNodes) {
node.recreate()
node.recreate?.()
}
this.modifications = {}
this.app.canvas.setDirty(true, true)
this.changeGroup(this.selectedGroup, false)
this.changeGroup(this.selectedGroup!, false)
}
},
'Save'
@@ -559,8 +562,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.element.replaceChildren(outer)
this.changeGroup(
type
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === type) ??
nodeType
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === nodeType) ??
groupNodes[0])
: groupNodes[0]
)

View File

@@ -102,16 +102,23 @@ export interface LGraphConfig {
links_ontop?: boolean
}
export interface GroupNodeConfigEntry {
input?: Record<string, { name?: string; visible?: boolean }>
output?: Record<number, { name?: string; visible?: boolean }>
}
export interface GroupNodeWorkflowData {
external: (number | string)[][]
links: SerialisedLLinkArray[]
nodes: {
index?: number
type?: string
title?: string
inputs?: unknown[]
outputs?: unknown[]
widgets_values?: unknown[]
}[]
config?: Record<number, unknown>
config?: Record<number, GroupNodeConfigEntry>
}
export interface LGraphExtra extends Dictionary<unknown> {

View File

@@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
})
function inner_clicked(
this: ContextMenu<string>,
this: ContextMenuDivElement,
v?: string | IContextMenuValue<string>
) {
if (!node || typeof v === 'string' || !v?.value) return
const rect = this.root.getBoundingClientRect()
const rect = this.getBoundingClientRect()
canvas.showEditPropertyValue(node, v.value, {
position: [rect.left, rect.top]
})

View File

@@ -104,6 +104,8 @@ export type {
} from './interfaces'
export {
LGraph,
type GroupNodeConfigEntry,
type GroupNodeWorkflowData,
type LGraphTriggerAction,
type LGraphTriggerParam
} from './LGraph'

View File

@@ -1,40 +1,42 @@
<template>
<div class="flex gap-3">
<div class="flex gap-3 items-center">
<SearchBox
:model-value="searchQuery"
:placeholder="$t('sideToolbar.searchAssets') + '...'"
@update:model-value="handleSearchChange"
/>
<MediaAssetFilterButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetFilterMenu
:media-type-filters="mediaTypeFilters"
:close="close"
@update:media-type-filters="handleMediaTypeFiltersChange"
/>
</template>
</MediaAssetFilterButton>
<AssetSortButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetSortMenu
v-model:sort-by="sortBy"
:show-generation-time-sort
:close="close"
/>
</template>
</AssetSortButton>
<MediaAssetViewModeToggle
v-if="isQueuePanelV2Enabled"
v-model:view-mode="viewMode"
/>
<div class="flex gap-1.5 items-center">
<MediaAssetFilterButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetFilterMenu
:media-type-filters
:close
@update:media-type-filters="handleMediaTypeFiltersChange"
/>
</template>
</MediaAssetFilterButton>
<AssetSortButton
v-if="isCloud"
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
size="md"
>
<template #default="{ close }">
<MediaAssetSortMenu
v-model:sort-by="sortBy"
:show-generation-time-sort
:close
/>
</template>
</AssetSortButton>
<MediaAssetViewModeToggle
v-if="isQueuePanelV2Enabled"
v-model:view-mode="viewMode"
/>
</div>
</div>
</template>

View File

@@ -9,6 +9,7 @@ type Distribution = 'desktop' | 'localhost' | 'cloud'
declare global {
const __DISTRIBUTION__: Distribution
const __IS_NIGHTLY__: boolean
}
/** Current distribution - replaced at compile time */
@@ -18,3 +19,10 @@ const DISTRIBUTION: Distribution = __DISTRIBUTION__
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
export const isCloud = DISTRIBUTION === 'cloud'
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
/**
* Whether this is a nightly build (from main branch).
* Nightly builds may show experimental features and surveys.
* @public
*/
export const isNightly = __IS_NIGHTLY__

View File

@@ -0,0 +1,131 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const STORAGE_KEY = 'Comfy.FeatureUsage'
describe('useFeatureUsageTracker', () => {
beforeEach(() => {
localStorage.clear()
vi.resetModules()
})
afterEach(() => {
localStorage.clear()
})
it('initializes with zero count for new feature', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount } = useFeatureUsageTracker('test-feature')
expect(useCount.value).toBe(0)
})
it('increments count on trackUsage', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount, trackUsage } = useFeatureUsageTracker('test-feature')
expect(useCount.value).toBe(0)
trackUsage()
expect(useCount.value).toBe(1)
trackUsage()
expect(useCount.value).toBe(2)
})
it('sets firstUsed only on first use', async () => {
vi.useFakeTimers()
const firstTs = 1000000
vi.setSystemTime(firstTs)
try {
const { useFeatureUsageTracker } =
await import('./useFeatureUsageTracker')
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
trackUsage()
expect(usage.value?.firstUsed).toBe(firstTs)
vi.setSystemTime(firstTs + 5000)
trackUsage()
expect(usage.value?.firstUsed).toBe(firstTs)
} finally {
vi.useRealTimers()
}
})
it('updates lastUsed on each use', async () => {
vi.useFakeTimers()
try {
const { useFeatureUsageTracker } =
await import('./useFeatureUsageTracker')
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
trackUsage()
const firstLastUsed = usage.value?.lastUsed ?? 0
vi.advanceTimersByTime(10)
trackUsage()
expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed)
} finally {
vi.useRealTimers()
}
})
it('reset clears feature data', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount, trackUsage, reset } =
useFeatureUsageTracker('test-feature')
trackUsage()
trackUsage()
expect(useCount.value).toBe(2)
reset()
expect(useCount.value).toBe(0)
})
it('tracks multiple features independently', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const featureA = useFeatureUsageTracker('feature-a')
const featureB = useFeatureUsageTracker('feature-b')
featureA.trackUsage()
featureA.trackUsage()
featureB.trackUsage()
expect(featureA.useCount.value).toBe(2)
expect(featureB.useCount.value).toBe(1)
})
it('persists to localStorage', async () => {
vi.useFakeTimers()
try {
const { useFeatureUsageTracker } =
await import('./useFeatureUsageTracker')
const { trackUsage } = useFeatureUsageTracker('persisted-feature')
trackUsage()
await vi.runAllTimersAsync()
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')
expect(stored['persisted-feature']?.useCount).toBe(1)
} finally {
vi.useRealTimers()
}
})
it('loads existing data from localStorage', async () => {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
'existing-feature': { useCount: 5, firstUsed: 1000, lastUsed: 2000 }
})
)
vi.resetModules()
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { useCount } = useFeatureUsageTracker('existing-feature')
expect(useCount.value).toBe(5)
})
})

View File

@@ -0,0 +1,46 @@
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
interface FeatureUsage {
useCount: number
firstUsed: number
lastUsed: number
}
type FeatureUsageRecord = Record<string, FeatureUsage>
const STORAGE_KEY = 'Comfy.FeatureUsage'
/**
* Tracks feature usage for survey eligibility.
* Persists to localStorage.
*/
export function useFeatureUsageTracker(featureId: string) {
const usageData = useStorage<FeatureUsageRecord>(STORAGE_KEY, {})
const usage = computed(() => usageData.value[featureId])
const useCount = computed(() => usage.value?.useCount ?? 0)
function trackUsage() {
const now = Date.now()
const existing = usageData.value[featureId]
usageData.value[featureId] = {
useCount: (existing?.useCount ?? 0) + 1,
firstUsed: existing?.firstUsed ?? now,
lastUsed: now
}
}
function reset() {
delete usageData.value[featureId]
}
return {
usage,
useCount,
trackUsage,
reset
}
}

View File

@@ -482,18 +482,30 @@ const lgraphNode = computed(() => {
const showAdvancedInputsButton = computed(() => {
const node = lgraphNode.value
if (!node || !(node instanceof SubgraphNode)) return false
if (!node) return false
// Check if there are hidden inputs (widgets not promoted)
const interiorNodes = node.subgraph.nodes
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
// For subgraph nodes: check for unpromoted widgets
if (node instanceof SubgraphNode) {
const interiorNodes = node.subgraph.nodes
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
}
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)
return hasAdvancedWidgets && !node.showAdvanced
})
function handleShowAdvancedInputs() {
const rightSidePanelStore = useRightSidePanelStore()
rightSidePanelStore.focusSection('advanced-inputs')
const node = lgraphNode.value
if (!node) return
if (node instanceof SubgraphNode) {
const rightSidePanelStore = useRightSidePanelStore()
rightSidePanelStore.focusSection('advanced-inputs')
} else {
node.showAdvanced = true
}
}
const nodeMedia = computed(() => {

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import RadioButton from 'primevue/radiobutton'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ControlOptions } from '@/types/simplifiedWidget'
@@ -14,14 +13,8 @@ type ControlOption = {
title: string
}
const popover = ref()
const settingStore = useSettingStore()
const toggle = (event: Event) => {
popover.value.toggle(event)
}
defineExpose({ toggle })
const controlOptions: ControlOption[] = [
{
mode: 'fixed',
@@ -57,70 +50,63 @@ const controlMode = defineModel<ControlOptions>()
</script>
<template>
<Popover
ref="popover"
class="bg-interface-panel-surface border border-interface-stroke rounded-lg"
>
<div class="w-113 max-w-md p-4 space-y-4">
<div class="text-sm text-muted-foreground leading-tight">
{{ $t('widgets.valueControl.header.prefix') }}
<span class="text-base-foreground font-medium">
{{
widgetControlMode === 'before'
? $t('widgets.valueControl.header.before')
: $t('widgets.valueControl.header.after')
}}
</span>
{{ $t('widgets.valueControl.header.postfix') }}
</div>
<div class="w-113 max-w-md p-4 space-y-4">
<div class="text-sm text-muted-foreground leading-tight">
{{ $t('widgets.valueControl.header.prefix') }}
<span class="text-base-foreground font-medium">
{{
widgetControlMode === 'before'
? $t('widgets.valueControl.header.before')
: $t('widgets.valueControl.header.after')
}}
</span>
{{ $t('widgets.valueControl.header.postfix') }}
</div>
<div class="space-y-2">
<div
v-for="option in controlOptions"
:key="option.mode"
class="flex items-center justify-between py-2 gap-7"
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<div
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
<div class="space-y-2">
<div
v-for="option in controlOptions"
:key="option.mode"
class="flex items-center justify-between py-2 gap-7"
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<div
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
>
<i
v-if="option.icon"
:class="option.icon"
class="text-base text-base-foreground"
/>
<span
v-if="option.text"
class="text-xs font-normal text-base-foreground"
>
<i
v-if="option.icon"
:class="option.icon"
class="text-base text-base-foreground"
/>
<span
v-if="option.text"
class="text-xs font-normal text-base-foreground"
>
{{ option.text }}
</span>
</div>
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<div
class="text-sm font-normal text-base-foreground leading-tight"
>
<span>
{{ $t(`widgets.valueControl.${option.title}`) }}
</span>
</div>
<div
class="text-sm font-normal text-muted-foreground leading-tight"
>
{{ $t(`widgets.valueControl.${option.description}`) }}
</div>
</div>
{{ option.text }}
</span>
</div>
<RadioButton
v-model="controlMode"
class="flex-shrink-0"
:input-id="option.mode"
:value="option.mode"
/>
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<div class="text-sm font-normal text-base-foreground leading-tight">
<span>
{{ $t(`widgets.valueControl.${option.title}`) }}
</span>
</div>
<div
class="text-sm font-normal text-muted-foreground leading-tight"
>
{{ $t(`widgets.valueControl.${option.description}`) }}
</div>
</div>
</div>
<RadioButton
v-model="controlMode"
class="flex-shrink-0"
:input-id="option.mode"
:value="option.mode"
/>
</div>
</div>
</Popover>
</div>
</template>

View File

@@ -24,13 +24,5 @@ onMounted(() => {
})
</script>
<template>
<div
ref="domEl"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
@mousedown.stop
@mousemove.stop
@mouseup.stop
/>
<div ref="domEl" @pointerdown.stop @pointermove.stop @pointerup.stop />
</template>

View File

@@ -3,6 +3,7 @@ import { onClickOutside } from '@vueuse/core'
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
@@ -65,7 +66,6 @@ function updateValue(e: UIEvent) {
textEdit.value = false
}
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
const canDecrement = computed(
() =>
modelValue.value > filteredProps.value.min &&
@@ -205,16 +205,17 @@ const sliderWidth = computed(() => {
class="bg-primary-background/15 absolute left-0 bottom-0 h-full rounded-lg pointer-events-none"
:style="{ width: `${sliderWidth}%` }"
/>
<button
<Button
v-if="!buttonsDisabled"
data-testid="decrement"
:class="
cn(sharedButtonClass, 'pi pi-minus', !canDecrement && 'opacity-60')
"
class="h-full w-8 rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
variant="muted-textonly"
:disabled="!canDecrement"
tabindex="-1"
@click="modelValue -= stepValue"
/>
>
<i class="pi pi-minus" />
</Button>
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
<input
ref="inputField"
@@ -262,16 +263,17 @@ const sliderWidth = computed(() => {
</div>
<slot />
<button
<Button
v-if="!buttonsDisabled"
data-testid="increment"
:class="
cn(sharedButtonClass, 'pi pi-plus', !canIncrement && 'opacity-60')
"
class="h-full w-8 rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
variant="muted-textonly"
:disabled="!canIncrement"
tabindex="-1"
@click="modelValue += stepValue"
/>
>
<i class="pi pi-plus" />
</Button>
</div>
</WidgetLayoutField>
</template>

View File

@@ -2,6 +2,7 @@
import { computed, defineAsyncComponent, ref, watch } from 'vue'
import type { Component } from 'vue'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import type {
SimplifiedControlWidget,
@@ -19,8 +20,6 @@ const props = defineProps<{
const modelValue = defineModel<T>()
const popover = ref()
const controlModel = ref(props.widget.controlWidget.value)
const controlButtonIcon = computed(() => {
@@ -37,24 +36,24 @@ const controlButtonIcon = computed(() => {
})
watch(controlModel, props.widget.controlWidget.update)
const togglePopover = (event: Event) => {
popover.value.toggle(event)
}
</script>
<template>
<div class="relative grid grid-cols-subgrid">
<component :is="component" v-bind="$attrs" v-model="modelValue" :widget>
<Button
variant="textonly"
size="sm"
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
@click.stop.prevent="togglePopover"
>
<i :class="`${controlButtonIcon} text-blue-100 text-xs size-3.5`" />
</Button>
<Popover>
<template #button>
<Button
variant="textonly"
size="sm"
class="h-4 w-7 p-0 self-center rounded-xl bg-primary-background/30 hover:bg-primary-background-hover/30"
>
<i
:class="`${controlButtonIcon} text-primary-background text-xs w-full`"
/>
</Button>
</template>
<ValueControlPopover v-model="controlModel" />
</Popover>
</component>
<ValueControlPopover ref="popover" v-model="controlModel" />
</div>
</template>

View File

@@ -13,6 +13,9 @@ export type PromptId = z.infer<typeof zPromptId>
export const resultItemType = z.enum(['input', 'output', 'temp'])
export type ResultItemType = z.infer<typeof resultItemType>
const zCustomNodesI18n = z.record(z.string(), z.unknown())
export type CustomNodesI18n = z.infer<typeof zCustomNodesI18n>
const zResultItem = z.object({
filename: z.string().optional(),
subfolder: z.string().optional(),

View File

@@ -22,6 +22,7 @@ import type {
} from '@/platform/workflow/validation/schemas/workflowSchema'
import type {
AssetDownloadWsMessage,
CustomNodesI18n,
EmbeddingsResponse,
ExecutedWsMessage,
ExecutingWsMessage,
@@ -35,6 +36,7 @@ import type {
LogsRawResponse,
LogsWsMessage,
NotificationWsMessage,
PreviewMethod,
ProgressStateWsMessage,
ProgressTextWsMessage,
ProgressWsMessage,
@@ -44,8 +46,7 @@ import type {
StatusWsMessageStatus,
SystemStats,
User,
UserDataFullInfo,
PreviewMethod
UserDataFullInfo
} from '@/schemas/apiSchema'
import type {
JobDetail,
@@ -951,7 +952,7 @@ export class ComfyApi extends EventTarget {
* @param {*} type The endpoint to post to
* @param {*} body Optional POST data
*/
async #postItem(type: string, body: any) {
async #postItem(type: string, body?: Record<string, unknown>) {
try {
await this.fetchApi('/' + type, {
method: 'POST',
@@ -1074,7 +1075,7 @@ export class ComfyApi extends EventTarget {
*/
async storeUserData(
file: string,
data: any,
data: unknown,
options: RequestInit & {
overwrite?: boolean
stringify?: boolean
@@ -1091,7 +1092,7 @@ export class ComfyApi extends EventTarget {
`/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}&full_info=${options.full_info}`,
{
method: 'POST',
body: options?.stringify ? JSON.stringify(data) : data,
body: options?.stringify ? JSON.stringify(data) : (data as BodyInit),
...options
}
)
@@ -1251,7 +1252,7 @@ export class ComfyApi extends EventTarget {
*
* @returns The custom nodes i18n data
*/
async getCustomNodesI18n(): Promise<Record<string, any>> {
async getCustomNodesI18n(): Promise<CustomNodesI18n> {
return (await axios.get(this.apiURL('/i18n'))).data
}

View File

@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
import * as jsondiffpatch from 'jsondiffpatch'
import log from 'loglevel'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
import {
ComfyWorkflow,
@@ -40,7 +41,7 @@ export class ChangeTracker {
_restoringState: boolean = false
ds?: { scale: number; offset: [number, number] }
nodeOutputs?: Record<string, any>
nodeOutputs?: Record<string, ExecutedWsMessage['output']>
private subgraphState?: {
navigation: string[]
@@ -303,11 +304,11 @@ export class ChangeTracker {
const prompt = LGraphCanvas.prototype.prompt
LGraphCanvas.prototype.prompt = function (
title: string,
value: any,
callback: (v: any) => void,
event: any
value: string | number,
callback: (v: string) => void,
event: CanvasPointerEvent
) {
const extendedCallback = (v: any) => {
const extendedCallback = (v: string) => {
callback(v)
checkState()
}

View File

@@ -1,28 +1,28 @@
import { $el } from '../../ui'
import { ComfyDialog } from '../dialog'
export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
// @ts-expect-error fixme ts strict error
#resolve: (value: any) => void
type DialogAction<T> = string | { value?: T; text: string }
constructor(actions?: Array<string | { value?: any; text: string }>) {
export class ComfyAsyncDialog<
T = string | null
> extends ComfyDialog<HTMLDialogElement> {
#resolve: (value: T | null) => void = () => {}
constructor(actions?: Array<DialogAction<T>>) {
super(
'dialog.comfy-dialog.comfyui-dialog',
// @ts-expect-error fixme ts strict error
actions?.map((opt) => {
if (typeof opt === 'string') {
opt = { text: opt }
}
const action = typeof opt === 'string' ? { text: opt } : opt
return $el('button.comfyui-button', {
type: 'button',
textContent: opt.text,
onclick: () => this.close(opt.value ?? opt.text)
})
textContent: action.text,
onclick: () => this.close((action.value ?? action.text) as T)
}) as HTMLButtonElement
})
)
}
override show(html: string | HTMLElement | HTMLElement[]) {
override show(html: string | HTMLElement | HTMLElement[]): Promise<T | null> {
this.element.addEventListener('close', () => {
this.close()
})
@@ -34,7 +34,7 @@ export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
})
}
showModal(html: string | HTMLElement | HTMLElement[]) {
showModal(html: string | HTMLElement | HTMLElement[]): Promise<T | null> {
this.element.addEventListener('close', () => {
this.close()
})
@@ -47,22 +47,22 @@ export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
})
}
override close(result = null) {
override close(result: T | null = null) {
this.#resolve(result)
this.element.close()
super.close()
}
static async prompt({
static async prompt<U = string>({
title = null,
message,
actions
}: {
title: string | null
message: string
actions: Array<string | { value?: any; text: string }>
}) {
const dialog = new ComfyAsyncDialog(actions)
actions: Array<DialogAction<U>>
}): Promise<U | null> {
const dialog = new ComfyAsyncDialog<U>(actions)
const content = [$el('span', message)]
if (title) {
content.unshift($el('h3', title))

View File

@@ -4,11 +4,10 @@ export class ComfyDialog<
T extends HTMLElement = HTMLElement
> extends EventTarget {
element: T
// @ts-expect-error fixme ts strict error
textElement: HTMLElement
textElement!: HTMLElement
#buttons: HTMLButtonElement[] | null
constructor(type = 'div', buttons = null) {
constructor(type = 'div', buttons: HTMLButtonElement[] | null = null) {
super()
this.#buttons = buttons
this.element = $el(type + '.comfy-modal', { parent: document.body }, [
@@ -35,8 +34,7 @@ export class ComfyDialog<
this.element.style.display = 'none'
}
// @ts-expect-error fixme ts strict error
show(html) {
show(html: string | HTMLElement | HTMLElement[]): void {
if (typeof html === 'string') {
this.textElement.innerHTML = html
} else {

View File

@@ -51,6 +51,14 @@ const DISTRIBUTION: 'desktop' | 'localhost' | 'cloud' =
? 'cloud'
: 'localhost'
// Nightly builds are from main branch; RC/stable builds are from core/* branches
// Can be overridden via IS_NIGHTLY env var for testing
const IS_NIGHTLY =
process.env.IS_NIGHTLY === 'true' ||
(process.env.IS_NIGHTLY !== 'false' &&
process.env.CI === 'true' &&
process.env.GITHUB_REF_NAME === 'main')
// Disable Vue DevTools for production cloud distribution
const DISABLE_VUE_PLUGINS =
process.env.DISABLE_VUE_PLUGINS === 'true' ||
@@ -412,77 +420,83 @@ export default defineConfig({
],
build: {
minify: SHOULD_MINIFY ? 'esbuild' : false,
minify: SHOULD_MINIFY,
target: 'es2022',
sourcemap: GENERATE_SOURCEMAP,
rollupOptions: {
treeshake: true,
output: {
manualChunks: (id) => {
if (!id.includes('node_modules')) {
return undefined
}
if (id.includes('primevue') || id.includes('@primeuix')) {
return 'vendor-primevue'
}
if (id.includes('@tiptap')) {
return 'vendor-tiptap'
}
if (id.includes('chart.js')) {
return 'vendor-chart'
}
if (id.includes('three') || id.includes('@sparkjsdev')) {
return 'vendor-three'
}
if (id.includes('@xterm')) {
return 'vendor-xterm'
}
if (id.includes('/vue') || id.includes('pinia')) {
return 'vendor-vue'
}
if (id.includes('reka-ui')) {
return 'vendor-reka-ui'
}
return 'vendor-other'
}
}
}
},
esbuild: {
minifyIdentifiers: SHOULD_MINIFY,
keepNames: true,
minifySyntax: SHOULD_MINIFY,
minifyWhitespace: SHOULD_MINIFY,
pure: SHOULD_MINIFY
? [
'console.log',
rolldownOptions: {
treeshake: {
manualPureFunctions: [
'console.clear',
'console.count',
'console.countReset',
'console.debug',
'console.info',
'console.trace',
'console.dir',
'console.dirxml',
'console.group',
'console.groupCollapsed',
'console.groupEnd',
'console.info',
'console.log',
'console.profile',
'console.profileEnd',
'console.table',
'console.time',
'console.timeEnd',
'console.timeLog',
'console.count',
'console.countReset',
'console.profile',
'console.profileEnd',
'console.clear'
'console.trace'
]
: []
},
experimental: {
strictExecutionOrder: true
},
output: {
keepNames: true,
codeSplitting: {
groups: [
{
name: 'vendor-primevue',
test: /[\\/]node_modules[\\/](@?primevue|@primeuix)[\\/]/,
priority: 10
},
{
name: 'vendor-tiptap',
test: /[\\/]node_modules[\\/]@tiptap[\\/]/,
priority: 10
},
{
name: 'vendor-chart',
test: /[\\/]node_modules[\\/]chart\.js[\\/]/,
priority: 10
},
{
name: 'vendor-three',
test: /[\\/]node_modules[\\/](three|@sparkjsdev)[\\/]/,
priority: 10
},
{
name: 'vendor-xterm',
test: /[\\/]node_modules[\\/]@xterm[\\/]/,
priority: 10
},
{
name: 'vendor-vue',
test: /[\\/]node_modules[\\/](vue|pinia)[\\/]/,
priority: 10
},
{
name: 'vendor-reka-ui',
test: /[\\/]node_modules[\\/]reka-ui[\\/]/,
priority: 10
},
{
name: 'vendor-other',
test: /[\\/]node_modules[\\/]/,
priority: 0
}
]
}
}
}
},
define: {
@@ -496,7 +510,8 @@ export default defineConfig({
__ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''),
__ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''),
__USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true',
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION)
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION),
__IS_NIGHTLY__: JSON.stringify(IS_NIGHTLY)
},
resolve: {

View File

@@ -46,6 +46,7 @@ globalThis.__ALGOLIA_APP_ID__ = ''
globalThis.__ALGOLIA_API_KEY__ = ''
globalThis.__USE_PROD_CONFIG__ = false
globalThis.__DISTRIBUTION__ = 'localhost'
globalThis.__IS_NIGHTLY__ = false
// Define runtime config for tests
window.__CONFIG__ = {