Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39eeda8430 | ||
|
|
2878952b1d | ||
|
|
223a1f677b | ||
|
|
7b4b40db5b | ||
|
|
1052603a17 | ||
|
|
4ee1b23e9b | ||
|
|
326e0748c0 | ||
|
|
ea0f74a9f6 | ||
|
|
cdaac0d9bb | ||
|
|
f749734863 | ||
|
|
a15c4d1612 | ||
|
|
290bf52fc5 | ||
|
|
529e889d0e | ||
|
|
5a5a69de17 | ||
|
|
194549a4b0 | ||
|
|
4052fc55f3 | ||
|
|
82d03b5c1b | ||
|
|
c7f123766e | ||
|
|
88acabb355 | ||
|
|
e5f1eb8609 | ||
|
|
eb7ab0860d | ||
|
|
9ed3545b95 | ||
|
|
d223f3865b | ||
|
|
4538db86cf | ||
|
|
3931cae044 | ||
|
|
810a63f808 | ||
|
|
609984d400 | ||
|
|
a57c958058 | ||
|
|
b6dbe8f07b | ||
|
|
29d69338ef | ||
|
|
98de010811 | ||
|
|
63302a6634 | ||
|
|
8568e037bf | ||
|
|
6c4143ca94 |
@@ -4,6 +4,8 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import * as fs from 'fs'
|
||||
import { NodeBadgeMode } from '../src/types/nodeSource'
|
||||
import { NodeId } from '../src/types/comfyWorkflow'
|
||||
import { ManageGroupNode } from './helpers/manageGroupNode'
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
@@ -15,9 +17,37 @@ interface Size {
|
||||
height: number
|
||||
}
|
||||
|
||||
class ComfyNodeSearchFilterSelectionPanel {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
async selectFilterType(filterType: string) {
|
||||
await this.page
|
||||
.locator(
|
||||
`.filter-type-select .p-togglebutton-label:has-text("${filterType}")`
|
||||
)
|
||||
.click()
|
||||
}
|
||||
|
||||
async selectFilterValue(filterValue: string) {
|
||||
await this.page.locator('.filter-value-select .p-select-dropdown').click()
|
||||
await this.page
|
||||
.locator(
|
||||
`.p-select-overlay .p-select-list .p-select-option-label:text-is("${filterValue}")`
|
||||
)
|
||||
.click()
|
||||
}
|
||||
|
||||
async addFilter(filterValue: string, filterType: string) {
|
||||
await this.selectFilterType(filterType)
|
||||
await this.selectFilterValue(filterValue)
|
||||
await this.page.locator('.p-button-label:has-text("Add")').click()
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyNodeSearchBox {
|
||||
public readonly input: Locator
|
||||
public readonly dropdown: Locator
|
||||
public readonly filterSelectionPanel: ComfyNodeSearchFilterSelectionPanel
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.input = page.locator(
|
||||
@@ -26,6 +56,11 @@ class ComfyNodeSearchBox {
|
||||
this.dropdown = page.locator(
|
||||
'.comfy-vue-node-search-container .p-autocomplete-list'
|
||||
)
|
||||
this.filterSelectionPanel = new ComfyNodeSearchFilterSelectionPanel(page)
|
||||
}
|
||||
|
||||
get filterButton() {
|
||||
return this.page.locator('.comfy-vue-node-search-container ._filter-button')
|
||||
}
|
||||
|
||||
async fillAndSelectFirstNode(
|
||||
@@ -43,6 +78,21 @@ class ComfyNodeSearchBox {
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
.click()
|
||||
}
|
||||
|
||||
async addFilter(filterValue: string, filterType: string) {
|
||||
await this.filterButton.click()
|
||||
await this.filterSelectionPanel.addFilter(filterValue, filterType)
|
||||
}
|
||||
|
||||
get filterChips() {
|
||||
return this.page.locator(
|
||||
'.comfy-vue-node-search-container .p-autocomplete-chip-item'
|
||||
)
|
||||
}
|
||||
|
||||
async removeFilter(index: number) {
|
||||
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
|
||||
}
|
||||
}
|
||||
|
||||
class NodeLibrarySidebarTab {
|
||||
@@ -659,6 +709,177 @@ export class ComfyPage {
|
||||
await this.page.getByText('Convert to Group Node').click()
|
||||
await this.nextFrame()
|
||||
}
|
||||
async convertOffsetToCanvas(pos: [number, number]) {
|
||||
return this.page.evaluate((pos) => {
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
|
||||
}, pos)
|
||||
}
|
||||
async getNodeRefById(id: NodeId) {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
|
||||
return (
|
||||
await this.page.evaluate((type) => {
|
||||
return window['app'].graph._nodes
|
||||
.filter((n) => n.type === type)
|
||||
.map((n) => n.id)
|
||||
}, type)
|
||||
).map((id: NodeId) => this.getNodeRefById(id))
|
||||
}
|
||||
}
|
||||
class NodeSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
readonly index: number,
|
||||
readonly node: NodeReference
|
||||
) {}
|
||||
async getPosition() {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(
|
||||
node.getConnectionPos(type === 'input', index)
|
||||
)
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
async getLinkCount() {
|
||||
return await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
return node.inputs[index].link == null ? 0 : 1
|
||||
}
|
||||
return node.outputs[index].links?.length ?? 0
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
}
|
||||
async removeLinks() {
|
||||
await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
node.disconnectInput(index)
|
||||
} else {
|
||||
node.disconnectOutput(index)
|
||||
}
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
}
|
||||
}
|
||||
class NodeReference {
|
||||
constructor(
|
||||
readonly id: NodeId,
|
||||
readonly comfyPage: ComfyPage
|
||||
) {}
|
||||
async exists(): Promise<boolean> {
|
||||
return await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return !!node
|
||||
}, this.id)
|
||||
}
|
||||
getType(): Promise<string> {
|
||||
return this.getProperty('type')
|
||||
}
|
||||
async getPosition(): Promise<Position> {
|
||||
const pos = await this.comfyPage.convertOffsetToCanvas(
|
||||
await this.getProperty<[number, number]>('pos')
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
async getSize(): Promise<Size> {
|
||||
const size = await this.getProperty<[number, number]>('size')
|
||||
return {
|
||||
width: size[0],
|
||||
height: size[1]
|
||||
}
|
||||
}
|
||||
async getProperty<T>(prop: string): Promise<T> {
|
||||
return await this.comfyPage.page.evaluate(
|
||||
([id, prop]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error('Node not found')
|
||||
return node[prop]
|
||||
},
|
||||
[this.id, prop] as const
|
||||
)
|
||||
}
|
||||
async getOutput(index: number) {
|
||||
return new NodeSlotReference('output', index, this)
|
||||
}
|
||||
async getInput(index: number) {
|
||||
return new NodeSlotReference('input', index, this)
|
||||
}
|
||||
async click(position: 'title', options?: Parameters<Page['click']>[1]) {
|
||||
const nodePos = await this.getPosition()
|
||||
const nodeSize = await this.getSize()
|
||||
let clickPos: Position
|
||||
switch (position) {
|
||||
case 'title':
|
||||
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y + 15 }
|
||||
break
|
||||
default:
|
||||
throw new Error(`Invalid click position ${position}`)
|
||||
}
|
||||
await this.comfyPage.canvas.click({
|
||||
...options,
|
||||
position: clickPos
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
async connectOutput(
|
||||
originSlotIndex: number,
|
||||
targetNode: NodeReference,
|
||||
targetSlotIndex: number
|
||||
) {
|
||||
const originSlot = await this.getOutput(originSlotIndex)
|
||||
const targetSlot = await targetNode.getInput(targetSlotIndex)
|
||||
await this.comfyPage.dragAndDrop(
|
||||
await originSlot.getPosition(),
|
||||
await targetSlot.getPosition()
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
async clickContextMenuOption(optionText: string) {
|
||||
await this.click('title', { button: 'right' })
|
||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||
await ctx.getByText(optionText).click()
|
||||
}
|
||||
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
|
||||
this.comfyPage.page.once('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
})
|
||||
await this.clickContextMenuOption('Convert to Group Node')
|
||||
await this.comfyPage.nextFrame()
|
||||
const nodes = await this.comfyPage.getNodeRefsByType(
|
||||
`workflow/${groupNodeName}`
|
||||
)
|
||||
if (nodes.length !== 1) {
|
||||
throw new Error(`Did not find single group node (found=${nodes.length})`)
|
||||
}
|
||||
return nodes[0]
|
||||
}
|
||||
async manageGroupNode() {
|
||||
await this.clickContextMenuOption('Manage Group Node')
|
||||
await this.comfyPage.nextFrame()
|
||||
return new ManageGroupNode(
|
||||
this.comfyPage.page,
|
||||
this.comfyPage.page.locator('.comfy-group-manage')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
|
||||
This document outlines the setup and usage of Playwright for testing the ComfyUI_frontend project.
|
||||
|
||||
## WARNING
|
||||
|
||||
The browser tests will change the ComfyUI backend state, such as user settings and saved workflows.
|
||||
Please backup your ComfyUI data before running the tests locally.
|
||||
|
||||
## Setup
|
||||
|
||||
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
|
||||
ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing.
|
||||
|
||||
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
|
||||
|
||||
```bash
|
||||
|
||||
62
browser_tests/assets/force_input.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "DevToolsNodeWithForceInput",
|
||||
"pos": {
|
||||
"0": 9,
|
||||
"1": 39
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "int_input",
|
||||
"type": "INT",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "int_input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "float_input",
|
||||
"type": "FLOAT",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "float_input"
|
||||
},
|
||||
"shape": 7
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithForceInput"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
57
browser_tests/assets/optional_input_correct_shape.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "DevToolsNodeWithOptionalInput",
|
||||
"pos": {
|
||||
"0": 19,
|
||||
"1": 46
|
||||
},
|
||||
"size": {
|
||||
"0": 302.4000244140625,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "required_input",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "optional_input",
|
||||
"type": "IMAGE",
|
||||
"link": null,
|
||||
"shape": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithOptionalInput"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
56
browser_tests/assets/optional_input_no_shape.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "DevToolsNodeWithOptionalInput",
|
||||
"pos": {
|
||||
"0": 19,
|
||||
"1": 46
|
||||
},
|
||||
"size": {
|
||||
"0": 302.4000244140625,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "required_input",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "optional_input",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithOptionalInput"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
57
browser_tests/assets/optional_input_wrong_shape.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "DevToolsNodeWithOptionalInput",
|
||||
"pos": {
|
||||
"0": 19,
|
||||
"1": 46
|
||||
},
|
||||
"size": {
|
||||
"0": 302.4000244140625,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "required_input",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "optional_input",
|
||||
"type": "IMAGE",
|
||||
"link": null,
|
||||
"shape": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithOptionalInput"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -135,15 +135,35 @@ test.describe('Color Palette', () => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', {})
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
})
|
||||
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
)
|
||||
// Reset to default color palette for other tests
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
|
||||
})
|
||||
|
||||
test('Can change node opacity setting', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
|
||||
// Drag mouse to force canvas to redraw
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
|
||||
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
|
||||
await comfyPage.page.mouse.move(8, 8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -53,4 +53,40 @@ test.describe('Group Node', () => {
|
||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const expectSingleNode = async (type: string) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType(type)
|
||||
expect(nodes).toHaveLength(1)
|
||||
return nodes[0]
|
||||
}
|
||||
const latent = await expectSingleNode('EmptyLatentImage')
|
||||
const sampler = await expectSingleNode('KSampler')
|
||||
// Remove existing link
|
||||
const samplerInput = await sampler.getInput(0)
|
||||
await samplerInput.removeLinks()
|
||||
// Group latent + sampler
|
||||
await latent.click('title', {
|
||||
modifiers: ['Shift']
|
||||
})
|
||||
await sampler.click('title', {
|
||||
modifiers: ['Shift']
|
||||
})
|
||||
const groupNode = await sampler.convertToGroupNode()
|
||||
// Connect node to group
|
||||
const ckpt = await expectSingleNode('CheckpointLoaderSimple')
|
||||
const input = await ckpt.connectOutput(0, groupNode, 0)
|
||||
expect(await input.getLinkCount()).toBe(1)
|
||||
// Modify the group node via manage dialog
|
||||
const manage = await groupNode.manageGroupNode()
|
||||
await manage.selectNode('KSampler')
|
||||
await manage.changeTab('Inputs')
|
||||
await manage.setLabel('model', 'test')
|
||||
await manage.save()
|
||||
await manage.close()
|
||||
// Ensure the link is still present
|
||||
expect(await input.getLinkCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
37
browser_tests/helpers/manageGroupNode.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
|
||||
constructor(
|
||||
readonly page: Page,
|
||||
readonly root: Locator
|
||||
) {
|
||||
this.footer = root.locator('footer')
|
||||
}
|
||||
|
||||
async setLabel(name: string, label: string) {
|
||||
const active = this.root.locator('.comfy-group-manage-node-page.active')
|
||||
const input = active.getByPlaceholder(name)
|
||||
await input.fill(label)
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.footer.getByText('Save').click()
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.footer.getByText('Close').click()
|
||||
}
|
||||
|
||||
async selectNode(name: string) {
|
||||
const list = this.root.locator('.comfy-group-manage-list-items')
|
||||
const item = list.getByText(name)
|
||||
await item.click()
|
||||
}
|
||||
|
||||
async changeTab(name: 'Inputs' | 'Widgets' | 'Outputs') {
|
||||
const header = this.root.locator('.comfy-group-manage-node header')
|
||||
const tab = header.getByText(name)
|
||||
await tab.click()
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ test.describe('Node Badge', () => {
|
||||
const LGraphBadge = window['LGraphBadge']
|
||||
const app = window['app'] as ComfyApp
|
||||
const graph = app.graph
|
||||
// @ts-expect-error - accessing private property
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
@@ -27,7 +26,6 @@ test.describe('Node Badge', () => {
|
||||
const LGraphBadge = window['LGraphBadge']
|
||||
const app = window['app'] as ComfyApp
|
||||
const graph = app.graph
|
||||
// @ts-expect-error - accessing private property
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
@@ -48,7 +46,6 @@ test.describe('Node Badge', () => {
|
||||
const LGraphBadge = window['LGraphBadge']
|
||||
const app = window['app'] as ComfyApp
|
||||
const graph = app.graph
|
||||
// @ts-expect-error - accessing private property
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
26
browser_tests/nodeDisplay.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
// If an input is optional by node definition, it should be shown as
|
||||
// a hollow circle no matter what shape it was defined in the workflow JSON.
|
||||
test.describe('Optional input', () => {
|
||||
test('No shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('optional_input_no_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
})
|
||||
|
||||
test('Wrong shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('optional_input_wrong_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
})
|
||||
|
||||
test('Correct shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('optional_input_correct_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
})
|
||||
|
||||
test('Force input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('force_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -103,6 +103,30 @@ test.describe('Node search box', () => {
|
||||
await comfyPage.page.waitForTimeout(256)
|
||||
await expect(comfyPage.searchBox.input).not.toHaveCount(0)
|
||||
})
|
||||
|
||||
test.describe('Filtering', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
})
|
||||
|
||||
test('Can add filter', async ({ comfyPage }) => {
|
||||
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
|
||||
await expect(comfyPage.searchBox.filterChips).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('Can add multiple filters', async ({ comfyPage }) => {
|
||||
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
|
||||
await comfyPage.searchBox.addFilter('CLIP', 'Output Type')
|
||||
await expect(comfyPage.searchBox.filterChips).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('Can remove filter', async ({ comfyPage }) => {
|
||||
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
|
||||
await comfyPage.searchBox.addFilter('CLIP', 'Output Type')
|
||||
await comfyPage.searchBox.removeFilter(0)
|
||||
await expect(comfyPage.searchBox.filterChips).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Release context menu', () => {
|
||||
|
||||
@@ -14,7 +14,14 @@ export default [
|
||||
'src/types/vue-shim.d.ts'
|
||||
]
|
||||
},
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
|
||||
1
global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare const __COMFYUI_FRONTEND_VERSION__: string
|
||||
17
index.html
@@ -4,21 +4,6 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>ComfyUI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<!-- Browser Test Fonts -->
|
||||
<!-- <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||
}
|
||||
</style> -->
|
||||
<script type="module" src="node_modules/reflect-metadata/Reflect.js"></script>
|
||||
<script type="module">
|
||||
import 'reflect-metadata';
|
||||
window["__COMFYUI_FRONTEND_VERSION__"] = __COMFYUI_FRONTEND_VERSION__;
|
||||
console.log("ComfyUI Front-end version:", __COMFYUI_FRONTEND_VERSION__);
|
||||
</script>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
</head>
|
||||
@@ -51,5 +36,7 @@
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="node_modules/reflect-metadata/Reflect.js"></script>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
246
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.57",
|
||||
"version": "1.2.62",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.57",
|
||||
"version": "1.2.62",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.75",
|
||||
"@comfyorg/litegraph": "^0.7.77",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
@@ -33,6 +33,7 @@
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@iconify/json": "^2.2.245",
|
||||
"@pinia/testing": "^0.1.5",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.6",
|
||||
@@ -63,7 +64,7 @@
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.2.0",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"zip-dir": "^2.0.0"
|
||||
@@ -1909,9 +1910,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.7.75",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.75.tgz",
|
||||
"integrity": "sha512-RNYZVMoJ/a5btwP+S124FnrIVlwOdv6uNsTdYfwv7L8teDpwvf/TQa66QfCePqUlypBKEhKw+avTncLAu2FYUw==",
|
||||
"version": "0.7.77",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.77.tgz",
|
||||
"integrity": "sha512-R86ZK/7pvqPqNw6XrCx1dn00Fj9QDrPCfDQNHL+QiQ8BP3jH6pfBQW0ZBhaX4Mj/ALqoHRZ2VB6RZaxZ7sJ5Lg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -3350,6 +3351,49 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pinia/testing": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.5.tgz",
|
||||
"integrity": "sha512-AcGzuotkzhRoF00htuxLfIPBBHVE6HjjB3YC5Y3os8vRgKu6ipknK5GBQq9+pduwYQhZ+BcCZDC9TyLAUlUpoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pinia": ">=2.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinia/testing/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -3460,9 +3504,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
|
||||
"integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.0.tgz",
|
||||
"integrity": "sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3472,9 +3516,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz",
|
||||
"integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.0.tgz",
|
||||
"integrity": "sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3484,9 +3528,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz",
|
||||
"integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.0.tgz",
|
||||
"integrity": "sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3496,9 +3540,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz",
|
||||
"integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.0.tgz",
|
||||
"integrity": "sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3508,9 +3552,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz",
|
||||
"integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.0.tgz",
|
||||
"integrity": "sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3520,9 +3564,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz",
|
||||
"integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.0.tgz",
|
||||
"integrity": "sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3532,9 +3576,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz",
|
||||
"integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.0.tgz",
|
||||
"integrity": "sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3544,9 +3588,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz",
|
||||
"integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.0.tgz",
|
||||
"integrity": "sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3556,9 +3600,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz",
|
||||
"integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.0.tgz",
|
||||
"integrity": "sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -3568,9 +3612,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz",
|
||||
"integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.0.tgz",
|
||||
"integrity": "sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -3580,9 +3624,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz",
|
||||
"integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.0.tgz",
|
||||
"integrity": "sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -3592,9 +3636,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
|
||||
"integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.0.tgz",
|
||||
"integrity": "sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3604,9 +3648,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz",
|
||||
"integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.0.tgz",
|
||||
"integrity": "sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3616,9 +3660,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz",
|
||||
"integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.0.tgz",
|
||||
"integrity": "sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3628,9 +3672,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz",
|
||||
"integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.0.tgz",
|
||||
"integrity": "sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -3640,9 +3684,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz",
|
||||
"integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.0.tgz",
|
||||
"integrity": "sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10036,9 +10080,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -10074,12 +10118,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
|
||||
"integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz",
|
||||
"integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.5.0",
|
||||
"vue-demi": ">=0.14.5"
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
@@ -10099,10 +10144,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/vue-demi": {
|
||||
"version": "0.14.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
|
||||
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
@@ -10200,9 +10246,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -10219,8 +10265,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -10722,9 +10768,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
|
||||
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.0.tgz",
|
||||
"integrity": "sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
@@ -10736,22 +10782,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.18.0",
|
||||
"@rollup/rollup-android-arm64": "4.18.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.18.0",
|
||||
"@rollup/rollup-darwin-x64": "4.18.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.18.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.18.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.18.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.18.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.18.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.18.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.18.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.18.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.18.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.18.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.18.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.18.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.22.0",
|
||||
"@rollup/rollup-android-arm64": "4.22.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.22.0",
|
||||
"@rollup/rollup-darwin-x64": "4.22.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.22.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.22.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.22.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.22.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.22.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.22.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.22.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.22.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.22.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.22.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.22.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -10904,9 +10950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -11945,13 +11991,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
|
||||
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.39",
|
||||
"rollup": "^4.13.0"
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -11970,6 +12016,7 @@
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
@@ -11987,6 +12034,9 @@
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.57",
|
||||
"version": "1.2.62",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -25,6 +25,7 @@
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@iconify/json": "^2.2.245",
|
||||
"@pinia/testing": "^0.1.5",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.6",
|
||||
@@ -55,14 +56,14 @@
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.2.0",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.75",
|
||||
"@comfyorg/litegraph": "^0.7.77",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
|
||||
1
public/assets/CREDIT.txt
Normal file
@@ -0,0 +1 @@
|
||||
Thanks to OpenArt (https://openart.ai) for providing the sorted-custom-node-map data, captured in September 2024.
|
||||
2602
public/assets/sorted-custom-node-map.json
Normal file
91
src/App.vue
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
|
||||
<ProgressSpinner
|
||||
v-if="isLoading"
|
||||
class="absolute inset-0 flex justify-center items-center h-screen"
|
||||
/>
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<GlobalDialog />
|
||||
<GlobalToast />
|
||||
@@ -17,31 +20,39 @@ import {
|
||||
watch,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import config from '@/config'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { StatusWsMessageStatus } from '@/types/apiTypes'
|
||||
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { i18n } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import QueueSidebarTab from '@/components/sidebar/tabs/QueueSidebarTab.vue'
|
||||
import { app } from './scripts/app'
|
||||
import { useSettingStore } from './stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkspaceStore } from './stores/workspaceStateStore'
|
||||
import NodeLibrarySidebarTab from './components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import GlobalDialog from './components/dialog/GlobalDialog.vue'
|
||||
import GlobalToast from './components/toast/GlobalToast.vue'
|
||||
import UnloadWindowConfirmDialog from './components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import BrowserTabTitle from './components/BrowserTabTitle.vue'
|
||||
import { api } from './scripts/api'
|
||||
import { StatusWsMessageStatus } from './types/apiTypes'
|
||||
import { useQueuePendingTaskCountStore } from './stores/queueStore'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { i18n } from './i18n'
|
||||
import { useExecutionStore } from './stores/executionStore'
|
||||
import { useWorkflowStore } from './stores/workflowStore'
|
||||
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const theme = computed<string>(() =>
|
||||
useSettingStore().get('Comfy.ColorPalette')
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const settingStore = useSettingStore()
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const theme = computed<string>(() => settingStore.get('Comfy.ColorPalette'))
|
||||
|
||||
watch(
|
||||
theme,
|
||||
(newTheme) => {
|
||||
@@ -57,7 +68,7 @@ watch(
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
const fontSize = useSettingStore().get('Comfy.TextareaWidget.FontSize')
|
||||
const fontSize = settingStore.get('Comfy.TextareaWidget.FontSize')
|
||||
document.documentElement.style.setProperty(
|
||||
'--comfy-textarea-font-size',
|
||||
`${fontSize}px`
|
||||
@@ -65,7 +76,7 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const padding = useSettingStore().get('Comfy.TreeExplorer.ItemPadding')
|
||||
const padding = settingStore.get('Comfy.TreeExplorer.ItemPadding')
|
||||
document.documentElement.style.setProperty(
|
||||
'--comfy-tree-explorer-item-padding',
|
||||
`${padding}px`
|
||||
@@ -73,15 +84,14 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const locale = useSettingStore().get('Comfy.Locale')
|
||||
const locale = settingStore.get('Comfy.Locale')
|
||||
if (locale) {
|
||||
i18n.global.locale.value = locale
|
||||
i18n.global.locale.value = locale as 'en' | 'zh'
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const init = () => {
|
||||
useSettingStore().addSettings(app.ui.settings)
|
||||
settingStore.addSettings(app.ui.settings)
|
||||
app.extensionManager = useWorkspaceStore()
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: 'queue',
|
||||
@@ -105,19 +115,20 @@ const init = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
const onStatus = (e: CustomEvent<StatusWsMessageStatus>) =>
|
||||
const onStatus = (e: CustomEvent<StatusWsMessageStatus>) => {
|
||||
queuePendingTaskCountStore.update(e)
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
const reconnectingMessage: ToastMessageOptions = {
|
||||
severity: 'error',
|
||||
summary: t('reconnecting')
|
||||
}
|
||||
|
||||
const onReconnecting = () => {
|
||||
toast.remove(reconnectingMessage)
|
||||
toast.add(reconnectingMessage)
|
||||
}
|
||||
|
||||
const onReconnected = () => {
|
||||
toast.remove(reconnectingMessage)
|
||||
toast.add({
|
||||
@@ -127,23 +138,25 @@ const onReconnected = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
app.workflowManager.executionStore = executionStore
|
||||
watchEffect(() => {
|
||||
app.menu.workflows.buttonProgress.style.width = `${executionStore.executionProgress}%`
|
||||
})
|
||||
const workflowStore = useWorkflowStore()
|
||||
app.workflowManager.workflowStore = workflowStore
|
||||
|
||||
onMounted(() => {
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
console.log('ComfyUI Front-end version:', config.app_version)
|
||||
|
||||
api.addEventListener('status', onStatus)
|
||||
api.addEventListener('reconnecting', onReconnecting)
|
||||
api.addEventListener('reconnected', onReconnected)
|
||||
executionStore.bindExecutionEvents()
|
||||
|
||||
try {
|
||||
init()
|
||||
} catch (e) {
|
||||
console.error('Failed to init Vue app', e)
|
||||
console.error('Failed to init ComfyUI frontend', e)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -154,15 +167,3 @@ onUnmounted(() => {
|
||||
executionStore.unbindExecutionEvents()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.p-tree-node-content {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.spinner {
|
||||
@apply absolute inset-0 flex justify-center items-center h-screen;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -708,3 +708,7 @@ audio.comfy-audio.empty-audio-widget {
|
||||
.p-autocomplete-overlay {
|
||||
max-width: 25vw;
|
||||
}
|
||||
|
||||
.p-tree-node-content {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,18 @@ const workflowNameText = computed(() => {
|
||||
return workflowName ? isUnsavedText.value + workflowName : DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const title = computed(
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${executionStore.executingNodeProgress}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(betaMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
useTitle(title)
|
||||
</script>
|
||||
|
||||
@@ -60,7 +60,7 @@ watch(
|
||||
const start = 0
|
||||
const end = fileName.length
|
||||
const inputElement = inputRef.value.$el
|
||||
inputElement.setSelectionRange(start, end)
|
||||
inputElement.setSelectionRange?.(start, end)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -49,12 +49,12 @@ const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
|
||||
provide('expandedKeys', expandedKeys)
|
||||
const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
|
||||
provide('selectionKeys', selectionKeys)
|
||||
// Tracks whether the caller has set the selectionKeys model.
|
||||
const storeSelectionKeys = selectionKeys.value !== undefined
|
||||
|
||||
const props = defineProps<{
|
||||
roots: TreeExplorerNode[]
|
||||
class?: string
|
||||
extraMenuItems?:
|
||||
| MenuItem[]
|
||||
| ((targetNode: RenderedTreeExplorerNode) => MenuItem[])
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'nodeClick', node: RenderedTreeExplorerNode, event: MouseEvent): void
|
||||
@@ -92,15 +92,28 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
: children.reduce((acc, child) => acc + child.totalLeaves, 0)
|
||||
}
|
||||
}
|
||||
const onNodeContentClick = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
const onNodeContentClick = async (
|
||||
e: MouseEvent,
|
||||
node: RenderedTreeExplorerNode
|
||||
) => {
|
||||
if (!storeSelectionKeys) {
|
||||
selectionKeys.value = {}
|
||||
}
|
||||
if (node.handleClick) {
|
||||
node.handleClick(node, e)
|
||||
await node.handleClick(node, e)
|
||||
}
|
||||
emit('nodeClick', node, e)
|
||||
}
|
||||
const menu = ref(null)
|
||||
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('menuTargetNode', menuTargetNode)
|
||||
const extraMenuItems = computed(() => {
|
||||
return menuTargetNode.value?.contextMenuItems
|
||||
? typeof menuTargetNode.value.contextMenuItems === 'function'
|
||||
? menuTargetNode.value.contextMenuItems(menuTargetNode.value)
|
||||
: menuTargetNode.value.contextMenuItems
|
||||
: []
|
||||
})
|
||||
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('renameEditingNode', renameEditingNode)
|
||||
|
||||
@@ -108,8 +121,8 @@ const { t } = useI18n()
|
||||
const renameCommand = (node: RenderedTreeExplorerNode) => {
|
||||
renameEditingNode.value = node
|
||||
}
|
||||
const deleteCommand = (node: RenderedTreeExplorerNode) => {
|
||||
node.handleDelete?.(node)
|
||||
const deleteCommand = async (node: RenderedTreeExplorerNode) => {
|
||||
await node.handleDelete?.(node)
|
||||
emit('nodeDelete', node)
|
||||
}
|
||||
const menuItems = computed<MenuItem[]>(() =>
|
||||
@@ -124,16 +137,15 @@ const menuItems = computed<MenuItem[]>(() =>
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => deleteCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleDelete !== undefined
|
||||
visible: menuTargetNode.value?.handleDelete !== undefined,
|
||||
isAsync: true // The delete command can be async
|
||||
},
|
||||
...(props.extraMenuItems
|
||||
? typeof props.extraMenuItems === 'function'
|
||||
? props.extraMenuItems(menuTargetNode.value)
|
||||
: props.extraMenuItems
|
||||
: [])
|
||||
...extraMenuItems.value
|
||||
].map((menuItem) => ({
|
||||
...menuItem,
|
||||
command: wrapCommandWithErrorHandler(menuItem.command)
|
||||
command: wrapCommandWithErrorHandler(menuItem.command, {
|
||||
isAsync: menuItem.isAsync ?? false
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -147,12 +159,18 @@ const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
|
||||
|
||||
const errorHandling = useErrorHandling()
|
||||
const wrapCommandWithErrorHandler = (
|
||||
command: (event: MenuItemCommandEvent) => void
|
||||
command: (event: MenuItemCommandEvent) => void,
|
||||
{ isAsync = false }: { isAsync: boolean }
|
||||
) => {
|
||||
return errorHandling.wrapWithErrorHandling(
|
||||
command,
|
||||
menuTargetNode.value?.handleError
|
||||
)
|
||||
return isAsync
|
||||
? errorHandling.wrapWithErrorHandlingAsync(
|
||||
command as (...args: any[]) => Promise<any>,
|
||||
menuTargetNode.value?.handleError
|
||||
)
|
||||
: errorHandling.wrapWithErrorHandling(
|
||||
command,
|
||||
menuTargetNode.value?.handleError
|
||||
)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
]"
|
||||
ref="container"
|
||||
>
|
||||
<div class="node-content">
|
||||
<span class="node-label">
|
||||
<div class="node-content truncate">
|
||||
<span class="node-label text-sm">
|
||||
<slot name="before-label" :node="props.node"></slot>
|
||||
<EditableText
|
||||
:modelValue="node.label"
|
||||
@@ -46,6 +46,7 @@ import type {
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
|
||||
const props = defineProps<{
|
||||
node: RenderedTreeExplorerNode
|
||||
@@ -67,10 +68,14 @@ const renameEditingNode =
|
||||
const isEditing = computed(
|
||||
() => labelEditable.value && renameEditingNode.value?.key === props.node.key
|
||||
)
|
||||
const handleRename = (newName: string) => {
|
||||
props.node.handleRename(props.node, newName)
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
const errorHandling = useErrorHandling()
|
||||
const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
async (newName: string) => {
|
||||
await props.node.handleRename(props.node, newName)
|
||||
renameEditingNode.value = null
|
||||
},
|
||||
props.node.handleError
|
||||
)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
@@ -83,10 +88,10 @@ onMounted(() => {
|
||||
if (props.node.droppable) {
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: treeNodeElement.value,
|
||||
onDrop: (event) => {
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
props.node.handleDrop?.(props.node, dndData)
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,20 @@ import { mount } from '@vue/test-utils'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import Badge from 'primevue/badge'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createApp } from 'vue'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
// Create a mock i18n instance
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {}
|
||||
})
|
||||
|
||||
describe('TreeExplorerTreeNode', () => {
|
||||
const mockNode = {
|
||||
@@ -12,15 +25,28 @@ describe('TreeExplorerTreeNode', () => {
|
||||
leaf: false,
|
||||
totalLeaves: 3,
|
||||
icon: 'pi pi-folder',
|
||||
type: 'folder'
|
||||
type: 'folder',
|
||||
handleRename: () => {}
|
||||
} as RenderedTreeExplorerNode
|
||||
|
||||
beforeAll(() => {
|
||||
// Create a Vue app instance for PrimeVuePrimeVue
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge },
|
||||
provide: { renameEditingNode: { value: null } }
|
||||
provide: { renameEditingNode: { value: null } },
|
||||
plugins: [createTestingPinia(), i18n]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,4 +58,78 @@ describe('TreeExplorerTreeNode', () => {
|
||||
)
|
||||
expect(wrapper.findComponent(Badge).props()['value']).toBe(3)
|
||||
})
|
||||
|
||||
it('makes node label editable when renamingEditingNode matches', async () => {
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
expect(editableText.props('isEditing')).toBe(true)
|
||||
})
|
||||
|
||||
it('triggers handleRename callback when editing is finished', async () => {
|
||||
const handleRenameMock = vi.fn()
|
||||
const nodeWithMockRename = {
|
||||
...mockNode,
|
||||
handleRename: handleRenameMock
|
||||
}
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: nodeWithMockRename },
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows error toast when handleRename promise rejects', async () => {
|
||||
const handleRenameMock = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Rename failed'))
|
||||
const nodeWithMockRename = {
|
||||
...mockNode,
|
||||
handleRename: handleRenameMock
|
||||
}
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: nodeWithMockRename },
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const toastStore = useToastStore()
|
||||
const addToastSpy = vi.spyOn(toastStore, 'add')
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
|
||||
// Wait for the promise to reject and the toast to be added
|
||||
vi.runAllTimers()
|
||||
|
||||
// Wait for any pending promises to resolve
|
||||
await new Promise(process.nextTick)
|
||||
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
expect(addToastSpy).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'error',
|
||||
detail: 'Rename failed',
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,11 @@ import { ref, computed, onUnmounted, onMounted, watchEffect } from 'vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import {
|
||||
LiteGraph,
|
||||
@@ -38,6 +42,9 @@ import {
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { applyOpacity } from '@/utils/colorUtil'
|
||||
import { getColorPalette } from '@/extensions/core/colorPalette'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -86,6 +93,24 @@ watchEffect(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const updateNodeOpacity = (nodeOpacity: number) => {
|
||||
const colorPalette = getColorPalette()
|
||||
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
const nodeBgColor = colorPalette?.colors?.litegraph_base?.NODE_DEFAULT_BGCOLOR
|
||||
if (nodeBgColor) {
|
||||
LiteGraph.NODE_DEFAULT_BGCOLOR = applyOpacity(nodeBgColor, nodeOpacity)
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedUpdateNodeOpacity = debounce(updateNodeOpacity, 128)
|
||||
|
||||
watchEffect(() => {
|
||||
const nodeOpacity = settingStore.get('Comfy.Node.Opacity')
|
||||
debouncedUpdateNodeOpacity(nodeOpacity)
|
||||
})
|
||||
|
||||
let dropTargetCleanup = () => {}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -140,6 +165,8 @@ onMounted(async () => {
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.endsWithFilterStartSequence('')
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
useNodeFrequencyStore().loadNodeFrequencies()
|
||||
emit('ready')
|
||||
})
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ const onIdle = () => {
|
||||
[0, 0]
|
||||
)
|
||||
if (outputSlot !== -1) {
|
||||
return showTooltip(nodeDef.output.all?.[outputSlot].tooltip)
|
||||
return showTooltip(nodeDef.output.all?.[outputSlot]?.tooltip)
|
||||
}
|
||||
|
||||
const widget = getHoveredWidget()
|
||||
|
||||
@@ -5,7 +5,13 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
<template>
|
||||
<div class="_sb_node_preview">
|
||||
<div class="_sb_table">
|
||||
<div class="node_header">
|
||||
<div
|
||||
class="node_header"
|
||||
:style="{
|
||||
backgroundColor: litegraphColors.NODE_DEFAULT_COLOR,
|
||||
color: litegraphColors.NODE_TITLE_COLOR
|
||||
}"
|
||||
>
|
||||
<div class="_sb_dot headdot"></div>
|
||||
{{ nodeDef.display_name }}
|
||||
</div>
|
||||
@@ -22,7 +28,12 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
</div>
|
||||
<div class="_sb_col">{{ slotInput ? slotInput.name : '' }}</div>
|
||||
<div class="_sb_col middle-column"></div>
|
||||
<div class="_sb_col _sb_inherit">
|
||||
<div
|
||||
class="_sb_col _sb_inherit"
|
||||
:style="{
|
||||
color: litegraphColors.NODE_TEXT_COLOR
|
||||
}"
|
||||
>
|
||||
{{ slotOutput ? slotOutput.name : '' }}
|
||||
</div>
|
||||
<div class="_sb_col">
|
||||
@@ -37,15 +48,32 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
:key="widgetInput.name"
|
||||
>
|
||||
<div class="_sb_col _sb_arrow">◀</div>
|
||||
<div class="_sb_col">{{ widgetInput.name }}</div>
|
||||
<div
|
||||
class="_sb_col"
|
||||
:style="{
|
||||
color: litegraphColors.WIDGET_SECONDARY_TEXT_COLOR
|
||||
}"
|
||||
>
|
||||
{{ widgetInput.name }}
|
||||
</div>
|
||||
<div class="_sb_col middle-column"></div>
|
||||
<div class="_sb_col _sb_inherit">
|
||||
<div
|
||||
class="_sb_col _sb_inherit"
|
||||
:style="{ color: litegraphColors.WIDGET_TEXT_COLOR }"
|
||||
>
|
||||
{{ truncateDefaultValue(widgetInput.default) }}
|
||||
</div>
|
||||
<div class="_sb_col _sb_arrow">▶</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_sb_description" v-if="nodeDef.description">
|
||||
<div
|
||||
class="_sb_description"
|
||||
v-if="nodeDef.description"
|
||||
:style="{
|
||||
color: litegraphColors.WIDGET_SECONDARY_TEXT_COLOR,
|
||||
backgroundColor: litegraphColors.WIDGET_BGCOLOR
|
||||
}"
|
||||
>
|
||||
{{ nodeDef.description }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,6 +81,10 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import {
|
||||
getColorPalette,
|
||||
defaultColorPalette
|
||||
} from '@/extensions/core/colorPalette'
|
||||
import _ from 'lodash'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -62,6 +94,13 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
// Node preview currently is recreated every time something is hovered.
|
||||
// So not reactive to the color palette changes after setup is fine.
|
||||
// If later we want NodePreview to be shown more persistently, then we should
|
||||
// make the getColorPalette() call reactive.
|
||||
const colors = getColorPalette()?.colors?.litegraph_base
|
||||
const litegraphColors = colors ?? defaultColorPalette.colors.litegraph_base
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
const nodeDef = props.nodeDef
|
||||
@@ -106,7 +145,6 @@ const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
|
||||
.node_header {
|
||||
line-height: 1;
|
||||
padding: 8px 13px 7px;
|
||||
background: var(--comfy-input-bg);
|
||||
margin-bottom: 5px;
|
||||
font-size: 15px;
|
||||
text-wrap: nowrap;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<Chip :class="nodeSource.className">
|
||||
{{ nodeSource.displayText }}
|
||||
</Chip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getNodeSource } from '@/types/nodeSource'
|
||||
import Chip from 'primevue/chip'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
python_module: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const nodeSource = computed(() => getNodeSource(props.python_module))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-core,
|
||||
.comfy-custom-nodes,
|
||||
.comfy-unknown {
|
||||
font-size: small;
|
||||
font-weight: lighter;
|
||||
}
|
||||
</style>
|
||||
@@ -33,6 +33,7 @@
|
||||
:suggestions="suggestions"
|
||||
:min-length="0"
|
||||
:delay="100"
|
||||
:loading="!nodeFrequencyStore.isLoaded"
|
||||
@complete="search($event.query)"
|
||||
@option-select="emit('addNode', $event.value)"
|
||||
@focused-option-changed="setHoverSuggestion($event)"
|
||||
@@ -67,7 +68,11 @@ import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import { type FilterAndValue } from '@/services/nodeSearchService'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||
@@ -98,13 +103,18 @@ const placeholder = computed(() => {
|
||||
return props.filters.length === 0 ? t('searchNodes') + '...' : ''
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
const search = (query: string) => {
|
||||
const queryIsEmpty = query === '' && props.filters.length === 0
|
||||
currentQuery.value = query
|
||||
suggestions.value = [
|
||||
...useNodeDefStore().nodeSearchService.searchNode(query, props.filters, {
|
||||
limit: props.searchLimit
|
||||
})
|
||||
]
|
||||
suggestions.value = queryIsEmpty
|
||||
? nodeFrequencyStore.topNodeDefs
|
||||
: [
|
||||
...nodeDefStore.nodeSearchService.searchNode(query, props.filters, {
|
||||
limit: props.searchLimit
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { app } from '@/scripts/app'
|
||||
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, toRaw, watchEffect } from 'vue'
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { ConnectingLink } from '@comfyorg/litegraph'
|
||||
@@ -66,7 +66,9 @@ const addFilter = (filter: FilterAndValue) => {
|
||||
nodeFilters.value.push(filter)
|
||||
}
|
||||
const removeFilter = (filter: FilterAndValue) => {
|
||||
nodeFilters.value = nodeFilters.value.filter((f) => f !== filter)
|
||||
nodeFilters.value = nodeFilters.value.filter(
|
||||
(f) => toRaw(f) !== toRaw(filter)
|
||||
)
|
||||
}
|
||||
const clearFilters = () => {
|
||||
nodeFilters.value = []
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div class="_content">
|
||||
<SelectButton
|
||||
class="filter-type-select"
|
||||
v-model="selectedFilter"
|
||||
:options="filters"
|
||||
:allowEmpty="false"
|
||||
optionLabel="name"
|
||||
@change="updateSelectedFilterValue"
|
||||
/>
|
||||
<Select v-model="selectedFilterValue" :options="filterValues" filter />
|
||||
<Select
|
||||
class="filter-value-select"
|
||||
v-model="selectedFilterValue"
|
||||
:options="filterValues"
|
||||
filter
|
||||
/>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<Button type="button" :label="$t('add')" @click="submit"></Button>
|
||||
|
||||
@@ -33,22 +33,31 @@
|
||||
:value="$t('deprecated')"
|
||||
severity="danger"
|
||||
/>
|
||||
<NodeSourceChip
|
||||
v-if="nodeDef.python_module !== undefined"
|
||||
:python_module="nodeDef.python_module"
|
||||
<Tag
|
||||
v-if="showNodeFrequency && nodeFrequency > 0"
|
||||
:value="formatNumberWithSuffix(nodeFrequency, { roundToInt: true })"
|
||||
severity="secondary"
|
||||
/>
|
||||
<Chip
|
||||
v-if="nodeDef.nodeSource.type !== NodeSourceType.Unknown"
|
||||
class="text-sm font-light"
|
||||
>
|
||||
{{ nodeDef.nodeSource.displayText }}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
import NodeSourceChip from '@/components/node/NodeSourceChip.vue'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import Chip from 'primevue/chip'
|
||||
import { NodeSourceType } from '@/types/nodeSource'
|
||||
import { ComfyNodeDefImpl, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import { highlightQuery } from '@/utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { formatNumberWithSuffix } from '@/utils/formatUtil'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const showCategory = computed(() =>
|
||||
@@ -57,6 +66,13 @@ const showCategory = computed(() =>
|
||||
const showIdName = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.ShowIdName')
|
||||
)
|
||||
const showNodeFrequency = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.ShowNodeFrequency')
|
||||
)
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
const nodeFrequency = computed(() =>
|
||||
nodeFrequencyStore.getNodeFrequency(props.nodeDef)
|
||||
)
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const isBookmarked = computed(() =>
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
class="node-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
:extraMenuItems="extraMenuItems"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
<NodeTreeFolder :node="node" />
|
||||
@@ -89,6 +88,37 @@ watch(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const extraMenuItems = (
|
||||
menuTargetNode: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
) => [
|
||||
{
|
||||
label: t('newFolder'),
|
||||
icon: 'pi pi-folder-plus',
|
||||
command: () => {
|
||||
addNewBookmarkFolder(menuTargetNode)
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
},
|
||||
{
|
||||
label: t('customize'),
|
||||
icon: 'pi pi-palette',
|
||||
command: () => {
|
||||
const customization =
|
||||
nodeBookmarkStore.bookmarksCustomization[menuTargetNode.data.nodePath]
|
||||
initialIcon.value =
|
||||
customization?.icon || nodeBookmarkStore.defaultBookmarkIcon
|
||||
initialColor.value =
|
||||
customization?.color || nodeBookmarkStore.defaultBookmarkColor
|
||||
|
||||
showCustomizationDialog.value = true
|
||||
customizationTargetNodePath.value = menuTargetNode.data.nodePath
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
}
|
||||
]
|
||||
|
||||
const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
() => {
|
||||
const fillNodeInfo = (
|
||||
@@ -145,6 +175,7 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
},
|
||||
contextMenuItems: extraMenuItems,
|
||||
...(node.leaf
|
||||
? {}
|
||||
: {
|
||||
@@ -201,34 +232,4 @@ const updateCustomization = (icon: string, color: string) => {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const extraMenuItems = computed(
|
||||
() => (menuTargetNode: RenderedTreeExplorerNode<ComfyNodeDefImpl>) => [
|
||||
{
|
||||
label: t('newFolder'),
|
||||
icon: 'pi pi-folder-plus',
|
||||
command: () => {
|
||||
addNewBookmarkFolder(menuTargetNode)
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
},
|
||||
{
|
||||
label: t('customize'),
|
||||
icon: 'pi pi-palette',
|
||||
command: () => {
|
||||
const customization =
|
||||
nodeBookmarkStore.bookmarksCustomization[menuTargetNode.data.nodePath]
|
||||
initialIcon.value =
|
||||
customization?.icon || nodeBookmarkStore.defaultBookmarkIcon
|
||||
initialColor.value =
|
||||
customization?.color || nodeBookmarkStore.defaultBookmarkColor
|
||||
|
||||
showCustomizationDialog.value = true
|
||||
customizationTargetNodePath.value = menuTargetNode.data.nodePath
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
}
|
||||
]
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -125,9 +125,4 @@ onUnmounted(() => {
|
||||
.node-lib-node-container {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
|
||||
.bookmark-button {
|
||||
width: unset;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
4
src/config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
app_title: 'ComfyUI',
|
||||
app_version: __COMFYUI_FRONTEND_VERSION__
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { app } from '../../scripts/app'
|
||||
import { $el } from '../../scripts/ui'
|
||||
import type { ColorPalettes, Palette } from '@/types/colorPalette'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { applyOpacity } from '@/utils/colorUtil'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
// Manage color palettes
|
||||
|
||||
@@ -682,7 +684,13 @@ app.registerExtension({
|
||||
colorPalette.colors.litegraph_base.hasOwnProperty(key) &&
|
||||
LiteGraph.hasOwnProperty(key)
|
||||
) {
|
||||
LiteGraph[key] = colorPalette.colors.litegraph_base[key]
|
||||
LiteGraph[key] =
|
||||
key === 'NODE_DEFAULT_BGCOLOR'
|
||||
? applyOpacity(
|
||||
colorPalette.colors.litegraph_base[key],
|
||||
useSettingStore().get('Comfy.Node.Opacity')
|
||||
)
|
||||
: colorPalette.colors.litegraph_base[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,6 +828,9 @@ export class GroupNodeHandler {
|
||||
]
|
||||
|
||||
// Remove all converted nodes and relink them
|
||||
const builder = new GroupNodeBuilder(nodes)
|
||||
const nodeData = builder.getNodeData()
|
||||
groupNode[GROUP].groupData.nodeData.links = nodeData.links
|
||||
groupNode[GROUP].replaceNodes(nodes)
|
||||
return groupNode
|
||||
}
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
import { app, type ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import type { ComfyLGraphNode } from '@/types/comfyLGraphNode'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphBadge } from '@comfyorg/litegraph'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { computed, ComputedRef, watch } from 'vue'
|
||||
import {
|
||||
getNodeSource as getNodeSourceFromPythonModule,
|
||||
NodeBadgeMode
|
||||
} from '@/types/nodeSource'
|
||||
import { NodeBadgeMode, NodeSource, NodeSourceType } from '@/types/nodeSource'
|
||||
import _ from 'lodash'
|
||||
import { getColorPalette, defaultColorPalette } from './colorPalette'
|
||||
import { BadgePosition } from '@comfyorg/litegraph'
|
||||
import type { Palette } from '@/types/colorPalette'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
function getNodeSource(node: ComfyLGraphNode) {
|
||||
const pythonModule = (node.constructor as typeof ComfyLGraphNode).nodeData
|
||||
?.python_module
|
||||
return pythonModule ? getNodeSourceFromPythonModule(pythonModule) : null
|
||||
function getNodeSource(node: LGraphNode): NodeSource | null {
|
||||
const nodeDef = node.constructor.nodeData
|
||||
// Frontend-only nodes don't have nodeDef
|
||||
if (!nodeDef) {
|
||||
return null
|
||||
}
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
return nodeDefStore.nodeDefsByName[nodeDef.name]?.nodeSource ?? null
|
||||
}
|
||||
|
||||
function isCoreNode(node: ComfyLGraphNode) {
|
||||
return getNodeSource(node)?.type === 'core'
|
||||
function isCoreNode(node: LGraphNode) {
|
||||
return getNodeSource(node)?.type === NodeSourceType.Core
|
||||
}
|
||||
|
||||
function badgeTextVisible(
|
||||
node: ComfyLGraphNode,
|
||||
badgeMode: NodeBadgeMode
|
||||
): boolean {
|
||||
function badgeTextVisible(node: LGraphNode, badgeMode: NodeBadgeMode): boolean {
|
||||
return (
|
||||
badgeMode === NodeBadgeMode.None ||
|
||||
(isCoreNode(node) && badgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
)
|
||||
}
|
||||
|
||||
function getNodeIdBadgeText(
|
||||
node: ComfyLGraphNode,
|
||||
nodeIdBadgeMode: NodeBadgeMode
|
||||
) {
|
||||
function getNodeIdBadgeText(node: LGraphNode, nodeIdBadgeMode: NodeBadgeMode) {
|
||||
return badgeTextVisible(node, nodeIdBadgeMode) ? '' : `#${node.id}`
|
||||
}
|
||||
|
||||
function getNodeSourceBadgeText(
|
||||
node: ComfyLGraphNode,
|
||||
node: LGraphNode,
|
||||
nodeSourceBadgeMode: NodeBadgeMode
|
||||
) {
|
||||
const nodeSource = getNodeSource(node)
|
||||
@@ -51,11 +48,11 @@ function getNodeSourceBadgeText(
|
||||
}
|
||||
|
||||
function getNodeLifeCycleBadgeText(
|
||||
node: ComfyLGraphNode,
|
||||
node: LGraphNode,
|
||||
nodeLifeCycleBadgeMode: NodeBadgeMode
|
||||
) {
|
||||
let text = ''
|
||||
const nodeDef = (node.constructor as typeof ComfyLGraphNode).nodeData
|
||||
const nodeDef = node.constructor.nodeData
|
||||
|
||||
// Frontend-only nodes don't have nodeDef
|
||||
if (!nodeDef) {
|
||||
@@ -114,7 +111,7 @@ class NodeBadgeExtension implements ComfyExtension {
|
||||
})
|
||||
}
|
||||
|
||||
nodeCreated(node: ComfyLGraphNode, app: ComfyApp) {
|
||||
nodeCreated(node: LGraphNode, app: ComfyApp) {
|
||||
node.badgePosition = BadgePosition.TopRight
|
||||
// @ts-expect-error Disable ComfyUI-Manager's badge drawing by setting badge_enabled to true. Remove this when ComfyUI-Manager's badge drawing is removed.
|
||||
node.badge_enabled = true
|
||||
|
||||
@@ -378,7 +378,6 @@ app.registerExtension({
|
||||
const nodeIds = Object.keys(app.canvas.selected_nodes)
|
||||
for (let i = 0; i < nodeIds.length; i++) {
|
||||
const node = app.graph.getNodeById(nodeIds[i])
|
||||
// @ts-expect-error
|
||||
const nodeData = node?.constructor.nodeData
|
||||
|
||||
let groupData = GroupNodeHandler.getGroupData(node)
|
||||
|
||||
@@ -26,7 +26,6 @@ app.registerExtension({
|
||||
// Should we extends LGraphNode? Yesss
|
||||
this,
|
||||
'',
|
||||
// @ts-expect-error
|
||||
['', { default: this.properties.text, multiline: true }],
|
||||
app
|
||||
)
|
||||
|
||||
@@ -74,7 +74,6 @@ app.registerExtension({
|
||||
const link = app.graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = app.graph.getNodeById(link.origin_id)
|
||||
// @ts-expect-error Nodes that extend LGraphNode will not have a static type property
|
||||
const type = node.constructor.type
|
||||
if (type === 'Reroute') {
|
||||
if (node === this) {
|
||||
@@ -113,7 +112,6 @@ app.registerExtension({
|
||||
if (!link) continue
|
||||
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error Nodes that extend LGraphNode will not have a static type property
|
||||
const type = node.constructor.type
|
||||
|
||||
if (type === 'Reroute') {
|
||||
@@ -179,7 +177,6 @@ app.registerExtension({
|
||||
}
|
||||
if (!targetWidget) {
|
||||
targetWidget = targetNode.widgets?.find(
|
||||
// @ts-expect-error fix widget types
|
||||
(w) => w.name === targetInput.widget.name
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,11 +95,9 @@ app.registerExtension({
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(
|
||||
inputName,
|
||||
/* name=*/ 'audioUI',
|
||||
audio
|
||||
audio,
|
||||
{ serialize: false }
|
||||
)
|
||||
// @ts-expect-error
|
||||
// TODO: Sort out the DOMWidget type.
|
||||
audioUIWidget.serialize = false
|
||||
|
||||
const isOutputNode = node.constructor.nodeData.output_node
|
||||
if (isOutputNode) {
|
||||
@@ -193,10 +191,10 @@ app.registerExtension({
|
||||
/* value=*/ '',
|
||||
() => {
|
||||
fileInput.click()
|
||||
}
|
||||
},
|
||||
{ serialize: false }
|
||||
)
|
||||
uploadWidget.label = 'choose file to upload'
|
||||
uploadWidget.serialize = false
|
||||
|
||||
return { widget: uploadWidget }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { app } from '../../scripts/app'
|
||||
import { applyTextReplacements } from '../../scripts/utils'
|
||||
import { LiteGraph, LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { INodeInputSlot, IWidget } from '@comfyorg/litegraph'
|
||||
import type { InputSpec } from '@/types/apiTypes'
|
||||
|
||||
const CONVERTED_TYPE = 'converted-widget'
|
||||
const VALID_TYPES = ['STRING', 'combo', 'number', 'toggle', 'BOOLEAN']
|
||||
@@ -447,15 +448,19 @@ function showWidget(widget) {
|
||||
}
|
||||
}
|
||||
|
||||
function convertToInput(node, widget, config) {
|
||||
function convertToInput(node: LGraphNode, widget: IWidget, config: InputSpec) {
|
||||
hideWidget(node, widget)
|
||||
|
||||
const { type } = getWidgetType(config)
|
||||
|
||||
// Add input and store widget config for creating on primitive node
|
||||
const sz = node.size
|
||||
const inputIsOptional = !!widget.options?.inputIsOptional
|
||||
node.addInput(widget.name, type, {
|
||||
widget: { name: widget.name, [GET_CONFIG]: () => config }
|
||||
// @ts-expect-error GET_CONFIG is not defined in LiteGraph
|
||||
widget: { name: widget.name, [GET_CONFIG]: () => config },
|
||||
// @ts-expect-error LiteGraph.SlotShape is not typed.
|
||||
...(inputIsOptional ? { shape: LiteGraph.SlotShape.HollowCircle } : {})
|
||||
})
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
@@ -479,7 +484,7 @@ function convertToWidget(node, widget) {
|
||||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
|
||||
}
|
||||
|
||||
function getWidgetType(config) {
|
||||
function getWidgetType(config: InputSpec) {
|
||||
// Special handling for COMBO so we restrict links based on the entries
|
||||
let type = config[0]
|
||||
if (type instanceof Array) {
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useErrorHandling() {
|
||||
const toast = useToast()
|
||||
const toast = useToastStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const toastErrorHandler = (error: any) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('error'),
|
||||
detail: error.message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
|
||||
const wrapWithErrorHandling =
|
||||
(action: (...args: any[]) => any, errorHandler?: (error: any) => void) =>
|
||||
(...args: any[]) => {
|
||||
try {
|
||||
return action(...args)
|
||||
} catch (e) {
|
||||
if (errorHandler) {
|
||||
errorHandler(e)
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('error'),
|
||||
detail: e.message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
;(errorHandler ?? toastErrorHandler)(e)
|
||||
}
|
||||
}
|
||||
|
||||
return { wrapWithErrorHandling }
|
||||
const wrapWithErrorHandlingAsync =
|
||||
(
|
||||
action: (...args: any[]) => Promise<any>,
|
||||
errorHandler?: (error: any) => void
|
||||
) =>
|
||||
async (...args: any[]) => {
|
||||
try {
|
||||
return await action(...args)
|
||||
} catch (e) {
|
||||
;(errorHandler ?? toastErrorHandler)(e)
|
||||
}
|
||||
}
|
||||
|
||||
return { wrapWithErrorHandling, wrapWithErrorHandlingAsync }
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import router from '@/router'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { i18n } from './i18n'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import 'reflect-metadata'
|
||||
|
||||
import '@comfyorg/litegraph/style.css'
|
||||
import '@/assets/css/style.css'
|
||||
|
||||
@@ -51,7 +51,6 @@ import { useToastStore } from '@/stores/toastStore'
|
||||
import { ModelStore, useModelStore } from '@/stores/modelStore'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { ComfyLGraphNode } from '@/types/comfyLGraphNode'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
@@ -1286,10 +1285,8 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
if (e.type == 'keydown' && !e.repeat) {
|
||||
const key = e.code
|
||||
|
||||
// Ctrl + M mute/unmute
|
||||
if (key === 'KeyM' && (e.metaKey || e.ctrlKey)) {
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey)) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
if (this.selected_nodes[i].mode === 2) {
|
||||
@@ -1304,7 +1301,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
// Ctrl + B bypass
|
||||
if (key === 'KeyB' && (e.metaKey || e.ctrlKey)) {
|
||||
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
if (this.selected_nodes[i].mode === 4) {
|
||||
@@ -1319,7 +1316,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
// p pin/unpin
|
||||
if (key === 'KeyP') {
|
||||
if (e.key === 'p') {
|
||||
if (this.selected_nodes) {
|
||||
for (const i in this.selected_nodes) {
|
||||
const node = this.selected_nodes[i]
|
||||
@@ -1330,7 +1327,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
// Alt + C collapse/uncollapse
|
||||
if (key === 'KeyC' && e.altKey) {
|
||||
if (e.key === 'c' && e.altKey) {
|
||||
if (this.selected_nodes) {
|
||||
for (var i in this.selected_nodes) {
|
||||
this.selected_nodes[i].collapse()
|
||||
@@ -1340,18 +1337,22 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
// Ctrl+C Copy
|
||||
if (key === 'KeyC' && (e.metaKey || e.ctrlKey)) {
|
||||
if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||
// Trigger onCopy
|
||||
return true
|
||||
}
|
||||
|
||||
// Ctrl+V Paste
|
||||
if (key === 'KeyV' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
||||
if (
|
||||
(e.key === 'v' || e.key == 'V') &&
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
!e.shiftKey
|
||||
) {
|
||||
// Trigger onPaste
|
||||
return true
|
||||
}
|
||||
|
||||
if ((key === 'NumpadAdd' || key === 'Equal') && e.altKey) {
|
||||
if (e.key === '+' && e.altKey) {
|
||||
block_default = true
|
||||
let scale = this.ds.scale * 1.1
|
||||
this.ds.changeScale(scale, [
|
||||
@@ -1361,9 +1362,9 @@ export class ComfyApp {
|
||||
this.graph.change()
|
||||
}
|
||||
|
||||
if ((key === 'NumpadSubtract' || key === 'Minus') && e.altKey) {
|
||||
if (e.key === '-' && e.altKey) {
|
||||
block_default = true
|
||||
let scale = this.ds.scale / 1.1
|
||||
let scale = (this.ds.scale * 1) / 1.1
|
||||
this.ds.changeScale(scale, [
|
||||
this.ds.element.width / 2,
|
||||
this.ds.element.height / 2
|
||||
@@ -1935,7 +1936,6 @@ export class ComfyApp {
|
||||
{
|
||||
name,
|
||||
display_name: name,
|
||||
// @ts-expect-error
|
||||
category: node.category || '__frontend_only__',
|
||||
input: { required: {}, optional: {} },
|
||||
output: [],
|
||||
@@ -1989,7 +1989,7 @@ export class ComfyApp {
|
||||
|
||||
async registerNodeDef(nodeId: string, nodeData: ComfyNodeDef) {
|
||||
const self = this
|
||||
const node: new () => ComfyLGraphNode = class ComfyNode extends LGraphNode {
|
||||
const node = class ComfyNode extends LGraphNode {
|
||||
static comfyClass? = nodeData.name
|
||||
// TODO: change to "title?" once litegraph.d.ts has been updated
|
||||
static title = nodeData.display_name || nodeData.name
|
||||
@@ -1998,6 +1998,8 @@ export class ComfyApp {
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title)
|
||||
const requiredInputs = nodeData.input.required
|
||||
|
||||
var inputs = nodeData['input']['required']
|
||||
if (nodeData['input']['optional'] != undefined) {
|
||||
inputs = Object.assign(
|
||||
@@ -2010,6 +2012,7 @@ export class ComfyApp {
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName]
|
||||
const type = inputData[0]
|
||||
const inputIsRequired = inputName in requiredInputs
|
||||
|
||||
let widgetCreated = true
|
||||
const widgetType = self.getWidgetType(inputData, inputName)
|
||||
@@ -2027,9 +2030,22 @@ export class ComfyApp {
|
||||
}
|
||||
} else {
|
||||
// Node connection inputs
|
||||
this.addInput(inputName, type)
|
||||
const inputOptions = inputIsRequired
|
||||
? {}
|
||||
: // @ts-expect-error LiteGraph.SlotShape is not typed.
|
||||
{ shape: LiteGraph.SlotShape.HollowCircle }
|
||||
this.addInput(inputName, type, inputOptions)
|
||||
widgetCreated = false
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
if (widgetCreated && !inputIsRequired && config?.widget) {
|
||||
// @ts-expect-error
|
||||
if (!config.widget.options) config.widget.options = {}
|
||||
// @ts-expect-error
|
||||
config.widget.options.inputIsOptional = true
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
if (widgetCreated && inputData[1]?.forceInput && config?.widget) {
|
||||
// @ts-expect-error
|
||||
@@ -2050,10 +2066,11 @@ export class ComfyApp {
|
||||
let output = nodeData['output'][o]
|
||||
if (output instanceof Array) output = 'COMBO'
|
||||
const outputName = nodeData['output_name'][o] || output
|
||||
const outputShape = nodeData['output_is_list'][o]
|
||||
? LiteGraph.GRID_SHAPE
|
||||
: LiteGraph.CIRCLE_SHAPE
|
||||
this.addOutput(outputName, output, { shape: outputShape })
|
||||
const outputIsList = nodeData['output_is_list'][o]
|
||||
const outputOptions = outputIsList
|
||||
? { shape: LiteGraph.GRID_SHAPE }
|
||||
: {}
|
||||
this.addOutput(outputName, output, outputOptions)
|
||||
}
|
||||
|
||||
const s = this.computeSize()
|
||||
@@ -2064,6 +2081,29 @@ export class ComfyApp {
|
||||
|
||||
app.#invokeExtensionsAsync('nodeCreated', this)
|
||||
}
|
||||
|
||||
configure(data: any) {
|
||||
// Keep 'name', 'type', and 'shape' information from the original node definition.
|
||||
const merge = (
|
||||
current: Record<string, any>,
|
||||
incoming: Record<string, any>
|
||||
) => {
|
||||
const result = { ...incoming }
|
||||
for (const key of ['name', 'type', 'shape']) {
|
||||
if (current[key] !== undefined) {
|
||||
result[key] = current[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
for (const field of ['inputs', 'outputs']) {
|
||||
const slots = data[field] ?? []
|
||||
data[field] = slots.map((slot, i) =>
|
||||
merge(this[field][i] ?? {}, slot)
|
||||
)
|
||||
}
|
||||
super.configure(data)
|
||||
}
|
||||
}
|
||||
node.prototype.comfyClass = nodeData.name
|
||||
|
||||
@@ -2074,7 +2114,6 @@ export class ComfyApp {
|
||||
await this.#invokeExtensionsAsync('beforeRegisterNodeDef', node, nodeData)
|
||||
LiteGraph.registerNodeType(nodeId, node)
|
||||
// Note: Do not move this to the class definition, it will be overwritten
|
||||
// @ts-expect-error
|
||||
node.category = nodeData.category
|
||||
}
|
||||
|
||||
|
||||
@@ -82,10 +82,10 @@ export class ChangeTracker {
|
||||
|
||||
async undoRedo(e) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.code === 'KeyY') {
|
||||
if (e.key === 'y') {
|
||||
this.updateState(this.redo, this.undo)
|
||||
return true
|
||||
} else if (e.code === 'KeyZ') {
|
||||
} else if (e.key === 'z') {
|
||||
this.updateState(this.undo, this.redo)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { api } from './api'
|
||||
import './domWidget'
|
||||
import type { ComfyApp } from './app'
|
||||
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { InputSpec } from '@/types/apiTypes'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
export type ComfyWidgetConstructor = (
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
inputData: ComfyNodeDef,
|
||||
inputData: InputSpec,
|
||||
app?: ComfyApp,
|
||||
widgetName?: string
|
||||
) => { widget: IWidget; minWidth?: number; minHeight?: number }
|
||||
@@ -27,7 +27,7 @@ const IS_CONTROL_WIDGET = Symbol()
|
||||
const HAS_EXECUTED = Symbol()
|
||||
|
||||
function getNumberDefaults(
|
||||
inputData: ComfyNodeDef,
|
||||
inputData: InputSpec,
|
||||
defaultStep,
|
||||
precision,
|
||||
enable_rounding
|
||||
@@ -62,7 +62,7 @@ export function addValueControlWidget(
|
||||
defaultValue = 'randomize',
|
||||
values,
|
||||
widgetName,
|
||||
inputData: ComfyNodeDef
|
||||
inputData: InputSpec
|
||||
) {
|
||||
let name = inputData[1]?.control_after_generate
|
||||
if (typeof name !== 'string') {
|
||||
@@ -86,7 +86,7 @@ export function addValueControlWidgets(
|
||||
targetWidget,
|
||||
defaultValue = 'randomize',
|
||||
options,
|
||||
inputData: ComfyNodeDef
|
||||
inputData: InputSpec
|
||||
) {
|
||||
if (!defaultValue) defaultValue = 'randomize'
|
||||
if (!options) options = {}
|
||||
@@ -259,7 +259,7 @@ export function addValueControlWidgets(
|
||||
return widgets
|
||||
}
|
||||
|
||||
function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
|
||||
function seedWidget(node, inputName, inputData: InputSpec, app, widgetName) {
|
||||
const seed = createIntWidget(node, inputName, inputData, app, true)
|
||||
const seedControl = addValueControlWidget(
|
||||
node,
|
||||
@@ -277,7 +277,7 @@ function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
|
||||
function createIntWidget(
|
||||
node,
|
||||
inputName,
|
||||
inputData: ComfyNodeDef,
|
||||
inputData: InputSpec,
|
||||
app,
|
||||
isSeedInput: boolean = false
|
||||
) {
|
||||
@@ -382,7 +382,7 @@ export function initWidgets(app) {
|
||||
export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
'INT:seed': seedWidget,
|
||||
'INT:noise_seed': seedWidget,
|
||||
FLOAT(node, inputName, inputData: ComfyNodeDef, app) {
|
||||
FLOAT(node, inputName, inputData: InputSpec, app) {
|
||||
let widgetType: 'number' | 'slider' = isSlider(inputData[1]['display'], app)
|
||||
let precision = app.ui.settings.getSettingValue(
|
||||
'Comfy.FloatRoundingPrecision'
|
||||
@@ -416,7 +416,7 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
)
|
||||
}
|
||||
},
|
||||
INT(node, inputName, inputData: ComfyNodeDef, app) {
|
||||
INT(node, inputName, inputData: InputSpec, app) {
|
||||
return createIntWidget(node, inputName, inputData, app)
|
||||
},
|
||||
BOOLEAN(node, inputName, inputData) {
|
||||
@@ -431,7 +431,7 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
widget: node.addWidget('toggle', inputName, defaultVal, () => {}, options)
|
||||
}
|
||||
},
|
||||
STRING(node, inputName, inputData: ComfyNodeDef, app) {
|
||||
STRING(node, inputName, inputData: InputSpec, app) {
|
||||
const defaultVal = inputData[1].default || ''
|
||||
const multiline = !!inputData[1].multiline
|
||||
|
||||
@@ -454,7 +454,7 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
|
||||
return res
|
||||
},
|
||||
COMBO(node, inputName, inputData: ComfyNodeDef) {
|
||||
COMBO(node, inputName, inputData: InputSpec) {
|
||||
const type = inputData[0]
|
||||
let defaultValue = type[0]
|
||||
if (inputData[1] && inputData[1].default) {
|
||||
@@ -477,12 +477,7 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
}
|
||||
return res
|
||||
},
|
||||
IMAGEUPLOAD(
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
inputData: ComfyNodeDef,
|
||||
app
|
||||
) {
|
||||
IMAGEUPLOAD(node: LGraphNode, inputName: string, inputData: InputSpec, app) {
|
||||
// TODO make image upload handle a custom node type?
|
||||
const imageWidget = node.widgets.find(
|
||||
(w) => w.name === (inputData[1]?.widget ?? 'image')
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { getNodeSource } from '@/types/nodeSource'
|
||||
import Fuse, { IFuseOptions, FuseSearchOptions } from 'fuse.js'
|
||||
import _ from 'lodash'
|
||||
|
||||
type SearchAuxScore = [number, number, number, number]
|
||||
export type SearchAuxScore = number[]
|
||||
|
||||
interface ExtraSearchOptions {
|
||||
matchWildcards?: boolean
|
||||
@@ -32,11 +31,10 @@ export class FuseSearch<T> {
|
||||
}
|
||||
|
||||
public search(query: string, options?: FuseSearchOptions): T[] {
|
||||
if (!query || query === '') {
|
||||
return [...this.data]
|
||||
}
|
||||
const fuseResult = !query
|
||||
? this.data.map((x) => ({ item: x, score: 0 }))
|
||||
: this.fuse.search(query, options)
|
||||
|
||||
const fuseResult = this.fuse.search(query, options)
|
||||
if (!this.advancedScoring) {
|
||||
return fuseResult.map((x) => x.item)
|
||||
}
|
||||
@@ -51,17 +49,20 @@ export class FuseSearch<T> {
|
||||
return aux.map((x) => x.item)
|
||||
}
|
||||
|
||||
public calcAuxScores(query: string, entry: T, score: number) {
|
||||
public calcAuxScores(query: string, entry: T, score: number): SearchAuxScore {
|
||||
let values: string[] = []
|
||||
if (!this.keys.length) values = [entry as string]
|
||||
else values = this.keys.map((x) => entry[x])
|
||||
const scores = values.map((x) => this.calcAuxSingle(query, x, score))
|
||||
const result = scores.sort(this.compareAux)[0]
|
||||
let result = scores.sort(this.compareAux)[0]
|
||||
|
||||
const deprecated = values.some((x) =>
|
||||
x.toLocaleLowerCase().includes('deprecated')
|
||||
)
|
||||
result[0] += deprecated && result[0] != 0 ? 5 : 0
|
||||
if (entry['postProcessSearchScores']) {
|
||||
result = entry['postProcessSearchScores'](result) as SearchAuxScore
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -117,7 +118,12 @@ export class FuseSearch<T> {
|
||||
}
|
||||
|
||||
public compareAux(a: SearchAuxScore, b: SearchAuxScore) {
|
||||
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]
|
||||
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return a[i] - b[i]
|
||||
}
|
||||
}
|
||||
return a.length - b.length
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +201,7 @@ export class NodeSourceFilter extends NodeFilter<string> {
|
||||
public readonly longInvokeSequence = 'source'
|
||||
|
||||
public override getNodeOptions(node: ComfyNodeDefImpl): string[] {
|
||||
return [getNodeSource(node.python_module).displayText]
|
||||
return [node.nodeSource.displayText]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl.ShowNodeFrequency',
|
||||
category: ['Comfy', 'Node Search Box', 'ShowNodeFrequency'],
|
||||
name: 'Show node frequency in search results',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.Location',
|
||||
category: ['Comfy', 'Sidebar', 'Location'],
|
||||
@@ -131,6 +139,17 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.Opacity',
|
||||
name: 'Node opacity',
|
||||
type: 'slider',
|
||||
defaultValue: 1,
|
||||
attrs: {
|
||||
min: 0.01,
|
||||
max: 1,
|
||||
step: 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.ShowMissingNodesWarning',
|
||||
name: 'Show missing nodes warning',
|
||||
|
||||
@@ -1,16 +1,50 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '../scripts/api'
|
||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||
import type { ComfyNode, ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
|
||||
export interface QueuedPrompt {
|
||||
nodes: Record<string, boolean>
|
||||
workflow?: any // TODO: Replace 'any' with the actual type of workflow
|
||||
workflow?: ComfyWorkflow
|
||||
}
|
||||
|
||||
interface NodeProgress {
|
||||
value: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export const useExecutionStore = defineStore('execution', () => {
|
||||
const activePromptId = ref<string | null>(null)
|
||||
const queuedPrompts = ref<Record<string, QueuedPrompt>>({})
|
||||
const executingNodeId = ref<string | null>(null)
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
const workflow: ComfyWorkflow | null = activePrompt.value?.workflow
|
||||
if (!workflow) return null
|
||||
|
||||
const canvasState: ComfyWorkflowJSON | null =
|
||||
workflow.changeTracker?.activeState
|
||||
if (!canvasState) return null
|
||||
|
||||
return (
|
||||
canvasState.nodes.find((n) => String(n.id) === executingNodeId.value) ??
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
// This is the progress of the currently executing node, if any
|
||||
const _executingNodeProgress = ref<NodeProgress | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
? Math.round(
|
||||
(_executingNodeProgress.value.value /
|
||||
_executingNodeProgress.value.max) *
|
||||
100
|
||||
)
|
||||
: null
|
||||
)
|
||||
|
||||
const activePrompt = computed(() => queuedPrompts.value[activePromptId.value])
|
||||
|
||||
@@ -38,6 +72,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.addEventListener('execution_cached', handleExecutionCached)
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
api.addEventListener('progress', handleProgress)
|
||||
}
|
||||
|
||||
function unbindExecutionEvents() {
|
||||
@@ -45,6 +80,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.removeEventListener('execution_cached', handleExecutionCached)
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
api.removeEventListener('progress', handleProgress)
|
||||
}
|
||||
|
||||
function handleExecutionStart(e: CustomEvent) {
|
||||
@@ -65,19 +101,26 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
|
||||
function handleExecuting(e: CustomEvent) {
|
||||
// Clear the current node progress when a new node starts executing
|
||||
_executingNodeProgress.value = null
|
||||
|
||||
if (!activePrompt.value) return
|
||||
|
||||
if (executingNodeId.value) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
}
|
||||
executingNodeId.value = e.detail
|
||||
executingNodeId.value = e.detail ? String(e.detail) : null
|
||||
if (!executingNodeId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
activePromptId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgress(e: CustomEvent) {
|
||||
_executingNodeProgress.value = e.detail
|
||||
}
|
||||
|
||||
function storePrompt({
|
||||
nodes,
|
||||
id,
|
||||
@@ -112,6 +155,8 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
totalNodesToExecute,
|
||||
nodesExecuted,
|
||||
executionProgress,
|
||||
executingNode,
|
||||
executingNodeProgress,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||
import {
|
||||
NodeSearchService,
|
||||
type SearchAuxScore
|
||||
} from '@/services/nodeSearchService'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Type, Transform, plainToClass, Expose } from 'class-transformer'
|
||||
import { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import { TreeNode } from 'primevue/treenode'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
import { computed, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { type NodeSource, getNodeSource } from '@/types/nodeSource'
|
||||
|
||||
export class BaseInputSpec<T = any> {
|
||||
name: string
|
||||
@@ -189,6 +195,12 @@ export class ComfyNodeDefImpl {
|
||||
@Transform(({ obj }) => ComfyNodeDefImpl.transformOutputSpec(obj))
|
||||
output: ComfyOutputsSpec
|
||||
|
||||
@Transform(({ obj }) => getNodeSource(obj.python_module), {
|
||||
toClassOnly: true
|
||||
})
|
||||
@Expose()
|
||||
nodeSource: NodeSource
|
||||
|
||||
private static transformOutputSpec(obj: any): ComfyOutputsSpec {
|
||||
const { output, output_is_list, output_name, output_tooltips } = obj
|
||||
const result = output.map((type: string | any[], index: number) => {
|
||||
@@ -213,6 +225,12 @@ export class ComfyNodeDefImpl {
|
||||
get isDummyFolder(): boolean {
|
||||
return this.name === ''
|
||||
}
|
||||
|
||||
postProcessSearchScores(scores: SearchAuxScore): SearchAuxScore {
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
const nodeFrequency = nodeFrequencyStore.getNodeFrequencyByName(this.name)
|
||||
return [scores[0], -nodeFrequency, ...scores.slice(1)]
|
||||
}
|
||||
}
|
||||
|
||||
export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDef> = {
|
||||
@@ -338,3 +356,49 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const useNodeFrequencyStore = defineStore('nodeFrequency', () => {
|
||||
const topNodeDefLimit = ref(64)
|
||||
const nodeFrequencyLookup = ref<Record<string, number>>({})
|
||||
const nodeNamesByFrequency = computed(() =>
|
||||
Object.keys(nodeFrequencyLookup.value)
|
||||
)
|
||||
const isLoaded = ref(false)
|
||||
|
||||
const loadNodeFrequencies = async () => {
|
||||
if (!isLoaded.value) {
|
||||
try {
|
||||
const response = await axios.get('/assets/sorted-custom-node-map.json')
|
||||
nodeFrequencyLookup.value = response.data
|
||||
isLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error('Error loading node frequencies:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeFrequency = (nodeDef: ComfyNodeDefImpl) => {
|
||||
return getNodeFrequencyByName(nodeDef.name)
|
||||
}
|
||||
|
||||
const getNodeFrequencyByName = (nodeName: string) => {
|
||||
return nodeFrequencyLookup.value[nodeName] ?? 0
|
||||
}
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const topNodeDefs = computed<ComfyNodeDefImpl[]>(() => {
|
||||
return nodeNamesByFrequency.value
|
||||
.map((nodeName: string) => nodeDefStore.nodeDefsByName[nodeName])
|
||||
.filter((nodeDef: ComfyNodeDefImpl) => nodeDef !== undefined)
|
||||
.slice(0, topNodeDefLimit.value)
|
||||
})
|
||||
|
||||
return {
|
||||
nodeNamesByFrequency,
|
||||
topNodeDefs,
|
||||
isLoaded,
|
||||
loadNodeFrequencies,
|
||||
getNodeFrequency,
|
||||
getNodeFrequencyByName
|
||||
}
|
||||
})
|
||||
|
||||
@@ -227,13 +227,13 @@ export function validateTaskItem(taskItem: unknown) {
|
||||
return result
|
||||
}
|
||||
|
||||
function inputSpec(
|
||||
spec: [ZodType, ZodType],
|
||||
function inputSpec<TType extends ZodType, TSpec extends ZodType>(
|
||||
spec: [TType, TSpec],
|
||||
allowUpcast: boolean = true
|
||||
): ZodType {
|
||||
) {
|
||||
const [inputType, inputSpec] = spec
|
||||
// e.g. "INT" => ["INT", {}]
|
||||
const upcastTypes: ZodType[] = allowUpcast
|
||||
const upcastTypes = allowUpcast
|
||||
? [inputType.transform((type) => [type, {}])]
|
||||
: []
|
||||
|
||||
@@ -361,6 +361,7 @@ const zComfyNodeDef = z.object({
|
||||
})
|
||||
|
||||
// `/object_info`
|
||||
export type InputSpec = z.infer<typeof zInputSpec>
|
||||
export type ComfyInputsSpec = z.infer<typeof zComfyInputsSpec>
|
||||
export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
|
||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { ComfyNodeDef } from './apiTypes'
|
||||
|
||||
export declare class ComfyLGraphNode extends LGraphNode {
|
||||
static comfyClass: string
|
||||
static title: string
|
||||
static nodeData?: ComfyNodeDef
|
||||
static category?: string
|
||||
|
||||
constructor(title?: string)
|
||||
}
|
||||
36
src/types/litegraph-augmentation.d.ts
vendored
@@ -1,14 +1,33 @@
|
||||
import '@comfyorg/litegraph'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
import type { LLink } from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* ComfyUI extensions of litegraph
|
||||
*/
|
||||
declare module '@comfyorg/litegraph' {
|
||||
interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||
type?: string
|
||||
comfyClass: string
|
||||
title: string
|
||||
nodeData?: ComfyNodeDef
|
||||
category?: string
|
||||
new (): T
|
||||
}
|
||||
|
||||
interface LGraphNode {
|
||||
constructor: LGraphNodeConstructor
|
||||
|
||||
/**
|
||||
* Callback fired on each node after the graph is configured
|
||||
*/
|
||||
onAfterGraphConfigured?(): void
|
||||
onNodeCreated?(this: LGraphNode): void
|
||||
setInnerNodes?(nodes: LGraphNode[]): void
|
||||
applyToGraph?(extraLinks?: LLink[]): void
|
||||
updateLink?(link: LLink): LLink | null
|
||||
|
||||
comfyClass?: string
|
||||
|
||||
/**
|
||||
* If the node is a frontend only node and should not be serialized into the prompt.
|
||||
@@ -24,25 +43,30 @@ declare module '@comfyorg/litegraph' {
|
||||
}
|
||||
|
||||
interface IWidget<TValue = any, TOptions = any> {
|
||||
type?: string
|
||||
|
||||
/**
|
||||
* Allows for additional cleanup when removing a widget when converting to input.
|
||||
*/
|
||||
onRemove?(): void
|
||||
|
||||
serializeValue?(node?: LGraphNode, i?: string)
|
||||
beforeQueued?(): void
|
||||
|
||||
/**
|
||||
* DOM element used for the widget
|
||||
*/
|
||||
element?: HTMLElement
|
||||
|
||||
tooltip?: string
|
||||
|
||||
origType?: IWidget['type']
|
||||
origComputeSize?: IWidget['computeSize']
|
||||
origSerializeValue?: IWidget['serializeValue']
|
||||
}
|
||||
|
||||
interface INodeOutputSlot {
|
||||
widget?: unknown
|
||||
}
|
||||
|
||||
interface INodeInputSlot {
|
||||
widget?: unknown
|
||||
interface INodeSlot {
|
||||
widget?: unknown & { name?: string }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
src/types/litegraph-core-augmentation.d.ts
vendored
@@ -6,6 +6,7 @@ declare module '@comfyorg/litegraph' {
|
||||
interface LiteGraphExtended {
|
||||
search_filter_enabled: boolean
|
||||
middle_click_slot_add_default_node: boolean
|
||||
registered_node_types: Record<string, LGraphNodeConstructor>
|
||||
registered_slot_out_types: Record<string, { nodes: string[] }>
|
||||
registered_slot_in_types: Record<string, { nodes: string[] }>
|
||||
slot_types_out: string[]
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type NodeSourceType = 'core' | 'custom_nodes'
|
||||
export enum NodeSourceType {
|
||||
Core = 'core',
|
||||
CustomNodes = 'custom_nodes',
|
||||
Unknown = 'unknown'
|
||||
}
|
||||
|
||||
export type NodeSource = {
|
||||
type: NodeSourceType
|
||||
className: string
|
||||
@@ -6,24 +11,41 @@ export type NodeSource = {
|
||||
badgeText: string
|
||||
}
|
||||
|
||||
export const getNodeSource = (python_module: string): NodeSource => {
|
||||
const UNKNOWN_NODE_SOURCE: NodeSource = {
|
||||
type: NodeSourceType.Unknown,
|
||||
className: 'comfy-unknown',
|
||||
displayText: 'Unknown',
|
||||
badgeText: '?'
|
||||
}
|
||||
|
||||
const shortenNodeName = (name: string) => {
|
||||
return name
|
||||
.replace(/^(ComfyUI-|ComfyUI_|Comfy-|Comfy_)/, '')
|
||||
.replace(/(-ComfyUI|_ComfyUI|-Comfy|_Comfy)$/, '')
|
||||
}
|
||||
|
||||
export const getNodeSource = (python_module?: string): NodeSource => {
|
||||
if (!python_module) {
|
||||
return UNKNOWN_NODE_SOURCE
|
||||
}
|
||||
const modules = python_module.split('.')
|
||||
if (['nodes', 'comfy_extras'].includes(modules[0])) {
|
||||
return {
|
||||
type: 'core',
|
||||
type: NodeSourceType.Core,
|
||||
className: 'comfy-core',
|
||||
displayText: 'Comfy Core',
|
||||
badgeText: '🦊'
|
||||
}
|
||||
} else if (modules[0] === 'custom_nodes') {
|
||||
const displayName = shortenNodeName(modules[1])
|
||||
return {
|
||||
type: 'custom_nodes',
|
||||
type: NodeSourceType.CustomNodes,
|
||||
className: 'comfy-custom-nodes',
|
||||
displayText: modules[1],
|
||||
badgeText: modules[1]
|
||||
displayText: displayName,
|
||||
badgeText: displayName
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown node source: ${python_module}`)
|
||||
return UNKNOWN_NODE_SOURCE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
|
||||
export interface TreeExplorerNode<T = any> {
|
||||
key: string
|
||||
label: string
|
||||
@@ -7,14 +9,17 @@ export interface TreeExplorerNode<T = any> {
|
||||
icon?: string
|
||||
getIcon?: (node: TreeExplorerNode<T>) => string
|
||||
// Function to handle renaming the node
|
||||
handleRename?: (node: TreeExplorerNode<T>, newName: string) => void
|
||||
handleRename?: (
|
||||
node: TreeExplorerNode<T>,
|
||||
newName: string
|
||||
) => void | Promise<void>
|
||||
// Function to handle deleting the node
|
||||
handleDelete?: (node: TreeExplorerNode<T>) => void
|
||||
handleDelete?: (node: TreeExplorerNode<T>) => void | Promise<void>
|
||||
// Function to handle adding a child node
|
||||
handleAddChild?: (
|
||||
node: TreeExplorerNode<T>,
|
||||
child: TreeExplorerNode<T>
|
||||
) => void
|
||||
) => void | Promise<void>
|
||||
// Whether the node is draggable
|
||||
draggable?: boolean
|
||||
// Whether the node is droppable
|
||||
@@ -23,11 +28,18 @@ export interface TreeExplorerNode<T = any> {
|
||||
handleDrop?: (
|
||||
node: TreeExplorerNode<T>,
|
||||
data: TreeExplorerDragAndDropData
|
||||
) => void
|
||||
) => void | Promise<void>
|
||||
// Function to handle clicking a node
|
||||
handleClick?: (node: TreeExplorerNode<T>, event: MouseEvent) => void
|
||||
handleClick?: (
|
||||
node: TreeExplorerNode<T>,
|
||||
event: MouseEvent
|
||||
) => void | Promise<void>
|
||||
// Function to handle errors
|
||||
handleError?: (error: Error) => void
|
||||
// Extra context menu items
|
||||
contextMenuItems?:
|
||||
| MenuItem[]
|
||||
| ((targetNode: RenderedTreeExplorerNode) => MenuItem[])
|
||||
}
|
||||
|
||||
export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
type RGB = { r: number; g: number; b: number }
|
||||
type HSL = { h: number; s: number; l: number }
|
||||
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||
|
||||
function rgbToHsl({ r, g, b }: RGB): HSL {
|
||||
r /= 255
|
||||
@@ -92,6 +93,16 @@ function rgbToHex({ r, g, b }: RGB): string {
|
||||
)
|
||||
}
|
||||
|
||||
function identifyColorFormat(color: string): ColorFormat | null {
|
||||
if (!color) return null
|
||||
if (color.startsWith('#')) return 'hex'
|
||||
if (/^rgba?\(\d+,\s*\d+,\s*\d+/.test(color))
|
||||
return color.includes('rgba') ? 'rgba' : 'rgb'
|
||||
if (/^hsla?\(\d+(\.\d+)?,\s*\d+(\.\d+)?%,\s*\d+(\.\d+)?%/.test(color))
|
||||
return color.includes('hsla') ? 'hsla' : 'hsl'
|
||||
return null
|
||||
}
|
||||
|
||||
export function lightenColor(hex: string, amount: number): string {
|
||||
let rgb = hexToRgb(hex)
|
||||
const hsl = rgbToHsl(rgb)
|
||||
@@ -99,3 +110,46 @@ export function lightenColor(hex: string, amount: number): string {
|
||||
rgb = hslToRgb(hsl)
|
||||
return rgbToHex(rgb)
|
||||
}
|
||||
|
||||
export function applyOpacity(color: string, opacity: number): string {
|
||||
const colorFormat = identifyColorFormat(color)
|
||||
|
||||
if (!colorFormat) {
|
||||
console.warn(
|
||||
`Unsupported color format in user color palette for color: ${color}`
|
||||
)
|
||||
return color
|
||||
}
|
||||
|
||||
const clampedOpacity = Math.max(0, Math.min(1, opacity))
|
||||
|
||||
switch (colorFormat) {
|
||||
case 'hex': {
|
||||
const { r, g, b } = hexToRgb(color)
|
||||
if (isNaN(r) || isNaN(g) || isNaN(b)) {
|
||||
return color
|
||||
}
|
||||
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`
|
||||
}
|
||||
case 'rgb': {
|
||||
return color.replace('rgb', 'rgba').replace(')', `, ${clampedOpacity})`)
|
||||
}
|
||||
case 'rgba': {
|
||||
return color.replace(
|
||||
/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,[^)]+\)/,
|
||||
`rgba($1, $2, $3, ${clampedOpacity})`
|
||||
)
|
||||
}
|
||||
case 'hsl': {
|
||||
return color.replace('hsl', 'hsla').replace(')', `, ${clampedOpacity})`)
|
||||
}
|
||||
case 'hsla': {
|
||||
return color.replace(
|
||||
/hsla\(\s*(\d+)\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*,[^)]+\)/,
|
||||
`hsla($1, $2%, $3%, ${clampedOpacity})`
|
||||
)
|
||||
}
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,3 +39,23 @@ export function highlightQuery(text: string, query: string) {
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
return text.replace(regex, '<span class="highlight">$1</span>')
|
||||
}
|
||||
|
||||
export function formatNumberWithSuffix(
|
||||
num: number,
|
||||
{
|
||||
precision = 1,
|
||||
roundToInt = false
|
||||
}: { precision?: number; roundToInt?: boolean } = {}
|
||||
): string {
|
||||
const suffixes = ['', 'k', 'm', 'b', 't']
|
||||
const absNum = Math.abs(num)
|
||||
|
||||
if (absNum < 1000) {
|
||||
return roundToInt ? Math.round(num).toString() : num.toFixed(precision)
|
||||
}
|
||||
|
||||
const exp = Math.min(Math.floor(Math.log10(absNum) / 3), suffixes.length - 1)
|
||||
const formattedNum = (num / Math.pow(1000, exp)).toFixed(precision)
|
||||
|
||||
return `${formattedNum}${suffixes[exp]}`
|
||||
}
|
||||
|
||||
6
src/vite-env.d.ts
vendored
@@ -1 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__COMFYUI_FRONTEND_VERSION__: string
|
||||
}
|
||||
}
|
||||
|
||||
50
tests-ui/tests/colorUtil.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { applyOpacity } from '@/utils/colorUtil'
|
||||
|
||||
describe('colorUtil - applyOpacity', () => {
|
||||
// Same color in various formats
|
||||
const solarized = {
|
||||
hex: '#073642',
|
||||
rgb: 'rgb(7, 54, 66)',
|
||||
rgba: 'rgba(7, 54, 66, 1)',
|
||||
hsl: 'hsl(192, 80.80%, 14.30%)',
|
||||
hsla: 'hsla(192, 80.80%, 14.30%, 1)'
|
||||
}
|
||||
|
||||
const opacity = 0.5
|
||||
|
||||
it('applies opacity consistently to hex, rgb, and rgba formats', () => {
|
||||
const hexResult = applyOpacity(solarized.hex, opacity)
|
||||
const rgbResult = applyOpacity(solarized.rgb, opacity)
|
||||
const rgbaResult = applyOpacity(solarized.rgba, opacity)
|
||||
|
||||
expect(hexResult).toBe(rgbResult)
|
||||
expect(rgbResult).toBe(rgbaResult)
|
||||
})
|
||||
|
||||
it('applies opacity consistently to hsl and hsla formats', () => {
|
||||
const hslResult = applyOpacity(solarized.hsl, opacity)
|
||||
const hslaResult = applyOpacity(solarized.hsla, opacity)
|
||||
|
||||
expect(hslResult).toBe(hslaResult)
|
||||
})
|
||||
|
||||
it('returns the original value for invalid color formats', () => {
|
||||
const invalidColors = [
|
||||
'#GGGGGG', // Invalid hex code (non-hex characters)
|
||||
'rgb(300, -10, 256)', // Invalid RGB values (out of range)
|
||||
'xyz(255, 255, 255)', // Unsupported format
|
||||
'rgba(255, 255, 255)', // Missing alpha in RGBA
|
||||
'hsl(100, 50, 50%)' // Missing percentage sign for saturation
|
||||
]
|
||||
|
||||
invalidColors.forEach((color) => {
|
||||
const result = applyOpacity(color, opacity)
|
||||
expect(result).toBe(color)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the original value for null or undefined inputs', () => {
|
||||
expect(applyOpacity(null, opacity)).toBe(null)
|
||||
expect(applyOpacity(undefined, opacity)).toBe(undefined)
|
||||
})
|
||||
})
|
||||
@@ -51,7 +51,11 @@ const EXAMPLE_NODE_DEFS: ComfyNodeDefImpl[] = [
|
||||
category: 'latent/batch',
|
||||
output_node: false
|
||||
}
|
||||
].map((nodeDef) => plainToClass(ComfyNodeDefImpl, nodeDef))
|
||||
].map((nodeDef) => {
|
||||
const def = plainToClass(ComfyNodeDefImpl, nodeDef)
|
||||
def['postProcessSearchScores'] = (s) => s
|
||||
return def
|
||||
})
|
||||
|
||||
describe('nodeSearchService', () => {
|
||||
it('searches with input filter', () => {
|
||||
|
||||
@@ -132,6 +132,9 @@ export const mockNodeDefStore = () => {
|
||||
}
|
||||
|
||||
jest.mock('@/stores/nodeDefStore', () => ({
|
||||
useNodeDefStore: jest.fn(() => mockedNodeDefStore)
|
||||
useNodeDefStore: jest.fn(() => mockedNodeDefStore),
|
||||
useNodeFrequencyStore: jest.fn(() => ({
|
||||
getNodeFrequencyByName: jest.fn(() => 0)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"src/types/**/*.d.ts",
|
||||
"tests-ui/**/*"
|
||||
"tests-ui/**/*",
|
||||
"global.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -167,5 +167,9 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': '/src'
|
||||
}
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
exclude: ['@comfyorg/litegraph']
|
||||
}
|
||||
}) as UserConfigExport
|
||||
|
||||