Compare commits
137 Commits
v1.15.3
...
export-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a98e3286 | ||
|
|
9ce3cccfd4 | ||
|
|
9935b322f0 | ||
|
|
60dd242b23 | ||
|
|
cec0dcbccd | ||
|
|
907632a250 | ||
|
|
45c450cdb9 | ||
|
|
ca85b2b144 | ||
|
|
1f28e6ef33 | ||
|
|
fee444c64b | ||
|
|
851739a768 | ||
|
|
1631665efb | ||
|
|
1a066c7062 | ||
|
|
e45f5bdebb | ||
|
|
c270e7734a | ||
|
|
8d7a21e008 | ||
|
|
29e63baca6 | ||
|
|
b22713daf0 | ||
|
|
c8b8953e0a | ||
|
|
731ce8599d | ||
|
|
ec8e55c1c1 | ||
|
|
04d38f2538 | ||
|
|
1c41db75f8 | ||
|
|
c7a7397000 | ||
|
|
e660e1d678 | ||
|
|
fb19752389 | ||
|
|
d098d6ae4e | ||
|
|
e4a5355f58 | ||
|
|
42c004d41d | ||
|
|
009c389607 | ||
|
|
b449dbd26b | ||
|
|
67835edfca | ||
|
|
60c0ce228a | ||
|
|
1990f25638 | ||
|
|
30c473db77 | ||
|
|
2371288fed | ||
|
|
2337fe6f8e | ||
|
|
25e6386b2a | ||
|
|
a03841cb1a | ||
|
|
dc5d7ea1be | ||
|
|
59e20964a0 | ||
|
|
8f00d8ca6a | ||
|
|
05e0036898 | ||
|
|
9e7690405a | ||
|
|
d687ea2cde | ||
|
|
c801a0c854 | ||
|
|
615c183059 | ||
|
|
27c8389b9f | ||
|
|
261f671ef0 | ||
|
|
22ae30132c | ||
|
|
7d3bf372b0 | ||
|
|
cd35373c25 | ||
|
|
a500a96c4a | ||
|
|
dc9ea44f3a | ||
|
|
2dc33b1eb9 | ||
|
|
ed8f9a5a4f | ||
|
|
6e72e1924e | ||
|
|
f7854a4e0b | ||
|
|
05023b7889 | ||
|
|
609496957b | ||
|
|
a879f413bb | ||
|
|
21d679a662 | ||
|
|
34f9603961 | ||
|
|
cf27a896f3 | ||
|
|
e9a98161ca | ||
|
|
fa132e4106 | ||
|
|
a489c19b07 | ||
|
|
46af2f03f3 | ||
|
|
3a1c95fb10 | ||
|
|
7a6f0e210e | ||
|
|
ac3bd7a848 | ||
|
|
77b5e487cf | ||
|
|
a7a8459e18 | ||
|
|
65c9c264c6 | ||
|
|
8ea070df12 | ||
|
|
2c02d4ebb3 | ||
|
|
a2b3048b94 | ||
|
|
549a42716f | ||
|
|
fa75614dc3 | ||
|
|
ac53296b2e | ||
|
|
6eb2b76621 | ||
|
|
9dd3b9fff5 | ||
|
|
785cad70ba | ||
|
|
026f076b8a | ||
|
|
65f1561ec6 | ||
|
|
bb094cf0ae | ||
|
|
ec684ee6b8 | ||
|
|
3978613f14 | ||
|
|
0a40e07f7e | ||
|
|
577af51ff8 | ||
|
|
df7c7383e2 | ||
|
|
1279f30f5a | ||
|
|
9ab4b549c0 | ||
|
|
10de4e5445 | ||
|
|
30420f2c0a | ||
|
|
39c3a57c11 | ||
|
|
6d09b7165f | ||
|
|
8fc6840434 | ||
|
|
db575425fe | ||
|
|
ccb71bf1a3 | ||
|
|
733d71aaac | ||
|
|
e059b9b82f | ||
|
|
cfaf769a65 | ||
|
|
b80e0e1a3c | ||
|
|
7b7d9905a7 | ||
|
|
594fc5945c | ||
|
|
e5abf765bd | ||
|
|
712c127bb5 | ||
|
|
854501ef27 | ||
|
|
aea4493b4d | ||
|
|
df47226fd4 | ||
|
|
f26f5f25bb | ||
|
|
284902cabe | ||
|
|
58dec5ea42 | ||
|
|
7e76665a22 | ||
|
|
cb06d96930 | ||
|
|
b01ddb6aff | ||
|
|
10bed33383 | ||
|
|
a57e60d60a | ||
|
|
8c789bd05d | ||
|
|
28def833f9 | ||
|
|
fcc22f06ac | ||
|
|
3922a5882b | ||
|
|
4a40e83b98 | ||
|
|
21e0caa1b1 | ||
|
|
04af8cda4d | ||
|
|
504b717575 | ||
|
|
62fdcd4949 | ||
|
|
cb7adaef9b | ||
|
|
6aad5222ab | ||
|
|
690326c374 | ||
|
|
25ce267b2e | ||
|
|
78e3a20773 | ||
|
|
56dbcbbd22 | ||
|
|
4bfc8e9e33 | ||
|
|
6e72207927 | ||
|
|
71968ae133 |
11
.cursorrules
@@ -8,6 +8,15 @@ const vue3CompositionApiBestPractices = [
|
|||||||
"Use watch and watchEffect for side effects",
|
"Use watch and watchEffect for side effects",
|
||||||
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
|
||||||
"Utilize provide/inject for dependency injection",
|
"Utilize provide/inject for dependency injection",
|
||||||
|
"Use vue 3.5 style of default prop declaration. Example:
|
||||||
|
|
||||||
|
const { nodes, showTotal = true } = defineProps<{
|
||||||
|
nodes: ApiNodeCost[]
|
||||||
|
showTotal?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
",
|
||||||
|
"Organize vue component in <template> <script> <style> order",
|
||||||
]
|
]
|
||||||
|
|
||||||
// Folder structure
|
// Folder structure
|
||||||
@@ -40,4 +49,6 @@ const additionalInstructions = `
|
|||||||
7. Implement proper error handling
|
7. Implement proper error handling
|
||||||
8. Follow Vue 3 style guide and naming conventions
|
8. Follow Vue 3 style guide and naming conventions
|
||||||
9. Use Vite for fast development and building
|
9. Use Vite for fast development and building
|
||||||
|
10. Use vue-i18n in composition API for any string literals. Place new translation
|
||||||
|
entries in src/locales/en/main.json.
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Local development playwright target
|
# Local development playwright target
|
||||||
|
# Note: Don't add a trailing / after the port
|
||||||
PLAYWRIGHT_TEST_URL=http://localhost:5173
|
PLAYWRIGHT_TEST_URL=http://localhost:5173
|
||||||
# PLAYWRIGHT_TEST_URL=http://localhost:8188
|
# PLAYWRIGHT_TEST_URL=http://localhost:8188
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/test-ui.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||||
ref: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be'
|
ref: '49c8220be49120dbaff85f32813d854d6dff2d05'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module.exports = defineConfig({
|
|||||||
entryLocale: 'en',
|
entryLocale: 'en',
|
||||||
output: 'src/locales',
|
output: 'src/locales',
|
||||||
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
|
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, controlnet, lora.
|
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora.
|
||||||
'latent' is the short form of 'latent space'.
|
'latent' is the short form of 'latent space'.
|
||||||
'mask' is in the context of image processing.
|
'mask' is in the context of image processing.
|
||||||
`
|
`
|
||||||
|
|||||||
25
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"austenc.tailwind-docs",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"figma.figma-vscode-extension",
|
||||||
|
"github.vscode-github-actions",
|
||||||
|
"github.vscode-pull-request-github",
|
||||||
|
"hbenl.vscode-test-explorer",
|
||||||
|
"lokalise.i18n-ally",
|
||||||
|
"ms-playwright.playwright",
|
||||||
|
"vitest.explorer",
|
||||||
|
"vue.volar",
|
||||||
|
"sonarsource.sonarlint-vscode",
|
||||||
|
"deque-systems.vscode-axe-linter",
|
||||||
|
"kisstkondoros.vscode-codemetrics",
|
||||||
|
"donjayamanne.githistory",
|
||||||
|
"wix.vscode-import-cost",
|
||||||
|
"prograhammer.tslint-vue",
|
||||||
|
"antfu.vite"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -9,15 +9,26 @@ If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directo
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
|
### ComfyUI devtools
|
||||||
ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing.
|
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
|
||||||
|
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
|
||||||
|
|
||||||
|
### Node.js & Playwright Prerequisites
|
||||||
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
|
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright install chromium --with-deps
|
npx playwright install chromium --with-deps
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Ensure the environment variables in `.env` are set correctly according to your setup.
|
||||||
|
|
||||||
|
The `.env` file will not exist until you create it yourself.
|
||||||
|
|
||||||
|
A template with helpful information can be found in `.env_example`.
|
||||||
|
|
||||||
|
### Multiple Tests
|
||||||
|
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
There are two ways to run the tests:
|
There are two ways to run the tests:
|
||||||
@@ -34,8 +45,6 @@ There are two ways to run the tests:
|
|||||||
```
|
```
|
||||||
This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
|
This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
|
||||||
|
|
||||||
To run the same test multiple times in Playwright's UI mode, you must launch the main ComfyUI process with the `--multi-user` flag.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Screenshot Expectations
|
## Screenshot Expectations
|
||||||
|
|||||||
126
browser_tests/assets/bad_link.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 5,
|
||||||
|
"last_link_id": 3,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
867.4669799804688,
|
||||||
|
347.22369384765625
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
262
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "steps",
|
||||||
|
"type": "INT",
|
||||||
|
"widget": {
|
||||||
|
"name": "steps"
|
||||||
|
},
|
||||||
|
"link": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
0,
|
||||||
|
"randomize",
|
||||||
|
20,
|
||||||
|
8,
|
||||||
|
"euler",
|
||||||
|
"normal",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "PrimitiveInt",
|
||||||
|
"pos": [
|
||||||
|
443.0852355957031,
|
||||||
|
441.131591796875
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
82
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "INT",
|
||||||
|
"type": "INT",
|
||||||
|
"links": [
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "PrimitiveInt"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
0,
|
||||||
|
"randomize"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
"INT"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 1.9487171000000016,
|
||||||
|
"offset": [
|
||||||
|
-325.57196748514497,
|
||||||
|
-168.13150517966463
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
53
browser_tests/assets/default_input.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"id": "9bcb9451-8319-492a-88d4-fb711d8c3d25",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 6,
|
||||||
|
"last_link_id": 0,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "DevToolsNodeWithDefaultInput",
|
||||||
|
"pos": [
|
||||||
|
8.39722728729248,
|
||||||
|
29.727279663085938
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
82
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "float_input",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "FLOAT",
|
||||||
|
"link": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "DevToolsNodeWithDefaultInput"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 2.1600300525920346,
|
||||||
|
"offset": [
|
||||||
|
63.071794466403446,
|
||||||
|
75.18055335968394
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
82
browser_tests/assets/dynamically_added_input.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 9,
|
||||||
|
"last_link_id": 13,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
0,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 315,
|
||||||
|
"1": 262
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": null
|
||||||
|
} ,
|
||||||
|
{
|
||||||
|
"name": "dynamic_input",
|
||||||
|
"type": "FLOAT",
|
||||||
|
"link": null,
|
||||||
|
"_meta": "Dynamically added input via frontend JS logic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [],
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
156680208700286,
|
||||||
|
"randomize",
|
||||||
|
20,
|
||||||
|
8,
|
||||||
|
"euler",
|
||||||
|
"normal",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 1,
|
||||||
|
"offset": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
74
browser_tests/assets/input_order_swap.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 2,
|
||||||
|
"last_link_id": 1,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [904, 466],
|
||||||
|
"size": [400, 200],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"type": "STRING",
|
||||||
|
"widget": {
|
||||||
|
"name": "text"
|
||||||
|
},
|
||||||
|
"link": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [""]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "PrimitiveString",
|
||||||
|
"pos": [556.8589477539062, 472.94342041015625],
|
||||||
|
"size": [315, 58],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "STRING",
|
||||||
|
"type": "STRING",
|
||||||
|
"links": [1]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "PrimitiveString"
|
||||||
|
},
|
||||||
|
"widgets_values": ["foo"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [[1, 2, 0, 1, 0, "STRING"]],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 1.7715610000000013,
|
||||||
|
"offset": [-388.521484375, -162.31336975097656]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -51,7 +51,10 @@
|
|||||||
0.85,
|
0.85,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
""
|
"",
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export class ComfyPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getVisibleToastCount() {
|
async getVisibleToastCount() {
|
||||||
return await this.page.locator('.p-toast:visible').count()
|
return await this.page.locator('.p-toast-message:visible').count()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickTextEncodeNode1() {
|
async clickTextEncodeNode1() {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export class NodeWidgetReference {
|
|||||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||||
|
|
||||||
const [x, y, w, h] = node.getBounding()
|
const [x, y, w, h] = node.getBounding()
|
||||||
return window['app'].canvas.ds.convertOffsetToCanvas([
|
return window['app'].canvasPosToClientPos([
|
||||||
x + w / 2,
|
x + w / 2,
|
||||||
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
||||||
])
|
])
|
||||||
@@ -94,6 +94,36 @@ export class NodeWidgetReference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The position of the widget's associated socket
|
||||||
|
*/
|
||||||
|
async getSocketPosition(): Promise<Position> {
|
||||||
|
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||||
|
([id, index]) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
if (!node) throw new Error(`Node ${id} not found.`)
|
||||||
|
const widget = node.widgets[index]
|
||||||
|
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||||
|
|
||||||
|
const slot = node.inputs.find(
|
||||||
|
(slot) => slot.widget?.name === widget.name
|
||||||
|
)
|
||||||
|
if (!slot) throw new Error(`Socket ${widget.name} not found.`)
|
||||||
|
|
||||||
|
const [x, y] = node.getBounding()
|
||||||
|
return window['app'].canvasPosToClientPos([
|
||||||
|
x + slot.pos[0],
|
||||||
|
y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT']
|
||||||
|
])
|
||||||
|
},
|
||||||
|
[this.node.id, this.index] as const
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
x: pos[0],
|
||||||
|
y: pos[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async click() {
|
async click() {
|
||||||
await this.node.comfyPage.canvas.click({
|
await this.node.comfyPage.canvas.click({
|
||||||
position: await this.getPosition()
|
position: await this.getPosition()
|
||||||
@@ -250,7 +280,7 @@ export class NodeReference {
|
|||||||
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
||||||
await this.comfyPage.dragAndDrop(
|
await this.comfyPage.dragAndDrop(
|
||||||
await originSlot.getPosition(),
|
await originSlot.getPosition(),
|
||||||
await targetWidget.getPosition()
|
await targetWidget.getSocketPosition()
|
||||||
)
|
)
|
||||||
return originSlot
|
return originSlot
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test.describe('Keybindings', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await comfyPage.executeCommand('TestCommand')
|
await comfyPage.executeCommand('TestCommand')
|
||||||
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
|
expect(await comfyPage.getToastErrorCount()).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Should handle async command errors', async ({ comfyPage }) => {
|
test('Should handle async command errors', async ({ comfyPage }) => {
|
||||||
@@ -45,6 +45,6 @@ test.describe('Keybindings', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await comfyPage.executeCommand('TestCommand')
|
await comfyPage.executeCommand('TestCommand')
|
||||||
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
|
expect(await comfyPage.getToastErrorCount()).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -323,7 +323,21 @@ test.describe('Error dialog', () => {
|
|||||||
|
|
||||||
await comfyPage.loadWorkflow('default')
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
const errorDialog = comfyPage.page.locator('.error-dialog-content')
|
const errorDialog = comfyPage.page.locator('.comfy-error-report')
|
||||||
|
await expect(errorDialog).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should display an error dialog when prompt execution fails', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.page.evaluate(async () => {
|
||||||
|
const app = window['app']
|
||||||
|
app.api.queuePrompt = () => {
|
||||||
|
throw new Error('Error on queuePrompt!')
|
||||||
|
}
|
||||||
|
await app.queuePrompt(0)
|
||||||
|
})
|
||||||
|
const errorDialog = comfyPage.page.locator('.comfy-error-report')
|
||||||
await expect(errorDialog).toBeVisible()
|
await expect(errorDialog).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
20
browser_tests/tests/execution.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('Execution', () => {
|
||||||
|
test('Report error on unconnected slot', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.disconnectEdge()
|
||||||
|
await comfyPage.clickEmptySpace()
|
||||||
|
|
||||||
|
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||||
|
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
|
||||||
|
await comfyPage.page.locator('.p-dialog-close-button').click()
|
||||||
|
await comfyPage.page.locator('.comfy-error-report').waitFor({
|
||||||
|
state: 'hidden'
|
||||||
|
})
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
'execution-error-unconnected-slot.png'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
After Width: | Height: | Size: 97 KiB |
21
browser_tests/tests/graph.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('Graph', () => {
|
||||||
|
// Should be able to fix link input slot index after swap the input order
|
||||||
|
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
|
||||||
|
test('Fix link input slots', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('input_order_swap')
|
||||||
|
expect(
|
||||||
|
await comfyPage.page.evaluate(() => {
|
||||||
|
return window['app'].graph.links.get(1)?.target_slot
|
||||||
|
})
|
||||||
|
).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate workflow links', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('bad_link')
|
||||||
|
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
@@ -25,6 +25,11 @@ test.describe('Optional input', () => {
|
|||||||
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Default input', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default_input')
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
|
||||||
|
})
|
||||||
|
|
||||||
test('Only optional inputs', async ({ comfyPage }) => {
|
test('Only optional inputs', async ({ comfyPage }) => {
|
||||||
await comfyPage.loadWorkflow('only_optional_inputs')
|
await comfyPage.loadWorkflow('only_optional_inputs')
|
||||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||||
@@ -67,4 +72,10 @@ test.describe('Optional input', () => {
|
|||||||
'missing_nodes_converted_widget.png'
|
'missing_nodes_converted_widget.png'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
test('dynamically added input', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('dynamically_added_input')
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
'dynamically_added_input.png'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 103 KiB |
@@ -43,4 +43,15 @@ test.describe('Primitive Node', () => {
|
|||||||
'static_primitive_connected.png'
|
'static_primitive_connected.png'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Report missing nodes when connect to missing node', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.loadWorkflow(
|
||||||
|
'primitive/primitive_node_connect_missing_node'
|
||||||
|
)
|
||||||
|
// 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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 65 KiB |
@@ -39,6 +39,10 @@ test.describe('Reroute Node', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.describe('LiteGraph Native Reroute Node', () => {
|
test.describe('LiteGraph Native Reroute Node', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
|
||||||
|
})
|
||||||
|
|
||||||
test('loads from workflow', async ({ comfyPage }) => {
|
test('loads from workflow', async ({ comfyPage }) => {
|
||||||
await comfyPage.loadWorkflow('reroute/native_reroute')
|
await comfyPage.loadWorkflow('reroute/native_reroute')
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
@@ -88,63 +88,6 @@ test.describe('Node Right Click Menu', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Widget conversion', () => {
|
|
||||||
const convertibleWidgetTypes = ['text', 'string', 'number', 'toggle']
|
|
||||||
|
|
||||||
test('Can convert widget to input', async ({ comfyPage }) => {
|
|
||||||
await comfyPage.rightClickEmptyLatentNode()
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
|
||||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
// The submenu has an identical entry as the base menu - use last
|
|
||||||
await comfyPage.page.getByText('Convert width to input').last().click()
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'right-click-node-widget-converted.png'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Can convert widget without submenu', async ({ comfyPage }) => {
|
|
||||||
// Right-click the width widget
|
|
||||||
await comfyPage.rightClickEmptyLatentNode()
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
|
||||||
await comfyPage.page.getByText('Convert width to input').click()
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'right-click-node-widget-converted.png'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
convertibleWidgetTypes.forEach((widgetType) => {
|
|
||||||
test(`Can convert ${widgetType} widget to input`, async ({
|
|
||||||
comfyPage
|
|
||||||
}) => {
|
|
||||||
const nodeType = 'KSampler'
|
|
||||||
|
|
||||||
// To avoid needing multiple clicks, disable nesting of conversion options
|
|
||||||
await comfyPage.setSetting('Comfy.NodeInputConversionSubmenus', false)
|
|
||||||
|
|
||||||
// Add the widget using the node's `addWidget` method
|
|
||||||
await comfyPage.page.evaluate(
|
|
||||||
([nodeType, widgetType]) => {
|
|
||||||
const node = window['app'].graph.nodes.find(
|
|
||||||
(n) => n.type === nodeType
|
|
||||||
)
|
|
||||||
node.addWidget(widgetType, widgetType, 'defaultValue', () => {}, {})
|
|
||||||
},
|
|
||||||
[nodeType, widgetType]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify the context menu includes the conversion option
|
|
||||||
const node = (await comfyPage.getNodeRefsByType(nodeType))[0]
|
|
||||||
const menuOptions = await node.getContextMenuOptionNames()
|
|
||||||
expect(menuOptions.includes(`Convert ${widgetType} to input`)).toBe(
|
|
||||||
true
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Can pin and unpin', async ({ comfyPage }) => {
|
test('Can pin and unpin', async ({ comfyPage }) => {
|
||||||
await comfyPage.rightClickEmptyLatentNode()
|
await comfyPage.rightClickEmptyLatentNode()
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
@@ -246,5 +246,24 @@ test.describe('Selection Toolbox', () => {
|
|||||||
)
|
)
|
||||||
await expect(colorPickerButton).toHaveCSS('color', BLUE_COLOR)
|
await expect(colorPickerButton).toHaveCSS('color', BLUE_COLOR)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('colorization via color picker can be undone', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Select a node and color it
|
||||||
|
await comfyPage.selectNodes(['KSampler'])
|
||||||
|
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('.color-picker-container i[data-testid="blue"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Undo the colorization
|
||||||
|
await comfyPage.page.keyboard.press('Control+Z')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
// Node should be uncolored again
|
||||||
|
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||||
|
expect(await selectedNode.getProperty('color')).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -192,3 +192,19 @@ test.describe('Load audio widget', () => {
|
|||||||
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Unserialized widgets', () => {
|
||||||
|
test('Unserialized widgets values do not mark graph as modified', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized)
|
||||||
|
await comfyPage.loadWorkflow('widgets/load_image_widget')
|
||||||
|
|
||||||
|
// Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts`
|
||||||
|
await comfyPage.page.mouse.move(10, 10)
|
||||||
|
await comfyPage.page.mouse.click(10, 10)
|
||||||
|
|
||||||
|
// Expect the graph to not be modified
|
||||||
|
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,4 +1,5 @@
|
|||||||
import pluginJs from '@eslint/js'
|
import pluginJs from '@eslint/js'
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||||
import unusedImports from 'eslint-plugin-unused-imports'
|
import unusedImports from 'eslint-plugin-unused-imports'
|
||||||
import pluginVue from 'eslint-plugin-vue'
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
@@ -20,21 +21,26 @@ export default [
|
|||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||||
|
},
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
extraFileExtensions: ['.vue']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pluginJs.configs.recommended,
|
pluginJs.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
...pluginVue.configs['flat/essential'],
|
...pluginVue.configs['flat/recommended'],
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
{
|
{
|
||||||
files: ['src/**/*.vue'],
|
files: ['src/**/*.vue'],
|
||||||
languageOptions: { parserOptions: { parser: tseslint.parser } }
|
languageOptions: {
|
||||||
},
|
parserOptions: {
|
||||||
{
|
parser: tseslint.parser
|
||||||
rules: {
|
}
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/prefer-as-const': 'off'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -42,10 +48,12 @@ export default [
|
|||||||
'unused-imports': unusedImports
|
'unused-imports': unusedImports
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'@typescript-eslint/prefer-as-const': 'off',
|
'@typescript-eslint/prefer-as-const': 'off',
|
||||||
'unused-imports/no-unused-imports': 'error'
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'vue/no-v-html': 'off'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
1281
package-lock.json
generated
10
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@comfyorg/comfyui-frontend",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.15.3",
|
"version": "1.17.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||||
"homepage": "https://comfy.org",
|
"homepage": "https://comfy.org",
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.12.0",
|
||||||
|
"eslint-config-prettier": "^10.1.2",
|
||||||
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
@@ -60,7 +62,7 @@
|
|||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"unplugin-icons": "^0.19.3",
|
"unplugin-icons": "^0.19.3",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
"vite": "^5.4.15",
|
"vite": "^5.4.18",
|
||||||
"vite-plugin-dts": "^4.3.0",
|
"vite-plugin-dts": "^4.3.0",
|
||||||
"vitest": "^2.0.0",
|
"vitest": "^2.0.0",
|
||||||
"vue-tsc": "^2.1.10",
|
"vue-tsc": "^2.1.10",
|
||||||
@@ -71,7 +73,7 @@
|
|||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||||
"@comfyorg/litegraph": "^0.11.3",
|
"@comfyorg/litegraph": "^0.13.3",
|
||||||
"@primevue/forms": "^4.2.5",
|
"@primevue/forms": "^4.2.5",
|
||||||
"@primevue/themes": "^4.2.5",
|
"@primevue/themes": "^4.2.5",
|
||||||
"@sentry/vue": "^8.48.0",
|
"@sentry/vue": "^8.48.0",
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
"algoliasearch": "^5.21.0",
|
"algoliasearch": "^5.21.0",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
"firebase": "^11.6.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"jsondiffpatch": "^0.6.0",
|
"jsondiffpatch": "^0.6.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^9.14.3",
|
"vue-i18n": "^9.14.3",
|
||||||
"vue-router": "^4.4.3",
|
"vue-router": "^4.4.3",
|
||||||
|
"vuefire": "^3.2.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.3.0"
|
"zod-validation-error": "^3.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/assets/images/Comfy_Logo_x32.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
@@ -398,6 +398,7 @@ button.comfy-queue-btn {
|
|||||||
.graphdialog {
|
.graphdialog {
|
||||||
min-height: 1em;
|
min-height: 1em;
|
||||||
background-color: var(--comfy-menu-bg);
|
background-color: var(--comfy-menu-bg);
|
||||||
|
z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.graphdialog .name {
|
.graphdialog .name {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ const TITLE_SUFFIX = ' - ComfyUI'
|
|||||||
|
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
const executionText = computed(() =>
|
const executionText = computed(() =>
|
||||||
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
|
executionStore.isIdle
|
||||||
|
? ''
|
||||||
|
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||||
)
|
)
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
@@ -41,7 +43,7 @@ const workflowNameText = computed(() => {
|
|||||||
|
|
||||||
const nodeExecutionTitle = computed(() =>
|
const nodeExecutionTitle = computed(() =>
|
||||||
executionStore.executingNode && executionStore.executingNodeProgress
|
executionStore.executingNode && executionStore.executingNodeProgress
|
||||||
? `${executionText.value}[${executionStore.executingNodeProgress}%] ${executionStore.executingNode.type}`
|
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<Splitter
|
<Splitter
|
||||||
|
:key="activeSidebarTabId ?? undefined"
|
||||||
class="splitter-overlay-root splitter-overlay"
|
class="splitter-overlay-root splitter-overlay"
|
||||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||||
:key="activeSidebarTabId ?? undefined"
|
:state-key="activeSidebarTabId ?? undefined"
|
||||||
:stateKey="activeSidebarTabId ?? undefined"
|
state-storage="local"
|
||||||
stateStorage="local"
|
|
||||||
>
|
>
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
class="side-bar-panel"
|
|
||||||
:minSize="10"
|
|
||||||
:size="20"
|
|
||||||
v-show="sidebarPanelVisible"
|
v-show="sidebarPanelVisible"
|
||||||
v-if="sidebarLocation === 'left'"
|
v-if="sidebarLocation === 'left'"
|
||||||
|
class="side-bar-panel"
|
||||||
|
:min-size="10"
|
||||||
|
:size="20"
|
||||||
>
|
>
|
||||||
<slot name="side-bar-panel"></slot>
|
<slot name="side-bar-panel" />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
|
|
||||||
<SplitterPanel :size="100">
|
<SplitterPanel :size="100">
|
||||||
@@ -21,26 +21,26 @@
|
|||||||
class="splitter-overlay max-w-full"
|
class="splitter-overlay max-w-full"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
||||||
stateKey="bottom-panel-splitter"
|
state-key="bottom-panel-splitter"
|
||||||
stateStorage="local"
|
state-storage="local"
|
||||||
>
|
>
|
||||||
<SplitterPanel class="graph-canvas-panel relative">
|
<SplitterPanel class="graph-canvas-panel relative">
|
||||||
<slot name="graph-canvas-panel"></slot>
|
<slot name="graph-canvas-panel" />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterPanel class="bottom-panel" v-show="bottomPanelVisible">
|
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
|
||||||
<slot name="bottom-panel"></slot>
|
<slot name="bottom-panel" />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</Splitter>
|
</Splitter>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
|
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
class="side-bar-panel"
|
|
||||||
:minSize="10"
|
|
||||||
:size="20"
|
|
||||||
v-show="sidebarPanelVisible"
|
v-show="sidebarPanelVisible"
|
||||||
v-if="sidebarLocation === 'right'"
|
v-if="sidebarLocation === 'right'"
|
||||||
|
class="side-bar-panel"
|
||||||
|
:min-size="10"
|
||||||
|
:size="20"
|
||||||
>
|
>
|
||||||
<slot name="side-bar-panel"></slot>
|
<slot name="side-bar-panel" />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</Splitter>
|
</Splitter>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
:style="positionCSS"
|
:style="positionCSS"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||||
icon="pi pi-bars"
|
icon="pi pi-bars"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
text
|
text
|
||||||
size="large"
|
size="large"
|
||||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
|
||||||
:aria-label="$t('menu.showMenu')"
|
:aria-label="$t('menu.showMenu')"
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
@click="exitFocusMode"
|
@click="exitFocusMode"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="batch-count"
|
|
||||||
v-tooltip.bottom="{
|
v-tooltip.bottom="{
|
||||||
value: $t('menu.batchCount'),
|
value: $t('menu.batchCount'),
|
||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
|
class="batch-count"
|
||||||
:aria-label="$t('menu.batchCount')"
|
:aria-label="$t('menu.batchCount')"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
class="w-14"
|
|
||||||
v-model="batchCount"
|
v-model="batchCount"
|
||||||
|
class="w-14"
|
||||||
:min="minQueueCount"
|
:min="minQueueCount"
|
||||||
:max="maxQueueCount"
|
:max="maxQueueCount"
|
||||||
fluid
|
fluid
|
||||||
showButtons
|
show-buttons
|
||||||
:pt="{
|
:pt="{
|
||||||
incrementButton: {
|
incrementButton: {
|
||||||
class: 'w-6',
|
class: 'w-6',
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
:style="style"
|
:style="style"
|
||||||
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
||||||
>
|
>
|
||||||
<div class="actionbar-content flex items-center select-none" ref="panelRef">
|
<div ref="panelRef" class="actionbar-content flex items-center select-none">
|
||||||
<span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef">
|
<span ref="dragHandleRef" class="drag-handle cursor-move mr-2 p-0!" />
|
||||||
</span>
|
|
||||||
<ComfyQueueButton />
|
<ComfyQueueButton />
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -89,9 +88,9 @@ const setInitialPosition = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMounted(setInitialPosition)
|
onMounted(setInitialPosition)
|
||||||
watch(visible, (newVisible) => {
|
watch(visible, async (newVisible) => {
|
||||||
if (newVisible) {
|
if (newVisible) {
|
||||||
nextTick(setInitialPosition)
|
await nextTick(setInitialPosition)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="queue-button-group flex">
|
<div class="queue-button-group flex">
|
||||||
<SplitButton
|
<SplitButton
|
||||||
class="comfyui-queue-button"
|
|
||||||
:label="activeQueueModeMenuItem.label"
|
|
||||||
severity="primary"
|
|
||||||
size="small"
|
|
||||||
@click="queuePrompt"
|
|
||||||
:model="queueModeMenuItems"
|
|
||||||
data-testid="queue-button"
|
|
||||||
v-tooltip.bottom="{
|
v-tooltip.bottom="{
|
||||||
value: workspaceStore.shiftDown
|
value: workspaceStore.shiftDown
|
||||||
? $t('menu.runWorkflowFront')
|
? $t('menu.runWorkflowFront')
|
||||||
: $t('menu.runWorkflow'),
|
: $t('menu.runWorkflow'),
|
||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
|
class="comfyui-queue-button"
|
||||||
|
:label="activeQueueModeMenuItem.label"
|
||||||
|
severity="primary"
|
||||||
|
size="small"
|
||||||
|
:model="queueModeMenuItems"
|
||||||
|
data-testid="queue-button"
|
||||||
|
@click="queuePrompt"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
|
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
|
||||||
@@ -23,15 +23,15 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<Button
|
<Button
|
||||||
|
v-tooltip="{
|
||||||
|
value: item.tooltip,
|
||||||
|
showDelay: 600
|
||||||
|
}"
|
||||||
:label="String(item.label)"
|
:label="String(item.label)"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||||
size="small"
|
size="small"
|
||||||
text
|
text
|
||||||
v-tooltip="{
|
|
||||||
value: item.tooltip,
|
|
||||||
showDelay: 600
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</SplitButton>
|
</SplitButton>
|
||||||
@@ -48,8 +48,7 @@
|
|||||||
text
|
text
|
||||||
:aria-label="$t('menu.interrupt')"
|
:aria-label="$t('menu.interrupt')"
|
||||||
@click="() => commandStore.execute('Comfy.Interrupt')"
|
@click="() => commandStore.execute('Comfy.Interrupt')"
|
||||||
>
|
/>
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
v-tooltip.bottom="{
|
v-tooltip.bottom="{
|
||||||
value: $t('sideToolbar.queueTab.clearPendingTasks'),
|
value: $t('sideToolbar.queueTab.clearPendingTasks'),
|
||||||
@@ -135,12 +134,12 @@ const hasPendingTasks = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const queuePrompt = (e: Event) => {
|
const queuePrompt = async (e: Event) => {
|
||||||
const commandId =
|
const commandId =
|
||||||
'shiftKey' in e && e.shiftKey
|
'shiftKey' in e && e.shiftKey
|
||||||
? 'Comfy.QueuePromptFront'
|
? 'Comfy.QueuePromptFront'
|
||||||
: 'Comfy.QueuePrompt'
|
: 'Comfy.QueuePrompt'
|
||||||
commandStore.execute(commandId)
|
await commandStore.execute(commandId)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
|
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
|
||||||
<TabList pt:tabList="border-none">
|
<TabList pt:tab-list="border-none">
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<div class="tabs-container">
|
<div class="tabs-container">
|
||||||
<Tab
|
<Tab
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative overflow-hidden h-full w-full bg-black" ref="rootEl">
|
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
|
||||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||||
<div class="h-full terminal-host" ref="terminalEl"></div>
|
<div ref="terminalEl" class="h-full terminal-host" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ const terminalCreated = (
|
|||||||
root,
|
root,
|
||||||
autoRows: true,
|
autoRows: true,
|
||||||
autoCols: true,
|
autoCols: true,
|
||||||
onResize: () => {
|
onResize: async () => {
|
||||||
// If we aren't visible, don't resize
|
// If we aren't visible, don't resize
|
||||||
if (!terminal.element?.offsetParent) return
|
if (!terminal.element?.offsetParent) return
|
||||||
|
|
||||||
terminalApi.resize(terminal.cols, terminal.rows)
|
await terminalApi.resize(terminal.cols, terminal.rows)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
offData = terminal.onData(async (message: string) => {
|
offData = terminal.onData(async (message: string) => {
|
||||||
terminalApi.write(message)
|
await terminalApi.write(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
offOutput = terminalApi.onOutput((message) => {
|
offOutput = terminalApi.onOutput((message) => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-black h-full w-full">
|
<div class="bg-black h-full w-full">
|
||||||
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
|
<p v-if="errorMessage" class="p-4 text-center">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
<ProgressSpinner
|
<ProgressSpinner
|
||||||
v-else-if="loading"
|
v-else-if="loading"
|
||||||
class="relative inset-0 flex justify-center items-center h-full z-10"
|
class="relative inset-0 flex justify-center items-center h-full z-10"
|
||||||
@@ -57,7 +59,7 @@ const terminalCreated = (
|
|||||||
if (!clientId.value) {
|
if (!clientId.value) {
|
||||||
await until(clientId).not.toBeNull()
|
await until(clientId).not.toBeNull()
|
||||||
}
|
}
|
||||||
api.subscribeLogs(true)
|
await api.subscribeLogs(true)
|
||||||
api.addEventListener('logs', logReceived)
|
api.addEventListener('logs', logReceived)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +78,9 @@ const terminalCreated = (
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(async () => {
|
||||||
if (api.clientId) {
|
if (api.clientId) {
|
||||||
api.subscribeLogs(false)
|
await api.subscribeLogs(false)
|
||||||
}
|
}
|
||||||
api.removeEventListener('logs', logReceived)
|
api.removeEventListener('logs', logReceived)
|
||||||
})
|
})
|
||||||
|
|||||||
75
src/components/common/ApiNodesCostBreakdown.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-3 h-full">
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||||
|
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
|
||||||
|
</div>
|
||||||
|
<ScrollPanel class="flex-grow h-0">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="node in nodes"
|
||||||
|
:key="node.name"
|
||||||
|
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-base font-medium leading-tight">{{
|
||||||
|
node.name
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Tag
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-dollar"
|
||||||
|
rounded
|
||||||
|
class="text-amber-400 p-1"
|
||||||
|
/>
|
||||||
|
<span class="text-base font-medium leading-tight">
|
||||||
|
{{ node.cost.toFixed(costPrecision) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollPanel>
|
||||||
|
<template v-if="showTotal && nodes.length > 1">
|
||||||
|
<Divider class="my-2" />
|
||||||
|
<div class="flex justify-between items-center border-t px-3">
|
||||||
|
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Tag
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-dollar"
|
||||||
|
rounded
|
||||||
|
class="text-yellow-500 p-1"
|
||||||
|
/>
|
||||||
|
<span>{{ totalCost.toFixed(costPrecision) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
|
import Tag from 'primevue/tag'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const {
|
||||||
|
nodes,
|
||||||
|
showTotal = true,
|
||||||
|
costPrecision = 3
|
||||||
|
} = defineProps<{
|
||||||
|
nodes: ApiNodeCost[]
|
||||||
|
showTotal?: boolean
|
||||||
|
costPrecision?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const totalCost = computed(() =>
|
||||||
|
nodes.reduce((sum, node) => sum + node.cost, 0)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="selectedColorOption"
|
v-model="selectedColorOption"
|
||||||
:options="colorOptionsWithCustom"
|
:options="colorOptionsWithCustom"
|
||||||
optionLabel="name"
|
option-label="name"
|
||||||
dataKey="value"
|
data-key="value"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
>
|
>
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
backgroundColor: slotProps.option.value,
|
backgroundColor: slotProps.option.value,
|
||||||
borderRadius: '50%'
|
borderRadius: '50%'
|
||||||
}"
|
}"
|
||||||
></div>
|
/>
|
||||||
<i v-else class="pi pi-palette text-lg"></i>
|
<i v-else class="pi pi-palette text-lg" />
|
||||||
</template>
|
</template>
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
|||||||
@@ -8,22 +8,22 @@
|
|||||||
<img
|
<img
|
||||||
v-if="contain"
|
v-if="contain"
|
||||||
:src="src"
|
:src="src"
|
||||||
@error="handleImageError"
|
|
||||||
:data-test="src"
|
:data-test="src"
|
||||||
class="comfy-image-blur"
|
class="comfy-image-blur"
|
||||||
:style="{ 'background-image': `url(${src})` }"
|
:style="{ 'background-image': `url(${src})` }"
|
||||||
:alt="alt"
|
:alt="alt"
|
||||||
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
:src="src"
|
:src="src"
|
||||||
@error="handleImageError"
|
|
||||||
class="comfy-image-main"
|
class="comfy-image-main"
|
||||||
:class="classProp"
|
:class="classProp"
|
||||||
:alt="alt"
|
:alt="alt"
|
||||||
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div v-if="imageBroken" class="broken-image-placeholder">
|
<div v-if="imageBroken" class="broken-image-placeholder">
|
||||||
<i class="pi pi-image"></i>
|
<i class="pi pi-image" />
|
||||||
<span>{{ $t('g.imageFailedToLoad') }}</span>
|
<span>{{ $t('g.imageFailedToLoad') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="container"></div>
|
<div ref="container" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="selectedIcon"
|
v-model="selectedIcon"
|
||||||
:options="iconOptions"
|
:options="iconOptions"
|
||||||
optionLabel="name"
|
option-label="name"
|
||||||
dataKey="value"
|
data-key="value"
|
||||||
>
|
>
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
<i
|
<i
|
||||||
:class="['pi', slotProps.option.value, 'mr-2']"
|
:class="['pi', slotProps.option.value, 'mr-2']"
|
||||||
:style="{ color: finalColor }"
|
:style="{ color: finalColor }"
|
||||||
></i>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,14 +30,14 @@
|
|||||||
<Button
|
<Button
|
||||||
:label="$t('g.reset')"
|
:label="$t('g.reset')"
|
||||||
icon="pi pi-refresh"
|
icon="pi pi-refresh"
|
||||||
@click="resetCustomization"
|
|
||||||
class="p-button-text"
|
class="p-button-text"
|
||||||
|
@click="resetCustomization"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:label="$t('g.confirm')"
|
:label="$t('g.confirm')"
|
||||||
icon="pi pi-check"
|
icon="pi pi-check"
|
||||||
@click="confirmCustomization"
|
|
||||||
autofocus
|
autofocus
|
||||||
|
@click="confirmCustomization"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<template v-for="col in deviceColumns" :key="col.field">
|
<template v-for="col in deviceColumns" :key="col.field">
|
||||||
<div class="font-medium">{{ col.header }}</div>
|
<div class="font-medium">
|
||||||
|
{{ col.header }}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ formatValue(props.device[col.field], col.field) }}
|
{{ formatValue(props.device[col.field], col.field) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,19 +6,19 @@
|
|||||||
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
|
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
|
||||||
<InputText
|
<InputText
|
||||||
v-else
|
v-else
|
||||||
|
ref="inputRef"
|
||||||
|
v-model:modelValue="inputValue"
|
||||||
|
v-focus
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
fluid
|
fluid
|
||||||
v-model:modelValue="inputValue"
|
|
||||||
ref="inputRef"
|
|
||||||
@keyup.enter="blurInputElement"
|
|
||||||
@click.stop
|
|
||||||
:pt="{
|
:pt="{
|
||||||
root: {
|
root: {
|
||||||
onBlur: finishEditing
|
onBlur: finishEditing
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
v-focus
|
@keyup.enter="blurInputElement"
|
||||||
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,10 +45,10 @@ const finishEditing = () => {
|
|||||||
}
|
}
|
||||||
watch(
|
watch(
|
||||||
() => isEditing,
|
() => isEditing,
|
||||||
(newVal) => {
|
async (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
inputValue.value = modelValue
|
inputValue.value = modelValue
|
||||||
nextTick(() => {
|
await nextTick(() => {
|
||||||
if (!inputRef.value) return
|
if (!inputRef.value) return
|
||||||
const fileName = inputValue.value.includes('.')
|
const fileName = inputValue.value.includes('.')
|
||||||
? inputValue.value.split('.').slice(0, -1).join('.')
|
? inputValue.value.split('.').slice(0, -1).join('.')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<i class="pi pi-check text-green-500" v-if="status === 'completed'" />
|
<i v-if="status === 'completed'" class="pi pi-check text-green-500" />
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
<div class="file-details">
|
<div class="file-details">
|
||||||
<span class="file-type" :title="hint">{{ label }}</span>
|
<span class="file-type" :title="hint">{{ label }}</span>
|
||||||
@@ -14,20 +14,20 @@
|
|||||||
|
|
||||||
<div class="file-action">
|
<div class="file-action">
|
||||||
<Button
|
<Button
|
||||||
|
v-if="status === null || status === 'error'"
|
||||||
class="file-action-button"
|
class="file-action-button"
|
||||||
:label="$t('g.download') + ' (' + fileSize + ')'"
|
:label="$t('g.download') + ' (' + fileSize + ')'"
|
||||||
size="small"
|
size="small"
|
||||||
outlined
|
outlined
|
||||||
:disabled="!!props.error"
|
:disabled="!!props.error"
|
||||||
@click="triggerDownload"
|
|
||||||
v-if="status === null || status === 'error'"
|
|
||||||
icon="pi pi-download"
|
icon="pi pi-download"
|
||||||
|
@click="triggerDownload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-center gap-2"
|
|
||||||
v-if="status === 'in_progress' || status === 'paused'"
|
v-if="status === 'in_progress' || status === 'paused'"
|
||||||
|
class="flex flex-row items-center gap-2"
|
||||||
>
|
>
|
||||||
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
|
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
|
||||||
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
|
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
|
||||||
@@ -39,36 +39,36 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="file-action-button"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
:disabled="!!props.error"
|
|
||||||
@click="triggerPauseDownload"
|
|
||||||
v-if="status === 'in_progress'"
|
v-if="status === 'in_progress'"
|
||||||
icon="pi pi-pause-circle"
|
|
||||||
v-tooltip.top="t('electronFileDownload.pause')"
|
v-tooltip.top="t('electronFileDownload.pause')"
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="file-action-button"
|
class="file-action-button"
|
||||||
size="small"
|
size="small"
|
||||||
outlined
|
outlined
|
||||||
:disabled="!!props.error"
|
:disabled="!!props.error"
|
||||||
@click="triggerResumeDownload"
|
icon="pi pi-pause-circle"
|
||||||
|
@click="triggerPauseDownload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
v-if="status === 'paused'"
|
v-if="status === 'paused'"
|
||||||
icon="pi pi-play-circle"
|
|
||||||
v-tooltip.top="t('electronFileDownload.resume')"
|
v-tooltip.top="t('electronFileDownload.resume')"
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
class="file-action-button"
|
class="file-action-button"
|
||||||
size="small"
|
size="small"
|
||||||
outlined
|
outlined
|
||||||
:disabled="!!props.error"
|
:disabled="!!props.error"
|
||||||
@click="triggerCancelDownload"
|
icon="pi pi-play-circle"
|
||||||
|
@click="triggerResumeDownload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-tooltip.top="t('electronFileDownload.cancel')"
|
||||||
|
class="file-action-button"
|
||||||
|
size="small"
|
||||||
|
outlined
|
||||||
|
:disabled="!!props.error"
|
||||||
icon="pi pi-times-circle"
|
icon="pi pi-times-circle"
|
||||||
severity="danger"
|
severity="danger"
|
||||||
v-tooltip.top="t('electronFileDownload.cancel')"
|
@click="triggerCancelDownload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<component v-if="extension.type === 'vue'" :is="extension.component" />
|
<component :is="extension.component" v-if="extension.type === 'vue'" />
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
:ref="
|
:ref="
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
></div>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:src="modelValue"
|
:src="modelValue"
|
||||||
class="max-w-full max-h-full object-contain"
|
class="max-w-full max-h-full object-contain"
|
||||||
/>
|
/>
|
||||||
<i v-else class="pi pi-image text-gray-400 text-xl"></i>
|
<i v-else class="pi pi-image text-gray-400 text-xl" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -3,26 +3,26 @@
|
|||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<div class="form-label flex flex-grow items-center">
|
<div class="form-label flex flex-grow items-center">
|
||||||
<span
|
<span
|
||||||
|
:id="`${props.id}-label`"
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
:class="props.labelClass"
|
:class="props.labelClass"
|
||||||
:id="`${props.id}-label`"
|
|
||||||
>
|
>
|
||||||
<slot name="name-prefix"></slot>
|
<slot name="name-prefix" />
|
||||||
{{ props.item.name }}
|
{{ props.item.name }}
|
||||||
<i
|
<i
|
||||||
v-if="props.item.tooltip"
|
v-if="props.item.tooltip"
|
||||||
class="pi pi-info-circle bg-transparent"
|
|
||||||
v-tooltip="props.item.tooltip"
|
v-tooltip="props.item.tooltip"
|
||||||
|
class="pi pi-info-circle bg-transparent"
|
||||||
/>
|
/>
|
||||||
<slot name="name-suffix"></slot>
|
<slot name="name-suffix" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-input flex justify-end">
|
<div class="form-input flex justify-end">
|
||||||
<component
|
<component
|
||||||
:is="markRaw(getFormComponent(props.item))"
|
:is="markRaw(getFormComponent(props.item))"
|
||||||
:id="props.id"
|
:id="props.id"
|
||||||
:aria-labelledby="`${props.id}-label`"
|
|
||||||
v-model:modelValue="formValue"
|
v-model:modelValue="formValue"
|
||||||
|
:aria-labelledby="`${props.id}-label`"
|
||||||
v-bind="getFormAttrs(props.item)"
|
v-bind="getFormAttrs(props.item)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-knob flex flex-row items-center gap-2">
|
<div class="input-knob flex flex-row items-center gap-2">
|
||||||
<Knob
|
<Knob
|
||||||
:modelValue="modelValue"
|
:model-value="modelValue"
|
||||||
@update:modelValue="updateValue"
|
:value-template="displayValue"
|
||||||
:valueTemplate="displayValue"
|
|
||||||
class="knob-part"
|
class="knob-part"
|
||||||
:class="knobClass"
|
:class="knobClass"
|
||||||
:min="min"
|
:min="min"
|
||||||
:max="max"
|
:max="max"
|
||||||
:step="step"
|
:step="step"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
@update:model-value="updateValue"
|
||||||
/>
|
/>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
:modelValue="modelValue"
|
:model-value="modelValue"
|
||||||
@update:modelValue="updateValue"
|
|
||||||
class="input-part"
|
class="input-part"
|
||||||
:max-fraction-digits="3"
|
:max-fraction-digits="3"
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
:min="min"
|
:min="min"
|
||||||
:max="max"
|
:max="max"
|
||||||
:step="step"
|
:step="step"
|
||||||
:allowEmpty="false"
|
:allow-empty="false"
|
||||||
|
@update:model-value="updateValue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-slider flex flex-row items-center gap-2">
|
<div class="input-slider flex flex-row items-center gap-2">
|
||||||
<Slider
|
<Slider
|
||||||
:modelValue="modelValue"
|
:model-value="modelValue"
|
||||||
@update:modelValue="(value) => updateValue(value as number)"
|
|
||||||
class="slider-part"
|
class="slider-part"
|
||||||
:class="sliderClass"
|
:class="sliderClass"
|
||||||
:min="min"
|
:min="min"
|
||||||
:max="max"
|
:max="max"
|
||||||
:step="step"
|
:step="step"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
@update:model-value="(value) => updateValue(value as number)"
|
||||||
/>
|
/>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
:modelValue="modelValue"
|
:model-value="modelValue"
|
||||||
@update:modelValue="updateValue"
|
|
||||||
class="input-part"
|
class="input-part"
|
||||||
:max-fraction-digits="3"
|
:max-fraction-digits="3"
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
:min="min"
|
:min="min"
|
||||||
:max="max"
|
:max="max"
|
||||||
:step="step"
|
:step="step"
|
||||||
:allowEmpty="false"
|
:allow-empty="false"
|
||||||
|
@update:model-value="updateValue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
<Card>
|
<Card>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem"></i>
|
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem" />
|
||||||
<h3>{{ title }}</h3>
|
<h3>{{ title }}</h3>
|
||||||
<p class="whitespace-pre-line text-center">{{ message }}</p>
|
<p class="whitespace-pre-line text-center">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
v-if="buttonLabel"
|
v-if="buttonLabel"
|
||||||
:label="buttonLabel"
|
:label="buttonLabel"
|
||||||
@click="$emit('action')"
|
|
||||||
class="p-button-text"
|
class="p-button-text"
|
||||||
|
@click="$emit('action')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
class="p-button-icon pi pi-refresh transition-all"
|
class="p-button-icon pi pi-refresh transition-all"
|
||||||
:class="{ 'opacity-0': active }"
|
:class="{ 'opacity-0': active }"
|
||||||
data-pc-section="icon"
|
data-pc-section="icon"
|
||||||
></span>
|
/>
|
||||||
<span class="p-button-label" data-pc-section="label"> </span>
|
<span class="p-button-label" data-pc-section="label"> </span>
|
||||||
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
|
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
|
|
||||||
import { VueSeverity } from '@/types/primeVueTypes'
|
import { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
disabled,
|
disabled,
|
||||||
@@ -41,7 +41,7 @@ const {
|
|||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
outlined?: boolean
|
outlined?: boolean
|
||||||
severity?: VueSeverity
|
severity?: PrimeVueSeverity
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Model
|
// Model
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
/>
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
class="search-box-input w-full"
|
class="search-box-input w-full"
|
||||||
@input="handleInput"
|
:model-value="modelValue"
|
||||||
:modelValue="modelValue"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<InputIcon v-if="!modelValue" :class="icon" />
|
<InputIcon v-if="!modelValue" :class="icon" />
|
||||||
<Button
|
<Button
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
/>
|
/>
|
||||||
</IconField>
|
</IconField>
|
||||||
<div
|
<div
|
||||||
class="search-filters pt-2 flex flex-wrap gap-2"
|
|
||||||
v-if="filters?.length"
|
v-if="filters?.length"
|
||||||
|
class="search-filters pt-2 flex flex-wrap gap-2"
|
||||||
>
|
>
|
||||||
<SearchFilterChip
|
<SearchFilterChip
|
||||||
v-for="filter in filters"
|
v-for="filter in filters"
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="system-stats">
|
<div class="system-stats">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.systemInfo') }}</h2>
|
<h2 class="text-2xl font-semibold mb-4">
|
||||||
|
{{ $t('g.systemInfo') }}
|
||||||
|
</h2>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<template v-for="col in systemColumns" :key="col.field">
|
<template v-for="col in systemColumns" :key="col.field">
|
||||||
<div class="font-medium">{{ col.header }}</div>
|
<div class="font-medium">
|
||||||
|
{{ col.header }}
|
||||||
|
</div>
|
||||||
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
|
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,7 +17,9 @@
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.devices') }}</h2>
|
<h2 class="text-2xl font-semibold mb-4">
|
||||||
|
{{ $t('g.devices') }}
|
||||||
|
</h2>
|
||||||
<TabView v-if="props.stats.devices.length > 1">
|
<TabView v-if="props.stats.devices.length > 1">
|
||||||
<TabPanel
|
<TabPanel
|
||||||
v-for="device in props.stats.devices"
|
v-for="device in props.stats.devices"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tree
|
<Tree
|
||||||
class="tree-explorer py-0 px-2 2xl:px-4"
|
|
||||||
:class="props.class"
|
|
||||||
v-model:expandedKeys="expandedKeys"
|
v-model:expandedKeys="expandedKeys"
|
||||||
v-model:selectionKeys="selectionKeys"
|
v-model:selectionKeys="selectionKeys"
|
||||||
|
class="tree-explorer py-0 px-2 2xl:px-4"
|
||||||
|
:class="props.class"
|
||||||
:value="renderedRoot.children"
|
:value="renderedRoot.children"
|
||||||
selectionMode="single"
|
selection-mode="single"
|
||||||
:pt="{
|
:pt="{
|
||||||
nodeLabel: 'tree-explorer-node-label',
|
nodeLabel: 'tree-explorer-node-label',
|
||||||
nodeContent: ({ context }) => ({
|
nodeContent: ({ context }) => ({
|
||||||
@@ -186,9 +186,9 @@ const menuItems = computed<MenuItem[]>(() =>
|
|||||||
{
|
{
|
||||||
label: t('g.delete'),
|
label: t('g.delete'),
|
||||||
icon: 'pi pi-trash',
|
icon: 'pi pi-trash',
|
||||||
command: () => {
|
command: async () => {
|
||||||
if (menuTargetNode.value) {
|
if (menuTargetNode.value) {
|
||||||
deleteCommand(menuTargetNode.value)
|
await deleteCommand(menuTargetNode.value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visible: menuTargetNode.value?.handleDelete !== undefined,
|
visible: menuTargetNode.value?.handleDelete !== undefined,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="container"
|
||||||
:class="[
|
:class="[
|
||||||
'tree-node',
|
'tree-node',
|
||||||
{
|
{
|
||||||
@@ -8,17 +9,16 @@
|
|||||||
'tree-leaf': props.node.leaf
|
'tree-leaf': props.node.leaf
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
ref="container"
|
|
||||||
>
|
>
|
||||||
<div class="node-content">
|
<div class="node-content">
|
||||||
<span class="node-label">
|
<span class="node-label">
|
||||||
<slot name="before-label" :node="props.node"></slot>
|
<slot name="before-label" :node="props.node" />
|
||||||
<EditableText
|
<EditableText
|
||||||
:modelValue="node.label"
|
:model-value="node.label"
|
||||||
:isEditing="isEditing"
|
:is-editing="isEditing"
|
||||||
@edit="handleRename"
|
@edit="handleRename"
|
||||||
/>
|
/>
|
||||||
<slot name="after-label" :node="props.node"></slot>
|
<slot name="after-label" :node="props.node" />
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="showNodeBadgeText"
|
v-if="showNodeBadgeText"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div
|
<div
|
||||||
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||||
>
|
>
|
||||||
<slot name="actions" :node="props.node"></slot>
|
<slot name="actions" :node="props.node" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
|
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
|
||||||
<div :style="gridStyle">
|
<div :style="gridStyle">
|
||||||
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
|
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
|
||||||
<slot name="item" :item="item"> </slot>
|
<slot name="item" :item="item" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ describe('UrlInput', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ describe('UrlInput', () => {
|
|||||||
validateUrlFn: () => Promise.resolve(true)
|
validateUrlFn: () => Promise.resolve(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ describe('UrlInput', () => {
|
|||||||
validateUrlFn: () => Promise.resolve(false)
|
validateUrlFn: () => Promise.resolve(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
@@ -141,14 +141,14 @@ describe('UrlInput', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// Trigger multiple validations in quick succession
|
// Trigger multiple validations in quick succession
|
||||||
wrapper.find('.pi-spinner').trigger('click')
|
await wrapper.find('.pi-spinner').trigger('click')
|
||||||
wrapper.find('.pi-spinner').trigger('click')
|
await wrapper.find('.pi-spinner').trigger('click')
|
||||||
wrapper.find('.pi-spinner').trigger('click')
|
await wrapper.find('.pi-spinner').trigger('click')
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<component
|
<component
|
||||||
v-if="item.headerComponent"
|
|
||||||
:is="item.headerComponent"
|
:is="item.headerComponent"
|
||||||
|
v-if="item.headerComponent"
|
||||||
:id="item.key"
|
:id="item.key"
|
||||||
/>
|
/>
|
||||||
<h3 v-else :id="item.key">{{ item.title || ' ' }}</h3>
|
<h3 v-else :id="item.key">
|
||||||
|
{{ item.title || ' ' }}
|
||||||
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<component
|
<component
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
:maximized="item.dialogComponentProps.maximized"
|
:maximized="item.dialogComponentProps.maximized"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #footer v-if="item.footerComponent">
|
<template v-if="item.footerComponent" #footer>
|
||||||
<component :is="item.footerComponent" />
|
<component :is="item.footerComponent" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
43
src/components/dialog/content/ApiNodesSignInContent.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!-- Prompt user that the workflow contains API nodes that needs login to run -->
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4 max-w-96 h-110 p-2">
|
||||||
|
<div class="text-2xl font-medium mb-2">
|
||||||
|
{{ t('apiNodesSignInDialog.title') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-base mb-4">
|
||||||
|
{{ t('apiNodesSignInDialog.message') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ApiNodesCostBreakdown :nodes="apiNodes" :show-total="true" />
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<Button :label="t('g.learnMore')" link />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
:label="t('g.cancel')"
|
||||||
|
outlined
|
||||||
|
severity="secondary"
|
||||||
|
@click="onCancel?.()"
|
||||||
|
/>
|
||||||
|
<Button :label="t('g.login')" @click="onLogin?.()" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import ApiNodesCostBreakdown from '@/components/common/ApiNodesCostBreakdown.vue'
|
||||||
|
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { apiNodes, onLogin, onCancel } = defineProps<{
|
||||||
|
apiNodes: ApiNodeCost[]
|
||||||
|
onLogin?: () => void
|
||||||
|
onCancel?: () => void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
<section class="prompt-dialog-content flex flex-col gap-6 m-2 mt-4">
|
<section class="prompt-dialog-content flex flex-col gap-6 m-2 mt-4">
|
||||||
<span>{{ message }}</span>
|
<span>{{ message }}</span>
|
||||||
<ul v-if="itemList?.length" class="pl-4 m-0 flex flex-col gap-2">
|
<ul v-if="itemList?.length" class="pl-4 m-0 flex flex-col gap-2">
|
||||||
<li v-for="item of itemList" :key="item">{{ item }}</li>
|
<li v-for="item of itemList" :key="item">
|
||||||
|
{{ item }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Message
|
<Message
|
||||||
v-if="hint"
|
v-if="hint"
|
||||||
@@ -18,53 +20,53 @@
|
|||||||
:label="$t('g.cancel')"
|
:label="$t('g.cancel')"
|
||||||
icon="pi pi-undo"
|
icon="pi pi-undo"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
@click="onCancel"
|
|
||||||
autofocus
|
autofocus
|
||||||
|
@click="onCancel"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="type === 'default'"
|
v-if="type === 'default'"
|
||||||
:label="$t('g.confirm')"
|
:label="$t('g.confirm')"
|
||||||
severity="primary"
|
severity="primary"
|
||||||
@click="onConfirm"
|
|
||||||
icon="pi pi-check"
|
icon="pi pi-check"
|
||||||
|
@click="onConfirm"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="type === 'delete'"
|
v-else-if="type === 'delete'"
|
||||||
:label="$t('g.delete')"
|
:label="$t('g.delete')"
|
||||||
severity="danger"
|
severity="danger"
|
||||||
@click="onConfirm"
|
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
|
@click="onConfirm"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="type === 'overwrite'"
|
v-else-if="type === 'overwrite'"
|
||||||
:label="$t('g.overwrite')"
|
:label="$t('g.overwrite')"
|
||||||
severity="warn"
|
severity="warn"
|
||||||
@click="onConfirm"
|
|
||||||
icon="pi pi-save"
|
icon="pi pi-save"
|
||||||
|
@click="onConfirm"
|
||||||
/>
|
/>
|
||||||
<template v-else-if="type === 'dirtyClose'">
|
<template v-else-if="type === 'dirtyClose'">
|
||||||
<Button
|
<Button
|
||||||
:label="$t('g.no')"
|
:label="$t('g.no')"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
@click="onDeny"
|
|
||||||
icon="pi pi-times"
|
icon="pi pi-times"
|
||||||
|
@click="onDeny"
|
||||||
/>
|
/>
|
||||||
<Button :label="$t('g.save')" @click="onConfirm" icon="pi pi-save" />
|
<Button :label="$t('g.save')" icon="pi pi-save" @click="onConfirm" />
|
||||||
</template>
|
</template>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="type === 'reinstall'"
|
v-else-if="type === 'reinstall'"
|
||||||
:label="$t('desktopMenu.reinstall')"
|
:label="$t('desktopMenu.reinstall')"
|
||||||
severity="warn"
|
severity="warn"
|
||||||
@click="onConfirm"
|
|
||||||
icon="pi pi-eraser"
|
icon="pi pi-eraser"
|
||||||
|
@click="onConfirm"
|
||||||
/>
|
/>
|
||||||
<!-- Invalid - just show a close button. -->
|
<!-- Invalid - just show a close button. -->
|
||||||
<Button
|
<Button
|
||||||
v-else
|
v-else
|
||||||
:label="$t('g.close')"
|
:label="$t('g.close')"
|
||||||
severity="primary"
|
severity="primary"
|
||||||
@click="onCancel"
|
|
||||||
icon="pi pi-times"
|
icon="pi pi-times"
|
||||||
|
@click="onCancel"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,80 +1,159 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="error-dialog-content flex flex-col gap-4">
|
<div class="comfy-error-report flex flex-col gap-4">
|
||||||
<NoResultsPlaceholder
|
<NoResultsPlaceholder
|
||||||
class="pb-0"
|
class="pb-0"
|
||||||
icon="pi pi-exclamation-circle"
|
icon="pi pi-exclamation-circle"
|
||||||
:title="title"
|
:title="title"
|
||||||
:message="errorMessage"
|
:message="error.exceptionMessage"
|
||||||
/>
|
/>
|
||||||
<pre
|
<template v-if="error.extensionFile">
|
||||||
class="stack-trace p-5 text-neutral-400 text-xs max-h-[50vh] overflow-auto bg-black/20"
|
|
||||||
>
|
|
||||||
{{ stackTrace }}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<template v-if="extensionFile">
|
|
||||||
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
|
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="font-bold">{{ extensionFile }}</span>
|
<span class="font-bold">{{ error.extensionFile }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Button
|
<div class="flex gap-2 justify-center">
|
||||||
v-show="!sendReportOpen"
|
<Button
|
||||||
text
|
v-show="!reportOpen"
|
||||||
fluid
|
text
|
||||||
:label="$t('issueReport.helpFix')"
|
:label="$t('g.showReport')"
|
||||||
@click="showSendReport"
|
@click="showReport"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-show="!sendReportOpen"
|
||||||
|
text
|
||||||
|
:label="$t('issueReport.helpFix')"
|
||||||
|
@click="showSendReport"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template v-if="reportOpen">
|
||||||
|
<Divider />
|
||||||
|
<ScrollPanel class="w-full h-[400px] max-w-[80vw]">
|
||||||
|
<pre class="whitespace-pre-wrap break-words">{{ reportContent }}</pre>
|
||||||
|
</ScrollPanel>
|
||||||
|
<Divider />
|
||||||
|
</template>
|
||||||
<ReportIssuePanel
|
<ReportIssuePanel
|
||||||
v-if="sendReportOpen"
|
v-if="sendReportOpen"
|
||||||
:error-type="errorType"
|
:title="$t('issueReport.submitErrorReport')"
|
||||||
:extra-fields="[
|
:error-type="error.reportType ?? 'unknownError'"
|
||||||
{
|
:extra-fields="[stackTraceField]"
|
||||||
label: t('issueReport.stackTrace'),
|
|
||||||
value: 'StackTrace',
|
|
||||||
optIn: true,
|
|
||||||
getData: () => stackTrace
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
:tags="{
|
:tags="{
|
||||||
exceptionMessage: errorMessage,
|
exceptionMessage: error.exceptionMessage,
|
||||||
extensionFile: extensionFile ?? 'UNKNOWN'
|
nodeType: error.nodeType ?? 'UNKNOWN'
|
||||||
}"
|
}"
|
||||||
:title="t('issueReport.submitErrorReport')"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="flex gap-4 justify-end">
|
||||||
|
<FindIssueButton
|
||||||
|
:error-message="error.exceptionMessage"
|
||||||
|
:repo-owner="repoOwner"
|
||||||
|
:repo-name="repoName"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="reportOpen"
|
||||||
|
:label="$t('g.copyToClipboard')"
|
||||||
|
icon="pi pi-copy"
|
||||||
|
@click="copyReportToClipboard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { computed, ref } from 'vue'
|
import Divider from 'primevue/divider'
|
||||||
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
|
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||||
|
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||||
|
import type { ReportField } from '@/types/issueReportTypes'
|
||||||
|
import {
|
||||||
|
type ErrorReportData,
|
||||||
|
generateErrorReport
|
||||||
|
} from '@/utils/errorReportUtil'
|
||||||
|
|
||||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { error } = defineProps<{
|
||||||
const {
|
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
|
||||||
title: _title,
|
/**
|
||||||
errorMessage,
|
* The type of error report to submit.
|
||||||
stackTrace: _stackTrace,
|
* @default 'unknownError'
|
||||||
extensionFile,
|
*/
|
||||||
errorType = 'frontendError'
|
reportType?: string
|
||||||
} = defineProps<{
|
/**
|
||||||
title?: string
|
* The file name of the extension that caused the error.
|
||||||
errorMessage: string
|
*/
|
||||||
stackTrace?: string
|
extensionFile?: string
|
||||||
extensionFile?: string
|
}
|
||||||
errorType?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const title = computed(() => _title ?? t('errorDialog.defaultTitle'))
|
const repoOwner = 'comfyanonymous'
|
||||||
const stackTrace = computed(() => _stackTrace ?? t('errorDialog.noStackTrace'))
|
const repoName = 'ComfyUI'
|
||||||
|
const reportContent = ref('')
|
||||||
|
const reportOpen = ref(false)
|
||||||
|
const showReport = () => {
|
||||||
|
reportOpen.value = true
|
||||||
|
}
|
||||||
const sendReportOpen = ref(false)
|
const sendReportOpen = ref(false)
|
||||||
function showSendReport() {
|
const showSendReport = () => {
|
||||||
sendReportOpen.value = true
|
sendReportOpen.value = true
|
||||||
}
|
}
|
||||||
|
const toast = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const systemStatsStore = useSystemStatsStore()
|
||||||
|
|
||||||
|
const title = computed<string>(
|
||||||
|
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
|
||||||
|
)
|
||||||
|
|
||||||
|
const stackTraceField = computed<ReportField>(() => {
|
||||||
|
return {
|
||||||
|
label: t('issueReport.stackTrace'),
|
||||||
|
value: 'StackTrace',
|
||||||
|
optIn: true,
|
||||||
|
getData: () => error.traceback
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!systemStatsStore.systemStats) {
|
||||||
|
await systemStatsStore.fetchSystemStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [logs] = await Promise.all([api.getLogs()])
|
||||||
|
|
||||||
|
reportContent.value = generateErrorReport({
|
||||||
|
systemStats: systemStatsStore.systemStats!,
|
||||||
|
serverLogs: logs,
|
||||||
|
workflow: app.graph.serialize(),
|
||||||
|
exceptionType: error.exceptionType,
|
||||||
|
exceptionMessage: error.exceptionMessage,
|
||||||
|
traceback: error.traceback,
|
||||||
|
nodeId: error.nodeId,
|
||||||
|
nodeType: error.nodeType
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching logs:', error)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('g.error'),
|
||||||
|
detail: t('toastMessages.failedToFetchLogs'),
|
||||||
|
life: 5000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { copyToClipboard } = useCopyToClipboard()
|
||||||
|
const copyReportToClipboard = async () => {
|
||||||
|
await copyToClipboard(reportContent.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NoResultsPlaceholder
|
|
||||||
icon="pi pi-exclamation-circle"
|
|
||||||
:title="props.error.node_type"
|
|
||||||
:message="props.error.exception_message"
|
|
||||||
/>
|
|
||||||
<div class="comfy-error-report">
|
|
||||||
<div class="flex gap-2 justify-center">
|
|
||||||
<Button
|
|
||||||
v-show="!reportOpen"
|
|
||||||
text
|
|
||||||
:label="$t('g.showReport')"
|
|
||||||
@click="showReport"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
v-show="!sendReportOpen"
|
|
||||||
text
|
|
||||||
:label="$t('issueReport.helpFix')"
|
|
||||||
@click="showSendReport"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template v-if="reportOpen">
|
|
||||||
<Divider />
|
|
||||||
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
|
|
||||||
<pre class="wrapper-pre">{{ reportContent }}</pre>
|
|
||||||
</ScrollPanel>
|
|
||||||
<Divider />
|
|
||||||
</template>
|
|
||||||
<ReportIssuePanel
|
|
||||||
v-if="sendReportOpen"
|
|
||||||
:title="$t('issueReport.submitErrorReport')"
|
|
||||||
error-type="graphExecutionError"
|
|
||||||
:extra-fields="[stackTraceField]"
|
|
||||||
:tags="{
|
|
||||||
exceptionMessage: props.error.exception_message,
|
|
||||||
nodeType: props.error.node_type
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div class="action-container">
|
|
||||||
<FindIssueButton
|
|
||||||
:errorMessage="props.error.exception_message"
|
|
||||||
:repoOwner="repoOwner"
|
|
||||||
:repoName="repoName"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
v-if="reportOpen"
|
|
||||||
:label="$t('g.copyToClipboard')"
|
|
||||||
icon="pi pi-copy"
|
|
||||||
@click="copyReportToClipboard"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Button from 'primevue/button'
|
|
||||||
import Divider from 'primevue/divider'
|
|
||||||
import ScrollPanel from 'primevue/scrollpanel'
|
|
||||||
import { useToast } from 'primevue/usetoast'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
|
||||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
|
||||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
|
||||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/schemas/apiSchema'
|
|
||||||
import { api } from '@/scripts/api'
|
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
import type { ReportField } from '@/types/issueReportTypes'
|
|
||||||
|
|
||||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
error: ExecutionErrorWsMessage
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const repoOwner = 'comfyanonymous'
|
|
||||||
const repoName = 'ComfyUI'
|
|
||||||
const reportContent = ref('')
|
|
||||||
const reportOpen = ref(false)
|
|
||||||
const showReport = () => {
|
|
||||||
reportOpen.value = true
|
|
||||||
}
|
|
||||||
const sendReportOpen = ref(false)
|
|
||||||
const showSendReport = () => {
|
|
||||||
sendReportOpen.value = true
|
|
||||||
}
|
|
||||||
const toast = useToast()
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const stackTraceField = computed<ReportField>(() => {
|
|
||||||
return {
|
|
||||||
label: t('issueReport.stackTrace'),
|
|
||||||
value: 'StackTrace',
|
|
||||||
optIn: true,
|
|
||||||
getData: () => props.error.traceback?.join('\n')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const [systemStats, logs] = await Promise.all([
|
|
||||||
api.getSystemStats(),
|
|
||||||
api.getLogs()
|
|
||||||
])
|
|
||||||
generateReport(systemStats, logs)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching system stats or logs:', error)
|
|
||||||
toast.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'Failed to fetch system information',
|
|
||||||
life: 5000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const generateReport = (systemStats: SystemStats, logs: string) => {
|
|
||||||
// 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 ID:** ${props.error.node_id}
|
|
||||||
- **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
|
|
||||||
- **ComfyUI Version:** ${systemStats.system.comfyui_version}
|
|
||||||
- **Arguments:** ${systemStats.system.argv.join(' ')}
|
|
||||||
- **OS:** ${systemStats.system.os}
|
|
||||||
- **Python Version:** ${systemStats.system.python_version}
|
|
||||||
- **Embedded Python:** ${systemStats.system.embedded_python}
|
|
||||||
- **PyTorch Version:** ${systemStats.system.pytorch_version}
|
|
||||||
## 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')}
|
|
||||||
## Logs
|
|
||||||
\`\`\`
|
|
||||||
${logs}
|
|
||||||
\`\`\`
|
|
||||||
## 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 { copyToClipboard } = useCopyToClipboard()
|
|
||||||
const copyReportToClipboard = async () => {
|
|
||||||
await copyToClipboard(reportContent.value)
|
|
||||||
}
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<header class="flex flex-col items-center w-full">
|
<header class="flex flex-col items-center w-full">
|
||||||
<h2 id="issue-report-title" class="text-4xl">{{ title }}</h2>
|
<h2 id="issue-report-title" class="text-4xl">
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
|
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
/>
|
/>
|
||||||
<ListBox
|
<ListBox
|
||||||
:options="uniqueNodes"
|
:options="uniqueNodes"
|
||||||
optionLabel="label"
|
option-label="label"
|
||||||
scrollHeight="100%"
|
scroll-height="100%"
|
||||||
class="comfy-missing-nodes"
|
class="comfy-missing-nodes"
|
||||||
:pt="{
|
:pt="{
|
||||||
list: { class: 'border-none' }
|
list: { class: 'border-none' }
|
||||||
@@ -22,14 +22,17 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
<Button
|
<Button
|
||||||
v-if="slotProps.option.action"
|
v-if="slotProps.option.action"
|
||||||
@click="slotProps.option.action.callback"
|
|
||||||
:label="slotProps.option.action.text"
|
:label="slotProps.option.action.text"
|
||||||
size="small"
|
size="small"
|
||||||
outlined
|
outlined
|
||||||
|
@click="slotProps.option.action.callback"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
<div class="flex justify-end py-3">
|
||||||
|
<Button label="Open Manager" size="small" outlined @click="openManager" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -38,7 +41,9 @@ import ListBox from 'primevue/listbox'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import type { MissingNodeType } from '@/types/comfy'
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
|
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
missingNodeTypes: MissingNodeType[]
|
missingNodeTypes: MissingNodeType[]
|
||||||
@@ -64,6 +69,12 @@ const uniqueNodes = computed(() => {
|
|||||||
return { label: node }
|
return { label: node }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const openManager = () => {
|
||||||
|
useDialogService().showManagerDialog({
|
||||||
|
initialTab: ManagerTab.Missing
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -129,9 +129,12 @@ const missingModels = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(async () => {
|
||||||
if (doNotAskAgain.value) {
|
if (doNotAskAgain.value) {
|
||||||
useSettingStore().set('Comfy.Workflow.ShowMissingModelsWarning', false)
|
await useSettingStore().set(
|
||||||
|
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,13 +4,15 @@
|
|||||||
<InputText
|
<InputText
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
|
autofocus
|
||||||
@keyup.enter="onConfirm"
|
@keyup.enter="onConfirm"
|
||||||
@focus="selectAllText"
|
@focus="selectAllText"
|
||||||
autofocus
|
|
||||||
/>
|
/>
|
||||||
<label>{{ message }}</label>
|
<label>{{ message }}</label>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
<Button @click="onConfirm">{{ $t('g.confirm') }}</Button>
|
<Button @click="onConfirm">
|
||||||
|
{{ $t('g.confirm') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-48 2xl:w-64">
|
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-48 2xl:w-64">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
class="settings-search-box w-full mb-2"
|
|
||||||
v-model:modelValue="searchQuery"
|
v-model:modelValue="searchQuery"
|
||||||
@search="handleSearch"
|
class="settings-search-box w-full mb-2"
|
||||||
:placeholder="$t('g.searchSettings') + '...'"
|
:placeholder="$t('g.searchSettings') + '...'"
|
||||||
:debounce-time="128"
|
:debounce-time="128"
|
||||||
|
@search="handleSearch"
|
||||||
/>
|
/>
|
||||||
<Listbox
|
<Listbox
|
||||||
v-model="activeCategory"
|
v-model="activeCategory"
|
||||||
:options="categories"
|
:options="categories"
|
||||||
optionLabel="translatedLabel"
|
option-label="translatedLabel"
|
||||||
scrollHeight="100%"
|
scroll-height="100%"
|
||||||
:optionDisabled="
|
:option-disabled="
|
||||||
(option: SettingTreeNode) =>
|
(option: SettingTreeNode) =>
|
||||||
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
|
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
|
||||||
"
|
"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<Tabs :value="tabValue" :lazy="true" class="settings-content h-full w-full">
|
<Tabs :value="tabValue" :lazy="true" class="settings-content h-full w-full">
|
||||||
<TabPanels class="settings-tab-panels h-full w-full pr-0">
|
<TabPanels class="settings-tab-panels h-full w-full pr-0">
|
||||||
<PanelTemplate value="Search Results">
|
<PanelTemplate value="Search Results">
|
||||||
<SettingsPanel :settingGroups="searchResults" />
|
<SettingsPanel :setting-groups="searchResults" />
|
||||||
</PanelTemplate>
|
</PanelTemplate>
|
||||||
|
|
||||||
<PanelTemplate
|
<PanelTemplate
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
|
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
|
||||||
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
|
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
|
||||||
</template>
|
</template>
|
||||||
<SettingsPanel :settingGroups="sortedGroups(category)" />
|
<SettingsPanel :setting-groups="sortedGroups(category)" />
|
||||||
</PanelTemplate>
|
</PanelTemplate>
|
||||||
|
|
||||||
<AboutPanel />
|
<AboutPanel />
|
||||||
@@ -293,6 +293,10 @@ watch(activeCategory, (_, oldValue) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.settings-container {
|
.settings-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
124
src/components/dialog/content/SignInContent.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-96 p-2">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-col gap-4 mb-8">
|
||||||
|
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||||
|
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-base my-0">
|
||||||
|
<span class="text-muted">{{
|
||||||
|
isSignIn
|
||||||
|
? t('auth.login.newUser')
|
||||||
|
: t('auth.signup.alreadyHaveAccount')
|
||||||
|
}}</span>
|
||||||
|
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
|
||||||
|
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||||
|
}}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||||
|
<SignUpForm v-else @submit="signInWithEmail" />
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<Divider align="center" layout="horizontal" class="my-8">
|
||||||
|
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<!-- Social Login Buttons -->
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
class="h-10"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="signInWithGoogle"
|
||||||
|
>
|
||||||
|
<i class="pi pi-google mr-2"></i>
|
||||||
|
{{
|
||||||
|
isSignIn
|
||||||
|
? t('auth.login.loginWithGoogle')
|
||||||
|
: t('auth.signup.signUpWithGoogle')
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
class="h-10"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="signInWithGithub"
|
||||||
|
>
|
||||||
|
<i class="pi pi-github mr-2"></i>
|
||||||
|
{{
|
||||||
|
isSignIn
|
||||||
|
? t('auth.login.loginWithGithub')
|
||||||
|
: t('auth.signup.signUpWithGithub')
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<!-- Terms -->
|
||||||
|
<p class="text-xs text-muted mt-8">
|
||||||
|
{{ t('auth.login.termsText') }}
|
||||||
|
<span class="text-blue-500 cursor-pointer">{{
|
||||||
|
t('auth.login.termsLink')
|
||||||
|
}}</span>
|
||||||
|
{{ t('auth.login.andText') }}
|
||||||
|
<span class="text-blue-500 cursor-pointer">{{
|
||||||
|
t('auth.login.privacyLink')
|
||||||
|
}}</span
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||||
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||||
|
|
||||||
|
import SignInForm from './signin/SignInForm.vue'
|
||||||
|
import SignUpForm from './signin/SignUpForm.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { onSuccess } = defineProps<{
|
||||||
|
onSuccess: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const firebaseAuthStore = useFirebaseAuthStore()
|
||||||
|
|
||||||
|
const isSignIn = ref(true)
|
||||||
|
const toggleState = () => {
|
||||||
|
isSignIn.value = !isSignIn.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const signInWithGoogle = () => {
|
||||||
|
// Implement Google login
|
||||||
|
console.log(isSignIn.value)
|
||||||
|
console.log('Google login clicked')
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
const signInWithGithub = () => {
|
||||||
|
// Implement Github login
|
||||||
|
console.log(isSignIn.value)
|
||||||
|
console.log('Github login clicked')
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
const signInWithEmail = async (values: SignInData | SignUpData) => {
|
||||||
|
const { email, password } = values
|
||||||
|
if (isSignIn.value) {
|
||||||
|
await firebaseAuthStore.login(email, password)
|
||||||
|
} else {
|
||||||
|
await firebaseAuthStore.register(email, password)
|
||||||
|
}
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
@click="openGitHubIssues"
|
|
||||||
:label="$t('g.findIssues')"
|
:label="$t('g.findIssues')"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
icon="pi pi-github"
|
icon="pi pi-github"
|
||||||
>
|
@click="openGitHubIssues"
|
||||||
</Button>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<Form
|
<Form
|
||||||
v-slot="$form"
|
v-slot="$form"
|
||||||
@submit="submit"
|
|
||||||
:resolver="zodResolver(issueReportSchema)"
|
:resolver="zodResolver(issueReportSchema)"
|
||||||
|
@submit="submit"
|
||||||
>
|
>
|
||||||
<Panel :pt="$attrs.pt as any">
|
<Panel :pt="$attrs.pt as any">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -33,15 +33,15 @@
|
|||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-bind="$field"
|
v-bind="$field"
|
||||||
:inputId="field.value"
|
|
||||||
:value="field.value"
|
|
||||||
v-model="selection"
|
v-model="selection"
|
||||||
|
:input-id="field.value"
|
||||||
|
:value="field.value"
|
||||||
/>
|
/>
|
||||||
<label :for="field.value">{{ field.label }}</label>
|
<label :for="field.value">{{ field.label }}</label>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormField class="mb-4" v-slot="$field" name="details">
|
<FormField v-slot="$field" class="mb-4" name="details">
|
||||||
<Textarea
|
<Textarea
|
||||||
v-bind="$field"
|
v-bind="$field"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -83,9 +83,9 @@
|
|||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-bind="$field"
|
v-bind="$field"
|
||||||
:inputId="checkbox.value"
|
|
||||||
:value="checkbox.value"
|
|
||||||
v-model="contactPrefs"
|
v-model="contactPrefs"
|
||||||
|
:input-id="checkbox.value"
|
||||||
|
:value="checkbox.value"
|
||||||
:disabled="
|
:disabled="
|
||||||
$form.contactInfo?.error || !$form.contactInfo?.value
|
$form.contactInfo?.error || !$form.contactInfo?.value
|
||||||
"
|
"
|
||||||
@@ -101,7 +101,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||||
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
|
|
||||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||||
import type { CaptureContext, User } from '@sentry/core'
|
import type { CaptureContext, User } from '@sentry/core'
|
||||||
import { captureMessage } from '@sentry/core'
|
import { captureMessage } from '@sentry/core'
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
<div class="flex flex-1 relative overflow-hidden">
|
<div class="flex flex-1 relative overflow-hidden">
|
||||||
<ManagerNavSidebar
|
<ManagerNavSidebar
|
||||||
v-if="isSideNavOpen"
|
v-if="isSideNavOpen"
|
||||||
:tabs="tabs"
|
|
||||||
v-model:selectedTab="selectedTab"
|
v-model:selectedTab="selectedTab"
|
||||||
|
:tabs="tabs"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex-1 overflow-auto pr-80"
|
class="flex-1 overflow-auto pr-80"
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
<RegistrySearchBar
|
<RegistrySearchBar
|
||||||
v-model:searchQuery="searchQuery"
|
v-model:searchQuery="searchQuery"
|
||||||
v-model:searchMode="searchMode"
|
v-model:searchMode="searchMode"
|
||||||
:searchResults="searchResults"
|
v-model:sortField="sortField"
|
||||||
|
:search-results="searchResults"
|
||||||
:suggestions="suggestions"
|
:suggestions="suggestions"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
@@ -56,16 +57,16 @@
|
|||||||
<VirtualGrid
|
<VirtualGrid
|
||||||
:items="resultsWithKeys"
|
:items="resultsWithKeys"
|
||||||
:buffer-rows="3"
|
:buffer-rows="3"
|
||||||
:gridStyle="GRID_STYLE"
|
:grid-style="GRID_STYLE"
|
||||||
@approach-end="onApproachEnd"
|
@approach-end="onApproachEnd"
|
||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<PackCard
|
<PackCard
|
||||||
@click.stop="(event) => selectNodePack(item, event)"
|
|
||||||
:node-pack="item"
|
:node-pack="item"
|
||||||
:is-selected="
|
:is-selected="
|
||||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||||
"
|
"
|
||||||
|
@click.stop="(event) => selectNodePack(item, event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VirtualGrid>
|
</VirtualGrid>
|
||||||
@@ -107,19 +108,18 @@ import RegistrySearchBar from '@/components/dialog/content/manager/registrySearc
|
|||||||
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
|
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
|
||||||
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
||||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||||
|
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||||
|
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||||
import { components } from '@/types/comfyRegistryTypes'
|
import { components } from '@/types/comfyRegistryTypes'
|
||||||
|
|
||||||
enum ManagerTab {
|
const { initialTab = ManagerTab.All } = defineProps<{
|
||||||
All = 'all',
|
initialTab: ManagerTab
|
||||||
Installed = 'installed',
|
}>()
|
||||||
Workflow = 'workflow',
|
|
||||||
Missing = 'missing'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const comfyManagerStore = useComfyManagerStore()
|
const comfyManagerStore = useComfyManagerStore()
|
||||||
@@ -150,9 +150,16 @@ const tabs = ref<TabItem[]>([
|
|||||||
id: ManagerTab.Missing,
|
id: ManagerTab.Missing,
|
||||||
label: t('g.missing'),
|
label: t('g.missing'),
|
||||||
icon: 'pi-exclamation-circle'
|
icon: 'pi-exclamation-circle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ManagerTab.UpdateAvailable,
|
||||||
|
label: t('g.updateAvailable'),
|
||||||
|
icon: 'pi-sync'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
const selectedTab = ref<TabItem>(tabs.value[0])
|
const selectedTab = ref<TabItem>(
|
||||||
|
tabs.value.find((tab) => tab.id === initialTab) || tabs.value[0]
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -160,6 +167,7 @@ const {
|
|||||||
isLoading: isSearchLoading,
|
isLoading: isSearchLoading,
|
||||||
searchResults,
|
searchResults,
|
||||||
searchMode,
|
searchMode,
|
||||||
|
sortField,
|
||||||
suggestions
|
suggestions
|
||||||
} = useRegistrySearch()
|
} = useRegistrySearch()
|
||||||
pageNumber.value = 0
|
pageNumber.value = 0
|
||||||
@@ -178,90 +186,140 @@ const {
|
|||||||
startFetchInstalled,
|
startFetchInstalled,
|
||||||
filterInstalledPack,
|
filterInstalledPack,
|
||||||
installedPacks,
|
installedPacks,
|
||||||
isLoading: isLoadingInstalled
|
isLoading: isLoadingInstalled,
|
||||||
|
isReady: installedPacksReady
|
||||||
} = useInstalledPacks()
|
} = useInstalledPacks()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
startFetchWorkflowPacks,
|
startFetchWorkflowPacks,
|
||||||
filterWorkflowPack,
|
filterWorkflowPack,
|
||||||
workflowPacks,
|
workflowPacks,
|
||||||
isLoading: isLoadingWorkflow
|
isLoading: isLoadingWorkflow,
|
||||||
|
isReady: workflowPacksReady
|
||||||
} = useWorkflowPacks()
|
} = useWorkflowPacks()
|
||||||
|
|
||||||
const getInstalledResults = () => {
|
|
||||||
if (isEmptySearch.value) {
|
|
||||||
startFetchInstalled()
|
|
||||||
return installedPacks.value
|
|
||||||
} else {
|
|
||||||
return filterInstalledPack(searchResults.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInWorkflowResults = () => {
|
|
||||||
if (isEmptySearch.value) {
|
|
||||||
startFetchWorkflowPacks()
|
|
||||||
return workflowPacks.value
|
|
||||||
} else {
|
|
||||||
return filterWorkflowPack(searchResults.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
||||||
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
||||||
|
|
||||||
const setMissingPacks = () => {
|
const isUpdateAvailableTab = computed(
|
||||||
displayPacks.value = filterMissingPacks(workflowPacks.value)
|
() => selectedTab.value?.id === ManagerTab.UpdateAvailable
|
||||||
}
|
)
|
||||||
|
const isInstalledTab = computed(
|
||||||
|
() => selectedTab.value?.id === ManagerTab.Installed
|
||||||
|
)
|
||||||
|
const isMissingTab = computed(
|
||||||
|
() => selectedTab.value?.id === ManagerTab.Missing
|
||||||
|
)
|
||||||
|
const isWorkflowTab = computed(
|
||||||
|
() => selectedTab.value?.id === ManagerTab.Workflow
|
||||||
|
)
|
||||||
|
const isAllTab = computed(() => selectedTab.value?.id === ManagerTab.All)
|
||||||
|
|
||||||
const getMissingPacks = () => {
|
const isOutdatedPack = (pack: components['schemas']['Node']) => {
|
||||||
if (isEmptySearch.value) {
|
const { isUpdateAvailable } = usePackUpdateStatus(pack)
|
||||||
startFetchWorkflowPacks()
|
return isUpdateAvailable.value === true
|
||||||
whenever(() => workflowPacks.value.length, setMissingPacks, {
|
|
||||||
immediate: true,
|
|
||||||
once: true
|
|
||||||
})
|
|
||||||
return filterMissingPacks(workflowPacks.value)
|
|
||||||
} else {
|
|
||||||
return filterMissingPacks(filterWorkflowPack(searchResults.value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
|
||||||
|
packs.filter(isOutdatedPack)
|
||||||
|
|
||||||
const onTabChange = () => {
|
watch(
|
||||||
switch (selectedTab.value?.id) {
|
[isUpdateAvailableTab, installedPacks],
|
||||||
case ManagerTab.Installed:
|
async () => {
|
||||||
displayPacks.value = getInstalledResults()
|
if (!isUpdateAvailableTab.value) return
|
||||||
break
|
|
||||||
case ManagerTab.Workflow:
|
if (!isEmptySearch.value) {
|
||||||
displayPacks.value = getInWorkflowResults()
|
displayPacks.value = filterOutdatedPacks(installedPacks.value)
|
||||||
break
|
} else if (!installedPacks.value.length) {
|
||||||
case ManagerTab.Missing:
|
await startFetchInstalled()
|
||||||
displayPacks.value = getMissingPacks()
|
} else {
|
||||||
break
|
displayPacks.value = filterOutdatedPacks(installedPacks.value)
|
||||||
default:
|
}
|
||||||
displayPacks.value = searchResults.value
|
},
|
||||||
}
|
{ immediate: true }
|
||||||
}
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[isInstalledTab, installedPacks],
|
||||||
|
async () => {
|
||||||
|
if (!isInstalledTab.value) return
|
||||||
|
|
||||||
|
if (!isEmptySearch.value) {
|
||||||
|
displayPacks.value = filterInstalledPack(searchResults.value)
|
||||||
|
} else if (
|
||||||
|
!installedPacks.value.length &&
|
||||||
|
!installedPacksReady.value &&
|
||||||
|
!isLoadingInstalled.value
|
||||||
|
) {
|
||||||
|
await startFetchInstalled()
|
||||||
|
} else {
|
||||||
|
displayPacks.value = installedPacks.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[isMissingTab, isWorkflowTab, workflowPacks, installedPacks],
|
||||||
|
async () => {
|
||||||
|
if (!isWorkflowTab.value && !isMissingTab.value) return
|
||||||
|
|
||||||
|
if (!isEmptySearch.value) {
|
||||||
|
displayPacks.value = isMissingTab.value
|
||||||
|
? filterMissingPacks(filterWorkflowPack(searchResults.value))
|
||||||
|
: filterWorkflowPack(searchResults.value)
|
||||||
|
} else if (
|
||||||
|
!workflowPacks.value.length &&
|
||||||
|
!isLoadingWorkflow.value &&
|
||||||
|
!workflowPacksReady.value
|
||||||
|
) {
|
||||||
|
await startFetchWorkflowPacks()
|
||||||
|
if (isMissingTab.value) {
|
||||||
|
await startFetchInstalled()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayPacks.value = isMissingTab.value
|
||||||
|
? filterMissingPacks(workflowPacks.value)
|
||||||
|
: workflowPacks.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch([isAllTab, searchResults], () => {
|
||||||
|
if (!isAllTab.value) return
|
||||||
|
displayPacks.value = searchResults.value
|
||||||
|
})
|
||||||
|
|
||||||
const onResultsChange = () => {
|
const onResultsChange = () => {
|
||||||
switch (selectedTab.value?.id) {
|
switch (selectedTab.value?.id) {
|
||||||
case ManagerTab.Installed:
|
case ManagerTab.Installed:
|
||||||
displayPacks.value = filterInstalledPack(searchResults.value)
|
displayPacks.value = isEmptySearch.value
|
||||||
|
? installedPacks.value
|
||||||
|
: filterInstalledPack(searchResults.value)
|
||||||
break
|
break
|
||||||
case ManagerTab.Workflow:
|
case ManagerTab.Workflow:
|
||||||
displayPacks.value = filterWorkflowPack(searchResults.value)
|
displayPacks.value = isEmptySearch.value
|
||||||
|
? workflowPacks.value
|
||||||
|
: filterWorkflowPack(searchResults.value)
|
||||||
break
|
break
|
||||||
case ManagerTab.Missing:
|
case ManagerTab.Missing:
|
||||||
displayPacks.value = filterMissingPacks(
|
if (!isEmptySearch.value) {
|
||||||
filterWorkflowPack(searchResults.value)
|
displayPacks.value = filterMissingPacks(
|
||||||
)
|
filterWorkflowPack(searchResults.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ManagerTab.UpdateAvailable:
|
||||||
|
displayPacks.value = isEmptySearch.value
|
||||||
|
? filterOutdatedPacks(installedPacks.value)
|
||||||
|
: filterOutdatedPacks(searchResults.value)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
displayPacks.value = searchResults.value
|
displayPacks.value = searchResults.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
whenever(selectedTab, onTabChange)
|
watch(searchResults, onResultsChange, { flush: 'post' })
|
||||||
watch(searchResults, onResultsChange, { flush: 'pre' })
|
|
||||||
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
|
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
|
||||||
|
|
||||||
const isLoading = computed(() => {
|
const isLoading = computed(() => {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<Listbox
|
<Listbox
|
||||||
v-model="selectedTab"
|
v-model="selectedTab"
|
||||||
:options="tabs"
|
:options="tabs"
|
||||||
optionLabel="label"
|
option-label="label"
|
||||||
listStyle="max-height:unset"
|
list-style="max-height:unset"
|
||||||
class="w-full border-0 bg-transparent shadow-none"
|
class="w-full border-0 bg-transparent shadow-none"
|
||||||
:pt="{
|
:pt="{
|
||||||
list: { class: 'p-5' },
|
list: { class: 'p-5' },
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
>
|
>
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
<div class="text-left flex items-center">
|
<div class="text-left flex items-center">
|
||||||
<i :class="['pi', slotProps.option.icon, 'mr-3']"></i>
|
<i :class="['pi', slotProps.option.icon, 'mr-3']" />
|
||||||
<span class="text-lg">{{ slotProps.option.label }}</span>
|
<span class="text-lg">{{ slotProps.option.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<i
|
<i
|
||||||
class="pi pi-circle-fill mr-1.5 text-[0.6rem] p-0"
|
class="pi pi-circle-fill mr-1.5 text-[0.6rem] p-0"
|
||||||
:style="{ opacity: 0.8 }"
|
:style="{ opacity: 0.8 }"
|
||||||
></i>
|
/>
|
||||||
{{ $t(`manager.status.${statusLabel}`) }}
|
{{ $t(`manager.status.${statusLabel}`) }}
|
||||||
</Message>
|
</Message>
|
||||||
</template>
|
</template>
|
||||||
@@ -20,15 +20,16 @@ import Message from 'primevue/message'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { components } from '@/types/comfyRegistryTypes'
|
import { components } from '@/types/comfyRegistryTypes'
|
||||||
import { VueSeverity } from '@/types/primeVueTypes'
|
|
||||||
|
|
||||||
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
||||||
type PackStatus = components['schemas']['NodeStatus']
|
type PackStatus = components['schemas']['NodeStatus']
|
||||||
type Status = PackVersionStatus | PackStatus
|
type Status = PackVersionStatus | PackStatus
|
||||||
|
|
||||||
|
type MessageProps = InstanceType<typeof Message>['$props']
|
||||||
|
type MessageSeverity = MessageProps['severity']
|
||||||
type StatusProps = {
|
type StatusProps = {
|
||||||
label: string
|
label: string
|
||||||
severity: VueSeverity
|
severity: MessageSeverity
|
||||||
}
|
}
|
||||||
|
|
||||||
const { statusType } = defineProps<{
|
const { statusType } = defineProps<{
|
||||||
@@ -46,7 +47,7 @@ const statusPropsMap: Record<Status, StatusProps> = {
|
|||||||
},
|
},
|
||||||
NodeStatusBanned: {
|
NodeStatusBanned: {
|
||||||
label: 'banned',
|
label: 'banned',
|
||||||
severity: 'danger'
|
severity: 'error'
|
||||||
},
|
},
|
||||||
NodeVersionStatusActive: {
|
NodeVersionStatusActive: {
|
||||||
label: 'active',
|
label: 'active',
|
||||||
@@ -62,11 +63,11 @@ const statusPropsMap: Record<Status, StatusProps> = {
|
|||||||
},
|
},
|
||||||
NodeVersionStatusFlagged: {
|
NodeVersionStatusFlagged: {
|
||||||
label: 'flagged',
|
label: 'flagged',
|
||||||
severity: 'danger'
|
severity: 'error'
|
||||||
},
|
},
|
||||||
NodeVersionStatusBanned: {
|
NodeVersionStatusBanned: {
|
||||||
label: 'banned',
|
label: 'banned',
|
||||||
severity: 'danger'
|
severity: 'error'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ import PackVersionSelectorPopover from '@/components/dialog/content/manager/Pack
|
|||||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||||
import { components } from '@/types/comfyRegistryTypes'
|
import { components } from '@/types/comfyRegistryTypes'
|
||||||
|
import { isSemVer } from '@/utils/formatUtil'
|
||||||
|
|
||||||
|
const TRUNCATED_HASH_LENGTH = 7
|
||||||
|
|
||||||
const { nodePack } = defineProps<{
|
const { nodePack } = defineProps<{
|
||||||
nodePack: components['schemas']['Node']
|
nodePack: components['schemas']['Node']
|
||||||
@@ -50,11 +53,13 @@ const managerStore = useComfyManagerStore()
|
|||||||
|
|
||||||
const installedVersion = computed(() => {
|
const installedVersion = computed(() => {
|
||||||
if (!nodePack.id) return SelectedVersion.NIGHTLY
|
if (!nodePack.id) return SelectedVersion.NIGHTLY
|
||||||
return (
|
const version =
|
||||||
managerStore.installedPacks[nodePack.id]?.ver ??
|
managerStore.installedPacks[nodePack.id]?.ver ??
|
||||||
nodePack.latest_version?.version ??
|
nodePack.latest_version?.version ??
|
||||||
SelectedVersion.NIGHTLY
|
SelectedVersion.NIGHTLY
|
||||||
)
|
|
||||||
|
// If Git hash, truncate to 7 characters
|
||||||
|
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleVersionSelector = (event: Event) => {
|
const toggleVersionSelector = (event: Event) => {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import {
|
|||||||
SelectedVersion
|
SelectedVersion
|
||||||
} from '@/types/comfyManagerTypes'
|
} from '@/types/comfyManagerTypes'
|
||||||
import { components } from '@/types/comfyRegistryTypes'
|
import { components } from '@/types/comfyRegistryTypes'
|
||||||
|
import { isSemVer } from '@/utils/formatUtil'
|
||||||
|
|
||||||
const { nodePack } = defineProps<{
|
const { nodePack } = defineProps<{
|
||||||
nodePack: components['schemas']['Node']
|
nodePack: components['schemas']['Node']
|
||||||
@@ -93,12 +94,26 @@ const isQueueing = ref(false)
|
|||||||
|
|
||||||
const selectedVersion = ref<string>(SelectedVersion.LATEST)
|
const selectedVersion = ref<string>(SelectedVersion.LATEST)
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
|
||||||
selectedVersion.value =
|
selectedVersion.value =
|
||||||
nodePack.publisher?.name === 'Unclaimed'
|
// Use NIGHTLY when version is a Git hash
|
||||||
? SelectedVersion.NIGHTLY
|
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
|
||||||
: nodePack.latest_version?.version ?? SelectedVersion.NIGHTLY
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getInitialSelectedVersion = () => {
|
||||||
|
if (!nodePack.id) return
|
||||||
|
|
||||||
|
// If unclaimed, set selected version to nightly
|
||||||
|
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
|
||||||
|
|
||||||
|
// If node pack is installed, set selected version to the installed version
|
||||||
|
if (managerStore.isPackInstalled(nodePack.id))
|
||||||
|
return managerStore.getInstalledPackVersion(nodePack.id)
|
||||||
|
|
||||||
|
// If node pack is not installed, set selected version to latest
|
||||||
|
return nodePack.latest_version?.version
|
||||||
|
}
|
||||||
|
|
||||||
const fetchVersions = async () => {
|
const fetchVersions = async () => {
|
||||||
if (!nodePack?.id) return []
|
if (!nodePack?.id) return []
|
||||||
return (await registryService.getPackVersions(nodePack.id)) || []
|
return (await registryService.getPackVersions(nodePack.id)) || []
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ vi.mock('@/stores/comfyManagerStore', () => ({
|
|||||||
installPack: {
|
installPack: {
|
||||||
call: mockInstallPack,
|
call: mockInstallPack,
|
||||||
clear: vi.fn()
|
clear: vi.fn()
|
||||||
}
|
},
|
||||||
|
isPackInstalled: vi.fn(() => false),
|
||||||
|
getInstalledPackVersion: vi.fn(() => undefined)
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
'w-full': fullWidth,
|
'w-full': fullWidth,
|
||||||
'w-min-content': !fullWidth
|
'w-min-content': !fullWidth
|
||||||
}"
|
}"
|
||||||
:disabled="isInstalling"
|
:disabled="loading"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<span class="py-2.5 px-3">
|
<span class="py-2.5 px-3">
|
||||||
<template v-if="isInstalling">
|
<template v-if="loading">
|
||||||
{{ loadingMessage ?? $t('g.loading') }}
|
{{ loadingMessage ?? $t('g.loading') }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -23,9 +23,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { inject, ref } from 'vue'
|
|
||||||
|
|
||||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
@@ -33,6 +30,7 @@ const {
|
|||||||
fullWidth = false
|
fullWidth = false
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
label: string
|
label: string
|
||||||
|
loading?: boolean
|
||||||
loadingMessage?: string
|
loadingMessage?: string
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
}>()
|
}>()
|
||||||
@@ -45,10 +43,7 @@ defineOptions({
|
|||||||
inheritAttrs: false
|
inheritAttrs: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
|
||||||
|
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
isInstalling.value = true
|
|
||||||
emit('action')
|
emit('action')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install')
|
nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install')
|
||||||
"
|
"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
|
:loading="isInstalling"
|
||||||
:loading-message="$t('g.installing')"
|
:loading-message="$t('g.installing')"
|
||||||
@action="installAllPacks"
|
@action="installAllPacks"
|
||||||
|
@click="onClick"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -31,6 +33,10 @@ const { nodePacks } = defineProps<{
|
|||||||
|
|
||||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||||
|
|
||||||
|
const onClick = (): void => {
|
||||||
|
isInstalling.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const managerStore = useComfyManagerStore()
|
const managerStore = useComfyManagerStore()
|
||||||
|
|
||||||
const createPayload = (installItem: NodePack) => {
|
const createPayload = (installItem: NodePack) => {
|
||||||
|
|||||||