Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa2fa269d | ||
|
|
a2cf6a7be2 | ||
|
|
e493473c35 | ||
|
|
415a2e7fa5 | ||
|
|
ba9a3b4a9b | ||
|
|
174c52958f | ||
|
|
4e41db2d6a | ||
|
|
e8daebdc0c | ||
|
|
582acd7bd1 | ||
|
|
48fe14e263 | ||
|
|
f9fd0f59ff | ||
|
|
3fe4b4b856 | ||
|
|
c510b344af | ||
|
|
980dd285ad | ||
|
|
2b60244e4a | ||
|
|
45a866f194 | ||
|
|
091b8a74fb | ||
|
|
74fa4a2c2d | ||
|
|
327b67a022 | ||
|
|
d0a4db5f4f | ||
|
|
861eaa155f | ||
|
|
3550e7f7f1 | ||
|
|
7d25d976d1 | ||
|
|
7025e321de | ||
|
|
429fa75fcc | ||
|
|
347563adf9 | ||
|
|
9bdb3c0332 | ||
|
|
12c699cc87 | ||
|
|
588cfeca4b | ||
|
|
f983f42c45 | ||
|
|
fef780a72f | ||
|
|
ebdcd92977 | ||
|
|
c98ea5ba01 | ||
|
|
48f84a46cd | ||
|
|
9483cfe915 | ||
|
|
862e2c2607 | ||
|
|
a08ec196c7 | ||
|
|
17db1e6074 | ||
|
|
65a8dbb7e0 | ||
|
|
efd8b5c19d | ||
|
|
0a188aaf72 | ||
|
|
eb45cca031 | ||
|
|
d8d6fa86e4 | ||
|
|
880ac4fa5a | ||
|
|
7d3b8dc44c |
@@ -108,10 +108,29 @@ class NodeLibrarySidebarTab {
|
||||
class ComfyMenu {
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly themeToggleButton: Locator
|
||||
public readonly saveButton: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.sideToolbar = page.locator('.side-tool-bar-container')
|
||||
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
||||
this.saveButton = page
|
||||
.locator('button[title="Save the current workflow"]')
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
async saveWorkflow(name: string) {
|
||||
const acceptDialog = async (dialog) => {
|
||||
await dialog.accept(name)
|
||||
}
|
||||
this.page.on('dialog', acceptDialog)
|
||||
|
||||
await this.saveButton.click()
|
||||
|
||||
// Wait a moment to ensure the dialog has been handled
|
||||
await this.page.waitForTimeout(300)
|
||||
|
||||
// Remove the dialog listener
|
||||
this.page.off('dialog', acceptDialog)
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
@@ -172,12 +191,16 @@ export class ComfyPage {
|
||||
|
||||
async getGraphNodesCount(): Promise<number> {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app']?.graph?._nodes?.length || 0
|
||||
return window['app']?.graph?.nodes?.length || 0
|
||||
})
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.goto()
|
||||
await this.page.evaluate(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
// Unify font for consistent screenshots.
|
||||
await this.page.addStyleTag({
|
||||
url: 'https://fonts.googleapis.com/css2?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'
|
||||
@@ -366,25 +389,40 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
// Default graph positions
|
||||
get clipTextEncodeNode1InputSlot(): Position {
|
||||
return { x: 427, y: 198 }
|
||||
}
|
||||
|
||||
get clipTextEncodeNode2InputSlot(): Position {
|
||||
return { x: 422, y: 402 }
|
||||
}
|
||||
|
||||
// A point on input edge.
|
||||
get clipTextEncodeNode2InputLinkPath(): Position {
|
||||
return {
|
||||
x: 395,
|
||||
y: 422
|
||||
}
|
||||
}
|
||||
|
||||
get loadCheckpointNodeClipOutputSlot(): Position {
|
||||
return { x: 332, y: 509 }
|
||||
}
|
||||
|
||||
get emptySpace(): Position {
|
||||
return { x: 427, y: 98 }
|
||||
}
|
||||
|
||||
async disconnectEdge() {
|
||||
// CLIP input anchor
|
||||
await this.page.mouse.move(427, 198)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(427, 98)
|
||||
await this.page.mouse.up()
|
||||
// Move out the way to avoid highlight of menu item.
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.nextFrame()
|
||||
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
|
||||
}
|
||||
|
||||
async connectEdge() {
|
||||
// CLIP output anchor on Load Checkpoint Node.
|
||||
await this.page.mouse.move(332, 509)
|
||||
await this.page.mouse.down()
|
||||
// CLIP input anchor on CLIP Text Encode Node.
|
||||
await this.page.mouse.move(427, 198)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
await this.dragAndDrop(
|
||||
this.loadCheckpointNodeClipOutputSlot,
|
||||
this.clipTextEncodeNode1InputSlot
|
||||
)
|
||||
}
|
||||
|
||||
async adjustWidgetValue() {
|
||||
@@ -422,6 +460,24 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async panWithTouch(offset: Position, safeSpot?: Position) {
|
||||
safeSpot = safeSpot || { x: 10, y: 10 }
|
||||
const client = await this.page.context().newCDPSession(this.page)
|
||||
await client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints: [safeSpot]
|
||||
})
|
||||
await client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchMove',
|
||||
touchPoints: [{ x: offset.x + safeSpot.x, y: offset.y + safeSpot.y }]
|
||||
})
|
||||
await client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: []
|
||||
})
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async rightClickCanvas() {
|
||||
await this.page.mouse.click(10, 10, { button: 'right' })
|
||||
await this.nextFrame()
|
||||
@@ -593,6 +649,16 @@ export class ComfyPage {
|
||||
revertAfter
|
||||
)
|
||||
}
|
||||
|
||||
async convertAllNodesToGroupNode(groupNodeName: string) {
|
||||
this.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
})
|
||||
await this.canvas.press('Control+a')
|
||||
await this.rightClickEmptyLatentNode()
|
||||
await this.page.getByText('Convert to Group Node').click()
|
||||
await this.nextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
|
||||
55
browser_tests/browserTabTitle.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Browser tab title', () => {
|
||||
test.describe('Beta Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('Can display workflow name', async ({ comfyPage }) => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].workflowManager.activeWorkflow.name
|
||||
})
|
||||
// Note: unsaved workflow name is always prepended with "*".
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName}`)
|
||||
})
|
||||
|
||||
test('Can display workflow name with unsaved changes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].workflowManager.activeWorkflow.name
|
||||
})
|
||||
// Note: unsaved workflow name is always prepended with "*".
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName}`)
|
||||
|
||||
await comfyPage.menu.saveWorkflow('test')
|
||||
expect(await comfyPage.page.title()).toBe('test')
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.fill('Hello World')
|
||||
await comfyPage.clickEmptySpace()
|
||||
expect(await comfyPage.page.title()).toBe(`*test`)
|
||||
|
||||
// Delete the saved workflow for cleanup.
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
window['app'].workflowManager.activeWorkflow.delete()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Legacy Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('Can display default title', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.page.title()).toBe('ComfyUI')
|
||||
})
|
||||
})
|
||||
})
|
||||
149
browser_tests/colorPalette.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
const customColorPalettes = {
|
||||
obsidian: {
|
||||
version: 102,
|
||||
id: 'obsidian',
|
||||
name: 'Obsidian',
|
||||
colors: {
|
||||
node_slot: {
|
||||
CLIP: '#FFD500',
|
||||
CLIP_VISION: '#A8DADC',
|
||||
CLIP_VISION_OUTPUT: '#ad7452',
|
||||
CONDITIONING: '#FFA931',
|
||||
CONTROL_NET: '#6EE7B7',
|
||||
IMAGE: '#64B5F6',
|
||||
LATENT: '#FF9CF9',
|
||||
MASK: '#81C784',
|
||||
MODEL: '#B39DDB',
|
||||
STYLE_MODEL: '#C2FFAE',
|
||||
VAE: '#FF6E6E',
|
||||
TAESD: '#DCC274',
|
||||
PIPE_LINE: '#7737AA',
|
||||
PIPE_LINE_SDXL: '#7737AA',
|
||||
INT: '#29699C',
|
||||
XYPLOT: '#74DA5D',
|
||||
X_Y: '#38291f'
|
||||
},
|
||||
litegraph_base: {
|
||||
BACKGROUND_IMAGE:
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=',
|
||||
CLEAR_BACKGROUND_COLOR: '#222222',
|
||||
NODE_TITLE_COLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_SELECTED_TITLE_COLOR: '#FFF',
|
||||
NODE_TEXT_SIZE: 14,
|
||||
NODE_TEXT_COLOR: '#b8b8b8',
|
||||
NODE_SUBTEXT_SIZE: 12,
|
||||
NODE_DEFAULT_COLOR: 'rgba(0,0,0,.8)',
|
||||
NODE_DEFAULT_BGCOLOR: 'rgba(22,22,22,.8)',
|
||||
NODE_DEFAULT_BOXCOLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_DEFAULT_SHAPE: 'box',
|
||||
NODE_BOX_OUTLINE_COLOR: '#236692',
|
||||
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0)',
|
||||
DEFAULT_GROUP_FONT: 24,
|
||||
WIDGET_BGCOLOR: '#242424',
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
|
||||
LINK_COLOR: '#9A9',
|
||||
EVENT_LINK_COLOR: '#A86',
|
||||
CONNECTING_LINK_COLOR: '#AFA'
|
||||
},
|
||||
comfy_base: {
|
||||
'fg-color': '#fff',
|
||||
'bg-color': '#242424',
|
||||
'comfy-menu-bg': 'rgba(24,24,24,.9)',
|
||||
'comfy-input-bg': '#262626',
|
||||
'input-text': '#ddd',
|
||||
'descrip-text': '#999',
|
||||
'drag-text': '#ccc',
|
||||
'error-text': '#ff4444',
|
||||
'border-color': '#29292c',
|
||||
'tr-even-bg-color': 'rgba(28,28,28,.9)',
|
||||
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
|
||||
}
|
||||
}
|
||||
},
|
||||
obsidian_dark: {
|
||||
version: 102,
|
||||
id: 'obsidian_dark',
|
||||
name: 'Obsidian Dark',
|
||||
colors: {
|
||||
node_slot: {
|
||||
CLIP: '#FFD500',
|
||||
CLIP_VISION: '#A8DADC',
|
||||
CLIP_VISION_OUTPUT: '#ad7452',
|
||||
CONDITIONING: '#FFA931',
|
||||
CONTROL_NET: '#6EE7B7',
|
||||
IMAGE: '#64B5F6',
|
||||
LATENT: '#FF9CF9',
|
||||
MASK: '#81C784',
|
||||
MODEL: '#B39DDB',
|
||||
STYLE_MODEL: '#C2FFAE',
|
||||
VAE: '#FF6E6E',
|
||||
TAESD: '#DCC274',
|
||||
PIPE_LINE: '#7737AA',
|
||||
PIPE_LINE_SDXL: '#7737AA',
|
||||
INT: '#29699C',
|
||||
XYPLOT: '#74DA5D',
|
||||
X_Y: '#38291f'
|
||||
},
|
||||
litegraph_base: {
|
||||
BACKGROUND_IMAGE:
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==',
|
||||
CLEAR_BACKGROUND_COLOR: '#000',
|
||||
NODE_TITLE_COLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_SELECTED_TITLE_COLOR: '#FFF',
|
||||
NODE_TEXT_SIZE: 14,
|
||||
NODE_TEXT_COLOR: '#b8b8b8',
|
||||
NODE_SUBTEXT_SIZE: 12,
|
||||
NODE_DEFAULT_COLOR: 'rgba(0,0,0,.8)',
|
||||
NODE_DEFAULT_BGCOLOR: 'rgba(22,22,22,.8)',
|
||||
NODE_DEFAULT_BOXCOLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_DEFAULT_SHAPE: 'box',
|
||||
NODE_BOX_OUTLINE_COLOR: '#236692',
|
||||
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0)',
|
||||
DEFAULT_GROUP_FONT: 24,
|
||||
WIDGET_BGCOLOR: '#242424',
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
|
||||
LINK_COLOR: '#9A9',
|
||||
EVENT_LINK_COLOR: '#A86',
|
||||
CONNECTING_LINK_COLOR: '#AFA'
|
||||
},
|
||||
comfy_base: {
|
||||
'fg-color': '#fff',
|
||||
'bg-color': '#242424',
|
||||
'comfy-menu-bg': 'rgba(24,24,24,.9)',
|
||||
'comfy-input-bg': '#262626',
|
||||
'input-text': '#ddd',
|
||||
'descrip-text': '#999',
|
||||
'drag-text': '#ccc',
|
||||
'error-text': '#ff4444',
|
||||
'border-color': '#29292c',
|
||||
'tr-even-bg-color': 'rgba(28,28,28,.9)',
|
||||
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Color Palette', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 96 KiB |
56
browser_tests/groupNode.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
expect(await tab.getFolder('group nodes').count()).toBe(1)
|
||||
})
|
||||
|
||||
test('Can be added to canvas using node library sidebar', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
||||
|
||||
// Add group node from node library sidebar
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
await tab.getFolder('group nodes').click()
|
||||
await tab.getFolder('workflow').click()
|
||||
await tab.getFolder('workflow').last().click()
|
||||
await tab.getNode(groupNodeName).click()
|
||||
|
||||
// Verify the node is added to the canvas
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
|
||||
})
|
||||
|
||||
test('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'group-node-copy-added-from-search.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
||||
await comfyPage.page.mouse.move(47, 173)
|
||||
const tooltipTimeout = 500
|
||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 78 KiB |
@@ -24,16 +24,56 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
|
||||
})
|
||||
|
||||
test('Can disconnect/connect edge', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'disconnected-edge-with-menu.png'
|
||||
)
|
||||
await comfyPage.connectEdge()
|
||||
// Litegraph renders edge with a slight offset.
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
|
||||
maxDiffPixels: 50
|
||||
test.describe('Edge Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
|
||||
})
|
||||
|
||||
test('Can disconnect/connect edge', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
||||
await comfyPage.connectEdge()
|
||||
// Litegraph renders edge with a slight offset.
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
|
||||
maxDiffPixels: 50
|
||||
})
|
||||
})
|
||||
|
||||
// Chromium 2x cannot move link.
|
||||
// See https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/10876381315/job/30176211513
|
||||
test.skip('Can move link', async ({ comfyPage }) => {
|
||||
await comfyPage.dragAndDrop(
|
||||
comfyPage.clipTextEncodeNode1InputSlot,
|
||||
comfyPage.emptySpace
|
||||
)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
||||
await comfyPage.dragAndDrop(
|
||||
comfyPage.clipTextEncodeNode2InputSlot,
|
||||
comfyPage.clipTextEncodeNode1InputSlot
|
||||
)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png')
|
||||
})
|
||||
|
||||
// Copy link is not working on CI at all
|
||||
// Chromium 2x recognize it as dragging canvas.
|
||||
// Chromium triggers search box after link release. The link is indeed copied.
|
||||
// See https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/10876381315/job/30176211513
|
||||
test.skip('Can copy link by shift-drag existing link', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDrop(
|
||||
comfyPage.clipTextEncodeNode1InputSlot,
|
||||
comfyPage.emptySpace
|
||||
)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(
|
||||
comfyPage.clipTextEncodeNode2InputLinkPath,
|
||||
comfyPage.clipTextEncodeNode1InputSlot
|
||||
)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -310,6 +350,12 @@ test.describe('Canvas Interaction', () => {
|
||||
await comfyPage.pan({ x: 800, y: 300 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
|
||||
})
|
||||
|
||||
test('@mobile Can pan with touch', async ({ comfyPage }) => {
|
||||
await comfyPage.closeMenu()
|
||||
await comfyPage.panWithTouch({ x: 200, y: 200 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Widget Interaction', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -10,7 +10,7 @@ test.describe('Node Badge', () => {
|
||||
const app = window['app'] as ComfyApp
|
||||
const graph = app.graph
|
||||
// @ts-expect-error - accessing private property
|
||||
const nodes = graph._nodes
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
|
||||
@@ -28,7 +28,7 @@ test.describe('Node Badge', () => {
|
||||
const app = window['app'] as ComfyApp
|
||||
const graph = app.graph
|
||||
// @ts-expect-error - accessing private property
|
||||
const nodes = graph._nodes
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
node.badges = [
|
||||
@@ -49,7 +49,7 @@ test.describe('Node Badge', () => {
|
||||
const app = window['app'] as ComfyApp
|
||||
const graph = app.graph
|
||||
// @ts-expect-error - accessing private property
|
||||
const nodes = graph._nodes
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
|
||||
|
||||
12
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.51",
|
||||
"version": "1.2.57",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.51",
|
||||
"version": "1.2.57",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.71",
|
||||
"@comfyorg/litegraph": "^0.7.75",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
@@ -1909,9 +1909,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.7.71",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.71.tgz",
|
||||
"integrity": "sha512-WjV5ZY+yfNfu9n99bdfeUTdeFvCkOW/8KIFsCFu6aqGGUbsuRRwTbXk+qOvcDquzPGzrnDmo4z7UQpaMqCT9nA==",
|
||||
"version": "0.7.75",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.75.tgz",
|
||||
"integrity": "sha512-RNYZVMoJ/a5btwP+S124FnrIVlwOdv6uNsTdYfwv7L8teDpwvf/TQa66QfCePqUlypBKEhKw+avTncLAu2FYUw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.51",
|
||||
"version": "1.2.57",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -62,7 +62,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.71",
|
||||
"@comfyorg/litegraph": "^0.7.75",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
|
||||
14
src/App.vue
@@ -5,6 +5,7 @@
|
||||
<GlobalDialog />
|
||||
<GlobalToast />
|
||||
<UnloadWindowConfirmDialog />
|
||||
<BrowserTabTitle />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -27,12 +28,15 @@ import NodeLibrarySidebarTab from './components/sidebar/tabs/NodeLibrarySidebarT
|
||||
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'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const theme = computed<string>(() =>
|
||||
@@ -123,10 +127,19 @@ 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(() => {
|
||||
api.addEventListener('status', onStatus)
|
||||
api.addEventListener('reconnecting', onReconnecting)
|
||||
api.addEventListener('reconnected', onReconnected)
|
||||
executionStore.bindExecutionEvents()
|
||||
try {
|
||||
init()
|
||||
} catch (e) {
|
||||
@@ -138,6 +151,7 @@ onUnmounted(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
api.removeEventListener('reconnecting', onReconnecting)
|
||||
api.removeEventListener('reconnected', onReconnected)
|
||||
executionStore.unbindExecutionEvents()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
40
src/components/BrowserTabTitle.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- This component does not render anything visible. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const executionStore = useExecutionStore()
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.previousWorkflowUnsaved ? ' *' : ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.name
|
||||
return workflowName ? isUnsavedText.value + workflowName : DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const title = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(betaMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
useTitle(title)
|
||||
</script>
|
||||
@@ -54,8 +54,8 @@ const updateValue = (newValue: number | null) => {
|
||||
newValue = Number(props.min) || 0
|
||||
}
|
||||
|
||||
const min = Number(props.min) || Number.NEGATIVE_INFINITY
|
||||
const max = Number(props.max) || Number.POSITIVE_INFINITY
|
||||
const min = Number(props.min ?? Number.NEGATIVE_INFINITY)
|
||||
const max = Number(props.max ?? Number.POSITIVE_INFINITY)
|
||||
const step = Number(props.step) || 1
|
||||
|
||||
// Ensure the value is within the allowed range
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="flex flex-col items-center">
|
||||
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem"></i>
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ message }}</p>
|
||||
<p class="whitespace-pre-line text-center">{{ message }}</p>
|
||||
<Button
|
||||
v-if="buttonLabel"
|
||||
:label="buttonLabel"
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
@click="$emit('showFilter', $event)"
|
||||
/>
|
||||
</IconField>
|
||||
<div class="search-filters" v-if="filters">
|
||||
<div
|
||||
class="search-filters pt-2 flex flex-wrap gap-2"
|
||||
v-if="filters?.length"
|
||||
>
|
||||
<SearchFilterChip
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
@@ -92,8 +95,4 @@ const handleInput = (event: Event) => {
|
||||
width: auto;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
@apply pt-2 flex flex-wrap gap-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class="tree-explorer"
|
||||
:class="props.class"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
:value="renderedRoots"
|
||||
selectionMode="single"
|
||||
:pt="{
|
||||
@@ -40,11 +41,14 @@ import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
|
||||
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
|
||||
provide('expandedKeys', expandedKeys)
|
||||
const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
|
||||
provide('selectionKeys', selectionKeys)
|
||||
const props = defineProps<{
|
||||
roots: TreeExplorerNode[]
|
||||
class?: string
|
||||
@@ -89,6 +93,9 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
}
|
||||
}
|
||||
const onNodeContentClick = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
if (node.handleClick) {
|
||||
node.handleClick(node, e)
|
||||
}
|
||||
emit('nodeClick', node, e)
|
||||
}
|
||||
const menu = ref(null)
|
||||
@@ -105,25 +112,31 @@ const deleteCommand = (node: RenderedTreeExplorerNode) => {
|
||||
node.handleDelete?.(node)
|
||||
emit('nodeDelete', node)
|
||||
}
|
||||
const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: t('rename'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: () => renameCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleRename !== undefined
|
||||
},
|
||||
{
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => deleteCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleDelete !== undefined
|
||||
},
|
||||
...(props.extraMenuItems
|
||||
? typeof props.extraMenuItems === 'function'
|
||||
? props.extraMenuItems(menuTargetNode.value)
|
||||
: props.extraMenuItems
|
||||
: [])
|
||||
])
|
||||
const menuItems = computed<MenuItem[]>(() =>
|
||||
[
|
||||
{
|
||||
label: t('rename'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: () => renameCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleRename !== undefined
|
||||
},
|
||||
{
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => deleteCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleDelete !== undefined
|
||||
},
|
||||
...(props.extraMenuItems
|
||||
? typeof props.extraMenuItems === 'function'
|
||||
? props.extraMenuItems(menuTargetNode.value)
|
||||
: props.extraMenuItems
|
||||
: [])
|
||||
].map((menuItem) => ({
|
||||
...menuItem,
|
||||
command: wrapCommandWithErrorHandler(menuItem.command)
|
||||
}))
|
||||
)
|
||||
|
||||
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
|
||||
menuTargetNode.value = node
|
||||
emit('contextMenu', node, e)
|
||||
@@ -131,6 +144,17 @@ const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
|
||||
menu.value?.show(e)
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandling = useErrorHandling()
|
||||
const wrapCommandWithErrorHandler = (
|
||||
command: (event: MenuItemCommandEvent) => void
|
||||
) => {
|
||||
return errorHandling.wrapWithErrorHandling(
|
||||
command,
|
||||
menuTargetNode.value?.handleError
|
||||
)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
renameCommand,
|
||||
deleteCommand
|
||||
@@ -145,6 +169,7 @@ defineExpose({
|
||||
margin-left: var(--p-tree-node-gap);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* The following styles are necessary to avoid layout shift when dragging nodes over folders.
|
||||
* By setting the position to relative on the parent and using an absolutely positioned pseudo-element,
|
||||
@@ -153,6 +178,7 @@ defineExpose({
|
||||
:deep(.p-tree-node-content:has(.tree-folder)) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.p-tree-node-content:has(.tree-folder.can-drop))::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- This component does not render anything visible. It is used to confirm
|
||||
the user wants to close the window, and if they do, it will call the
|
||||
beforeunload event. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
@@ -20,7 +20,7 @@ import EditableText from '@/components/common/EditableText.vue'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import type { LiteGraphCanvasEvent } from '@comfyorg/litegraph'
|
||||
import { useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -36,6 +36,8 @@ const inputStyle = ref<CSSProperties>({
|
||||
})
|
||||
|
||||
const titleEditorStore = useTitleEditorStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const previousCanvasDraggable = ref(true)
|
||||
|
||||
const onEdit = (newValue: string) => {
|
||||
if (titleEditorStore.titleEditorTarget && newValue.trim() !== '') {
|
||||
@@ -44,6 +46,7 @@ const onEdit = (newValue: string) => {
|
||||
}
|
||||
showInput.value = false
|
||||
titleEditorStore.titleEditorTarget = null
|
||||
canvasStore.canvas!.allow_dragcanvas = previousCanvasDraggable.value
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -54,6 +57,8 @@ watch(
|
||||
}
|
||||
editedTitle.value = target.title
|
||||
showInput.value = true
|
||||
previousCanvasDraggable.value = canvasStore.canvas!.allow_dragcanvas
|
||||
canvasStore.canvas!.allow_dragcanvas = false
|
||||
|
||||
if (target instanceof LGraphGroup) {
|
||||
const group = target
|
||||
|
||||
@@ -43,38 +43,7 @@
|
||||
:optionLabel="'display_name'"
|
||||
>
|
||||
<template v-slot:option="{ option }">
|
||||
<div class="option-container">
|
||||
<div class="option-display-name">
|
||||
<div>
|
||||
<span
|
||||
v-html="highlightQuery(option.display_name, currentQuery)"
|
||||
></span>
|
||||
<span> </span>
|
||||
<Tag v-if="showIdName" severity="secondary">
|
||||
<span v-html="highlightQuery(option.name, currentQuery)"></span>
|
||||
</Tag>
|
||||
</div>
|
||||
<div v-if="showCategory" class="option-category">
|
||||
{{ option.category.replaceAll('/', ' > ') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-badges">
|
||||
<Tag
|
||||
v-if="option.experimental"
|
||||
:value="$t('experimental')"
|
||||
severity="primary"
|
||||
/>
|
||||
<Tag
|
||||
v-if="option.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger"
|
||||
/>
|
||||
<NodeSourceChip
|
||||
v-if="option.python_module !== undefined"
|
||||
:python_module="option.python_module"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NodeSearchItem :nodeDef="option" :currentQuery="currentQuery" />
|
||||
</template>
|
||||
<!-- FilterAndValue -->
|
||||
<template v-slot:chip="{ value }">
|
||||
@@ -92,11 +61,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import Tag from 'primevue/tag'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Button from 'primevue/button'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSourceChip from '@/components/node/NodeSourceChip.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'
|
||||
@@ -110,12 +78,6 @@ const { t } = useI18n()
|
||||
const enableNodePreview = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
|
||||
)
|
||||
const showCategory = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.ShowCategory')
|
||||
)
|
||||
const showIdName = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.ShowIdName')
|
||||
)
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -145,12 +107,6 @@ const search = (query: string) => {
|
||||
]
|
||||
}
|
||||
|
||||
const highlightQuery = (text: string, query: string) => {
|
||||
if (!query) return text
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
return text.replace(regex, '<span class="highlight">$1</span>')
|
||||
}
|
||||
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
|
||||
const reFocusInput = () => {
|
||||
@@ -202,29 +158,6 @@ const setHoverSuggestion = (index: number) => {
|
||||
@apply z-10 flex-grow;
|
||||
}
|
||||
|
||||
.option-container {
|
||||
@apply flex justify-between items-center px-2 py-0 cursor-pointer overflow-hidden w-full;
|
||||
}
|
||||
|
||||
.option-display-name {
|
||||
@apply font-semibold flex flex-col;
|
||||
}
|
||||
|
||||
.option-category {
|
||||
@apply font-light text-sm text-gray-400 overflow-hidden text-ellipsis;
|
||||
/* Keeps the text on a single line by default */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.highlight) {
|
||||
background-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color);
|
||||
font-weight: bold;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0rem 0.125rem;
|
||||
margin: -0.125rem 0.125rem;
|
||||
}
|
||||
|
||||
._filter-button {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
81
src/components/searchbox/NodeSearchItem.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div
|
||||
class="option-container flex justify-between items-center px-2 py-0 cursor-pointer overflow-hidden w-full"
|
||||
>
|
||||
<div class="option-display-name font-semibold flex flex-col">
|
||||
<div>
|
||||
<span v-if="isBookmarked">
|
||||
<i class="pi pi-bookmark-fill text-sm mr-1"></i>
|
||||
</span>
|
||||
<span
|
||||
v-html="highlightQuery(nodeDef.display_name, currentQuery)"
|
||||
></span>
|
||||
<span> </span>
|
||||
<Tag v-if="showIdName" severity="secondary">
|
||||
<span v-html="highlightQuery(nodeDef.name, currentQuery)"></span>
|
||||
</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-if="showCategory"
|
||||
class="option-category font-light text-sm text-gray-400 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{{ nodeDef.category.replaceAll('/', ' > ') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-badges">
|
||||
<Tag
|
||||
v-if="nodeDef.experimental"
|
||||
:value="$t('experimental')"
|
||||
severity="primary"
|
||||
/>
|
||||
<Tag
|
||||
v-if="nodeDef.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger"
|
||||
/>
|
||||
<NodeSourceChip
|
||||
v-if="nodeDef.python_module !== undefined"
|
||||
:python_module="nodeDef.python_module"
|
||||
/>
|
||||
</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 { highlightQuery } from '@/utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const showCategory = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.ShowCategory')
|
||||
)
|
||||
const showIdName = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.ShowIdName')
|
||||
)
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const isBookmarked = computed(() =>
|
||||
nodeBookmarkStore.isBookmarked(props.nodeDef)
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
nodeDef: ComfyNodeDefImpl
|
||||
currentQuery: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.highlight) {
|
||||
background-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color);
|
||||
font-weight: bold;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0rem 0.125rem;
|
||||
margin: -0.125rem 0.125rem;
|
||||
}
|
||||
</style>
|
||||
@@ -19,35 +19,44 @@
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<SearchBox
|
||||
class="node-lib-search-box"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="node-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
|
||||
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider v-if="nodeBookmarkStore.bookmarks.length > 0" type="dashed" />
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-if="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="node-library-node-preview-container" />
|
||||
@@ -60,7 +69,7 @@ import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { computed, nextTick, onMounted, ref, Ref } from 'vue'
|
||||
import { computed, nextTick, ref, Ref } from 'vue'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import Popover from 'primevue/popover'
|
||||
import Divider from 'primevue/divider'
|
||||
@@ -83,7 +92,8 @@ import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const { expandedKeys, expandNode, toggleNodeOnEvent } = useTreeExpansion()
|
||||
const expandedKeys = ref<Record<string, boolean>>({})
|
||||
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
|
||||
|
||||
const nodeBookmarkTreeExplorerRef = ref<InstanceType<
|
||||
typeof NodeBookmarkTreeExplorer
|
||||
@@ -113,7 +123,17 @@ const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
||||
}
|
||||
},
|
||||
children,
|
||||
draggable: node.leaf
|
||||
draggable: node.leaf,
|
||||
handleClick: (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
app.addNodeOnGraph(node.data, { pos: app.getCanvasCenter() })
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fillNodeInfo(root.value)
|
||||
@@ -154,17 +174,6 @@ const handleSearch = (query: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleNodeClick = (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
app.addNodeOnGraph(node.data, { pos: app.getCanvasCenter() })
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
}
|
||||
|
||||
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||
filters.value.push({
|
||||
filter: filterAndValue,
|
||||
@@ -193,10 +202,6 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.node-lib-search-box) {
|
||||
@apply mx-4 mt-4;
|
||||
}
|
||||
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
@@ -205,10 +210,6 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
:deep(.node-lib-tree-explorer) {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
:deep(.p-divider) {
|
||||
margin: var(--comfy-tree-explorer-item-padding) 0px;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
class="toggle-expanded-button"
|
||||
v-tooltip="$t('sideToolbar.queueTab.showFlatList')"
|
||||
/>
|
||||
<Button
|
||||
v-if="queueStore.hasPendingTasks"
|
||||
icon="pi pi-stop"
|
||||
text
|
||||
severity="danger"
|
||||
@click="clearPendingTasks"
|
||||
class="clear-pending-button"
|
||||
v-tooltip="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
@@ -221,6 +230,16 @@ const confirmRemoveAll = (event: Event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const clearPendingTasks = async () => {
|
||||
await queueStore.clear(['queue'])
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Confirmed',
|
||||
detail: 'Pending tasks deleted',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
|
||||
const onStatus = async () => {
|
||||
await queueStore.update()
|
||||
updateVisibleTasks()
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
:extraMenuItems="extraMenuItems"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
<NodeTreeFolder :node="node" />
|
||||
@@ -36,29 +35,19 @@ import type {
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||
import { app } from '@/scripts/app'
|
||||
import { findNodeByKey } from '@/utils/treeUtil'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
|
||||
const props = defineProps<{
|
||||
filteredNodeDefs: ComfyNodeDefImpl[]
|
||||
}>()
|
||||
|
||||
const { expandedKeys, expandNode, toggleNodeOnEvent } = useTreeExpansion()
|
||||
|
||||
const handleNodeClick = (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
app.addNodeOnGraph(node.data, { pos: app.getCanvasCenter() })
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
}
|
||||
const expandedKeys = ref<Record<string, boolean>>({})
|
||||
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const bookmarkedRoot = computed<TreeNode>(() => {
|
||||
@@ -146,6 +135,16 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
const nodePath = folderNodeDef.category + '/' + nodeDefToAdd.name
|
||||
nodeBookmarkStore.addBookmark(nodePath)
|
||||
},
|
||||
handleClick: (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
app.addNodeOnGraph(node.data, { pos: app.getCanvasCenter() })
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
},
|
||||
...(node.leaf
|
||||
? {}
|
||||
: {
|
||||
@@ -182,22 +181,13 @@ defineExpose({
|
||||
addNewBookmarkFolder
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const handleRename = (node: TreeNode, newName: string) => {
|
||||
if (node.data && node.data.isDummyFolder) {
|
||||
try {
|
||||
const handleRename = useErrorHandling().wrapWithErrorHandling(
|
||||
(node: TreeNode, newName: string) => {
|
||||
if (node.data && node.data.isDummyFolder) {
|
||||
nodeBookmarkStore.renameBookmarkFolder(node.data, newName)
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('error'),
|
||||
detail: e.message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const showCustomizationDialog = ref(false)
|
||||
const initialIcon = ref(nodeBookmarkStore.defaultBookmarkIcon)
|
||||
@@ -212,6 +202,7 @@ const updateCustomization = (icon: string, color: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const extraMenuItems = computed(
|
||||
() => (menuTargetNode: RenderedTreeExplorerNode<ComfyNodeDefImpl>) => [
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mergeIfValid } from './widgetInputs'
|
||||
import { ManageGroupDialog } from './groupNodeManage'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
const GROUP = Symbol()
|
||||
|
||||
@@ -17,8 +18,7 @@ const Workflow = {
|
||||
const id = `workflow/${name}`
|
||||
// Check if lready registered/in use in this workflow
|
||||
if (app.graph.extra?.groupNodes?.[name]) {
|
||||
// @ts-expect-error
|
||||
if (app.graph._nodes.find((n) => n.type === id)) {
|
||||
if (app.graph.nodes.find((n) => n.type === id)) {
|
||||
return Workflow.InUse.InWorkflow
|
||||
} else {
|
||||
return Workflow.InUse.Registered
|
||||
@@ -195,6 +195,10 @@ export class GroupNodeConfig {
|
||||
display_name: this.name,
|
||||
category: 'group nodes' + ('/' + source),
|
||||
input: { required: {} },
|
||||
description: `Group node combining ${this.nodeData.nodes
|
||||
.map((n) => n.type)
|
||||
.join(', ')}`,
|
||||
python_module: 'custom_nodes.' + this.name,
|
||||
|
||||
[GROUP]: this
|
||||
}
|
||||
@@ -213,6 +217,7 @@ export class GroupNodeConfig {
|
||||
}
|
||||
this.#convertedToProcess = null
|
||||
await app.registerNodeDef('workflow/' + this.name, this.nodeDef)
|
||||
useNodeDefStore().addNodeDef(this.nodeDef)
|
||||
}
|
||||
|
||||
getLinks() {
|
||||
|
||||
@@ -388,8 +388,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
'button.comfy-btn',
|
||||
{
|
||||
onclick: (e) => {
|
||||
// @ts-expect-error
|
||||
const node = app.graph._nodes.find(
|
||||
const node = app.graph.nodes.find(
|
||||
(n) => n.type === 'workflow/' + this.selectedGroup
|
||||
)
|
||||
if (node) {
|
||||
@@ -470,8 +469,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
types[g] = type
|
||||
|
||||
if (!nodesByType) {
|
||||
// @ts-expect-error
|
||||
nodesByType = app.graph._nodes.reduce((p, n) => {
|
||||
nodesByType = app.graph.nodes.reduce((p, n) => {
|
||||
p[n.type] ??= []
|
||||
p[n.type].push(n)
|
||||
return p
|
||||
|
||||
@@ -15,7 +15,7 @@ function addNodesToGroup(group, nodes = []) {
|
||||
x1 = y1 = x2 = y2 = -1
|
||||
nx1 = ny1 = nx2 = ny2 = -1
|
||||
|
||||
for (var n of [group._nodes, nodes]) {
|
||||
for (var n of [group.nodes, nodes]) {
|
||||
for (var i in n) {
|
||||
node = n[i]
|
||||
|
||||
@@ -90,7 +90,7 @@ app.registerExtension({
|
||||
|
||||
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
||||
group.recomputeInsideNodes()
|
||||
const nodesInGroup = group._nodes
|
||||
const nodesInGroup = group.nodes
|
||||
|
||||
options.push({
|
||||
content: 'Add Selected Nodes To Group',
|
||||
|
||||
@@ -94,6 +94,17 @@ function prepare_mask(image, maskCanvas, maskCtx, maskColor) {
|
||||
maskCtx.putImageData(maskData, 0, 0)
|
||||
}
|
||||
|
||||
// Define the PointerType enum
|
||||
enum PointerType {
|
||||
Arc = 'arc',
|
||||
Rect = 'rect'
|
||||
}
|
||||
|
||||
enum CompositionOperation {
|
||||
SourceOver = 'source-over',
|
||||
DestinationOut = 'destination-out'
|
||||
}
|
||||
|
||||
class MaskEditorDialog extends ComfyDialog {
|
||||
static instance = null
|
||||
static mousedown_x: number | null = null
|
||||
@@ -120,6 +131,8 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
mousedown_pan_x: number
|
||||
mousedown_pan_y: number
|
||||
last_pressure: number
|
||||
pointer_type: PointerType
|
||||
brush_pointer_type_select: HTMLDivElement
|
||||
|
||||
static getInstance() {
|
||||
if (!MaskEditorDialog.instance) {
|
||||
@@ -176,7 +189,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
divElement.style.borderColor = 'var(--border-color)'
|
||||
divElement.style.borderStyle = 'solid'
|
||||
divElement.style.fontSize = '15px'
|
||||
divElement.style.height = '21px'
|
||||
divElement.style.height = '25px'
|
||||
divElement.style.padding = '1px 6px'
|
||||
divElement.style.display = 'flex'
|
||||
divElement.style.position = 'relative'
|
||||
@@ -210,7 +223,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
divElement.style.borderColor = 'var(--border-color)'
|
||||
divElement.style.borderStyle = 'solid'
|
||||
divElement.style.fontSize = '15px'
|
||||
divElement.style.height = '21px'
|
||||
divElement.style.height = '25px'
|
||||
divElement.style.padding = '1px 6px'
|
||||
divElement.style.display = 'flex'
|
||||
divElement.style.position = 'relative'
|
||||
@@ -233,8 +246,77 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
return divElement
|
||||
}
|
||||
|
||||
createPointerTypeSelect(self: any): HTMLDivElement {
|
||||
const divElement = document.createElement('div')
|
||||
divElement.id = 'maskeditor-pointer-type'
|
||||
divElement.style.cssFloat = 'left'
|
||||
divElement.style.fontFamily = 'sans-serif'
|
||||
divElement.style.marginRight = '4px'
|
||||
divElement.style.color = 'var(--input-text)'
|
||||
divElement.style.backgroundColor = 'var(--comfy-input-bg)'
|
||||
divElement.style.borderRadius = '8px'
|
||||
divElement.style.borderColor = 'var(--border-color)'
|
||||
divElement.style.borderStyle = 'solid'
|
||||
divElement.style.fontSize = '15px'
|
||||
divElement.style.height = '25px'
|
||||
divElement.style.padding = '1px 6px'
|
||||
divElement.style.display = 'flex'
|
||||
divElement.style.position = 'relative'
|
||||
divElement.style.top = '2px'
|
||||
divElement.style.pointerEvents = 'auto'
|
||||
|
||||
const labelElement = document.createElement('label')
|
||||
labelElement.textContent = 'Pointer Type:'
|
||||
|
||||
const selectElement = document.createElement('select')
|
||||
selectElement.style.borderRadius = '0'
|
||||
selectElement.style.borderColor = 'transparent'
|
||||
selectElement.style.borderStyle = 'unset'
|
||||
selectElement.style.fontSize = '0.9em'
|
||||
|
||||
const optionArc = document.createElement('option')
|
||||
optionArc.value = 'arc'
|
||||
optionArc.text = 'Circle'
|
||||
optionArc.selected = true // Fix for TypeScript, "selected" should be boolean
|
||||
|
||||
const optionRect = document.createElement('option')
|
||||
optionRect.value = 'rect'
|
||||
optionRect.text = 'Square'
|
||||
|
||||
selectElement.appendChild(optionArc)
|
||||
selectElement.appendChild(optionRect)
|
||||
|
||||
selectElement.addEventListener('change', (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
self.pointer_type = target.value
|
||||
this.setBrushBorderRadius(self)
|
||||
})
|
||||
|
||||
divElement.appendChild(labelElement)
|
||||
divElement.appendChild(selectElement)
|
||||
|
||||
return divElement
|
||||
}
|
||||
|
||||
setBrushBorderRadius(self: any): void {
|
||||
if (self.pointer_type === PointerType.Rect) {
|
||||
this.brush.style.borderRadius = '0%'
|
||||
// @ts-expect-error
|
||||
this.brush.style.MozBorderRadius = '0%'
|
||||
// @ts-expect-error
|
||||
this.brush.style.WebkitBorderRadius = '0%'
|
||||
} else {
|
||||
this.brush.style.borderRadius = '50%'
|
||||
// @ts-expect-error
|
||||
this.brush.style.MozBorderRadius = '50%'
|
||||
// @ts-expect-error
|
||||
this.brush.style.WebkitBorderRadius = '50%'
|
||||
}
|
||||
}
|
||||
|
||||
setlayout(imgCanvas: HTMLCanvasElement, maskCanvas: HTMLCanvasElement) {
|
||||
const self = this
|
||||
self.pointer_type = PointerType.Arc
|
||||
|
||||
// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
|
||||
// to prevent anomalies where it exceeds a certain size and goes outside of the window.
|
||||
@@ -251,15 +333,11 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
brush.style.backgroundColor = 'transparent'
|
||||
brush.style.outline = '1px dashed black'
|
||||
brush.style.boxShadow = '0 0 0 1px white'
|
||||
brush.style.borderRadius = '50%'
|
||||
// @ts-expect-error
|
||||
brush.style.MozBorderRadius = '50%'
|
||||
// @ts-expect-error
|
||||
brush.style.WebkitBorderRadius = '50%'
|
||||
brush.style.position = 'absolute'
|
||||
brush.style.zIndex = '8889'
|
||||
brush.style.pointerEvents = 'none'
|
||||
this.brush = brush
|
||||
this.setBrushBorderRadius(self)
|
||||
this.element.appendChild(imgCanvas)
|
||||
this.element.appendChild(maskCanvas)
|
||||
this.element.appendChild(bottom_panel)
|
||||
@@ -294,6 +372,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
}
|
||||
)
|
||||
|
||||
this.brush_pointer_type_select = this.createPointerTypeSelect(self)
|
||||
this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
|
||||
if (self.brush_color_mode === 'black') {
|
||||
self.brush_color_mode = 'white'
|
||||
@@ -325,6 +404,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
bottom_panel.appendChild(cancelButton)
|
||||
bottom_panel.appendChild(this.brush_size_slider)
|
||||
bottom_panel.appendChild(this.brush_opacity_slider)
|
||||
bottom_panel.appendChild(this.brush_pointer_type_select)
|
||||
bottom_panel.appendChild(this.colorButton)
|
||||
|
||||
imgCanvas.style.position = 'absolute'
|
||||
@@ -815,19 +895,14 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
|
||||
if (diff > 20 && !this.drawing_mode)
|
||||
requestAnimationFrame(() => {
|
||||
self.maskCtx.beginPath()
|
||||
self.maskCtx.fillStyle = this.getMaskFillStyle()
|
||||
self.maskCtx.globalCompositeOperation = 'source-over'
|
||||
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false)
|
||||
self.maskCtx.fill()
|
||||
self.init_shape(self, CompositionOperation.SourceOver)
|
||||
self.draw_shape(self, x, y, brush_size)
|
||||
self.lastx = x
|
||||
self.lasty = y
|
||||
})
|
||||
else
|
||||
requestAnimationFrame(() => {
|
||||
self.maskCtx.beginPath()
|
||||
self.maskCtx.fillStyle = this.getMaskFillStyle()
|
||||
self.maskCtx.globalCompositeOperation = 'source-over'
|
||||
self.init_shape(self, CompositionOperation.SourceOver)
|
||||
|
||||
var dx = x - self.lastx
|
||||
var dy = y - self.lasty
|
||||
@@ -839,8 +914,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
for (var i = 0; i < distance; i += 5) {
|
||||
var px = self.lastx + directionX * i
|
||||
var py = self.lasty + directionY * i
|
||||
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false)
|
||||
self.maskCtx.fill()
|
||||
self.draw_shape(self, px, py, brush_size)
|
||||
}
|
||||
self.lastx = x
|
||||
self.lasty = y
|
||||
@@ -873,17 +947,14 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
if (diff > 20 && !this.drawing_mode)
|
||||
// cannot tracking drawing_mode for touch event
|
||||
requestAnimationFrame(() => {
|
||||
self.maskCtx.beginPath()
|
||||
self.maskCtx.globalCompositeOperation = 'destination-out'
|
||||
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false)
|
||||
self.maskCtx.fill()
|
||||
self.init_shape(self, CompositionOperation.DestinationOut)
|
||||
self.draw_shape(self, x, y, brush_size)
|
||||
self.lastx = x
|
||||
self.lasty = y
|
||||
})
|
||||
else
|
||||
requestAnimationFrame(() => {
|
||||
self.maskCtx.beginPath()
|
||||
self.maskCtx.globalCompositeOperation = 'destination-out'
|
||||
self.init_shape(self, CompositionOperation.DestinationOut)
|
||||
|
||||
var dx = x - self.lastx
|
||||
var dy = y - self.lasty
|
||||
@@ -895,8 +966,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
for (var i = 0; i < distance; i += 5) {
|
||||
var px = self.lastx + directionX * i
|
||||
var py = self.lasty + directionY * i
|
||||
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false)
|
||||
self.maskCtx.fill()
|
||||
self.draw_shape(self, px, py, brush_size)
|
||||
}
|
||||
self.lastx = x
|
||||
self.lasty = y
|
||||
@@ -943,21 +1013,43 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
(event.offsetY || event.targetTouches[0].clientY - maskRect.top) /
|
||||
self.zoom_ratio
|
||||
|
||||
self.maskCtx.beginPath()
|
||||
if (!event.altKey && event.button == 0) {
|
||||
self.maskCtx.fillStyle = this.getMaskFillStyle()
|
||||
self.maskCtx.globalCompositeOperation = 'source-over'
|
||||
self.init_shape(self, CompositionOperation.SourceOver)
|
||||
} else {
|
||||
self.maskCtx.globalCompositeOperation = 'destination-out'
|
||||
self.init_shape(self, CompositionOperation.DestinationOut)
|
||||
}
|
||||
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false)
|
||||
self.maskCtx.fill()
|
||||
self.draw_shape(self, x, y, brush_size)
|
||||
self.lastx = x
|
||||
self.lasty = y
|
||||
self.lasttime = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
init_shape(self, compositionOperation) {
|
||||
self.maskCtx.beginPath()
|
||||
if (compositionOperation == CompositionOperation.SourceOver) {
|
||||
self.maskCtx.fillStyle = this.getMaskFillStyle()
|
||||
self.maskCtx.globalCompositeOperation = CompositionOperation.SourceOver
|
||||
} else if (compositionOperation == CompositionOperation.DestinationOut) {
|
||||
self.maskCtx.globalCompositeOperation =
|
||||
CompositionOperation.DestinationOut
|
||||
}
|
||||
}
|
||||
|
||||
draw_shape(self, x, y, brush_size) {
|
||||
if (self.pointer_type === PointerType.Rect) {
|
||||
self.maskCtx.rect(
|
||||
x - brush_size,
|
||||
y - brush_size,
|
||||
brush_size * 2,
|
||||
brush_size * 2
|
||||
)
|
||||
} else {
|
||||
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false)
|
||||
}
|
||||
self.maskCtx.fill()
|
||||
}
|
||||
|
||||
async save() {
|
||||
const backupCanvas = document.createElement('canvas')
|
||||
const backupCtx = backupCanvas.getContext('2d', {
|
||||
@@ -997,7 +1089,7 @@ class MaskEditorDialog extends ComfyDialog {
|
||||
backupData.data[i + 2] = 0
|
||||
}
|
||||
|
||||
backupCtx.globalCompositeOperation = 'source-over'
|
||||
backupCtx.globalCompositeOperation = CompositionOperation.SourceOver
|
||||
backupCtx.putImageData(backupData, 0, 0)
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -23,38 +23,67 @@ function isCoreNode(node: ComfyLGraphNode) {
|
||||
return getNodeSource(node)?.type === 'core'
|
||||
}
|
||||
|
||||
function getNodeIdBadge(node: ComfyLGraphNode, nodeIdBadgeMode: NodeBadgeMode) {
|
||||
return nodeIdBadgeMode === NodeBadgeMode.None ||
|
||||
(isCoreNode(node) && nodeIdBadgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
? ''
|
||||
: `#${node.id}`
|
||||
function badgeTextVisible(
|
||||
node: ComfyLGraphNode,
|
||||
badgeMode: NodeBadgeMode
|
||||
): boolean {
|
||||
return (
|
||||
badgeMode === NodeBadgeMode.None ||
|
||||
(isCoreNode(node) && badgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
)
|
||||
}
|
||||
|
||||
function getNodeSourceBadge(
|
||||
function getNodeIdBadgeText(
|
||||
node: ComfyLGraphNode,
|
||||
nodeIdBadgeMode: NodeBadgeMode
|
||||
) {
|
||||
return badgeTextVisible(node, nodeIdBadgeMode) ? '' : `#${node.id}`
|
||||
}
|
||||
|
||||
function getNodeSourceBadgeText(
|
||||
node: ComfyLGraphNode,
|
||||
nodeSourceBadgeMode: NodeBadgeMode
|
||||
) {
|
||||
const nodeSource = getNodeSource(node)
|
||||
return nodeSourceBadgeMode === NodeBadgeMode.None ||
|
||||
(isCoreNode(node) && nodeSourceBadgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
return badgeTextVisible(node, nodeSourceBadgeMode)
|
||||
? ''
|
||||
: nodeSource?.badgeText ?? ''
|
||||
}
|
||||
|
||||
function getNodeLifeCycleBadgeText(
|
||||
node: ComfyLGraphNode,
|
||||
nodeLifeCycleBadgeMode: NodeBadgeMode
|
||||
) {
|
||||
let text = ''
|
||||
const nodeDef = (node.constructor as typeof ComfyLGraphNode).nodeData
|
||||
|
||||
// Frontend-only nodes don't have nodeDef
|
||||
if (!nodeDef) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (nodeDef.deprecated) {
|
||||
text = '[DEPR]'
|
||||
}
|
||||
|
||||
if (nodeDef.experimental) {
|
||||
text = '[BETA]'
|
||||
}
|
||||
|
||||
return badgeTextVisible(node, nodeLifeCycleBadgeMode) ? '' : text
|
||||
}
|
||||
|
||||
class NodeBadgeExtension implements ComfyExtension {
|
||||
name = 'Comfy.NodeBadge'
|
||||
|
||||
constructor(
|
||||
public nodeIdBadgeMode: ComputedRef<NodeBadgeMode> | null = null,
|
||||
public nodeSourceBadgeMode: ComputedRef<NodeBadgeMode> | null = null,
|
||||
public nodeLifeCycleBadgeMode: ComputedRef<NodeBadgeMode> | null = null,
|
||||
public colorPalette: ComputedRef<Palette> | null = null
|
||||
) {}
|
||||
|
||||
init(app: ComfyApp) {
|
||||
if (!app.vueAppReady) {
|
||||
return
|
||||
}
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
this.nodeSourceBadgeMode = computed(
|
||||
() =>
|
||||
@@ -63,6 +92,12 @@ class NodeBadgeExtension implements ComfyExtension {
|
||||
this.nodeIdBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
this.nodeLifeCycleBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get(
|
||||
'Comfy.NodeBadge.NodeLifeCycleBadgeMode'
|
||||
) as NodeBadgeMode
|
||||
)
|
||||
this.colorPalette = computed(() =>
|
||||
getColorPalette(settingStore.get('Comfy.ColorPalette'))
|
||||
)
|
||||
@@ -74,13 +109,12 @@ class NodeBadgeExtension implements ComfyExtension {
|
||||
watch(this.nodeIdBadgeMode, () => {
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
})
|
||||
watch(this.nodeLifeCycleBadgeMode, () => {
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
})
|
||||
}
|
||||
|
||||
nodeCreated(node: ComfyLGraphNode, app: ComfyApp) {
|
||||
if (!app.vueAppReady) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
@@ -90,13 +124,17 @@ class NodeBadgeExtension implements ComfyExtension {
|
||||
new LGraphBadge({
|
||||
text: _.truncate(
|
||||
[
|
||||
getNodeIdBadge(node, this.nodeIdBadgeMode.value),
|
||||
getNodeSourceBadge(node, this.nodeSourceBadgeMode.value)
|
||||
getNodeIdBadgeText(node, this.nodeIdBadgeMode.value),
|
||||
getNodeLifeCycleBadgeText(
|
||||
node,
|
||||
this.nodeLifeCycleBadgeMode.value
|
||||
),
|
||||
getNodeSourceBadgeText(node, this.nodeSourceBadgeMode.value)
|
||||
]
|
||||
.filter((s) => s.length > 0)
|
||||
.join(' '),
|
||||
{
|
||||
length: 25
|
||||
length: 31
|
||||
}
|
||||
),
|
||||
fgColor:
|
||||
|
||||
@@ -130,7 +130,6 @@ app.registerExtension({
|
||||
: null
|
||||
if (
|
||||
inputType &&
|
||||
// @ts-expect-error Will self-resolve when LiteGraph types are generated
|
||||
!LiteGraph.isValidConnection(inputType, nodeOutType)
|
||||
) {
|
||||
// The output doesnt match our input so disconnect it
|
||||
|
||||
@@ -34,7 +34,7 @@ app.registerExtension({
|
||||
'When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.',
|
||||
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
||||
onChange(value) {
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value
|
||||
LiteGraph.CANVAS_GRID_SIZE = +value || 10
|
||||
}
|
||||
})
|
||||
|
||||
@@ -137,7 +137,7 @@ app.registerExtension({
|
||||
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
||||
// align the group itself.
|
||||
this.recomputeInsideNodes()
|
||||
for (const node of this._nodes) {
|
||||
for (const node of this.nodes) {
|
||||
node.alignToGrid()
|
||||
}
|
||||
LGraphNode.prototype.alignToGrid.apply(this)
|
||||
@@ -178,8 +178,7 @@ app.registerExtension({
|
||||
LGraphCanvas.onGroupAdd = function () {
|
||||
const v = onGroupAdd.apply(app.canvas, arguments)
|
||||
if (app.shiftDown) {
|
||||
// @ts-expect-error
|
||||
const lastGroup = app.graph._groups[app.graph._groups.length - 1]
|
||||
const lastGroup = app.graph.groups[app.graph.groups.length - 1]
|
||||
if (lastGroup) {
|
||||
roundVectorToGrid(lastGroup.pos)
|
||||
roundVectorToGrid(lastGroup.size)
|
||||
|
||||
@@ -751,6 +751,7 @@ app.registerExtension({
|
||||
|
||||
nodeType.prototype.onGraphConfigured = function () {
|
||||
if (!this.inputs) return
|
||||
this.widgets ??= []
|
||||
|
||||
for (const input of this.inputs) {
|
||||
if (input.widget) {
|
||||
@@ -826,8 +827,7 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
function isNodeAtPos(pos) {
|
||||
// @ts-expect-error Fix litegraph types
|
||||
for (const n of app.graph._nodes) {
|
||||
for (const n of app.graph.nodes) {
|
||||
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
|
||||
return true
|
||||
}
|
||||
|
||||
28
src/hooks/errorHooks.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useErrorHandling() {
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { wrapWithErrorHandling }
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { Ref } from 'vue'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
|
||||
export function useTreeExpansion() {
|
||||
const expandedKeys = ref<Record<string, boolean>>({})
|
||||
|
||||
export function useTreeExpansion(expandedKeys: Ref<Record<string, boolean>>) {
|
||||
const toggleNode = (node: TreeNode) => {
|
||||
if (node.key && typeof node.key === 'string') {
|
||||
if (node.key in expandedKeys.value) {
|
||||
@@ -63,7 +61,6 @@ export function useTreeExpansion() {
|
||||
}
|
||||
|
||||
return {
|
||||
expandedKeys,
|
||||
toggleNode,
|
||||
toggleNodeRecursive,
|
||||
expandNode,
|
||||
|
||||
@@ -54,7 +54,8 @@ const messages = {
|
||||
showFlatList: 'Show Flat List',
|
||||
backToAllTasks: 'Back to All Tasks',
|
||||
containImagePreview: 'Fill Image Preview',
|
||||
coverImagePreview: 'Fit Image Preview'
|
||||
coverImagePreview: 'Fit Image Preview',
|
||||
clearPendingTasks: 'Clear Pending Tasks'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -97,7 +98,8 @@ const messages = {
|
||||
},
|
||||
queueTab: {
|
||||
showFlatList: '平铺结果',
|
||||
backToAllTasks: '返回'
|
||||
backToAllTasks: '返回',
|
||||
clearPendingTasks: '清除待处理任务'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
PromptResponse,
|
||||
SystemStats,
|
||||
User,
|
||||
Settings
|
||||
Settings,
|
||||
UserDataFullInfo
|
||||
} from '@/types/apiTypes'
|
||||
import axios from 'axios'
|
||||
|
||||
@@ -671,6 +672,19 @@ class ComfyApi extends EventTarget {
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
async listUserDataFullInfo(dir: string): Promise<UserDataFullInfo[]> {
|
||||
const resp = await this.fetchApi(
|
||||
`/userdata?dir=${encodeURIComponent(dir)}&recurse=true&split=false&full_info=true`
|
||||
)
|
||||
if (resp.status === 404) return []
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(
|
||||
`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`
|
||||
)
|
||||
}
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
async getLogs(): Promise<string> {
|
||||
return (await axios.get(this.internalURL('/logs'))).data
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ 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'
|
||||
|
||||
@@ -119,7 +120,6 @@ export class ComfyApp {
|
||||
// x, y, scale
|
||||
zoom_drag_start: [number, number, number] | null
|
||||
lastNodeErrors: any[] | null
|
||||
runningNodeId: number | null
|
||||
lastExecutionError: { node_id: number } | null
|
||||
progress: { value: number; max: number } | null
|
||||
configuringGraph: boolean
|
||||
@@ -136,6 +136,12 @@ export class ComfyApp {
|
||||
canvasContainer: HTMLElement
|
||||
menu: ComfyAppMenu
|
||||
|
||||
// @deprecated
|
||||
// Use useExecutionStore().executingNodeId instead
|
||||
get runningNodeId(): string | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.vueAppReady = false
|
||||
this.ui = new ComfyUI(this)
|
||||
@@ -1390,7 +1396,7 @@ export class ComfyApp {
|
||||
return
|
||||
}
|
||||
|
||||
var groups = this.graph._groups
|
||||
var groups = this.graph.groups
|
||||
|
||||
ctx.save()
|
||||
ctx.globalAlpha = 0.7 * this.editor_alpha
|
||||
@@ -1591,36 +1597,17 @@ export class ComfyApp {
|
||||
)
|
||||
|
||||
api.addEventListener('progress', ({ detail }) => {
|
||||
if (
|
||||
this.workflowManager.activePrompt?.workflow &&
|
||||
this.workflowManager.activePrompt.workflow !==
|
||||
this.workflowManager.activeWorkflow
|
||||
)
|
||||
return
|
||||
this.progress = detail
|
||||
this.graph.setDirtyCanvas(true, false)
|
||||
})
|
||||
|
||||
api.addEventListener('executing', ({ detail }) => {
|
||||
if (
|
||||
this.workflowManager.activePrompt?.workflow &&
|
||||
this.workflowManager.activePrompt.workflow !==
|
||||
this.workflowManager.activeWorkflow
|
||||
)
|
||||
return
|
||||
this.progress = null
|
||||
this.runningNodeId = detail
|
||||
this.graph.setDirtyCanvas(true, false)
|
||||
delete this.nodePreviewImages[this.runningNodeId]
|
||||
})
|
||||
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
if (
|
||||
this.workflowManager.activePrompt?.workflow &&
|
||||
this.workflowManager.activePrompt.workflow !==
|
||||
this.workflowManager.activeWorkflow
|
||||
)
|
||||
return
|
||||
const output = this.nodeOutputs[detail.display_node || detail.node]
|
||||
if (detail.merge && output) {
|
||||
for (const k in detail.output ?? {}) {
|
||||
@@ -1644,10 +1631,8 @@ export class ComfyApp {
|
||||
})
|
||||
|
||||
api.addEventListener('execution_start', ({ detail }) => {
|
||||
this.runningNodeId = null
|
||||
this.lastExecutionError = null
|
||||
// @ts-expect-error
|
||||
this.graph._nodes.forEach((node) => {
|
||||
this.graph.nodes.forEach((node) => {
|
||||
// @ts-expect-error
|
||||
if (node.onExecutionStart)
|
||||
// @ts-expect-error
|
||||
@@ -1704,8 +1689,7 @@ export class ComfyApp {
|
||||
// @ts-expect-error
|
||||
app.graph.onConfigure = function () {
|
||||
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
|
||||
// @ts-expect-error
|
||||
for (const node of app.graph._nodes) {
|
||||
for (const node of app.graph.nodes) {
|
||||
// @ts-expect-error
|
||||
node.onGraphConfigured?.()
|
||||
}
|
||||
@@ -1713,8 +1697,7 @@ export class ComfyApp {
|
||||
const r = onConfigure?.apply(this, arguments)
|
||||
|
||||
// Fire after onConfigure, used by primitives to generate widget using input nodes config
|
||||
// @ts-expect-error _nodes is private.
|
||||
for (const node of app.graph._nodes) {
|
||||
for (const node of app.graph.nodes) {
|
||||
node.onAfterGraphConfigured?.()
|
||||
}
|
||||
|
||||
@@ -1912,9 +1895,7 @@ export class ComfyApp {
|
||||
|
||||
// Save current workflow automatically
|
||||
setInterval(() => {
|
||||
const sortNodes =
|
||||
this.vueAppReady &&
|
||||
useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
|
||||
const sortNodes = useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
|
||||
const workflow = JSON.stringify(this.graph.serialize({ sortNodes }))
|
||||
localStorage.setItem('workflow', workflow)
|
||||
if (api.clientId) {
|
||||
@@ -2151,10 +2132,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
if (
|
||||
this.vueAppReady &&
|
||||
useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')
|
||||
) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||
showLoadWorkflowWarning({
|
||||
missingNodeTypes,
|
||||
hasAddedNodes,
|
||||
@@ -2168,10 +2146,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
showMissingModelsError(missingModels) {
|
||||
if (
|
||||
this.vueAppReady &&
|
||||
useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')
|
||||
) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
|
||||
showMissingModelsWarning({
|
||||
missingModels,
|
||||
maximizable: true
|
||||
@@ -2227,10 +2202,7 @@ export class ComfyApp {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (
|
||||
this.vueAppReady &&
|
||||
useSettingStore().get('Comfy.Validation.Workflows')
|
||||
) {
|
||||
if (useSettingStore().get('Comfy.Validation.Workflows')) {
|
||||
// TODO: Show validation error in a dialog.
|
||||
const validatedGraphData = await validateComfyWorkflow(
|
||||
graphData,
|
||||
@@ -2348,8 +2320,7 @@ export class ComfyApp {
|
||||
|
||||
return
|
||||
}
|
||||
// @ts-expect-error
|
||||
for (const node of this.graph._nodes) {
|
||||
for (const node of this.graph.nodes) {
|
||||
const size = node.computeSize()
|
||||
size[0] = Math.max(node.size[0], size[0])
|
||||
size[1] = Math.max(node.size[1], size[1])
|
||||
@@ -2434,9 +2405,8 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
const sortNodes =
|
||||
this.vueAppReady &&
|
||||
useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
|
||||
const sortNodes = useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
|
||||
|
||||
const workflow = graph.serialize({ sortNodes })
|
||||
const output = {}
|
||||
// Process nodes in order of execution
|
||||
@@ -2907,10 +2877,8 @@ export class ComfyApp {
|
||||
for (const nodeId in defs) {
|
||||
this.registerNodeDef(nodeId, defs[nodeId])
|
||||
}
|
||||
// @ts-expect-error
|
||||
for (let nodeNum in this.graph._nodes) {
|
||||
// @ts-expect-error
|
||||
const node = this.graph._nodes[nodeNum]
|
||||
for (let nodeNum in this.graph.nodes) {
|
||||
const node = this.graph.nodes[nodeNum]
|
||||
const def = defs[node.type]
|
||||
// @ts-expect-error
|
||||
// Allow primitive nodes to handle refresh
|
||||
@@ -2966,7 +2934,6 @@ export class ComfyApp {
|
||||
this.nodePreviewImages = {}
|
||||
this.lastNodeErrors = null
|
||||
this.lastExecutionError = null
|
||||
this.runningNodeId = null
|
||||
}
|
||||
|
||||
addNodeOnGraph(
|
||||
|
||||
@@ -204,7 +204,8 @@ export class ChangeTracker {
|
||||
|
||||
// Store node outputs
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]
|
||||
const prompt =
|
||||
app.workflowManager.executionStore.queuedPrompts[detail.prompt_id]
|
||||
if (!prompt?.workflow) return
|
||||
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {})
|
||||
const output = nodeOutputs[detail.node]
|
||||
|
||||
@@ -224,8 +224,8 @@ const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes
|
||||
//@ts-ignore
|
||||
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
||||
const visibleNodes = computeVisibleNodes.apply(this, arguments)
|
||||
// @ts-expect-error
|
||||
for (const node of app.graph._nodes) {
|
||||
|
||||
for (const node of app.graph.nodes) {
|
||||
if (elementWidgets.has(node)) {
|
||||
const hidden = visibleNodes.indexOf(node) === -1
|
||||
for (const w of node.widgets) {
|
||||
@@ -354,8 +354,7 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
width: `${widgetWidth - margin * 2}px`,
|
||||
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
|
||||
position: 'absolute',
|
||||
// @ts-expect-error
|
||||
zIndex: app.graph._nodes.indexOf(node)
|
||||
zIndex: app.graph.nodes.indexOf(node)
|
||||
})
|
||||
|
||||
if (enableDomClipping) {
|
||||
|
||||
@@ -372,83 +372,10 @@ export class ComfyUI {
|
||||
this.history.update()
|
||||
})
|
||||
|
||||
const confirmClear = this.settings.addSetting({
|
||||
id: 'Comfy.ConfirmClear',
|
||||
category: ['Comfy', 'Workflow', 'ConfirmClear'],
|
||||
name: 'Require confirmation when clearing workflow',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
const promptFilename = this.settings.addSetting({
|
||||
id: 'Comfy.PromptFilename',
|
||||
category: ['Comfy', 'Workflow', 'PromptFilename'],
|
||||
name: 'Prompt for filename when saving workflow',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
/**
|
||||
* file format for preview
|
||||
*
|
||||
* format;quality
|
||||
*
|
||||
* ex)
|
||||
* webp;50 -> webp, quality 50
|
||||
* jpeg;80 -> rgb, jpeg, quality 80
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const previewImage = this.settings.addSetting({
|
||||
id: 'Comfy.PreviewFormat',
|
||||
category: ['Comfy', 'Node Widget', 'PreviewFormat'],
|
||||
name: 'Preview image format',
|
||||
tooltip:
|
||||
'When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.',
|
||||
type: 'text',
|
||||
defaultValue: ''
|
||||
})
|
||||
|
||||
this.settings.addSetting({
|
||||
id: 'Comfy.DisableSliders',
|
||||
category: ['Comfy', 'Node Widget', 'DisableSliders'],
|
||||
name: 'Disable node widget sliders',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
this.settings.addSetting({
|
||||
id: 'Comfy.DisableFloatRounding',
|
||||
category: ['Comfy', 'Node Widget', 'DisableFloatRounding'],
|
||||
name: 'Disable default float widget rounding.',
|
||||
tooltip:
|
||||
'(requires page reload) Cannot disable round when round is set by the node in the backend.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
this.settings.addSetting({
|
||||
id: 'Comfy.FloatRoundingPrecision',
|
||||
category: ['Comfy', 'Node Widget', 'FloatRoundingPrecision'],
|
||||
name: 'Float widget rounding decimal places [0 = auto].',
|
||||
tooltip: '(requires page reload)',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 6,
|
||||
step: 1
|
||||
},
|
||||
defaultValue: 0
|
||||
})
|
||||
|
||||
this.settings.addSetting({
|
||||
id: 'Comfy.EnableTooltips',
|
||||
category: ['Comfy', 'Node', 'EnableTooltips'],
|
||||
name: 'Enable Tooltips',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
this.setup(document.body)
|
||||
}
|
||||
|
||||
setup(containerElement: HTMLElement) {
|
||||
const fileInput = $el('input', {
|
||||
id: 'comfy-file-input',
|
||||
type: 'file',
|
||||
@@ -497,7 +424,7 @@ export class ComfyUI {
|
||||
this.menuHamburger = $el(
|
||||
'div.comfy-menu-hamburger',
|
||||
{
|
||||
parent: document.body,
|
||||
parent: containerElement,
|
||||
onclick: () => {
|
||||
this.menuContainer.style.display = 'block'
|
||||
this.menuHamburger.style.display = 'none'
|
||||
@@ -506,7 +433,7 @@ export class ComfyUI {
|
||||
[$el('div'), $el('div'), $el('div')]
|
||||
) as HTMLDivElement
|
||||
|
||||
this.menuContainer = $el('div.comfy-menu', { parent: document.body }, [
|
||||
this.menuContainer = $el('div.comfy-menu', { parent: containerElement }, [
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
@@ -661,7 +588,7 @@ export class ComfyUI {
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
let filename = 'workflow.json'
|
||||
if (promptFilename.value) {
|
||||
if (useSettingStore().get('Comfy.PromptFilename')) {
|
||||
filename = prompt('Save workflow as:', filename)
|
||||
if (!filename) return
|
||||
if (!filename.toLowerCase().endsWith('.json')) {
|
||||
@@ -692,7 +619,7 @@ export class ComfyUI {
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
let filename = 'workflow_api.json'
|
||||
if (promptFilename.value) {
|
||||
if (useSettingStore().get('Comfy.PromptFilename')) {
|
||||
filename = prompt('Save workflow (API) as:', filename)
|
||||
if (!filename) return
|
||||
if (!filename.toLowerCase().endsWith('.json')) {
|
||||
@@ -730,13 +657,17 @@ export class ComfyUI {
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
// @ts-expect-error Move to ComfyApp
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (!confirmClear.value || confirm('Clear workflow?')) {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
@@ -748,7 +679,10 @@ export class ComfyUI {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (!confirmClear.value || confirm('Load default workflow?')) {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
@@ -789,17 +723,6 @@ export class ComfyUI {
|
||||
})
|
||||
]) as HTMLDivElement
|
||||
|
||||
this.settings.addSetting({
|
||||
id: 'Comfy.DevMode',
|
||||
name: 'Enable dev mode options (API save, etc.)',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
onChange: function (value) {
|
||||
document.getElementById('comfy-dev-save-api-button').style.display =
|
||||
value ? 'flex' : 'none'
|
||||
}
|
||||
})
|
||||
|
||||
this.restoreMenuPosition = dragElement(this.menuContainer, this.settings)
|
||||
|
||||
this.setStatus({ exec_info: { queue_remaining: 'X' } })
|
||||
|
||||
@@ -181,7 +181,9 @@ export class ComfyAppMenu {
|
||||
resizeHandler = null
|
||||
}
|
||||
document.body.style.removeProperty('display')
|
||||
app.ui.menuContainer.style.removeProperty('display')
|
||||
if (app.ui.menuContainer) {
|
||||
app.ui.menuContainer.style.removeProperty('display')
|
||||
}
|
||||
this.element.style.display = 'none'
|
||||
app.ui.restoreMenuPosition()
|
||||
}
|
||||
@@ -192,7 +194,9 @@ export class ComfyAppMenu {
|
||||
|
||||
updatePosition(v: MenuPosition) {
|
||||
document.body.style.display = 'grid'
|
||||
this.app.ui.menuContainer.style.display = 'none'
|
||||
if (this.app.ui.menuContainer) {
|
||||
this.app.ui.menuContainer.style.display = 'none'
|
||||
}
|
||||
this.element.style.removeProperty('display')
|
||||
this.position = v
|
||||
if (v === 'Bottom') {
|
||||
|
||||
@@ -4,10 +4,12 @@ import { $el } from '../../ui'
|
||||
import { api } from '../../api'
|
||||
import { ComfyPopup } from '../components/popup'
|
||||
import { createSpinner } from '../spinner'
|
||||
import { ComfyWorkflow, trimJsonExt } from '../../workflows'
|
||||
import { ComfyWorkflow } from '../../workflows'
|
||||
import { ComfyAsyncDialog } from '../components/asyncDialog'
|
||||
import { trimJsonExt } from '@/utils/formatUtil'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyComponent } from '../components'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
export class ComfyWorkflowsMenu {
|
||||
#first = true
|
||||
@@ -67,19 +69,12 @@ export class ComfyWorkflowsMenu {
|
||||
this.unsaved = prop(this, 'unsaved', classList.unsaved, (v) => {
|
||||
classList.unsaved = v
|
||||
this.button.classList = classList
|
||||
setStorageValue('Comfy.PreviousWorkflowUnsaved', v)
|
||||
})
|
||||
}
|
||||
setStorageValue('Comfy.PreviousWorkflowUnsaved', String(v))
|
||||
|
||||
#updateProgress = () => {
|
||||
const prompt = this.app.workflowManager.activePrompt
|
||||
let percent = 0
|
||||
if (this.app.workflowManager.activeWorkflow === prompt?.workflow) {
|
||||
const total = Object.values(prompt.nodes)
|
||||
const done = total.filter(Boolean)
|
||||
percent = (done.length / total.length) * 100
|
||||
}
|
||||
this.buttonProgress.style.width = percent + '%'
|
||||
if (this.app.vueAppReady) {
|
||||
useWorkflowStore().previousWorkflowUnsaved = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#updateActive = () => {
|
||||
@@ -93,8 +88,6 @@ export class ComfyWorkflowsMenu {
|
||||
this.#first = false
|
||||
this.content.load()
|
||||
}
|
||||
|
||||
this.#updateProgress()
|
||||
}
|
||||
|
||||
#bindEvents() {
|
||||
@@ -109,10 +102,6 @@ export class ComfyWorkflowsMenu {
|
||||
this.unsaved = this.app.workflowManager.activeWorkflow.unsaved
|
||||
})
|
||||
|
||||
this.app.workflowManager.addEventListener('execute', (e) => {
|
||||
this.#updateProgress()
|
||||
})
|
||||
|
||||
api.addEventListener('graphChanged', () => {
|
||||
this.unsaved = true
|
||||
})
|
||||
@@ -371,9 +360,6 @@ export class ComfyWorkflowsContent {
|
||||
app.workflowManager.addEventListener(e, () => this.updateOpen())
|
||||
}
|
||||
this.app.workflowManager.addEventListener('rename', () => this.load())
|
||||
this.app.workflowManager.addEventListener('execute', (e) =>
|
||||
this.#updateActive()
|
||||
)
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
||||
@@ -56,14 +56,12 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
|
||||
}
|
||||
|
||||
// Find node with matching S&R property name
|
||||
// @ts-expect-error
|
||||
let nodes = app.graph._nodes.filter(
|
||||
let nodes = app.graph.nodes.filter(
|
||||
(n) => n.properties?.['Node name for S&R'] === split[0]
|
||||
)
|
||||
// If we cant, see if there is a node with that title
|
||||
if (!nodes.length) {
|
||||
// @ts-expect-error
|
||||
nodes = app.graph._nodes.filter((n) => n.title === split[0])
|
||||
nodes = app.graph.nodes.filter((n) => n.title === split[0])
|
||||
}
|
||||
if (!nodes.length) {
|
||||
console.warn('Unable to find node', split[0])
|
||||
@@ -155,7 +153,7 @@ export function prop<T>(
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
export function getStorageValue(id) {
|
||||
export function getStorageValue(id: string) {
|
||||
const clientId = api.clientId ?? api.initialClientId
|
||||
return (
|
||||
(clientId && sessionStorage.getItem(`${id}:${clientId}`)) ??
|
||||
@@ -163,7 +161,7 @@ export function getStorageValue(id) {
|
||||
)
|
||||
}
|
||||
|
||||
export function setStorageValue(id, value) {
|
||||
export function setStorageValue(id: string, value: string) {
|
||||
const clientId = api.clientId ?? api.initialClientId
|
||||
if (clientId) {
|
||||
sessionStorage.setItem(`${id}:${clientId}`, value)
|
||||
|
||||
@@ -361,7 +361,7 @@ export function initWidgets(app) {
|
||||
options: ['before', 'after'],
|
||||
onChange(value) {
|
||||
controlValueRunBefore = value === 'before'
|
||||
for (const n of app.graph._nodes) {
|
||||
for (const n of app.graph.nodes) {
|
||||
if (!n.widgets) continue
|
||||
for (const w of n.widgets) {
|
||||
if (w[IS_CONTROL_WIDGET]) {
|
||||
|
||||
@@ -4,93 +4,56 @@ import { ChangeTracker } from './changeTracker'
|
||||
import { ComfyAsyncDialog } from './ui/components/asyncDialog'
|
||||
import { getStorageValue, setStorageValue } from './utils'
|
||||
import { LGraphCanvas, LGraph } from '@comfyorg/litegraph'
|
||||
|
||||
function appendJsonExt(path: string) {
|
||||
if (!path.toLowerCase().endsWith('.json')) {
|
||||
path += '.json'
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function trimJsonExt(path: string) {
|
||||
return path?.replace(/\.json$/, '')
|
||||
}
|
||||
import { appendJsonExt, trimJsonExt } from '@/utils/formatUtil'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { markRaw, toRaw } from 'vue'
|
||||
|
||||
export class ComfyWorkflowManager extends EventTarget {
|
||||
#activePromptId: string | null = null
|
||||
#unsavedCount = 0
|
||||
#activeWorkflow: ComfyWorkflow
|
||||
executionStore: ReturnType<typeof useExecutionStore> | null
|
||||
workflowStore: ReturnType<typeof useWorkflowStore> | null
|
||||
|
||||
workflowLookup: Record<string, ComfyWorkflow> = {}
|
||||
workflows: Array<ComfyWorkflow> = []
|
||||
openWorkflows: Array<ComfyWorkflow> = []
|
||||
queuedPrompts: Record<
|
||||
string,
|
||||
{ workflow?: ComfyWorkflow; nodes?: Record<string, boolean> }
|
||||
> = {}
|
||||
app: ComfyApp
|
||||
#unsavedCount = 0
|
||||
|
||||
get activeWorkflow() {
|
||||
return this.#activeWorkflow ?? this.openWorkflows[0]
|
||||
get workflowLookup(): Record<string, ComfyWorkflow> {
|
||||
return this.workflowStore?.workflowLookup ?? {}
|
||||
}
|
||||
|
||||
get workflows(): ComfyWorkflow[] {
|
||||
return this.workflowStore?.workflows ?? []
|
||||
}
|
||||
|
||||
get openWorkflows(): ComfyWorkflow[] {
|
||||
return (this.workflowStore?.openWorkflows ?? []) as ComfyWorkflow[]
|
||||
}
|
||||
|
||||
get _activeWorkflow(): ComfyWorkflow | null {
|
||||
if (!this.app.vueAppReady) return null
|
||||
return toRaw(useWorkflowStore().activeWorkflow) as ComfyWorkflow | null
|
||||
}
|
||||
|
||||
set _activeWorkflow(workflow: ComfyWorkflow | null) {
|
||||
if (!this.app.vueAppReady) return
|
||||
useWorkflowStore().activeWorkflow = workflow ? markRaw(workflow) : null
|
||||
}
|
||||
|
||||
get activeWorkflow(): ComfyWorkflow | null {
|
||||
return this._activeWorkflow ?? this.openWorkflows[0]
|
||||
}
|
||||
|
||||
get activePromptId() {
|
||||
return this.#activePromptId
|
||||
return this.executionStore?.activePromptId
|
||||
}
|
||||
|
||||
get activePrompt() {
|
||||
return this.queuedPrompts[this.#activePromptId]
|
||||
return this.executionStore?.activePrompt
|
||||
}
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
super()
|
||||
this.app = app
|
||||
ChangeTracker.init(app)
|
||||
|
||||
this.#bindExecutionEvents()
|
||||
}
|
||||
|
||||
#bindExecutionEvents() {
|
||||
// TODO: on reload, set active prompt based on the latest ws message
|
||||
|
||||
const emit = () =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('execute', { detail: this.activePrompt })
|
||||
)
|
||||
let executing = null
|
||||
api.addEventListener('execution_start', (e) => {
|
||||
this.#activePromptId = e.detail.prompt_id
|
||||
|
||||
// This event can fire before the event is stored, so put a placeholder
|
||||
this.queuedPrompts[this.#activePromptId] ??= { nodes: {} }
|
||||
emit()
|
||||
})
|
||||
api.addEventListener('execution_cached', (e) => {
|
||||
if (!this.activePrompt) return
|
||||
for (const n of e.detail.nodes) {
|
||||
this.activePrompt.nodes[n] = true
|
||||
}
|
||||
emit()
|
||||
})
|
||||
api.addEventListener('executed', (e) => {
|
||||
if (!this.activePrompt) return
|
||||
this.activePrompt.nodes[e.detail.node] = true
|
||||
emit()
|
||||
})
|
||||
api.addEventListener('executing', (e) => {
|
||||
if (!this.activePrompt) return
|
||||
|
||||
if (executing) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
this.activePrompt.nodes[executing] = true
|
||||
}
|
||||
executing = e.detail
|
||||
if (!executing) {
|
||||
delete this.queuedPrompts[this.#activePromptId]
|
||||
this.#activePromptId = null
|
||||
}
|
||||
emit()
|
||||
})
|
||||
}
|
||||
|
||||
async loadWorkflows() {
|
||||
@@ -115,16 +78,13 @@ export class ComfyWorkflowManager extends EventTarget {
|
||||
w.slice(1),
|
||||
favorites.has(w[0])
|
||||
)
|
||||
this.workflowLookup[workflow.path] = workflow
|
||||
this.workflowLookup[workflow.path] = markRaw(workflow)
|
||||
}
|
||||
return workflow
|
||||
}
|
||||
)
|
||||
|
||||
this.workflows = workflows
|
||||
} catch (error) {
|
||||
alert('Error loading workflows: ' + (error.message ?? error))
|
||||
this.workflows = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,25 +124,21 @@ export class ComfyWorkflowManager extends EventTarget {
|
||||
const index = this.openWorkflows.indexOf(workflow)
|
||||
if (index === -1) {
|
||||
// Opening a new workflow
|
||||
this.openWorkflows.push(workflow)
|
||||
this.openWorkflows.push(markRaw(workflow))
|
||||
}
|
||||
|
||||
this.#activeWorkflow = workflow
|
||||
this._activeWorkflow = workflow
|
||||
|
||||
setStorageValue('Comfy.PreviousWorkflow', this.activeWorkflow.path ?? '')
|
||||
this.dispatchEvent(new CustomEvent('changeWorkflow'))
|
||||
}
|
||||
|
||||
storePrompt({ nodes, id }) {
|
||||
this.queuedPrompts[id] ??= {}
|
||||
this.queuedPrompts[id].nodes = {
|
||||
...nodes.reduce((p, n) => {
|
||||
p[n] = false
|
||||
return p
|
||||
}, {}),
|
||||
...this.queuedPrompts[id].nodes
|
||||
}
|
||||
this.queuedPrompts[id].workflow = this.activeWorkflow
|
||||
this.executionStore?.storePrompt({
|
||||
nodes,
|
||||
id,
|
||||
workflow: this.activeWorkflow
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,8 +175,8 @@ export class ComfyWorkflowManager extends EventTarget {
|
||||
workflow.changeTracker = null
|
||||
this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1)
|
||||
if (this.openWorkflows.length) {
|
||||
this.#activeWorkflow = this.openWorkflows[0]
|
||||
await this.#activeWorkflow.load()
|
||||
this._activeWorkflow = this.openWorkflows[0]
|
||||
await this._activeWorkflow.load()
|
||||
} else {
|
||||
// Load default
|
||||
await this.app.loadGraphData()
|
||||
|
||||
348
src/stores/coreSettings.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
import {
|
||||
LinkReleaseTriggerAction,
|
||||
LinkReleaseTriggerMode
|
||||
} from '@/types/searchBoxTypes'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
|
||||
export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.Validation.Workflows',
|
||||
name: 'Validate workflows',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl',
|
||||
category: ['Comfy', 'Node Search Box', 'Implementation'],
|
||||
experimental: true,
|
||||
name: 'Node search box implementation',
|
||||
type: 'combo',
|
||||
options: ['default', 'litegraph (legacy)'],
|
||||
defaultValue: 'default'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger',
|
||||
category: ['Comfy', 'Node Search Box', 'LinkReleaseTrigger'],
|
||||
name: 'Trigger on link release',
|
||||
type: 'hidden',
|
||||
options: Object.values(LinkReleaseTriggerMode),
|
||||
defaultValue: LinkReleaseTriggerMode.ALWAYS,
|
||||
deprecated: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LinkRelease.Action',
|
||||
name: 'Action on link release (No modifier)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LinkRelease.ActionShift',
|
||||
name: 'Action on link release (Shift)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl.NodePreview',
|
||||
category: ['Comfy', 'Node Search Box', 'NodePreview'],
|
||||
name: 'Node preview',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl.ShowCategory',
|
||||
category: ['Comfy', 'Node Search Box', 'ShowCategory'],
|
||||
name: 'Show node category in search results',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl.ShowIdName',
|
||||
category: ['Comfy', 'Node Search Box', 'ShowIdName'],
|
||||
name: 'Show node id name in search results',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.Location',
|
||||
category: ['Comfy', 'Sidebar', 'Location'],
|
||||
name: 'Sidebar location',
|
||||
type: 'combo',
|
||||
options: ['left', 'right'],
|
||||
defaultValue: 'left'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
category: ['Comfy', 'Sidebar', 'Size'],
|
||||
name: 'Sidebar size',
|
||||
type: 'combo',
|
||||
options: ['normal', 'small'],
|
||||
defaultValue: window.innerWidth < 1600 ? 'small' : 'normal'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TextareaWidget.FontSize',
|
||||
category: ['Comfy', 'Node Widget', 'TextareaWidget', 'FontSize'],
|
||||
name: 'Textarea widget font size',
|
||||
type: 'slider',
|
||||
defaultValue: 10,
|
||||
attrs: {
|
||||
min: 8,
|
||||
max: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TextareaWidget.Spellcheck',
|
||||
category: ['Comfy', 'Node Widget', 'TextareaWidget', 'Spellcheck'],
|
||||
name: 'Textarea widget spellcheck',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.SortNodeIdOnSave',
|
||||
name: 'Sort node IDs when saving workflow',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.CanvasInfo',
|
||||
name: 'Show canvas info (fps, etc.)',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.ShowDeprecated',
|
||||
name: 'Show deprecated nodes in search',
|
||||
tooltip:
|
||||
'Deprecated nodes are hidden by default in the UI, but remain functional in existing workflows that use them.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.ShowExperimental',
|
||||
name: 'Show experimental nodes in search',
|
||||
tooltip:
|
||||
'Experimental nodes are marked as such in the UI and may be subject to significant changes or removal in future versions. Use with caution in production workflows',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.ShowMissingNodesWarning',
|
||||
name: 'Show missing nodes warning',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
name: 'Show missing models warning',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ZoomSpeed',
|
||||
name: 'Canvas zoom speed',
|
||||
type: 'slider',
|
||||
defaultValue: 1.1,
|
||||
attrs: {
|
||||
min: 1.01,
|
||||
max: 2.5,
|
||||
step: 0.01
|
||||
}
|
||||
},
|
||||
// Bookmarks are stored in the settings store.
|
||||
// Bookmarks are in format of category/display_name. e.g. "conditioning/CLIPTextEncode"
|
||||
{
|
||||
id: 'Comfy.NodeLibrary.Bookmarks',
|
||||
name: 'Node library bookmarks with display name (deprecated)',
|
||||
type: 'hidden',
|
||||
defaultValue: [],
|
||||
deprecated: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeLibrary.Bookmarks.V2',
|
||||
name: 'Node library bookmarks v2 with unique name',
|
||||
type: 'hidden',
|
||||
defaultValue: []
|
||||
},
|
||||
// Stores mapping from bookmark folder name to its customization.
|
||||
{
|
||||
id: 'Comfy.NodeLibrary.BookmarksCustomization',
|
||||
name: 'Node library bookmarks customization',
|
||||
type: 'hidden',
|
||||
defaultValue: {}
|
||||
},
|
||||
// Hidden setting used by the queue for how to fit images
|
||||
{
|
||||
id: 'Comfy.Queue.ImageFit',
|
||||
name: 'Queue image fit',
|
||||
type: 'hidden',
|
||||
defaultValue: 'cover'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.ModelDownload.AllowedSources',
|
||||
name: 'Allowed model download sources',
|
||||
type: 'hidden',
|
||||
defaultValue: ['https://huggingface.co/', 'https://civitai.com/']
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.ModelDownload.AllowedSuffixes',
|
||||
name: 'Allowed model download suffixes',
|
||||
type: 'hidden',
|
||||
defaultValue: ['.safetensors', '.sft']
|
||||
},
|
||||
{
|
||||
id: 'Comfy.GroupSelectedNodes.Padding',
|
||||
name: 'Group selected nodes padding',
|
||||
type: 'slider',
|
||||
defaultValue: 10,
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Node.DoubleClickTitleToEdit',
|
||||
name: 'Double click node title to edit',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Group.DoubleClickTitleToEdit',
|
||||
name: 'Double click group title to edit',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Window.UnloadConfirmation',
|
||||
name: 'Show confirmation when closing window',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TreeExplorer.ItemPadding',
|
||||
name: 'Tree explorer item padding',
|
||||
type: 'slider',
|
||||
defaultValue: 2,
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 8,
|
||||
step: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
options: ['en', 'zh'],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeSourceBadgeMode',
|
||||
name: 'Node source badge mode',
|
||||
type: 'combo',
|
||||
options: Object.values(NodeBadgeMode),
|
||||
defaultValue: NodeBadgeMode.HideBuiltIn
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
name: 'Node ID badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
defaultValue: NodeBadgeMode.ShowAll
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
|
||||
name: 'Node life cycle badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
defaultValue: NodeBadgeMode.ShowAll
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ConfirmClear',
|
||||
category: ['Comfy', 'Workflow', 'ConfirmClear'],
|
||||
name: 'Require confirmation when clearing workflow',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.PromptFilename',
|
||||
category: ['Comfy', 'Workflow', 'PromptFilename'],
|
||||
name: 'Prompt for filename when saving workflow',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
/**
|
||||
* file format for preview
|
||||
*
|
||||
* format;quality
|
||||
*
|
||||
* ex)
|
||||
* webp;50 -> webp, quality 50
|
||||
* jpeg;80 -> rgb, jpeg, quality 80
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
{
|
||||
id: 'Comfy.PreviewFormat',
|
||||
category: ['Comfy', 'Node Widget', 'PreviewFormat'],
|
||||
name: 'Preview image format',
|
||||
tooltip:
|
||||
'When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.',
|
||||
type: 'text',
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DisableSliders',
|
||||
category: ['Comfy', 'Node Widget', 'DisableSliders'],
|
||||
name: 'Disable node widget sliders',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DisableFloatRounding',
|
||||
category: ['Comfy', 'Node Widget', 'DisableFloatRounding'],
|
||||
name: 'Disable default float widget rounding.',
|
||||
tooltip:
|
||||
'(requires page reload) Cannot disable round when round is set by the node in the backend.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.FloatRoundingPrecision',
|
||||
category: ['Comfy', 'Node Widget', 'FloatRoundingPrecision'],
|
||||
name: 'Float widget rounding decimal places [0 = auto].',
|
||||
tooltip: '(requires page reload)',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 6,
|
||||
step: 1
|
||||
},
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
id: 'Comfy.EnableTooltips',
|
||||
category: ['Comfy', 'Node', 'EnableTooltips'],
|
||||
name: 'Enable Tooltips',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DevMode',
|
||||
name: 'Enable dev mode options (API save, etc.)',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
onChange: (value) => {
|
||||
const element = document.getElementById('comfy-dev-save-api-button')
|
||||
if (element) {
|
||||
element.style.display = value ? 'flex' : 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
119
src/stores/executionStore.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '../scripts/api'
|
||||
|
||||
export interface QueuedPrompt {
|
||||
nodes: Record<string, boolean>
|
||||
workflow?: any // TODO: Replace 'any' with the actual type of workflow
|
||||
}
|
||||
|
||||
export const useExecutionStore = defineStore('execution', () => {
|
||||
const activePromptId = ref<string | null>(null)
|
||||
const queuedPrompts = ref<Record<string, QueuedPrompt>>({})
|
||||
const executingNodeId = ref<string | null>(null)
|
||||
|
||||
const activePrompt = computed(() => queuedPrompts.value[activePromptId.value])
|
||||
|
||||
const totalNodesToExecute = computed(() => {
|
||||
if (!activePrompt.value) return 0
|
||||
return Object.values(activePrompt.value.nodes).length
|
||||
})
|
||||
|
||||
const isIdle = computed(() => !activePromptId.value)
|
||||
|
||||
const nodesExecuted = computed(() => {
|
||||
if (!activePrompt.value) return 0
|
||||
return Object.values(activePrompt.value.nodes).filter(Boolean).length
|
||||
})
|
||||
|
||||
const executionProgress = computed(() => {
|
||||
if (!activePrompt.value) return 0
|
||||
const total = totalNodesToExecute.value
|
||||
const done = nodesExecuted.value
|
||||
return Math.round((done / total) * 100)
|
||||
})
|
||||
|
||||
function bindExecutionEvents() {
|
||||
api.addEventListener('execution_start', handleExecutionStart)
|
||||
api.addEventListener('execution_cached', handleExecutionCached)
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
}
|
||||
|
||||
function unbindExecutionEvents() {
|
||||
api.removeEventListener('execution_start', handleExecutionStart)
|
||||
api.removeEventListener('execution_cached', handleExecutionCached)
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
}
|
||||
|
||||
function handleExecutionStart(e: CustomEvent) {
|
||||
activePromptId.value = e.detail.prompt_id
|
||||
queuedPrompts.value[activePromptId.value] ??= { nodes: {} }
|
||||
}
|
||||
|
||||
function handleExecutionCached(e: CustomEvent) {
|
||||
if (!activePrompt.value) return
|
||||
for (const n of e.detail.nodes) {
|
||||
activePrompt.value.nodes[n] = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleExecuted(e: CustomEvent) {
|
||||
if (!activePrompt.value) return
|
||||
activePrompt.value.nodes[e.detail.node] = true
|
||||
}
|
||||
|
||||
function handleExecuting(e: CustomEvent) {
|
||||
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
|
||||
if (!executingNodeId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
activePromptId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function storePrompt({
|
||||
nodes,
|
||||
id,
|
||||
workflow
|
||||
}: {
|
||||
nodes: string[]
|
||||
id: string
|
||||
workflow: any
|
||||
}) {
|
||||
queuedPrompts.value[id] ??= { nodes: {} }
|
||||
const queuedPrompt = queuedPrompts.value[id]
|
||||
queuedPrompt.nodes = {
|
||||
...nodes.reduce((p, n) => {
|
||||
p[n] = false
|
||||
return p
|
||||
}, {}),
|
||||
...queuedPrompt.nodes
|
||||
}
|
||||
queuedPrompt.workflow = workflow
|
||||
|
||||
console.debug(
|
||||
`queued task ${id} with ${Object.values(queuedPrompt.nodes).length} nodes`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
isIdle,
|
||||
activePromptId,
|
||||
queuedPrompts,
|
||||
executingNodeId,
|
||||
activePrompt,
|
||||
totalNodesToExecute,
|
||||
nodesExecuted,
|
||||
executionProgress,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt
|
||||
}
|
||||
})
|
||||
@@ -314,6 +314,11 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
this.nodeDefsByName = newNodeDefsByName
|
||||
this.nodeDefsByDisplayName = nodeDefsByDisplayName
|
||||
},
|
||||
addNodeDef(nodeDef: ComfyNodeDef) {
|
||||
const nodeDefImpl = plainToClass(ComfyNodeDefImpl, nodeDef)
|
||||
this.nodeDefsByName[nodeDef.name] = nodeDefImpl
|
||||
this.nodeDefsByDisplayName[nodeDef.display_name] = nodeDefImpl
|
||||
},
|
||||
updateWidgets(widgets: Record<string, ComfyWidgetConstructor>) {
|
||||
this.widgets = widgets
|
||||
},
|
||||
|
||||
@@ -277,6 +277,9 @@ export const useQueueStore = defineStore('queue', {
|
||||
},
|
||||
lastHistoryQueueIndex(state) {
|
||||
return state.historyTasks.length ? state.historyTasks[0].queueIndex : -1
|
||||
},
|
||||
hasPendingTasks(state) {
|
||||
return state.pendingTasks.length > 0
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -325,10 +328,11 @@ export const useQueueStore = defineStore('queue', {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
async clear() {
|
||||
await Promise.all(
|
||||
['queue', 'history'].map((type) => api.clearItems(type))
|
||||
)
|
||||
async clear(targets: ('queue' | 'history')[] = ['queue', 'history']) {
|
||||
if (targets.length === 0) {
|
||||
return
|
||||
}
|
||||
await Promise.all(targets.map((type) => api.clearItems(type)))
|
||||
await this.update()
|
||||
},
|
||||
async delete(task: TaskItemImpl) {
|
||||
|
||||
@@ -9,16 +9,12 @@
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfySettingsDialog } from '@/scripts/ui/settings'
|
||||
import { Settings } from '@/types/apiTypes'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
import {
|
||||
LinkReleaseTriggerAction,
|
||||
LinkReleaseTriggerMode
|
||||
} from '@/types/searchBoxTypes'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import type { Settings } from '@/types/apiTypes'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { CORE_SETTINGS } from '@/stores/coreSettings'
|
||||
|
||||
export interface SettingTreeNode extends TreeNode {
|
||||
data?: SettingParams
|
||||
@@ -66,286 +62,8 @@ export const useSettingStore = defineStore('setting', {
|
||||
}
|
||||
this.settings = settings.settingsParamLookup
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Validation.Workflows',
|
||||
name: 'Validate workflows',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeSearchBoxImpl',
|
||||
category: ['Comfy', 'Node Search Box', 'Implementation'],
|
||||
experimental: true,
|
||||
name: 'Node search box implementation',
|
||||
type: 'combo',
|
||||
options: ['default', 'litegraph (legacy)'],
|
||||
defaultValue: 'default'
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger',
|
||||
category: ['Comfy', 'Node Search Box', 'LinkReleaseTrigger'],
|
||||
name: 'Trigger on link release',
|
||||
type: 'hidden',
|
||||
options: Object.values(LinkReleaseTriggerMode),
|
||||
defaultValue: LinkReleaseTriggerMode.ALWAYS,
|
||||
deprecated: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.LinkRelease.Action',
|
||||
name: 'Action on link release (No modifier)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.LinkRelease.ActionShift',
|
||||
name: 'Action on link release (Shift)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeSearchBoxImpl.NodePreview',
|
||||
category: ['Comfy', 'Node Search Box', 'NodePreview'],
|
||||
name: 'Node preview',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeSearchBoxImpl.ShowCategory',
|
||||
category: ['Comfy', 'Node Search Box', 'ShowCategory'],
|
||||
name: 'Show node category in search results',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeSearchBoxImpl.ShowIdName',
|
||||
category: ['Comfy', 'Node Search Box', 'ShowIdName'],
|
||||
name: 'Show node id name in search results',
|
||||
tooltip: 'Only applies to the default implementation',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Sidebar.Location',
|
||||
category: ['Comfy', 'Sidebar', 'Location'],
|
||||
name: 'Sidebar location',
|
||||
type: 'combo',
|
||||
options: ['left', 'right'],
|
||||
defaultValue: 'left'
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
category: ['Comfy', 'Sidebar', 'Size'],
|
||||
name: 'Sidebar size',
|
||||
type: 'combo',
|
||||
options: ['normal', 'small'],
|
||||
defaultValue: window.innerWidth < 1600 ? 'small' : 'normal'
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.TextareaWidget.FontSize',
|
||||
category: ['Comfy', 'Node Widget', 'TextareaWidget', 'FontSize'],
|
||||
name: 'Textarea widget font size',
|
||||
type: 'slider',
|
||||
defaultValue: 10,
|
||||
attrs: {
|
||||
min: 8,
|
||||
max: 24
|
||||
}
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.TextareaWidget.Spellcheck',
|
||||
category: ['Comfy', 'Node Widget', 'TextareaWidget', 'Spellcheck'],
|
||||
name: 'Textarea widget spellcheck',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Workflow.SortNodeIdOnSave',
|
||||
name: 'Sort node IDs when saving workflow',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Graph.CanvasInfo',
|
||||
name: 'Show canvas info (fps, etc.)',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Node.ShowDeprecated',
|
||||
name: 'Show deprecated nodes in search',
|
||||
tooltip:
|
||||
'Deprecated nodes are hidden by default in the UI, but remain functional in existing workflows that use them.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Node.ShowExperimental',
|
||||
name: 'Show experimental nodes in search',
|
||||
tooltip:
|
||||
'Experimental nodes are marked as such in the UI and may be subject to significant changes or removal in future versions. Use with caution in production workflows',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Workflow.ShowMissingNodesWarning',
|
||||
name: 'Show missing nodes warning',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
name: 'Show missing models warning',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Graph.ZoomSpeed',
|
||||
name: 'Canvas zoom speed',
|
||||
type: 'slider',
|
||||
defaultValue: 1.1,
|
||||
attrs: {
|
||||
min: 1.01,
|
||||
max: 2.5,
|
||||
step: 0.01
|
||||
}
|
||||
})
|
||||
|
||||
// Bookmarks are stored in the settings store.
|
||||
// Bookmarks are in format of category/display_name. e.g. "conditioning/CLIPTextEncode"
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeLibrary.Bookmarks',
|
||||
name: 'Node library bookmarks with display name (deprecated)',
|
||||
type: 'hidden',
|
||||
defaultValue: [],
|
||||
deprecated: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeLibrary.Bookmarks.V2',
|
||||
name: 'Node library bookmarks v2 with unique name',
|
||||
type: 'hidden',
|
||||
defaultValue: []
|
||||
})
|
||||
|
||||
// Stores mapping from bookmark folder name to its customization.
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeLibrary.BookmarksCustomization',
|
||||
name: 'Node library bookmarks customization',
|
||||
type: 'hidden',
|
||||
defaultValue: {}
|
||||
})
|
||||
|
||||
// Hidden setting used by the queue for how to fit images
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Queue.ImageFit',
|
||||
name: 'Queue image fit',
|
||||
type: 'hidden',
|
||||
defaultValue: 'cover'
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Workflow.ModelDownload.AllowedSources',
|
||||
name: 'Allowed model download sources',
|
||||
type: 'hidden',
|
||||
defaultValue: ['https://huggingface.co/', 'https://civitai.com/']
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Workflow.ModelDownload.AllowedSuffixes',
|
||||
name: 'Allowed model download suffixes',
|
||||
type: 'hidden',
|
||||
defaultValue: ['.safetensors', '.sft']
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.GroupSelectedNodes.Padding',
|
||||
name: 'Group selected nodes padding',
|
||||
type: 'slider',
|
||||
defaultValue: 10,
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Node.DoubleClickTitleToEdit',
|
||||
name: 'Double click node title to edit',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Group.DoubleClickTitleToEdit',
|
||||
name: 'Double click group title to edit',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Window.UnloadConfirmation',
|
||||
name: 'Show confirmation when closing window',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.TreeExplorer.ItemPadding',
|
||||
name: 'Tree explorer item padding',
|
||||
type: 'slider',
|
||||
defaultValue: 2,
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 8,
|
||||
step: 1
|
||||
}
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
options: ['en', 'zh'],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeBadge.NodeSourceBadgeMode',
|
||||
name: 'Node source badge mode',
|
||||
type: 'combo',
|
||||
options: Object.values(NodeBadgeMode),
|
||||
defaultValue: NodeBadgeMode.HideBuiltIn
|
||||
})
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: 'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
name: 'Node ID badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
defaultValue: NodeBadgeMode.ShowAll
|
||||
CORE_SETTINGS.forEach((setting: SettingParams) => {
|
||||
settings.addSetting(setting)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,173 +1,138 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
import { computed, ref } from 'vue'
|
||||
import { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
interface OpenFile {
|
||||
path: string
|
||||
content: string
|
||||
isModified: boolean
|
||||
originalContent: string
|
||||
export class UserFile {
|
||||
isLoading: boolean = false
|
||||
content: string | null = null
|
||||
originalContent: string | null = null
|
||||
|
||||
constructor(
|
||||
public path: string,
|
||||
public lastModified: number,
|
||||
public size: number
|
||||
) {}
|
||||
|
||||
get isOpen() {
|
||||
return !!this.content
|
||||
}
|
||||
|
||||
get isModified() {
|
||||
return this.content !== this.originalContent
|
||||
}
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
files: string[]
|
||||
openFiles: OpenFile[]
|
||||
}
|
||||
export const useUserFileStore = defineStore('userFile', () => {
|
||||
const userFilesByPath = ref(new Map<string, UserFile>())
|
||||
|
||||
interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
data?: T
|
||||
message?: string
|
||||
}
|
||||
const userFiles = computed(() => Array.from(userFilesByPath.value.values()))
|
||||
const modifiedFiles = computed(() =>
|
||||
userFiles.value.filter((file: UserFile) => file.isModified)
|
||||
)
|
||||
const openedFiles = computed(() =>
|
||||
userFiles.value.filter((file: UserFile) => file.isOpen)
|
||||
)
|
||||
|
||||
export const useUserFileStore = defineStore('userFile', {
|
||||
state: (): StoreState => ({
|
||||
files: [],
|
||||
openFiles: []
|
||||
}),
|
||||
const workflowsTree = computed<TreeExplorerNode<UserFile>>(
|
||||
() =>
|
||||
buildTree<UserFile>(userFiles.value, (userFile: UserFile) =>
|
||||
userFile.path.split('/')
|
||||
) as TreeExplorerNode<UserFile>
|
||||
)
|
||||
|
||||
getters: {
|
||||
getOpenFile: (state) => (path: string) =>
|
||||
state.openFiles.find((file) => file.path === path),
|
||||
modifiedFiles: (state) => state.openFiles.filter((file) => file.isModified),
|
||||
workflowsTree: (state): TreeNode =>
|
||||
buildTree(state.files, (path: string) => path.split('/'))
|
||||
},
|
||||
/**
|
||||
* Syncs the files in the given directory with the API.
|
||||
* @param dir The directory to sync.
|
||||
*/
|
||||
const syncFiles = async () => {
|
||||
const files = await api.listUserDataFullInfo('')
|
||||
|
||||
actions: {
|
||||
async openFile(path: string): Promise<void> {
|
||||
if (this.getOpenFile(path)) return
|
||||
for (const file of files) {
|
||||
const existingFile = userFilesByPath.value.get(file.path)
|
||||
|
||||
const { success, data } = await this.getFileData(path)
|
||||
if (success && data) {
|
||||
this.openFiles.push({
|
||||
path,
|
||||
content: data,
|
||||
isModified: false,
|
||||
originalContent: data
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
closeFile(path: string): void {
|
||||
const index = this.openFiles.findIndex((file) => file.path === path)
|
||||
if (index !== -1) {
|
||||
this.openFiles.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
updateFileContent(path: string, newContent: string): void {
|
||||
const file = this.getOpenFile(path)
|
||||
if (file) {
|
||||
file.content = newContent
|
||||
file.isModified = file.content !== file.originalContent
|
||||
}
|
||||
},
|
||||
|
||||
async saveOpenFile(path: string): Promise<ApiResponse> {
|
||||
const file = this.getOpenFile(path)
|
||||
if (file?.isModified) {
|
||||
const result = await this.saveFile(path, file.content)
|
||||
if (result.success) {
|
||||
file.isModified = false
|
||||
file.originalContent = file.content
|
||||
}
|
||||
return result
|
||||
}
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
discardChanges(path: string): void {
|
||||
const file = this.getOpenFile(path)
|
||||
if (file) {
|
||||
file.content = file.originalContent
|
||||
file.isModified = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadFiles(dir: string = './'): Promise<void> {
|
||||
this.files = (await api.listUserData(dir, true, false)).map(
|
||||
(filePath: string) => filePath.replaceAll('\\', '/')
|
||||
)
|
||||
|
||||
this.openFiles = (
|
||||
await Promise.all(
|
||||
this.openFiles.map(async (openFile) => {
|
||||
if (!this.files.includes(openFile.path)) return null
|
||||
|
||||
const { success, data } = await this.getFileData(openFile.path)
|
||||
if (success && data !== openFile.originalContent) {
|
||||
return {
|
||||
...openFile,
|
||||
content: data,
|
||||
originalContent: data,
|
||||
isModified: openFile.content !== data
|
||||
}
|
||||
}
|
||||
|
||||
return openFile
|
||||
})
|
||||
if (!existingFile) {
|
||||
// New file, add it to the map
|
||||
userFilesByPath.value.set(
|
||||
file.path,
|
||||
new UserFile(file.path, file.modified, file.size)
|
||||
)
|
||||
).filter((file): file is OpenFile => file !== null)
|
||||
},
|
||||
|
||||
async renameFile(oldPath: string, newPath: string): Promise<ApiResponse> {
|
||||
const resp = await api.moveUserData(oldPath, newPath)
|
||||
if (resp.status !== 200) {
|
||||
return { success: false, message: resp.statusText }
|
||||
} else if (existingFile.lastModified !== file.modified) {
|
||||
// File has been modified, update its properties
|
||||
existingFile.lastModified = file.modified
|
||||
existingFile.size = file.size
|
||||
existingFile.originalContent = null
|
||||
existingFile.content = null
|
||||
existingFile.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const openFile = this.openFiles.find((file) => file.path === oldPath)
|
||||
if (openFile) {
|
||||
openFile.path = newPath
|
||||
// Remove files that no longer exist
|
||||
for (const [path, _] of userFilesByPath.value) {
|
||||
if (!files.some((file) => file.path === path)) {
|
||||
userFilesByPath.value.delete(path)
|
||||
}
|
||||
|
||||
await this.loadFiles()
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async deleteFile(path: string): Promise<ApiResponse> {
|
||||
const resp = await api.deleteUserData(path)
|
||||
if (resp.status !== 204) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Error removing user data file '${path}': ${resp.status} ${resp.statusText}`
|
||||
}
|
||||
}
|
||||
|
||||
const index = this.openFiles.findIndex((file) => file.path === path)
|
||||
if (index !== -1) {
|
||||
this.openFiles.splice(index, 1)
|
||||
}
|
||||
|
||||
await this.loadFiles()
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async saveFile(path: string, data: string): Promise<ApiResponse> {
|
||||
const resp = await api.storeUserData(path, data, {
|
||||
stringify: false,
|
||||
throwOnError: false,
|
||||
overwrite: true
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Error saving user data file '${path}': ${resp.status} ${resp.statusText}`
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadFiles()
|
||||
return { success: true }
|
||||
},
|
||||
|
||||
async getFileData(path: string): Promise<ApiResponse<string>> {
|
||||
const resp = await api.getUserData(path)
|
||||
if (resp.status !== 200) {
|
||||
return { success: false, message: resp.statusText }
|
||||
}
|
||||
return { success: true, data: await resp.json() }
|
||||
}
|
||||
}
|
||||
|
||||
const loadFile = async (file: UserFile) => {
|
||||
file.isLoading = true
|
||||
const resp = await api.getUserData(file.path)
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to load file '${file.path}': ${resp.status} ${resp.statusText}`
|
||||
)
|
||||
}
|
||||
file.content = await resp.text()
|
||||
file.originalContent = file.content
|
||||
file.isLoading = false
|
||||
}
|
||||
|
||||
const saveFile = async (file: UserFile) => {
|
||||
if (file.isModified) {
|
||||
const resp = await api.storeUserData(file.path, file.content)
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to save file '${file.path}': ${resp.status} ${resp.statusText}`
|
||||
)
|
||||
}
|
||||
}
|
||||
await syncFiles()
|
||||
}
|
||||
|
||||
const deleteFile = async (file: UserFile) => {
|
||||
const resp = await api.deleteUserData(file.path)
|
||||
if (resp.status !== 204) {
|
||||
throw new Error(
|
||||
`Failed to delete file '${file.path}': ${resp.status} ${resp.statusText}`
|
||||
)
|
||||
}
|
||||
await syncFiles()
|
||||
}
|
||||
|
||||
const renameFile = async (file: UserFile, newPath: string) => {
|
||||
const resp = await api.moveUserData(file.path, newPath)
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to rename file '${file.path}': ${resp.status} ${resp.statusText}`
|
||||
)
|
||||
}
|
||||
file.path = newPath
|
||||
userFilesByPath.value.set(newPath, file)
|
||||
userFilesByPath.value.delete(file.path)
|
||||
await syncFiles()
|
||||
}
|
||||
|
||||
return {
|
||||
userFiles,
|
||||
modifiedFiles,
|
||||
openedFiles,
|
||||
workflowsTree,
|
||||
syncFiles,
|
||||
loadFile,
|
||||
saveFile,
|
||||
deleteFile,
|
||||
renameFile
|
||||
}
|
||||
})
|
||||
|
||||
23
src/stores/workflowStore.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||
import { getStorageValue } from '@/scripts/utils'
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
const activeWorkflow = ref<ComfyWorkflow | null>(null)
|
||||
const previousWorkflowUnsaved = ref<boolean>(
|
||||
Boolean(getStorageValue('Comfy.PreviousWorkflowUnsaved'))
|
||||
)
|
||||
|
||||
const workflowLookup = ref<Record<string, ComfyWorkflow>>({})
|
||||
const workflows = computed(() => Object.values(workflowLookup.value))
|
||||
const openWorkflows = ref<ComfyWorkflow[]>([])
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
previousWorkflowUnsaved,
|
||||
workflows,
|
||||
openWorkflows,
|
||||
workflowLookup
|
||||
}
|
||||
})
|
||||
@@ -247,7 +247,11 @@ function inputSpec(
|
||||
const zBaseInputSpecValue = z
|
||||
.object({
|
||||
default: z.any().optional(),
|
||||
forceInput: z.boolean().optional()
|
||||
defaultInput: z.boolean().optional(),
|
||||
forceInput: z.boolean().optional(),
|
||||
lazy: z.boolean().optional(),
|
||||
rawLink: z.boolean().optional(),
|
||||
tooltip: z.string().optional()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
@@ -415,7 +419,11 @@ const zUser = z.object({
|
||||
users: z.record(z.string(), z.unknown())
|
||||
})
|
||||
const zUserData = z.array(z.array(z.string(), z.string()))
|
||||
|
||||
const zUserDataFullInfo = z.object({
|
||||
path: z.string(),
|
||||
size: z.number(),
|
||||
modified: z.number()
|
||||
})
|
||||
const zBookmarkCustomization = z.object({
|
||||
icon: z.string().optional(),
|
||||
color: z.string().optional()
|
||||
@@ -492,7 +500,8 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
||||
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': zNodeBadgeMode,
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': zNodeBadgeMode
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': zNodeBadgeMode,
|
||||
'Comfy.NodeBadge.NodeLifeCycleBadgeMode': zNodeBadgeMode
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
@@ -505,3 +514,4 @@ export type DeviceStats = z.infer<typeof zDeviceStats>
|
||||
export type SystemStats = z.infer<typeof zSystemStats>
|
||||
export type User = z.infer<typeof zUser>
|
||||
export type UserData = z.infer<typeof zUserData>
|
||||
export type UserDataFullInfo = z.infer<typeof zUserDataFullInfo>
|
||||
|
||||
2
src/types/litegraph-core-augmentation.d.ts
vendored
@@ -11,6 +11,8 @@ declare module '@comfyorg/litegraph' {
|
||||
slot_types_out: string[]
|
||||
slot_types_default_out: Record<string, string[]>
|
||||
slot_types_default_in: Record<string, string[]>
|
||||
|
||||
isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean
|
||||
}
|
||||
|
||||
import type { LiteGraph as LG } from '@comfyorg/litegraph/dist/litegraph'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
ConnectingLink,
|
||||
LGraphNode,
|
||||
Vector2,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
INodeOutputSlot,
|
||||
INodeSlot
|
||||
} from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
export class ConnectingLinkImpl implements ConnectingLink {
|
||||
node: LGraphNode
|
||||
@@ -56,9 +57,8 @@ export class ConnectingLinkImpl implements ConnectingLink {
|
||||
this.releaseSlotType === 'output' ? newNode.outputs : newNode.inputs
|
||||
if (!newNodeSlots) return
|
||||
|
||||
const newNodeSlot = newNodeSlots.findIndex(
|
||||
(slot: INodeSlot) =>
|
||||
slot.type === this.type || slot.type === '*' || this.type === '*'
|
||||
const newNodeSlot = newNodeSlots.findIndex((slot: INodeSlot) =>
|
||||
LiteGraph.isValidConnection(slot.type, this.type)
|
||||
)
|
||||
|
||||
if (newNodeSlot === -1) {
|
||||
|
||||
@@ -24,6 +24,10 @@ export interface TreeExplorerNode<T = any> {
|
||||
node: TreeExplorerNode<T>,
|
||||
data: TreeExplorerDragAndDropData
|
||||
) => void
|
||||
// Function to handle clicking a node
|
||||
handleClick?: (node: TreeExplorerNode<T>, event: MouseEvent) => void
|
||||
// Function to handle errors
|
||||
handleError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
|
||||
|
||||
@@ -22,3 +22,20 @@ export function formatCamelCase(str: string): string {
|
||||
// Join the words with spaces
|
||||
return processedWords.join(' ')
|
||||
}
|
||||
|
||||
export function appendJsonExt(path: string) {
|
||||
if (!path.toLowerCase().endsWith('.json')) {
|
||||
path += '.json'
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function trimJsonExt(path?: string) {
|
||||
return path?.replace(/\.json$/, '')
|
||||
}
|
||||
|
||||
export function highlightQuery(text: string, query: string) {
|
||||
if (!query) return text
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
return text.replace(regex, '<span class="highlight">$1</span>')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import {
|
||||
start,
|
||||
createDefaultWorkflow,
|
||||
@@ -517,8 +518,8 @@ describe('group node', () => {
|
||||
vaeReroute
|
||||
])
|
||||
group1.menu.Clone.call()
|
||||
expect(app.graph._nodes).toHaveLength(4)
|
||||
const group2 = graph.find(app.graph._nodes[3])
|
||||
expect(app.graph.nodes).toHaveLength(4)
|
||||
const group2 = graph.find(app.graph.nodes[3])
|
||||
expect(group2.node.type).toEqual('workflow/test')
|
||||
expect(group2.id).not.toEqual(group1.id)
|
||||
|
||||
@@ -572,7 +573,6 @@ describe('group node', () => {
|
||||
new CustomEvent('executing', { detail: `${nodes.save.id}` })
|
||||
)
|
||||
// Event should be forwarded to group node id
|
||||
expect(+app.runningNodeId).toEqual(group.id)
|
||||
expect(group.node['imgs']).toBeFalsy()
|
||||
api.dispatchEvent(
|
||||
new CustomEvent('executed', {
|
||||
@@ -613,7 +613,6 @@ describe('group node', () => {
|
||||
api.dispatchEvent(new CustomEvent('execution_start', {}))
|
||||
api.dispatchEvent(new CustomEvent('executing', { detail: `${group.id}:5` }))
|
||||
// Event should be forwarded to group node id
|
||||
expect(+app.runningNodeId).toEqual(group.id)
|
||||
expect(group.node['imgs']).toBeFalsy()
|
||||
api.dispatchEvent(
|
||||
new CustomEvent('executed', {
|
||||
@@ -680,8 +679,8 @@ describe('group node', () => {
|
||||
|
||||
// Clone the node
|
||||
group1.menu.Clone.call()
|
||||
expect(app.graph._nodes).toHaveLength(3)
|
||||
const group2 = graph.find(app.graph._nodes[2])
|
||||
expect(app.graph.nodes).toHaveLength(3)
|
||||
const group2 = graph.find(app.graph.nodes[2])
|
||||
expect(group2.node.type).toEqual('workflow/test')
|
||||
expect(group2.id).not.toEqual(group1.id)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('LGraph', () => {
|
||||
const result1 = graph.serialize({ sortNodes: true })
|
||||
expect(result1.nodes).not.toHaveLength(0)
|
||||
// @ts-expect-error Access private property.
|
||||
graph._nodes = swapNodes(graph._nodes)
|
||||
graph._nodes = swapNodes(graph.nodes)
|
||||
const result2 = graph.serialize({ sortNodes: true })
|
||||
|
||||
expect(result1).toEqual(result2)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useUserFileStore } from '@/stores/userFileStore'
|
||||
import { UserFile, useUserFileStore } from '@/stores/userFileStore'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
// Mock the api
|
||||
jest.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
listUserData: jest.fn(),
|
||||
moveUserData: jest.fn(),
|
||||
deleteUserData: jest.fn(),
|
||||
listUserDataFullInfo: jest.fn(),
|
||||
getUserData: jest.fn(),
|
||||
storeUserData: jest.fn(),
|
||||
getUserData: jest.fn()
|
||||
deleteUserData: jest.fn(),
|
||||
moveUserData: jest.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -21,149 +21,136 @@ describe('useUserFileStore', () => {
|
||||
store = useUserFileStore()
|
||||
})
|
||||
|
||||
it('should open a file', async () => {
|
||||
const mockFileData = { success: true, data: 'file content' }
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => mockFileData.data
|
||||
it('should initialize with empty files', () => {
|
||||
expect(store.userFiles).toHaveLength(0)
|
||||
expect(store.modifiedFiles).toHaveLength(0)
|
||||
expect(store.openedFiles).toHaveLength(0)
|
||||
})
|
||||
|
||||
describe('syncFiles', () => {
|
||||
it('should add new files', async () => {
|
||||
const mockFiles = [
|
||||
{ path: 'file1.txt', modified: 123, size: 100 },
|
||||
{ path: 'file2.txt', modified: 456, size: 200 }
|
||||
]
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
await store.syncFiles()
|
||||
|
||||
expect(store.userFiles).toHaveLength(2)
|
||||
expect(store.userFiles[0].path).toBe('file1.txt')
|
||||
expect(store.userFiles[1].path).toBe('file2.txt')
|
||||
})
|
||||
|
||||
await store.openFile('test.txt')
|
||||
it('should update existing files', async () => {
|
||||
const initialFile = { path: 'file1.txt', modified: 123, size: 100 }
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([initialFile])
|
||||
await store.syncFiles()
|
||||
|
||||
expect(store.openFiles).toHaveLength(1)
|
||||
expect(store.openFiles[0]).toEqual({
|
||||
path: 'test.txt',
|
||||
content: 'file content',
|
||||
isModified: false,
|
||||
originalContent: 'file content'
|
||||
const updatedFile = { path: 'file1.txt', modified: 456, size: 200 }
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([updatedFile])
|
||||
await store.syncFiles()
|
||||
|
||||
expect(store.userFiles).toHaveLength(1)
|
||||
expect(store.userFiles[0].lastModified).toBe(456)
|
||||
expect(store.userFiles[0].size).toBe(200)
|
||||
})
|
||||
|
||||
it('should remove non-existent files', async () => {
|
||||
const initialFiles = [
|
||||
{ path: 'file1.txt', modified: 123, size: 100 },
|
||||
{ path: 'file2.txt', modified: 456, size: 200 }
|
||||
]
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(initialFiles)
|
||||
await store.syncFiles()
|
||||
|
||||
const updatedFiles = [{ path: 'file1.txt', modified: 123, size: 100 }]
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(updatedFiles)
|
||||
await store.syncFiles()
|
||||
|
||||
expect(store.userFiles).toHaveLength(1)
|
||||
expect(store.userFiles[0].path).toBe('file1.txt')
|
||||
})
|
||||
})
|
||||
|
||||
it('should close a file', () => {
|
||||
store.openFiles = [
|
||||
{
|
||||
path: 'test.txt',
|
||||
content: 'content',
|
||||
isModified: false,
|
||||
originalContent: 'content'
|
||||
}
|
||||
]
|
||||
describe('loadFile', () => {
|
||||
it('should load file content', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
text: () => Promise.resolve('file content')
|
||||
})
|
||||
|
||||
store.closeFile('test.txt')
|
||||
await store.loadFile(file)
|
||||
|
||||
expect(store.openFiles).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should update file content', () => {
|
||||
store.openFiles = [
|
||||
{
|
||||
path: 'test.txt',
|
||||
content: 'old content',
|
||||
isModified: false,
|
||||
originalContent: 'old content'
|
||||
}
|
||||
]
|
||||
|
||||
store.updateFileContent('test.txt', 'new content')
|
||||
|
||||
expect(store.openFiles[0].content).toBe('new content')
|
||||
expect(store.openFiles[0].isModified).toBe(true)
|
||||
})
|
||||
|
||||
it('should save an open file', async () => {
|
||||
store.openFiles = [
|
||||
{
|
||||
path: 'test.txt',
|
||||
content: 'modified content',
|
||||
isModified: true,
|
||||
originalContent: 'original content'
|
||||
}
|
||||
]
|
||||
;(api.storeUserData as jest.Mock).mockResolvedValue({ status: 200 })
|
||||
;(api.listUserData as jest.Mock).mockResolvedValue(['test.txt'])
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => 'modified content'
|
||||
expect(file.content).toBe('file content')
|
||||
expect(file.originalContent).toBe('file content')
|
||||
expect(file.isLoading).toBe(false)
|
||||
})
|
||||
|
||||
await store.saveOpenFile('test.txt')
|
||||
it('should throw error on failed load', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 404,
|
||||
statusText: 'Not Found'
|
||||
})
|
||||
|
||||
expect(store.openFiles[0].isModified).toBe(false)
|
||||
expect(store.openFiles[0].originalContent).toBe('modified content')
|
||||
await expect(store.loadFile(file)).rejects.toThrow(
|
||||
"Failed to load file 'file1.txt': 404 Not Found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should discard changes', () => {
|
||||
store.openFiles = [
|
||||
{
|
||||
path: 'test.txt',
|
||||
content: 'modified content',
|
||||
isModified: true,
|
||||
originalContent: 'original content'
|
||||
}
|
||||
]
|
||||
describe('saveFile', () => {
|
||||
it('should save modified file', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
file.content = 'modified content'
|
||||
file.originalContent = 'original content'
|
||||
;(api.storeUserData as jest.Mock).mockResolvedValue({ status: 200 })
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([])
|
||||
|
||||
store.discardChanges('test.txt')
|
||||
await store.saveFile(file)
|
||||
|
||||
expect(store.openFiles[0].content).toBe('original content')
|
||||
expect(store.openFiles[0].isModified).toBe(false)
|
||||
})
|
||||
|
||||
it('should load files', async () => {
|
||||
;(api.listUserData as jest.Mock).mockResolvedValue([
|
||||
'file1.txt',
|
||||
'file2.txt'
|
||||
])
|
||||
|
||||
await store.loadFiles()
|
||||
|
||||
expect(store.files).toEqual(['file1.txt', 'file2.txt'])
|
||||
})
|
||||
|
||||
it('should rename a file', async () => {
|
||||
store.openFiles = [
|
||||
{
|
||||
path: 'oldfile.txt',
|
||||
content: 'content',
|
||||
isModified: false,
|
||||
originalContent: 'content'
|
||||
}
|
||||
]
|
||||
;(api.moveUserData as jest.Mock).mockResolvedValue({ status: 200 })
|
||||
;(api.listUserData as jest.Mock).mockResolvedValue(['newfile.txt'])
|
||||
|
||||
await store.renameFile('oldfile.txt', 'newfile.txt')
|
||||
|
||||
expect(store.openFiles[0].path).toBe('newfile.txt')
|
||||
expect(store.files).toEqual(['newfile.txt'])
|
||||
})
|
||||
|
||||
it('should delete a file', async () => {
|
||||
store.openFiles = [
|
||||
{
|
||||
path: 'file.txt',
|
||||
content: 'content',
|
||||
isModified: false,
|
||||
originalContent: 'content'
|
||||
}
|
||||
]
|
||||
;(api.deleteUserData as jest.Mock).mockResolvedValue({ status: 204 })
|
||||
;(api.listUserData as jest.Mock).mockResolvedValue([])
|
||||
|
||||
await store.deleteFile('file.txt')
|
||||
|
||||
expect(store.openFiles).toHaveLength(0)
|
||||
expect(store.files).toEqual([])
|
||||
})
|
||||
|
||||
it('should get file data', async () => {
|
||||
const mockFileData = { content: 'file content' }
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => mockFileData
|
||||
expect(api.storeUserData).toHaveBeenCalledWith(
|
||||
'file1.txt',
|
||||
'modified content'
|
||||
)
|
||||
})
|
||||
|
||||
const result = await store.getFileData('test.txt')
|
||||
it('should not save unmodified file', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
file.content = 'content'
|
||||
file.originalContent = 'content'
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([])
|
||||
|
||||
expect(result).toEqual({ success: true, data: mockFileData })
|
||||
await store.saveFile(file)
|
||||
|
||||
expect(api.storeUserData).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteFile', () => {
|
||||
it('should delete file', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
;(api.deleteUserData as jest.Mock).mockResolvedValue({ status: 204 })
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([])
|
||||
|
||||
await store.deleteFile(file)
|
||||
|
||||
expect(api.deleteUserData).toHaveBeenCalledWith('file1.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameFile', () => {
|
||||
it('should rename file', async () => {
|
||||
const file = new UserFile('file1.txt', 123, 100)
|
||||
;(api.moveUserData as jest.Mock).mockResolvedValue({ status: 200 })
|
||||
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([])
|
||||
|
||||
await store.renameFile(file, 'newfile.txt')
|
||||
|
||||
expect(api.moveUserData).toHaveBeenCalledWith('file1.txt', 'newfile.txt')
|
||||
expect(file.path).toBe('newfile.txt')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -353,7 +353,7 @@ export class EzGraph {
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this.app.graph._nodes.map((n) => new EzNode(this.app, n))
|
||||
return this.app.graph.nodes.map((n) => new EzNode(this.app, n))
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIConfig, mockApi } from './setup'
|
||||
import { APIConfig, mockApi, mockSettingStore, mockNodeDefStore } from './setup'
|
||||
import { Ez, EzGraph, EzNameSpace } from './ezgraph'
|
||||
import lg from './litegraph'
|
||||
import fs from 'fs'
|
||||
@@ -40,7 +40,12 @@ export async function start(config: StartConfig = {}): Promise<StartResult> {
|
||||
document.body.innerHTML = html.toString()
|
||||
|
||||
mockApi(config)
|
||||
mockSettingStore()
|
||||
const { app } = await import('../../src/scripts/app')
|
||||
const { useSettingStore } = await import('../../src/stores/settingStore')
|
||||
useSettingStore().addSettings(app.ui.settings)
|
||||
mockNodeDefStore()
|
||||
|
||||
const { LiteGraph, LGraphCanvas } = await import('@comfyorg/litegraph')
|
||||
config.preSetup?.(app)
|
||||
const canvasEl = document.createElement('canvas')
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ComfySettingsDialog } from '@/scripts/ui/settings'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import '../../src/scripts/api'
|
||||
import { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
@@ -97,3 +100,38 @@ export function mockApi(config: APIConfig = {}) {
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export const mockSettingStore = () => {
|
||||
let app: ComfyApp | null = null
|
||||
|
||||
const mockedSettingStore = {
|
||||
addSettings(settings: ComfySettingsDialog) {
|
||||
app = settings.app
|
||||
},
|
||||
|
||||
set(key: string, value: any) {
|
||||
app?.ui.settings.setSettingValue(key, value)
|
||||
},
|
||||
|
||||
get(key: string) {
|
||||
return (
|
||||
app?.ui.settings.getSettingValue(key) ??
|
||||
app?.ui.settings.getSettingDefaultValue(key)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: jest.fn(() => mockedSettingStore)
|
||||
}))
|
||||
}
|
||||
|
||||
export const mockNodeDefStore = () => {
|
||||
const mockedNodeDefStore = {
|
||||
addNodeDef: jest.fn((nodeDef: ComfyNodeDef) => {})
|
||||
}
|
||||
|
||||
jest.mock('@/stores/nodeDefStore', () => ({
|
||||
useNodeDefStore: jest.fn(() => mockedNodeDefStore)
|
||||
}))
|
||||
}
|
||||
|
||||