diff --git a/.storybook/main.ts b/.storybook/main.ts index 5b7c126e9d..90fbb138a2 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -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 ( diff --git a/apps/desktop-ui/vite.config.mts b/apps/desktop-ui/vite.config.mts index 7cbc5307dc..771f0f85b8 100644 --- a/apps/desktop-ui/vite.config.mts +++ b/apps/desktop-ui/vite.config.mts @@ -64,7 +64,7 @@ export default defineConfig(() => { }) ], build: { - minify: SHOULD_MINIFY ? ('esbuild' as const) : false, + minify: SHOULD_MINIFY, target: 'es2022', sourcemap: true } diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png index 8ccc65f98f..200f932902 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png index 434c540e1d..19a5538ed8 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png index 6dc2d66372..86a1dac87d 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png index 1fec6b2592..dc312b134b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png differ diff --git a/package.json b/package.json index d0e8b7f85d..72eb659c34 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/vite-define-shim.ts b/scripts/vite-define-shim.ts index e12b126f32..970d09aef0 100644 --- a/scripts/vite-define-shim.ts +++ b/scripts/vite-define-shim.ts @@ -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 } @@ -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 diff --git a/src/components/common/TreeExplorerTreeNode.vue b/src/components/common/TreeExplorerTreeNode.vue index cea8ba451f..450e8d9d99 100644 --- a/src/components/common/TreeExplorerTreeNode.vue +++ b/src/components/common/TreeExplorerTreeNode.vue @@ -28,7 +28,7 @@ />
diff --git a/src/components/rightSidePanel/parameters/TabNormalInputs.vue b/src/components/rightSidePanel/parameters/TabNormalInputs.vue index 30689f1a41..add1288763 100644 --- a/src/components/rightSidePanel/parameters/TabNormalInputs.vue +++ b/src/components/rightSidePanel/parameters/TabNormalInputs.vue @@ -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 +}) diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index a7af7361a3..db543c30a0 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -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 diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 0e91af317f..38bddb129e 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -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, + source: Record +): Record { + for (const key in source) { + const sv = source[key] + if (typeof sv === 'object' && sv !== null) { + let tv = target[key] as Record | undefined + if (!tv) { + tv = target[key] = {} } + merge(tv, sv as Record) + } else { + target[key] = sv } } @@ -34,8 +38,7 @@ function merge(target, source) { } export class ManageGroupDialog extends ComfyDialog { - // @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 { > > > = {} - // @ts-expect-error fixme ts strict error - nodeItems: any[] + nodeItems!: HTMLLIElement[] app: ComfyApp - // @ts-expect-error fixme ts strict error - groupNodeType: LGraphNodeConstructor - groupNodeDef: any - groupData: any + groupNodeType!: LGraphNodeConstructor + 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 { }) 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 { this.groupNodeType = LiteGraph.registered_node_types[ `${PREFIX}${SEPARATOR}` + this.selectedGroup ] as unknown as LGraphNodeConstructor - 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 { 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).nodes ??= + {}) as Record>> 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 { } } - // @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 | undefined + )?.nodes as + | Record< + number, + Record> + > + | 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 { 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 { 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 { 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 { 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 { $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 { `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 { 'button.comfy-btn', { onclick: async () => { - let nodesByType - let recreateNodes = [] - const types = {} + type NodesByType = Record + let nodesByType: NodesByType | undefined + const recreateNodes: LGraphNode[] = [] + const types: Record = {} 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> + > + 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 = + {} 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, + nodeMods as Record + ) } - // @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( + (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 { 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] ) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 401de111bc..49ad351010 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -102,16 +102,23 @@ export interface LGraphConfig { links_ontop?: boolean } +export interface GroupNodeConfigEntry { + input?: Record + output?: Record +} + export interface GroupNodeWorkflowData { external: (number | string)[][] links: SerialisedLLinkArray[] nodes: { index?: number type?: string + title?: string inputs?: unknown[] outputs?: unknown[] + widgets_values?: unknown[] }[] - config?: Record + config?: Record } export interface LGraphExtra extends Dictionary { diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 30d7a3fd69..40893174f8 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher }) function inner_clicked( - this: ContextMenu, + this: ContextMenuDivElement, v?: string | IContextMenuValue ) { 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] }) diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index 0b24eb47b8..59d62a84fc 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -104,6 +104,8 @@ export type { } from './interfaces' export { LGraph, + type GroupNodeConfigEntry, + type GroupNodeWorkflowData, type LGraphTriggerAction, type LGraphTriggerParam } from './LGraph' diff --git a/src/platform/assets/components/MediaAssetFilterBar.vue b/src/platform/assets/components/MediaAssetFilterBar.vue index 6a0fcdb939..6e4ec5e4c1 100644 --- a/src/platform/assets/components/MediaAssetFilterBar.vue +++ b/src/platform/assets/components/MediaAssetFilterBar.vue @@ -1,40 +1,42 @@ diff --git a/src/platform/distribution/types.ts b/src/platform/distribution/types.ts index d03e6d53b8..53ba95beaf 100644 --- a/src/platform/distribution/types.ts +++ b/src/platform/distribution/types.ts @@ -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__ diff --git a/src/platform/surveys/useFeatureUsageTracker.test.ts b/src/platform/surveys/useFeatureUsageTracker.test.ts new file mode 100644 index 0000000000..5313e9eafd --- /dev/null +++ b/src/platform/surveys/useFeatureUsageTracker.test.ts @@ -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) + }) +}) diff --git a/src/platform/surveys/useFeatureUsageTracker.ts b/src/platform/surveys/useFeatureUsageTracker.ts new file mode 100644 index 0000000000..b8d825db48 --- /dev/null +++ b/src/platform/surveys/useFeatureUsageTracker.ts @@ -0,0 +1,46 @@ +import { useStorage } from '@vueuse/core' +import { computed } from 'vue' + +interface FeatureUsage { + useCount: number + firstUsed: number + lastUsed: number +} + +type FeatureUsageRecord = Record + +const STORAGE_KEY = 'Comfy.FeatureUsage' + +/** + * Tracks feature usage for survey eligibility. + * Persists to localStorage. + */ +export function useFeatureUsageTracker(featureId: string) { + const usageData = useStorage(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 + } +} diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index b990c957a1..1d96cda830 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -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(() => { diff --git a/src/renderer/extensions/vueNodes/widgets/components/ValueControlPopover.vue b/src/renderer/extensions/vueNodes/widgets/components/ValueControlPopover.vue index 095d64aba0..ecfba4592c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/ValueControlPopover.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/ValueControlPopover.vue @@ -1,7 +1,6 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue index e701489a00..76fb5a560e 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue @@ -24,13 +24,5 @@ onMounted(() => { }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue index d14dd2167a..5a201779b2 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue @@ -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}%` }" /> -
{
- diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue index f571d6dade..e4e1220bf6 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue @@ -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() -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) -} - diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 64b0b5208f..37833cf2fd 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -13,6 +13,9 @@ export type PromptId = z.infer export const resultItemType = z.enum(['input', 'output', 'temp']) export type ResultItemType = z.infer +const zCustomNodesI18n = z.record(z.string(), z.unknown()) +export type CustomNodesI18n = z.infer + const zResultItem = z.object({ filename: z.string().optional(), subfolder: z.string().optional(), diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 67f98826e0..41310b0bde 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -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) { 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> { + async getCustomNodesI18n(): Promise { return (await axios.get(this.apiURL('/i18n'))).data } diff --git a/src/scripts/changeTracker.ts b/src/scripts/changeTracker.ts index fa09ad49af..4db14677aa 100644 --- a/src/scripts/changeTracker.ts +++ b/src/scripts/changeTracker.ts @@ -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 + nodeOutputs?: Record 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() } diff --git a/src/scripts/ui/components/asyncDialog.ts b/src/scripts/ui/components/asyncDialog.ts index 1da1a5a4ef..d1f45a9dea 100644 --- a/src/scripts/ui/components/asyncDialog.ts +++ b/src/scripts/ui/components/asyncDialog.ts @@ -1,28 +1,28 @@ import { $el } from '../../ui' import { ComfyDialog } from '../dialog' -export class ComfyAsyncDialog extends ComfyDialog { - // @ts-expect-error fixme ts strict error - #resolve: (value: any) => void +type DialogAction = string | { value?: T; text: string } - constructor(actions?: Array) { +export class ComfyAsyncDialog< + T = string | null +> extends ComfyDialog { + #resolve: (value: T | null) => void = () => {} + + constructor(actions?: Array>) { 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 { this.element.addEventListener('close', () => { this.close() }) @@ -34,7 +34,7 @@ export class ComfyAsyncDialog extends ComfyDialog { }) } - showModal(html: string | HTMLElement | HTMLElement[]) { + showModal(html: string | HTMLElement | HTMLElement[]): Promise { this.element.addEventListener('close', () => { this.close() }) @@ -47,22 +47,22 @@ export class ComfyAsyncDialog extends ComfyDialog { }) } - override close(result = null) { + override close(result: T | null = null) { this.#resolve(result) this.element.close() super.close() } - static async prompt({ + static async prompt({ title = null, message, actions }: { title: string | null message: string - actions: Array - }) { - const dialog = new ComfyAsyncDialog(actions) + actions: Array> + }): Promise { + const dialog = new ComfyAsyncDialog(actions) const content = [$el('span', message)] if (title) { content.unshift($el('h3', title)) diff --git a/src/scripts/ui/dialog.ts b/src/scripts/ui/dialog.ts index 23d43c2bd3..12ca4a2e55 100644 --- a/src/scripts/ui/dialog.ts +++ b/src/scripts/ui/dialog.ts @@ -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 { diff --git a/vite.config.mts b/vite.config.mts index abfce8f35d..d01be687e6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -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: { diff --git a/vitest.setup.ts b/vitest.setup.ts index fca677557c..81c39723a5 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -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__ = {