Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a7188aeb0 | ||
|
|
2079a54ffa | ||
|
|
74baf2af12 | ||
|
|
5dec86861b | ||
|
|
fc05029f4e | ||
|
|
98064f301d | ||
|
|
bff1dc91fa | ||
|
|
f4242f8a66 | ||
|
|
845ab88d55 | ||
|
|
6c557eaa58 | ||
|
|
2fdaabd2c9 | ||
|
|
a2143d9120 | ||
|
|
f2b02dd10b | ||
|
|
b831a82360 | ||
|
|
d232e38c33 | ||
|
|
1a3cf4c3f3 | ||
|
|
31d172d4d9 | ||
|
|
92ce064ebf | ||
|
|
3d041dd742 | ||
|
|
af378262f4 | ||
|
|
57c5a78af3 | ||
|
|
233fd1347e | ||
|
|
60221254d9 | ||
|
|
7434691bed | ||
|
|
fbdc9d430b | ||
|
|
3e457f812d | ||
|
|
0466c79725 | ||
|
|
8b989c6415 | ||
|
|
2e51122778 | ||
|
|
8f8eac038a | ||
|
|
4b3bad4bc5 | ||
|
|
17c7f57d8f | ||
|
|
5542845710 | ||
|
|
f2de9b0d3c | ||
|
|
71fa71e82c | ||
|
|
77ba201367 | ||
|
|
6edfc9bc1b | ||
|
|
743dc4879a | ||
|
|
2c1bd662e1 | ||
|
|
7051d86ba7 | ||
|
|
7487c565c8 | ||
|
|
a86d10b02d | ||
|
|
3d89c245e5 | ||
|
|
9dd6da3dc2 | ||
|
|
269e468425 | ||
|
|
c3ef716d53 | ||
|
|
bd7bbd9e95 | ||
|
|
447d1c95ef | ||
|
|
c4bc0e8430 | ||
|
|
f3ab9cfb8e | ||
|
|
52c8c8194e | ||
|
|
9dbc114ae9 | ||
|
|
556edea299 | ||
|
|
d5584a1d39 | ||
|
|
628f2afc34 | ||
|
|
ea01fde607 | ||
|
|
b8a3e6b1ad | ||
|
|
cfad3cd918 | ||
|
|
339e201920 | ||
|
|
c227a8af9a | ||
|
|
6b47162606 | ||
|
|
a4c5a2a3d1 | ||
|
|
45a47be7c0 | ||
|
|
2252f0a134 | ||
|
|
0dfbcfb2d6 | ||
|
|
b46036f25d | ||
|
|
f5ce42d5d5 | ||
|
|
ce75a29202 | ||
|
|
6a8a68a240 | ||
|
|
f9adaadc7d | ||
|
|
727992048e | ||
|
|
dd1e3f087d | ||
|
|
f1c1a3dab7 | ||
|
|
4a43dfe6b9 | ||
|
|
8576d3797b | ||
|
|
22e2628479 | ||
|
|
4e1f14139b | ||
|
|
0c53ab9177 | ||
|
|
eb5f4d9bc7 | ||
|
|
19681fd43d | ||
|
|
add2f9baa0 | ||
|
|
ec5f1152da | ||
|
|
17aa44d9f6 | ||
|
|
d8887a434d | ||
|
|
9d3ca763d0 | ||
|
|
a45851d7a6 | ||
|
|
266336104a | ||
|
|
8c40f83b35 | ||
|
|
5ba524fd94 | ||
|
|
966b1dd057 | ||
|
|
069766337a | ||
|
|
a1a6eeed0f | ||
|
|
b33874db2b | ||
|
|
05c6193a1d | ||
|
|
73f7889f81 | ||
|
|
c73915e31e | ||
|
|
7626d08c98 | ||
|
|
f10554d914 | ||
|
|
238225dd12 | ||
|
|
9cea54686a | ||
|
|
d1715972e3 | ||
|
|
49a910d7b0 | ||
|
|
479ca63e3c | ||
|
|
7468555c06 | ||
|
|
14a395a0ce | ||
|
|
0a99a2bd53 | ||
|
|
955c703fde | ||
|
|
ef8b952d79 | ||
|
|
e6d29656fa | ||
|
|
9e3dffd7fd | ||
|
|
429e44f74d | ||
|
|
73b0ecc8cb | ||
|
|
775f536d30 | ||
|
|
1ff7a70579 | ||
|
|
51233b4be3 | ||
|
|
5c4d1c2cec | ||
|
|
711205b499 | ||
|
|
00fe3515b9 | ||
|
|
d8ee0b4584 | ||
|
|
fc1b0b3e53 | ||
|
|
a68f7c680b | ||
|
|
c6b6bdcb67 | ||
|
|
6993a56c2d | ||
|
|
4eb56a19ba | ||
|
|
784947fdea | ||
|
|
64ee131e64 | ||
|
|
1b59e3ab6d | ||
|
|
37b414d1b2 | ||
|
|
7245eee85b | ||
|
|
9f3696e70f | ||
|
|
d5deb6d2a0 | ||
|
|
41889c3cfe | ||
|
|
e8602e88fa | ||
|
|
b5f2d334d9 | ||
|
|
c459698956 | ||
|
|
f91e335ca7 | ||
|
|
1fc173b48f | ||
|
|
4fe44339fd | ||
|
|
f987f4f5f3 | ||
|
|
91e21b1387 | ||
|
|
a5be1f6072 | ||
|
|
d5f30be06d | ||
|
|
d607f6c7f7 | ||
|
|
d9df0328c5 | ||
|
|
aff059b98b | ||
|
|
cf53e8df6a | ||
|
|
d1d4324e57 | ||
|
|
c86abd3dc6 | ||
|
|
281ed0c5d1 | ||
|
|
edf0396619 | ||
|
|
2a5b2e8d12 | ||
|
|
9385014799 | ||
|
|
cfce1c6037 | ||
|
|
480679aa32 | ||
|
|
e2141a81e2 | ||
|
|
0f3b58b610 |
@@ -13,4 +13,7 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
|
||||
DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
|
||||
|
||||
# The directory containing the ComfyUI_examples repo used to extract test workflows.
|
||||
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
|
||||
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
|
||||
|
||||
# Whether to enable minification of the frontend code.
|
||||
ENABLE_MINIFY=true
|
||||
5
.github/workflows/test-ui.yaml
vendored
@@ -27,6 +27,11 @@ jobs:
|
||||
with:
|
||||
repository: "Comfy-Org/ComfyUI_frontend"
|
||||
path: "ComfyUI_frontend"
|
||||
- name: Checkout ComfyUI_devtools
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: "Comfy-Org/ComfyUI_devtools"
|
||||
path: "ComfyUI/custom_nodes/ComfyUI_devtools"
|
||||
- name: Get commit message
|
||||
id: commit-message
|
||||
run: echo "::set-output name=message::$(git log -1 --pretty=%B)"
|
||||
|
||||
53
.github/workflows/update-main.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Update Main Repo from PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
update-main-repo:
|
||||
if: github.event.label.name == 'Update Main Repo'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout frontend repo PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: "comfyanonymous/ComfyUI"
|
||||
path: ComfyUI
|
||||
ref: master
|
||||
|
||||
- name: Copy compiled assets
|
||||
run: |
|
||||
rm -rf ./ComfyUI/web/*
|
||||
cp -R dist/* ./ComfyUI/web/
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: 'Update frontend assets from PR #${{ github.event.pull_request.number }}'
|
||||
title: 'Update frontend assets from PR #${{ github.event.pull_request.number }}'
|
||||
body: |
|
||||
This PR updates the compiled frontend assets from PR #${{ github.event.pull_request.number }} in the frontend repo.
|
||||
|
||||
Frontend PR: ${{ github.event.pull_request.html_url }}
|
||||
branch: update-frontend-assets-pr-${{ github.event.pull_request.number }}
|
||||
base: main
|
||||
path: ComfyUI
|
||||
1
.gitignore
vendored
@@ -34,6 +34,7 @@ tests-ui/workflows/examples
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
browser_tests/*/*-win32.png
|
||||
|
||||
.env
|
||||
|
||||
|
||||
96
README.md
@@ -1,37 +1,62 @@
|
||||
<div align="center">
|
||||
|
||||
# ComfyUI_frontend
|
||||
|
||||
Front-end of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) modernized. This repo is fully compatible with the existing extension system.
|
||||
**Official front-end implementation of [ComfyUI](https://github.com/comfyanonymous/ComfyUI).**
|
||||
|
||||
## How To Use
|
||||
[![Website][website-shield]][website-url]
|
||||
[![Discord][discord-shield]][discord-url]
|
||||
[![Matrix][matrix-shield]][matrix-url]
|
||||
<br>
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-release-date-shield]][github-release-link]
|
||||
[![][github-downloads-shield]][github-downloads-link]
|
||||
[![][github-downloads-latest-shield]][github-downloads-link]
|
||||
|
||||
Add command line argument `--front-end-version Comfy-Org/ComfyUI_frontend@latest` to your
|
||||
ComfyUI launch script.
|
||||
|
||||
For Windows stand-alone build users, please edit the `run_cpu.bat` / `run_nvidia_gpu.bat` file as following
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/Comfy-Org/ComfyUI_frontend?style=flat&sort=semver
|
||||
[github-release-link]: https://github.com/Comfy-Org/ComfyUI_frontend/releases
|
||||
[github-release-date-shield]: https://img.shields.io/github/release-date/Comfy-Org/ComfyUI_frontend?style=flat
|
||||
[github-downloads-shield]: https://img.shields.io/github/downloads/Comfy-Org/ComfyUI_frontend/total?style=flat
|
||||
[github-downloads-latest-shield]: https://img.shields.io/github/downloads/Comfy-Org/ComfyUI_frontend/latest/total?style=flat&label=downloads%40latest
|
||||
[github-downloads-link]: https://github.com/Comfy-Org/ComfyUI_frontend/releases
|
||||
[matrix-shield]: https://img.shields.io/badge/Matrix-000000?style=flat&logo=matrix&logoColor=white
|
||||
[matrix-url]: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
|
||||
[website-shield]: https://img.shields.io/badge/ComfyOrg-4285F4?style=flat
|
||||
[website-url]: https://www.comfy.org/
|
||||
[discord-shield]: https://img.shields.io/discord/1218270712402415686?style=flat&logo=discord&logoColor=white&label=Discord
|
||||
[discord-url]: https://www.comfy.org/discord
|
||||
|
||||
</div>
|
||||
|
||||
## Release Schedule
|
||||
|
||||
### Nightly Release
|
||||
|
||||
Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
|
||||
|
||||
To use the latest nightly release, add the following command line argument to your ComfyUI launch script:
|
||||
|
||||
```
|
||||
--front-end-version Comfy-Org/ComfyUI_frontend@latest
|
||||
```
|
||||
|
||||
#### For Windows Stand-alone Build Users
|
||||
|
||||
Edit your `run_cpu.bat` or `run_nvidia_gpu.bat` file as follows:
|
||||
|
||||
```bat
|
||||
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --front-end-version Comfy-Org/ComfyUI_frontend@latest
|
||||
pause
|
||||
```
|
||||
|
||||
## Trouble Shooting
|
||||
<details>
|
||||
<summary>Empty white screen (Fixed by https://github.com/comfyanonymous/ComfyUI/pull/4211)</summary>
|
||||
### Stable Release
|
||||
|
||||
### Behavior
|
||||
After you enable the new frontend in commandline, and open ComfyUI in the browser, you see a blank screen. If you toggle dev tools with F12, you can observe `litegraph.core.js:1` 404 in console messages.
|
||||
Stable releases are published weekly in the ComfyUI main repository, aligned with ComfyUI backend's stable release schedule.
|
||||
|
||||
### Cause
|
||||
The browser is caching the `index.html` file previously served from `localhost:8188`.
|
||||
#### Feature Freeze
|
||||
|
||||
### How to fix
|
||||
Step 1: Disable cache in devtools
|
||||
|
||||

|
||||
|
||||
Step 2: Refresh your browser
|
||||
|
||||
</details>
|
||||
There will be a 2-day feature freeze before each stable release. During this period, no new major features will be merged.
|
||||
|
||||
## Release Summary
|
||||
|
||||
@@ -67,7 +92,7 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
<details>
|
||||
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
|
||||
|
||||
https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
@@ -92,9 +117,9 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
### Node developers API
|
||||
<details>
|
||||
<summary>v1.2.4: Extension API to register custom side bar tab</summary>
|
||||
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
|
||||
|
||||
Extensions now can call following API to register a sidebar tab.
|
||||
Extensions now can call the following API to register a sidebar tab.
|
||||
|
||||
```js
|
||||
app.extensionManager.registerSidebarTab({
|
||||
@@ -109,20 +134,35 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
});
|
||||
```
|
||||
|
||||
The list of supported icons can be find here: https://primevue.org/icons/#list
|
||||
The list of supported icons can be found here: <https://primevue.org/icons/#list>
|
||||
|
||||
We will support custom icon later.
|
||||
We will support custom icons later.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.27: Extension API to add toast message</summary>
|
||||
|
||||
Extensions can call the following API to add toast messages.
|
||||
|
||||
```js
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Loaded!',
|
||||
detail: 'Extension loaded!'
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
## Road Map
|
||||
|
||||
### What has been done
|
||||
|
||||
- Migrate all code to TypeScript with minimal change modification to the original logic.
|
||||
- Bundle all code with vite's rollup build.
|
||||
- Bundle all code with Vite's rollup build.
|
||||
- Added a shim layer to be backward compatible with the existing extension system. <https://github.com/huchenlei/ComfyUI_frontend/pull/15>
|
||||
- Front-end dev server.
|
||||
- Zod schema for input validation on ComfyUI workflow.
|
||||
@@ -175,7 +215,3 @@ This repo is using litegraph package hosted on https://github.com/Comfy-Org/lite
|
||||
|
||||
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
|
||||
- Option 2: Copy everything under `dist/` to `ComfyUI/web/` in your ComfyUI checkout manually.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- api.api_url now adds a prefix `api/` to every url going through the method. If the custom node registers a new api endpoint but does not offer the `api/` prefixed alt endpoint, it will have issue. Luckily there aren't many extensions that do that. We can perform an audit before launching to resolve this issue.
|
||||
|
||||
@@ -26,18 +26,24 @@ class ComfyNodeSearchBox {
|
||||
)
|
||||
}
|
||||
|
||||
async fillAndSelectFirstNode(nodeName: string) {
|
||||
async fillAndSelectFirstNode(
|
||||
nodeName: string,
|
||||
options?: { suggestionIndex: number }
|
||||
) {
|
||||
await this.input.waitFor({ state: 'visible' })
|
||||
await this.input.fill(nodeName)
|
||||
await this.dropdown.waitFor({ state: 'visible' })
|
||||
// Wait for some time for the auto complete list to update.
|
||||
// The auto complete list is debounced and may take some time to update.
|
||||
await this.page.waitForTimeout(500)
|
||||
await this.dropdown.locator('li').nth(0).click()
|
||||
await this.dropdown
|
||||
.locator('li')
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
class NodeLibrarySideBarTab {
|
||||
class NodeLibrarySidebarTab {
|
||||
public readonly tabId: string = 'node-library'
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
@@ -68,22 +74,30 @@ class NodeLibrarySideBarTab {
|
||||
await this.nodeLibraryTree.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async toggleFirstFolder() {
|
||||
await this.page.locator('.p-tree-node-toggle-button').nth(0).click()
|
||||
getFolder(folderName: string) {
|
||||
return this.page.locator(
|
||||
`.p-tree-node-content:has(> .node-lib-tree-node-label:has(.folder-label:has-text("${folderName}")))`
|
||||
)
|
||||
}
|
||||
|
||||
getNode(nodeName: string) {
|
||||
return this.page.locator(
|
||||
`.p-tree-node-content:has(> .node-lib-tree-node-label:has(.node-label:has-text("${nodeName}")))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyMenu {
|
||||
public readonly sideToolBar: Locator
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly themeToggleButton: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.sideToolBar = page.locator('.side-tool-bar-container')
|
||||
this.sideToolbar = page.locator('.side-tool-bar-container')
|
||||
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
return new NodeLibrarySideBarTab(this.page)
|
||||
return new NodeLibrarySidebarTab(this.page)
|
||||
}
|
||||
|
||||
async toggleTheme() {
|
||||
@@ -118,6 +132,7 @@ export class ComfyPage {
|
||||
|
||||
// Buttons
|
||||
public readonly resetViewButton: Locator
|
||||
public readonly queueButton: Locator
|
||||
|
||||
// Inputs
|
||||
public readonly workflowUploadInput: Locator
|
||||
@@ -131,6 +146,7 @@ export class ComfyPage {
|
||||
this.canvas = page.locator('#graph-canvas')
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
@@ -170,7 +186,22 @@ export class ComfyPage {
|
||||
await this.resetView()
|
||||
}
|
||||
|
||||
async realod() {
|
||||
async setSetting(settingId: string, settingValue: any) {
|
||||
return await this.page.evaluate(
|
||||
async ({ id, value }) => {
|
||||
await window['app'].ui.settings.setSettingValueAsync(id, value)
|
||||
},
|
||||
{ id: settingId, value: settingValue }
|
||||
)
|
||||
}
|
||||
|
||||
async getSetting(settingId: string) {
|
||||
return await this.page.evaluate(async (id) => {
|
||||
return await window['app'].ui.settings.getSettingValue(id)
|
||||
}, settingId)
|
||||
}
|
||||
|
||||
async reload() {
|
||||
await this.page.reload({ timeout: 15000 })
|
||||
await this.setup()
|
||||
}
|
||||
@@ -185,6 +216,10 @@ export class ComfyPage {
|
||||
})
|
||||
}
|
||||
|
||||
async delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async loadWorkflow(workflowName: string) {
|
||||
await this.workflowUploadInput.setInputFiles(
|
||||
`./browser_tests/assets/${workflowName}.json`
|
||||
@@ -211,6 +246,16 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickTextEncodeNodeToggler() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
x: 430,
|
||||
y: 171
|
||||
}
|
||||
})
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode2() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
@@ -280,16 +325,22 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async zoom(deltaY: number) {
|
||||
async zoom(deltaY: number, steps: number = 1) {
|
||||
await this.page.mouse.move(10, 10)
|
||||
await this.page.mouse.wheel(0, deltaY)
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await this.page.mouse.wheel(0, deltaY)
|
||||
}
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async pan(offset: Position) {
|
||||
await this.page.mouse.move(10, 10)
|
||||
async pan(offset: Position, safeSpot?: Position) {
|
||||
safeSpot = safeSpot || { x: 10, y: 10 }
|
||||
await this.page.mouse.move(safeSpot.x, safeSpot.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(offset.x, offset.y)
|
||||
// TEMPORARY HACK: Multiple pans open the search menu, so cheat and keep it closed.
|
||||
// TODO: Fix that (double-click at not-the-same-coordinations should not open the menu)
|
||||
await this.page.keyboard.press('Escape')
|
||||
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
41
browser_tests/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Playwright Testing for ComfyUI_frontend
|
||||
|
||||
This document outlines the setup and usage of Playwright for testing the ComfyUI_frontend project.
|
||||
|
||||
## Setup
|
||||
|
||||
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
|
||||
|
||||
```bash
|
||||
npx playwright install chromium --with-deps
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
There are two ways to run the tests:
|
||||
|
||||
1. **Headless mode with report generation:**
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
This runs all tests without a visible browser and generates a comprehensive test report.
|
||||
|
||||
2. **UI mode for interactive testing:**
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
|
||||
|
||||

|
||||
|
||||
## Screenshot Expectations
|
||||
|
||||
Due to variations in system font rendering, screenshot expectations are platform-specific. Please note:
|
||||
|
||||
- We maintain Linux screenshot expectations as our GitHub Action runner operates in a Linux environment.
|
||||
- To set new test expectations:
|
||||
1. Create a pull request from a `Comfy-Org/ComfyUI_frontend` branch.
|
||||
2. Add the `New Browser Test Expectation` tag to your pull request.
|
||||
3. This will trigger a GitHub action to update the screenshot expectations automatically.
|
||||
|
||||
> **Note:** If you're making a pull request from a forked repository, the GitHub action won't be able to commit updated screenshot expectations directly to your PR branch.
|
||||
82
browser_tests/assets/execution_error.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"last_node_id": 17,
|
||||
"last_link_id": 15,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 14,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
858,
|
||||
-41
|
||||
],
|
||||
"size": {
|
||||
"0": 213.8594970703125,
|
||||
"1": 50.65289306640625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "DevToolsErrorRaiseNode",
|
||||
"pos": [
|
||||
477,
|
||||
-40
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 26
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
15
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsErrorRaiseNode"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
15,
|
||||
17,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.2100000000000006,
|
||||
"offset": [
|
||||
-266.1038310281165,
|
||||
337.94335447664554
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
61
browser_tests/assets/missing_nodes.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "UNKNOWN NODE",
|
||||
"pos": [
|
||||
48,
|
||||
86
|
||||
],
|
||||
"size": {
|
||||
"0": 358.80780029296875,
|
||||
"1": 314.7989501953125
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [],
|
||||
"slot_index": 0,
|
||||
"shape": 6
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNKNOWN NODE"
|
||||
},
|
||||
"widgets_values": [
|
||||
"wd-v1-4-moat-tagger-v2",
|
||||
0.35,
|
||||
0.85,
|
||||
false,
|
||||
false,
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0, 0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 95 KiB |
27
browser_tests/dialog.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Load workflow warning', () => {
|
||||
test('Should display a warning when loading a workflow with missing nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_nodes')
|
||||
|
||||
// Wait for the element with the .comfy-missing-nodes selector to be visible
|
||||
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
|
||||
await expect(missingNodesWarning).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Execution error', () => {
|
||||
test('Should display an error message when an execution error occurs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for the element with the .comfy-execution-error selector to be visible
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(executionError).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { ComfyPage, comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Node Interaction', () => {
|
||||
test('Can enter prompt', async ({ comfyPage }) => {
|
||||
@@ -27,7 +27,7 @@ test.describe('Node Interaction', () => {
|
||||
|
||||
test('Can disconnect/connect edge', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
// Close search menu poped up.
|
||||
// Close search menu popped up.
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
@@ -100,6 +100,38 @@ test.describe('Node Interaction', () => {
|
||||
'batch-disconnect-links-disconnected.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNodeToggler()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'text-encode-toggled-off.png'
|
||||
)
|
||||
await comfyPage.delay(1000)
|
||||
await comfyPage.clickTextEncodeNodeToggler()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'text-encode-toggled-back-open.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can close prompt dialog with canvas click', async ({ comfyPage }) => {
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 724,
|
||||
y: 645
|
||||
}
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png')
|
||||
// Wait for 1s so that it does not trigger the search box by double click.
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 10,
|
||||
y: 10
|
||||
}
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Canvas Interaction', () => {
|
||||
@@ -110,8 +142,78 @@ test.describe('Canvas Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
|
||||
})
|
||||
|
||||
test('Can zoom very far out', async ({ comfyPage }) => {
|
||||
await comfyPage.zoom(100, 12)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png')
|
||||
await comfyPage.zoom(-100, 12)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png')
|
||||
})
|
||||
|
||||
test('Can zoom in/out with ctrl+shift+vertical-drag', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
|
||||
await comfyPage.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out-ctrl-shift.png')
|
||||
await comfyPage.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'zoomed-default-ctrl-shift.png'
|
||||
)
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
})
|
||||
|
||||
test('Can zoom in/out after decreasing canvas zoom speed setting', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
|
||||
await comfyPage.zoom(-100, 4)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'zoomed-in-low-zoom-speed.png'
|
||||
)
|
||||
await comfyPage.zoom(100, 8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'zoomed-out-low-zoom-speed.png'
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
||||
})
|
||||
|
||||
test('Can zoom in/out after increasing canvas zoom speed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
|
||||
await comfyPage.zoom(-100, 4)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'zoomed-in-high-zoom-speed.png'
|
||||
)
|
||||
await comfyPage.zoom(100, 8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'zoomed-out-high-zoom-speed.png'
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
||||
})
|
||||
|
||||
test('Can pan', async ({ comfyPage }) => {
|
||||
await comfyPage.pan({ x: 200, y: 200 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
|
||||
})
|
||||
|
||||
test('Can pan very far and back', async ({ comfyPage }) => {
|
||||
// intentionally slice the edge of where the clip text encode dom widgets are
|
||||
await comfyPage.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png')
|
||||
await comfyPage.pan({ x: -200, y: 0 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-two.png')
|
||||
await comfyPage.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-far-away.png')
|
||||
await comfyPage.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-from-far.png')
|
||||
await comfyPage.pan({ x: 200, y: 0 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-two.png')
|
||||
await comfyPage.pan({ x: 800, y: 300 }, { x: 1000, y: 10 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -3,12 +3,7 @@ import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
await window['app'].ui.settings.setSettingValueAsync(
|
||||
'Comfy.UseNewMenu',
|
||||
'Top'
|
||||
)
|
||||
})
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -16,15 +11,11 @@ test.describe('Menu', () => {
|
||||
if (currentThemeId !== 'dark') {
|
||||
await comfyPage.menu.toggleTheme()
|
||||
}
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
await window['app'].ui.settings.setSettingValueAsync(
|
||||
'Comfy.UseNewMenu',
|
||||
'Disabled'
|
||||
)
|
||||
})
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test('Toggle theme', async ({ comfyPage }) => {
|
||||
// Skip reason: Flaky.
|
||||
test.skip('Toggle theme', async ({ comfyPage }) => {
|
||||
test.setTimeout(30000)
|
||||
|
||||
expect(await comfyPage.menu.getThemeId()).toBe('dark')
|
||||
@@ -44,7 +35,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Can register sidebar tab', async ({ comfyPage }) => {
|
||||
const initialChildrenCount = await comfyPage.menu.sideToolBar.evaluate(
|
||||
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
|
||||
@@ -62,34 +53,83 @@ test.describe('Menu', () => {
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newChildrenCount = await comfyPage.menu.sideToolBar.evaluate(
|
||||
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
expect(newChildrenCount).toBe(initialChildrenCount + 1)
|
||||
})
|
||||
|
||||
test('Sidebar node preview and drag to canvas', async ({ comfyPage }) => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
await tab.toggleFirstFolder()
|
||||
test.describe('Node library sidebar', () => {
|
||||
test('Node preview and drag to canvas', async ({ comfyPage }) => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
await tab.getFolder('sampling').click()
|
||||
|
||||
// Hover over a node to display the preview
|
||||
const nodeSelector = '.p-tree-node-leaf'
|
||||
await comfyPage.page.hover(nodeSelector)
|
||||
// Hover over a node to display the preview
|
||||
const nodeSelector = '.p-tree-node-leaf'
|
||||
await comfyPage.page.hover(nodeSelector)
|
||||
|
||||
// Verify the preview is displayed
|
||||
const previewVisible = await comfyPage.page.isVisible(
|
||||
'.node-lib-node-preview'
|
||||
// Verify the preview is displayed
|
||||
const previewVisible = await comfyPage.page.isVisible(
|
||||
'.node-lib-node-preview'
|
||||
)
|
||||
expect(previewVisible).toBe(true)
|
||||
|
||||
const count = await comfyPage.getGraphNodesCount()
|
||||
// Drag the node onto the canvas
|
||||
const canvasSelector = '#graph-canvas'
|
||||
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector)
|
||||
|
||||
// Verify the node is added to the canvas
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
|
||||
})
|
||||
|
||||
test('Bookmark node', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [])
|
||||
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
await tab.getFolder('sampling').click()
|
||||
|
||||
// Bookmark the node
|
||||
await tab
|
||||
.getNode('KSampler (Advanced)')
|
||||
.locator('.bookmark-button')
|
||||
.click()
|
||||
|
||||
// Verify the bookmark is added to the bookmarks tab
|
||||
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
|
||||
['KSampler (Advanced)']
|
||||
)
|
||||
// Verify the bookmark node with the same name is added to the tree.
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
})
|
||||
|
||||
test('Ignores unrecognized node', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', ['foo'])
|
||||
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
|
||||
expect(await tab.getFolder('sampling').count()).toBe(1)
|
||||
expect(await tab.getNode('foo').count()).toBe(0)
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [])
|
||||
})
|
||||
})
|
||||
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const [defaultSpeed, maxSpeed] = [1.1, 2.5]
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
|
||||
defaultSpeed
|
||||
)
|
||||
expect(previewVisible).toBe(true)
|
||||
|
||||
const count = await comfyPage.getGraphNodesCount()
|
||||
// Drag the node onto the canvas
|
||||
const canvasSelector = '#graph-canvas'
|
||||
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector)
|
||||
|
||||
// Verify the node is added to the canvas
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.setup()
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', defaultSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,23 @@ import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Node search box', () => {
|
||||
test('Can trigger on empty canvas double click', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger',
|
||||
'always'
|
||||
)
|
||||
})
|
||||
;['always', 'hold shift', 'NOT hold shift'].forEach((triggerMode) => {
|
||||
test(`Can trigger on empty canvas double click (${triggerMode})`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger',
|
||||
triggerMode
|
||||
)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
|
||||
test('Can trigger on link release', async ({ comfyPage }) => {
|
||||
@@ -21,7 +35,10 @@ test.describe('Node search box', () => {
|
||||
|
||||
test('Can auto link node', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode')
|
||||
// Select the second item as the first item is always reroute
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
|
||||
suggestionIndex: 0
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
|
||||
})
|
||||
|
||||
@@ -40,7 +57,10 @@ test.describe('Node search box', () => {
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint')
|
||||
// Select the second item as the first item is always reroute
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
|
||||
suggestionIndex: 0
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'auto-linked-node-batch.png'
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 91 KiB |
24
browser_tests/propertiesPanel.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Properties Panel', () => {
|
||||
test('Can change property value', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await comfyPage.page.getByText('Properties Panel').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-properties-panel.png'
|
||||
)
|
||||
const propertyInput = comfyPage.page.locator('span.property_value').first()
|
||||
|
||||
// Ensure no keybinds are triggered while typing
|
||||
await propertyInput.pressSequentially('abcdefghijklmnopqrstuvwxyz')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-properties-panel-property-changed.png'
|
||||
)
|
||||
|
||||
await propertyInput.fill('Empty Latent Image')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
@@ -18,9 +18,9 @@
|
||||
window["__COMFYUI_FRONTEND_VERSION__"] = __COMFYUI_FRONTEND_VERSION__;
|
||||
console.log("ComfyUI Front-end version:", __COMFYUI_FRONTEND_VERSION__);
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/materialdesignicons.min.css" />
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
</head>
|
||||
<body class="litegraph">
|
||||
<div id="vue-app"></div>
|
||||
|
||||
209
package-lock.json
generated
@@ -1,24 +1,26 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.16",
|
||||
"version": "1.2.34",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.16",
|
||||
"version": "1.2.34",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.43",
|
||||
"@comfyorg/litegraph": "^0.7.48",
|
||||
"@primevue/themes": "^4.0.0-rc.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"axios": "^1.7.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.0-rc.2",
|
||||
"primevue": "^4.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
@@ -1879,9 +1881,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.7.43",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.43.tgz",
|
||||
"integrity": "sha512-IX+A/cqscFkSUyon/RgyYg2OU1Af2dfW2QSFr068huf+BcHZz8YMqzNjpSPH/SwZNw98EcUuDtYBajfqeWu38Q==",
|
||||
"version": "0.7.48",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.48.tgz",
|
||||
"integrity": "sha512-BmR91huOjoMvVvdQ8Pw+L/Iez+ZIxHXA/ApfLwUTOVFa+SvwlFY76qD6C0Hw64jOx9fous1jIQUp35X0xF0RGw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -3318,30 +3320,55 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.1.tgz",
|
||||
"integrity": "sha512-0psCSZr3906UwC4mTl2ol4aDoLvdbM0ekJFLKvCvC2oQ9z2YZhmUOVZQNNxBW34mChDzZYcAcRWXADQz3z5lBg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/utils": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz",
|
||||
"integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/core": {
|
||||
"version": "4.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-S0RGGdW/M/ogIKeif6JwkJrPLC8pyuBYy2zZlloXXj6Fpvk416sMpO308sx89fqvba2d5poT9a+AbkGob4TDtQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.0.tgz",
|
||||
"integrity": "sha512-M+GF1HYnl/x5J6uevXh1k42J0XnFhp0XHce+cHddWg7v3bVwgsn7LD3AKKcf0A/iQQPXVKX9nY/4/9eFVct67w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.0.1"
|
||||
"@primeuix/styled": "^0.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@primeuix/utils": "^0.0.5",
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/icons": {
|
||||
"version": "4.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-VZFETBtmpAduGxXhxcYoqiuGriidyz4XR/4NcrR87CQpUWmT13PXfRoqZ1hLXwNZp1XferT1F5GtY1mHcYxJdw==",
|
||||
"node_modules/@primevue/core/node_modules/@primeuix/styled": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz",
|
||||
"integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primevue/core": "4.0.0-rc.2"
|
||||
"@primeuix/utils": "^0.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/icons": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.0.tgz",
|
||||
"integrity": "sha512-gv9pbj7JjCuW59tW2csIJgg6btTJpkr/mjlfqscEIrYzDGqzCrbfxLur48gA2dyhYsiQPPTbIHFwL944piFgIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primevue/core": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
@@ -3748,6 +3775,12 @@
|
||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
||||
@@ -4186,6 +4219,94 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.0.0.tgz",
|
||||
"integrity": "sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "11.0.0",
|
||||
"@vueuse/shared": "11.0.0",
|
||||
"vue-demi": ">=0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.0.0.tgz",
|
||||
"integrity": "sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.0.0.tgz",
|
||||
"integrity": "sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -4365,8 +4486,7 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
@@ -4405,6 +4525,17 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -5035,7 +5166,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -5331,7 +5461,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -6151,6 +6280,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
|
||||
@@ -6183,7 +6332,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -9165,7 +9313,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -9174,7 +9321,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -9909,12 +10055,13 @@
|
||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
||||
},
|
||||
"node_modules/primevue": {
|
||||
"version": "4.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-1SICPga4FA5sx27h5FBnMHWidPJRn58mPZj0LNlJygp+9P5ksrtRRNdfDPzmRB+fWcmhvbIxk46meyHeMw1wTQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.0.tgz",
|
||||
"integrity": "sha512-2PFmmJqyXpOcKOdF+gbps5fpSXfoXZp2LwX+hya/b5SDseMt3UNboyEgVI6B+DNbJRrib35EbDiMw+7RIANQ1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primevue/core": "4.0.0-rc.2",
|
||||
"@primevue/icons": "4.0.0-rc.2"
|
||||
"@primevue/core": "4.0.0",
|
||||
"@primevue/icons": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
@@ -9946,6 +10093,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.16",
|
||||
"version": "1.2.34",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -56,16 +56,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.43",
|
||||
"@comfyorg/litegraph": "^0.7.48",
|
||||
"@primevue/themes": "^4.0.0-rc.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"axios": "^1.7.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.0-rc.2",
|
||||
"primevue": "^4.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
|
||||
4
public/materialdesignicons.min.css
vendored
75
src/App.vue
@@ -2,21 +2,35 @@
|
||||
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<GlobalDialog />
|
||||
<GlobalToast />
|
||||
<GraphCanvas />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, onMounted, watch } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
markRaw,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
watch,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
|
||||
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
|
||||
import QueueSidebarTab from '@/components/sidebar/tabs/QueueSidebarTab.vue'
|
||||
import { app } from './scripts/app'
|
||||
import { useSettingStore } from './stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkspaceStore } from './stores/workspaceStateStore'
|
||||
import NodeLibrarySideBarTab from './components/sidebar/tabs/NodeLibrarySideBarTab.vue'
|
||||
import NodeLibrarySidebarTab from './components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import GlobalDialog from './components/dialog/GlobalDialog.vue'
|
||||
import GlobalToast from './components/toast/GlobalToast.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'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const theme = computed<string>(() =>
|
||||
@@ -36,6 +50,14 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
const fontSize = useSettingStore().get('Comfy.TextareaWidget.FontSize')
|
||||
document.documentElement.style.setProperty(
|
||||
'--comfy-textarea-font-size',
|
||||
`${fontSize}px`
|
||||
)
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const init = () => {
|
||||
useSettingStore().addSettings(app.ui.settings)
|
||||
@@ -43,28 +65,63 @@ const init = () => {
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: 'queue',
|
||||
icon: 'pi pi-history',
|
||||
title: t('sideToolBar.queue'),
|
||||
tooltip: t('sideToolBar.queue'),
|
||||
component: markRaw(QueueSideBarTab),
|
||||
iconBadge: () => {
|
||||
const value = useQueuePendingTaskCountStore().count.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
title: t('sideToolbar.queue'),
|
||||
tooltip: t('sideToolbar.queue'),
|
||||
component: markRaw(QueueSidebarTab),
|
||||
type: 'vue'
|
||||
})
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: 'node-library',
|
||||
icon: 'pi pi-book',
|
||||
title: t('sideToolBar.nodeLibrary'),
|
||||
tooltip: t('sideToolBar.nodeLibrary'),
|
||||
component: markRaw(NodeLibrarySideBarTab),
|
||||
title: t('sideToolbar.nodeLibrary'),
|
||||
tooltip: t('sideToolbar.nodeLibrary'),
|
||||
component: markRaw(NodeLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
})
|
||||
}
|
||||
|
||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||
const onStatus = (e: CustomEvent<StatusWsMessageStatus>) =>
|
||||
queuePendingTaskCountStore.update(e)
|
||||
|
||||
const toast = useToast()
|
||||
const reconnectingMessage: ToastMessageOptions = {
|
||||
severity: 'error',
|
||||
summary: t('reconnecting')
|
||||
}
|
||||
const onReconnecting = () => {
|
||||
toast.remove(reconnectingMessage)
|
||||
toast.add(reconnectingMessage)
|
||||
}
|
||||
const onReconnected = () => {
|
||||
toast.remove(reconnectingMessage)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('reconnected'),
|
||||
life: 2000
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('status', onStatus)
|
||||
api.addEventListener('reconnecting', onReconnecting)
|
||||
api.addEventListener('reconnected', onReconnected)
|
||||
try {
|
||||
init()
|
||||
} catch (e) {
|
||||
console.error('Failed to init Vue app', e)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
api.removeEventListener('reconnecting', onReconnecting)
|
||||
api.removeEventListener('reconnected', onReconnected)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -103,6 +103,7 @@ body {
|
||||
#graph-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.comfyui-body-right {
|
||||
@@ -131,7 +132,7 @@ body {
|
||||
resize: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
font-size: var(--comfy-textarea-font-size);
|
||||
}
|
||||
|
||||
.comfy-modal {
|
||||
|
||||
@@ -4,27 +4,43 @@
|
||||
class="side-bar-panel"
|
||||
:minSize="10"
|
||||
:size="20"
|
||||
v-show="sideBarPanelVisible"
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'left'"
|
||||
>
|
||||
<slot name="side-bar-panel"></slot>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel class="graph-canvas-panel" :size="100">
|
||||
<div></div>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
class="side-bar-panel"
|
||||
:minSize="10"
|
||||
:size="20"
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'right'"
|
||||
>
|
||||
<slot name="side-bar-panel"></slot>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const sideBarPanelVisible = computed(
|
||||
const settingStore = useSettingStore()
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
|
||||
const sidebarPanelVisible = computed(
|
||||
() => useWorkspaceStore().activeSidebarTab !== null
|
||||
)
|
||||
const gutterClass = computed(() => {
|
||||
return sideBarPanelVisible.value ? '' : 'gutter-hidden'
|
||||
return sidebarPanelVisible.value ? '' : 'gutter-hidden'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
58
src/components/common/ComfyImage.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<!-- A image with placeholder fallback on error -->
|
||||
<template>
|
||||
<img
|
||||
:src="src"
|
||||
@error="handleImageError"
|
||||
:class="[{ 'broken-image': imageBroken }, ...classArray]"
|
||||
/>
|
||||
<div v-if="imageBroken" class="broken-image-placeholder">
|
||||
<i class="pi pi-image"></i>
|
||||
<span>{{ $t('imageFailedToLoad') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
src: string
|
||||
class?: string | string[] | object
|
||||
}>()
|
||||
|
||||
const imageBroken = ref(false)
|
||||
const handleImageError = (e: Event) => {
|
||||
imageBroken.value = true
|
||||
}
|
||||
|
||||
const classArray = computed(() => {
|
||||
if (Array.isArray(props.class)) {
|
||||
return props.class
|
||||
} else if (typeof props.class === 'string') {
|
||||
return props.class.split(' ')
|
||||
} else if (typeof props.class === 'object') {
|
||||
return Object.keys(props.class).filter((key) => props.class[key])
|
||||
}
|
||||
return []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.broken-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.broken-image-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.broken-image-placeholder i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -44,6 +44,7 @@ defineEmits(['action'])
|
||||
.no-results-placeholder :deep(.p-card) {
|
||||
background-color: var(--surface-ground);
|
||||
text-align: center;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.no-results-placeholder h3 {
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
<template>
|
||||
<IconField :class="props.class">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputIcon :class="props.icon" />
|
||||
<InputText
|
||||
class="search-box-input"
|
||||
@input="handleInput"
|
||||
:modelValue="props.modelValue"
|
||||
:placeholder="$t('searchSettings') + '...'"
|
||||
:placeholder="props.placeholder"
|
||||
/>
|
||||
</IconField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
class?: string
|
||||
modelValue: string
|
||||
}>()
|
||||
const emit = defineEmits(['update:modelValue', 'search'])
|
||||
const emitSearch = debounce((event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('search', target.value)
|
||||
}, 300)
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
}
|
||||
|
||||
const handleInput = (event: KeyboardEvent) => {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: 'Search...',
|
||||
icon: 'pi pi-search',
|
||||
debounceTime: 300
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'search'])
|
||||
|
||||
const emitSearch = debounce((value: string) => {
|
||||
emit('search', value)
|
||||
}, props.debounceTime)
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
emitSearch(event)
|
||||
emitSearch(target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
closable
|
||||
closeOnEscape
|
||||
dismissableMask
|
||||
:maximizable="dialogStore.props.maximizable ?? false"
|
||||
:maximizable="maximizable"
|
||||
@hide="dialogStore.closeDialog"
|
||||
@maximize="maximized = true"
|
||||
@unmaximize="maximized = false"
|
||||
@@ -19,20 +19,20 @@
|
||||
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="dialogStore.component"
|
||||
v-bind="dialogStore.props"
|
||||
:maximized="maximized"
|
||||
/>
|
||||
<component :is="dialogStore.component" v-bind="contentProps" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import Dialog from 'primevue/dialog'
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const maximizable = dialogStore.props.maximizable ?? false
|
||||
const maximized = ref(false)
|
||||
const contentProps = computed(() => ({
|
||||
...dialogStore.props,
|
||||
...(dialogStore.props.maximizable ? { maximized } : {})
|
||||
}))
|
||||
</script>
|
||||
|
||||
184
src/components/dialog/content/ExecutionErrorDialogContent.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-exclamation-circle"
|
||||
:title="props.error.node_type"
|
||||
:message="props.error.exception_message"
|
||||
/>
|
||||
<div class="comfy-error-report">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
:label="$t('showReport')"
|
||||
@click="showReport"
|
||||
text
|
||||
/>
|
||||
<template v-if="reportOpen">
|
||||
<Divider />
|
||||
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
|
||||
<pre class="wrapper-pre">{{ reportContent }}</pre>
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
|
||||
<div class="action-container">
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
:repoName="repoName"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('openNewIssue')"
|
||||
icon="pi pi-github"
|
||||
@click="openNewGithubIssue"
|
||||
class="p-button-secondary"
|
||||
/>
|
||||
<Button
|
||||
v-if="reportOpen"
|
||||
:label="$t('copyToClipboard')"
|
||||
icon="pi pi-copy"
|
||||
@click="copyReportToClipboard"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
}>()
|
||||
|
||||
const repoOwner = 'comfyanonymous'
|
||||
const repoName = 'ComfyUI'
|
||||
const reportContent = ref('')
|
||||
const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
const { copy, isSupported } = useClipboard()
|
||||
|
||||
onMounted(async () => {
|
||||
generateReport(await api.getSystemStats())
|
||||
})
|
||||
|
||||
const generateReport = (systemStats: SystemStats) => {
|
||||
// The default JSON workflow has about 3000 characters.
|
||||
const MAX_JSON_LENGTH = 20000
|
||||
const workflowJSONString = JSON.stringify(app.graph.serialize())
|
||||
const workflowText =
|
||||
workflowJSONString.length > MAX_JSON_LENGTH
|
||||
? 'Workflow too large. Please manually upload the workflow from local file system.'
|
||||
: workflowJSONString
|
||||
|
||||
reportContent.value = `
|
||||
# ComfyUI Error Report
|
||||
## Error Details
|
||||
- **Node Type:** ${props.error.node_type}
|
||||
- **Exception Type:** ${props.error.exception_type}
|
||||
- **Exception Message:** ${props.error.exception_message}
|
||||
## Stack Trace
|
||||
\`\`\`
|
||||
${props.error.traceback.join('\n')}
|
||||
\`\`\`
|
||||
## System Information
|
||||
- **OS:** ${systemStats.system.os}
|
||||
- **Python Version:** ${systemStats.system.python_version}
|
||||
- **Embedded Python:** ${systemStats.system.embedded_python}
|
||||
## Devices
|
||||
${systemStats.devices
|
||||
.map(
|
||||
(device) => `
|
||||
- **Name:** ${device.name}
|
||||
- **Type:** ${device.type}
|
||||
- **VRAM Total:** ${device.vram_total}
|
||||
- **VRAM Free:** ${device.vram_free}
|
||||
- **Torch VRAM Total:** ${device.torch_vram_total}
|
||||
- **Torch VRAM Free:** ${device.torch_vram_free}
|
||||
`
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
## Attached Workflow
|
||||
Please make sure that workflow does not contain any sensitive information such as API keys or passwords.
|
||||
\`\`\`
|
||||
${workflowText}
|
||||
\`\`\`
|
||||
|
||||
## Additional Context
|
||||
(Please add any additional context or steps to reproduce the error here)
|
||||
`
|
||||
}
|
||||
|
||||
const copyReportToClipboard = async () => {
|
||||
if (isSupported) {
|
||||
try {
|
||||
await copy(reportContent.value)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Report copied to clipboard',
|
||||
life: 3000
|
||||
})
|
||||
} catch (err) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to copy report'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Clipboard API not supported in your browser'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openNewGithubIssue = async () => {
|
||||
await copyReportToClipboard()
|
||||
const issueTitle = encodeURIComponent(
|
||||
`[Bug]: ${props.error.exception_type} in ${props.error.node_type}`
|
||||
)
|
||||
const issueBody = encodeURIComponent(
|
||||
'The report has been copied to the clipboard. Please paste it here.'
|
||||
)
|
||||
const url = `https://github.com/${repoOwner}/${repoName}/issues/new?title=${issueTitle}&body=${issueBody}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-error-report {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wrapper-pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.no-results-placeholder {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||