mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-04 19:49:09 +00:00
Compare commits
43 Commits
test/error
...
manager/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91e462dae8 | ||
|
|
267e07e26d | ||
|
|
a5153cd4b9 | ||
|
|
35873fae66 | ||
|
|
59e1945d1c | ||
|
|
33bf377350 | ||
|
|
4c99172106 | ||
|
|
7d0e971f3a | ||
|
|
87db9cc65f | ||
|
|
73f24896af | ||
|
|
63b6af5e27 | ||
|
|
42ad8ebc82 | ||
|
|
ff31e2d50a | ||
|
|
134341d4dd | ||
|
|
7389790e8e | ||
|
|
aefa3a9eb0 | ||
|
|
a1ea18c326 | ||
|
|
3e3448e0fd | ||
|
|
6470869eb1 | ||
|
|
7d1659c04a | ||
|
|
3352c0627d | ||
|
|
3c3ed2b532 | ||
|
|
2e4b510a50 | ||
|
|
b7f778bfc8 | ||
|
|
808adc06ac | ||
|
|
a2df972c8a | ||
|
|
8ceb5e4bef | ||
|
|
ec923e209c | ||
|
|
2d117b1144 | ||
|
|
0f97a6fd1b | ||
|
|
8a2747f29e | ||
|
|
e8e558b431 | ||
|
|
cf174d30b9 | ||
|
|
226f84eaf1 | ||
|
|
7ec70e5487 | ||
|
|
29cdd57de2 | ||
|
|
be7433be46 | ||
|
|
61fa2fd25a | ||
|
|
fd3362ed0d | ||
|
|
cd7d64ae5f | ||
|
|
d2d85650da | ||
|
|
d96265a93b | ||
|
|
5846117f4e |
2
.github/workflows/update-manager-types.yaml
vendored
2
.github/workflows/update-manager-types.yaml
vendored
@@ -99,4 +99,4 @@ jobs:
|
||||
labels: Manager
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
src/types/generatedManagerTypes.ts
|
||||
src/types/generatedManagerTypes.ts
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 69 KiB |
192
package-lock.json
generated
192
package-lock.json
generated
@@ -456,6 +456,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.5.0.tgz",
|
||||
"integrity": "sha512-dKnk2xlAyC7rvTkpkHmu+Qy/2Zc3Vm/l8PtNyIOGDBtXPY3kThfU4ORNEp3V7SXw5XSOb+tOJaUYpfquPzL/Tg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"package-manager-detector": "^0.2.5",
|
||||
"tinyexec": "^0.3.1"
|
||||
@@ -577,6 +578,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -630,6 +632,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -660,6 +663,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -919,9 +923,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
|
||||
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -2349,6 +2353,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz",
|
||||
"integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antfu/install-pkg": "^1.0.0",
|
||||
"@antfu/utils": "^8.1.0",
|
||||
@@ -2365,6 +2370,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
|
||||
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"package-manager-detector": "^1.3.0",
|
||||
"tinyexec": "^1.0.1"
|
||||
@@ -2378,6 +2384,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz",
|
||||
"integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
@@ -2386,13 +2393,15 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/utils/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -2410,6 +2419,7 @@
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
|
||||
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.0.1",
|
||||
@@ -2426,19 +2436,22 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz",
|
||||
"integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/utils/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/utils/node_modules/pkg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
"exsolve": "^1.0.7",
|
||||
@@ -2449,7 +2462,8 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
|
||||
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@inkjs/ui": {
|
||||
"version": "1.0.0",
|
||||
@@ -2558,6 +2572,18 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/synckit": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz",
|
||||
@@ -5233,7 +5259,8 @@
|
||||
"version": "7.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
|
||||
"integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.3",
|
||||
@@ -6876,15 +6903,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/broker-factory": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.7.tgz",
|
||||
"integrity": "sha512-RxbMXWq/Qvw9aLZMvuooMtVTm2/SV9JEpxpBbMuFhYAnDaZxctbJ+1b9ucHxADk/eQNqDijvWQjLVARqExAeyg==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.8.tgz",
|
||||
"integrity": "sha512-xmVnYN0FZtynhPUmAnN+/MFRdbDi3syCuxWV7o7s78FcIN0pjDtn9mUrVqEgdjQkbfojRhlPWbYbXJkMCyddrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
"worker-factory": "^7.0.44"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
@@ -7464,7 +7491,8 @@
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
@@ -8583,6 +8611,18 @@
|
||||
"eslint": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-compat-utils/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz",
|
||||
@@ -9297,7 +9337,8 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
|
||||
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
@@ -9320,53 +9361,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/extendable-media-recorder": {
|
||||
"version": "9.2.27",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder/-/extendable-media-recorder-9.2.27.tgz",
|
||||
"integrity": "sha512-2X+Ixi1cxLek0Cj9x9atmhQ+apG+LwJpP2p3ypP8Pxau0poDnicrg7FTfPVQV5PW/3DHFm/eQ16vbgo5Yk3HGQ==",
|
||||
"version": "9.2.28",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder/-/extendable-media-recorder-9.2.28.tgz",
|
||||
"integrity": "sha512-OIltlqo8rIUOcPn5c6aZ0qCvcTpXDnBgts8eRXK1VQKCI1omYBgZZyzkJ5vrQnB8xR34+GNOFs1Z+srq6tQsdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"media-encoder-host": "^9.0.20",
|
||||
"media-encoder-host": "^9.0.21",
|
||||
"multi-buffer-data-view": "^6.0.22",
|
||||
"recorder-audio-worklet": "^6.0.48",
|
||||
"recorder-audio-worklet": "^6.0.49",
|
||||
"standardized-audio-context": "^25.3.77",
|
||||
"subscribable-things": "^2.1.53",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extendable-media-recorder-wav-encoder": {
|
||||
"version": "7.0.129",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.129.tgz",
|
||||
"integrity": "sha512-/wqM2hnzvLy/iUlg/EU3JIF8MJcidy8I77Z7CCm5+CVEClDfcs6bH9PgghuisndwKTaud0Dh48RTD83gkfEjCw==",
|
||||
"version": "7.0.130",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.130.tgz",
|
||||
"integrity": "sha512-tVroIOesnMarsm+iIRiWUEYgmQj/lGqeMVNwJla7/tTVkX3ZPamh0NW59deILANwDIJq9lARLQmsUhyDWW57PA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"extendable-media-recorder-wav-encoder-broker": "^7.0.119",
|
||||
"extendable-media-recorder-wav-encoder-worker": "^8.0.116",
|
||||
"extendable-media-recorder-wav-encoder-broker": "^7.0.120",
|
||||
"extendable-media-recorder-wav-encoder-worker": "^8.0.117",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extendable-media-recorder-wav-encoder-broker": {
|
||||
"version": "7.0.119",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.119.tgz",
|
||||
"integrity": "sha512-BLrFOnqFLpsmmNpSk/TfjNs4j6ImCSGtoryIpRlqNu5S/Avt6gRJI0s4UYvdK7h17PCi+8vaDr75blvmU1sYlw==",
|
||||
"version": "7.0.120",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.120.tgz",
|
||||
"integrity": "sha512-wYRnDsHngGGR7LnEfl+tkNa7ck31KSV3PzqGLMCeKRmr02OrYxJUKyCeYf79TEe8O0EFtlBGBR9fiAj6OGDOhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"broker-factory": "^3.1.7",
|
||||
"extendable-media-recorder-wav-encoder-worker": "^8.0.116",
|
||||
"broker-factory": "^3.1.8",
|
||||
"extendable-media-recorder-wav-encoder-worker": "^8.0.117",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extendable-media-recorder-wav-encoder-worker": {
|
||||
"version": "8.0.116",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.116.tgz",
|
||||
"integrity": "sha512-bJPR0B7ZHeoqi9YoSie+UXAfEYya3efQ9eLiWuyK4KcOv+SuYQvWCoyzX5kjvb6GqIBCUnev5xulfeHRlyCwvw==",
|
||||
"version": "8.0.117",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.117.tgz",
|
||||
"integrity": "sha512-rnlIPkMB5F2sslesLXLdJ/Z0Kes4ROtdr+Kf/a6aF3233oJWK7165krWNZP5gpKZ4Z5Lhjx4fxVB12exZvlb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
"worker-factory": "^7.0.44"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
@@ -10072,6 +10113,7 @@
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -11342,6 +11384,18 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-eslint-parser/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jsondiffpatch": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
|
||||
@@ -12021,6 +12075,7 @@
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
|
||||
"integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.3",
|
||||
"pkg-types": "^1.2.1"
|
||||
@@ -12577,40 +12632,40 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/media-encoder-host": {
|
||||
"version": "9.0.20",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host/-/media-encoder-host-9.0.20.tgz",
|
||||
"integrity": "sha512-IyEYxw6az97RNuETOAZV4YZqNAPOiF9GKIp5mVZb4HOyWd6mhkWQ34ydOzhqAWogMyc4W05kjN/VCgTtgyFmsw==",
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host/-/media-encoder-host-9.0.21.tgz",
|
||||
"integrity": "sha512-hL41jBYmTKxGB2Z82rcR2hx4JCFBR3fmSOLR+G7EEkZn+uwSXDJTRFiYd7XWKnpjiHXy7ewf0ByEpby+2mv4Qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"media-encoder-host-broker": "^8.0.19",
|
||||
"media-encoder-host-worker": "^10.0.19",
|
||||
"media-encoder-host-broker": "^8.0.20",
|
||||
"media-encoder-host-worker": "^10.0.20",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/media-encoder-host-broker": {
|
||||
"version": "8.0.19",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host-broker/-/media-encoder-host-broker-8.0.19.tgz",
|
||||
"integrity": "sha512-lTpsNuaZdTCdtTHsOyww7Ae0Mwv+7mFS+O4YkFYWhXwVs0rm6XbRK5jRRn5JmcX3n1eTE1lQS5RgX8qbNaIjSg==",
|
||||
"version": "8.0.20",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host-broker/-/media-encoder-host-broker-8.0.20.tgz",
|
||||
"integrity": "sha512-Jab7frQx8d7Js+yBeMx7FeTrxRDjIpSmdARRkHSOtWNKJv9a+Hy2/OAPXoEJl6Y15jk1eiDwJkMMF6zvlnNMEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"broker-factory": "^3.1.7",
|
||||
"broker-factory": "^3.1.8",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"media-encoder-host-worker": "^10.0.19",
|
||||
"media-encoder-host-worker": "^10.0.20",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/media-encoder-host-worker": {
|
||||
"version": "10.0.19",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host-worker/-/media-encoder-host-worker-10.0.19.tgz",
|
||||
"integrity": "sha512-I8fwc6f41peER3RFSiwDxnIHbqU7p3pc2ghQozcw9CQfL0mWEo4IjQJtyswrrlL/HO2pgVSMQbaNzE4q/0mfDQ==",
|
||||
"version": "10.0.20",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host-worker/-/media-encoder-host-worker-10.0.20.tgz",
|
||||
"integrity": "sha512-Ysqjwlcu4VP6FlM+atQJZFFJwvfrw603gnmhWfyBlcE5+pl7ctc0wo2dbPiBpz9pc1pY4N5N6PHHhywkqHxfnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"extendable-media-recorder-wav-encoder-broker": "^7.0.119",
|
||||
"extendable-media-recorder-wav-encoder-broker": "^7.0.120",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
"worker-factory": "^7.0.44"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
@@ -13358,6 +13413,7 @@
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"pathe": "^2.0.1",
|
||||
@@ -13369,7 +13425,8 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
@@ -13970,6 +14027,7 @@
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
|
||||
"integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quansync": "^0.2.7"
|
||||
}
|
||||
@@ -14270,6 +14328,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.4",
|
||||
@@ -14280,7 +14339,8 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.52.0",
|
||||
@@ -15022,7 +15082,8 @@
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
@@ -15191,19 +15252,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recorder-audio-worklet": {
|
||||
"version": "6.0.48",
|
||||
"resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-6.0.48.tgz",
|
||||
"integrity": "sha512-PVlq/1hjCrPcUGqARg8rR30A303xDCao0jmlBTaUaKkN3Xme58RI7EQxurv8rw2eDwVrN+nrni0UoJoa5/v+zg==",
|
||||
"version": "6.0.49",
|
||||
"resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-6.0.49.tgz",
|
||||
"integrity": "sha512-JLKSKh5rAWxReHqca/f3h9L8pzBF1KnB+N2oIP2jkTD37n2yupkCazKD2f20pUOGkaX6AJG998gtKNCHYJn4nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"broker-factory": "^3.1.7",
|
||||
"broker-factory": "^3.1.8",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"recorder-audio-worklet-processor": "^5.0.35",
|
||||
"standardized-audio-context": "^25.3.77",
|
||||
"subscribable-things": "^2.1.53",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
"worker-factory": "^7.0.44"
|
||||
}
|
||||
},
|
||||
"node_modules/recorder-audio-worklet-processor": {
|
||||
@@ -15811,6 +15872,7 @@
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -16701,7 +16763,8 @@
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.0.1",
|
||||
@@ -17598,6 +17661,7 @@
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz",
|
||||
"integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.1",
|
||||
"picomatch": "^4.0.2",
|
||||
@@ -17612,6 +17676,7 @@
|
||||
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.22.0.tgz",
|
||||
"integrity": "sha512-CP+iZq5U7doOifer5bcM0jQ9t3Is7EGybIYt3myVxceI8Zuk8EZEpe1NPtJvh7iqMs1VdbK0L41t9+um9VuuLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antfu/install-pkg": "^0.5.0",
|
||||
"@antfu/utils": "^0.7.10",
|
||||
@@ -17658,6 +17723,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -17675,6 +17741,7 @@
|
||||
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.28.0.tgz",
|
||||
"integrity": "sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antfu/utils": "^0.7.10",
|
||||
"@rollup/pluginutils": "^5.1.4",
|
||||
@@ -17721,6 +17788,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -17753,6 +17821,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -18873,7 +18942,8 @@
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/websocket-driver": {
|
||||
"version": "0.7.4",
|
||||
@@ -19040,9 +19110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/worker-factory": {
|
||||
"version": "7.0.43",
|
||||
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.43.tgz",
|
||||
"integrity": "sha512-SACVoj3gWKtMVyT9N+VD11Pd/Xe58+ZFfp8b7y/PagOvj3i8lU3Uyj+Lj7WYTmSBvNLC0JFaQkx44E6DhH5+WA==",
|
||||
"version": "7.0.44",
|
||||
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.44.tgz",
|
||||
"integrity": "sha512-08AuUfWi+KeZI+KC7nU4pU/9tDeAFvE5NSWk+K9nIfuQc6UlOsZtjjeGVYVEn+DEchyXNJ5i10HCn0xRzFXEQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
|
||||
@@ -15,12 +15,14 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const conflictDetection = useConflictDetection()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
@@ -47,5 +49,9 @@ onMounted(() => {
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
// Initialize conflict detection in background
|
||||
// This runs async and doesn't block UI setup
|
||||
void conflictDetection.initializeConflictDetection()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -16,6 +16,14 @@ const meta: Meta<typeof IconButton> = {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent']
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
onClick: { action: 'clicked' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -11,6 +11,7 @@ import { computed } from 'vue'
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonTypeClasses,
|
||||
getIconButtonSizeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
@@ -22,6 +23,8 @@ interface IconButtonProps extends BaseButtonProps {
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
onClick
|
||||
} = defineProps<IconButtonProps>()
|
||||
@@ -29,7 +32,9 @@ const {
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} p-0`
|
||||
const sizeClasses = getIconButtonSizeClasses(size)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -28,6 +28,14 @@ const meta: Meta<typeof IconTextButton> = {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent']
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
iconPosition: {
|
||||
control: { type: 'select' },
|
||||
options: ['left', 'right']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
|
||||
<span>{{ label }}</span>
|
||||
<slot v-if="iconPosition === 'right'" name="icon"></slot>
|
||||
@@ -13,6 +13,7 @@ import { computed } from 'vue'
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonSizeClasses,
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
@@ -26,6 +27,8 @@ interface IconTextButtonProps extends BaseButtonProps {
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'primary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
iconPosition = 'left',
|
||||
label,
|
||||
@@ -35,7 +38,9 @@ const {
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -16,6 +16,14 @@ const meta: Meta<typeof TextButton> = {
|
||||
options: ['sm', 'md'],
|
||||
defaultValue: 'md'
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<span>{{ label }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -11,6 +11,7 @@ import { computed } from 'vue'
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonSizeClasses,
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
@@ -23,6 +24,8 @@ interface TextButtonProps extends BaseButtonProps {
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'primary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
label,
|
||||
onClick
|
||||
@@ -31,7 +34,9 @@ const {
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = getBaseButtonClasses()
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
|
||||
131
src/components/common/DotSpinner.vue
Normal file
131
src/components/common/DotSpinner.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center"
|
||||
:style="{ width: size + 'px', height: size + 'px' }"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
class="animate-spin"
|
||||
:style="{ animationDuration: duration }"
|
||||
>
|
||||
<g clip-path="url(#clip0_776_9582)">
|
||||
<!-- Top dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.25s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.5s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.75s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.125s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.625s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.875s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.375s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_776_9582">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const { size = 24, duration = '2s' } = defineProps<{
|
||||
size?: number
|
||||
duration?: string
|
||||
}>()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const color = computed(() =>
|
||||
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dot-animation {
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,7 +29,7 @@
|
||||
/>
|
||||
|
||||
<template v-if="item.footerComponent" #footer>
|
||||
<component :is="item.footerComponent" />
|
||||
<component :is="item.footerComponent" v-bind="item.footerProps" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -31,12 +31,17 @@
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
||||
<div v-if="!isLegacyManager" class="flex justify-end py-3">
|
||||
<PackInstallButton
|
||||
:disabled="isLoading || !!error || missingNodePacks.length === 0"
|
||||
size="md"
|
||||
:disabled="!!error || missingNodePacks.length === 0"
|
||||
:is-loading="isLoading"
|
||||
:node-packs="missingNodePacks"
|
||||
variant="black"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
:label="
|
||||
isLoading
|
||||
? $t('manager.gettingInfo')
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
/>
|
||||
<Button label="Open Manager" size="small" outlined @click="openManager" />
|
||||
</div>
|
||||
@@ -45,14 +50,14 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -60,22 +65,11 @@ const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
const isLegacyManager = ref(false)
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
@@ -103,6 +97,13 @@ const openManager = () => {
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const isLegacyResponse = await useComfyManagerService().isLegacyManagerUI()
|
||||
if (isLegacyResponse?.is_legacy_manager_ui) {
|
||||
isLegacyManager.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -26,6 +26,35 @@
|
||||
}"
|
||||
>
|
||||
<div class="px-6 flex flex-col h-full">
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<p class="text-sm font-bold m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.title') }}
|
||||
</p>
|
||||
<p class="text-xs m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.message') }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm font-bold m-0 cursor-pointer"
|
||||
@click="onClickWarningLink"
|
||||
>
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 w-6 h-6 border-none outline-none bg-transparent flex items-center justify-center text-yellow-600 rounded transition-colors"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:searchQuery="searchQuery"
|
||||
v-model:searchMode="searchMode"
|
||||
@@ -34,6 +63,7 @@
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
:sort-options="sortOptions"
|
||||
:is-update-available-tab="isUpdateAvailableTab"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
@@ -69,7 +99,9 @@
|
||||
:is-selected="
|
||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||
"
|
||||
@click.stop="(event) => selectNodePack(item, event)"
|
||||
@click.stop="
|
||||
(event: MouseEvent) => selectNodePack(item, event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
@@ -101,7 +133,8 @@ import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch
|
||||
watch,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -119,6 +152,7 @@ import { useManagerStatePersistence } from '@/composables/manager/useManagerStat
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
@@ -133,12 +167,13 @@ const { initialTab } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { getPackById } = useComfyRegistryStore()
|
||||
const conflictAcknowledgment = useConflictAcknowledgment()
|
||||
const persistedState = useManagerStatePersistence()
|
||||
const initialState = persistedState.loadStoredState()
|
||||
|
||||
const GRID_STYLE = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(19rem, 1fr))',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(17rem, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '1.5rem'
|
||||
} as const
|
||||
@@ -149,6 +184,13 @@ const {
|
||||
toggle: toggleSideNav
|
||||
} = useResponsiveCollapse()
|
||||
|
||||
// Use conflict acknowledgment state from composable
|
||||
const {
|
||||
shouldShowManagerBanner,
|
||||
dismissWarningBanner,
|
||||
dismissRedDotNotification
|
||||
} = conflictAcknowledgment
|
||||
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
|
||||
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi-box' },
|
||||
@@ -312,6 +354,13 @@ watch([isAllTab, searchResults], () => {
|
||||
displayPacks.value = searchResults.value
|
||||
})
|
||||
|
||||
const onClickWarningLink = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/troubleshooting/custom-node-issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
const onResultsChange = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
case ManagerTab.Installed:
|
||||
@@ -472,6 +521,10 @@ watch([searchQuery, selectedTab], () => {
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
dismissRedDotNotification()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
persistedState.persistState({
|
||||
selectedTabId: selectedTab.value?.id,
|
||||
|
||||
82
src/components/dialog/content/manager/ManagerHeader.test.ts
Normal file
82
src/components/dialog/content/manager/ManagerHeader.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import ManagerHeader from './ManagerHeader.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMessages
|
||||
}
|
||||
})
|
||||
|
||||
describe('ManagerHeader', () => {
|
||||
const createWrapper = () => {
|
||||
return mount(ManagerHeader, {
|
||||
global: {
|
||||
plugins: [createPinia(), PrimeVue, i18n],
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
components: {
|
||||
Tag
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the component title', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('h2').text()).toBe(
|
||||
enMessages.manager.discoverCommunityContent
|
||||
)
|
||||
})
|
||||
|
||||
it('displays the legacy manager UI tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
|
||||
})
|
||||
|
||||
it('applies info severity to the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('p-tag-info')
|
||||
})
|
||||
|
||||
it('displays info icon in the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const icon = wrapper.find('.pi-info-circle')
|
||||
expect(icon.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has cursor-help class on the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('cursor-help')
|
||||
})
|
||||
|
||||
it('has proper structure with flex container', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
|
||||
expect(flexContainer.exists()).toBe(true)
|
||||
|
||||
const tag = flexContainer.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,22 @@
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
<div class="flex justify-end ml-auto pr-4">
|
||||
<Tag
|
||||
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
:value="$t('manager.legacyManagerUI')"
|
||||
class="cursor-help"
|
||||
:pt="{
|
||||
root: { class: 'text-xs' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="w-[552px] flex flex-col">
|
||||
<ContentDivider :width="1" />
|
||||
<div class="px-4 py-6 w-full h-full flex flex-col gap-2">
|
||||
<!-- Description -->
|
||||
<div v-if="showAfterWhatsNew">
|
||||
<p
|
||||
class="text-sm leading-4 text-neutral-800 dark-theme:text-white m-0 mb-4"
|
||||
>
|
||||
{{ $t('manager.conflicts.description') }}
|
||||
<br /><br />
|
||||
{{ $t('manager.conflicts.info') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Import Failed List Wrapper -->
|
||||
<div
|
||||
v-if="importFailedConflicts.length > 0"
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleImportFailedPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ importFailedConflicts.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.importFailedExtensions') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
importFailedExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Import failed list -->
|
||||
<div
|
||||
v-if="importFailedExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="(packageName, i) in importFailedConflicts"
|
||||
:key="i"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
|
||||
{{ packageName }}
|
||||
</span>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Conflict List Wrapper -->
|
||||
<div
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleConflictsPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ allConflictDetails.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.conflicts') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
conflictsExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Conflicts list -->
|
||||
<div
|
||||
v-if="conflictsExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="(conflict, i) in allConflictDetails"
|
||||
:key="i"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span
|
||||
class="text-xs text-neutral-600 dark-theme:text-neutral-300"
|
||||
>{{ getConflictMessage(conflict, t) }}</span
|
||||
>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Extension List Wrapper -->
|
||||
<div
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleExtensionsPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ conflictData.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.extensionAtRisk') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
extensionsExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Extension list -->
|
||||
<div
|
||||
v-if="extensionsExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="conflictResult in conflictData"
|
||||
:key="conflictResult.package_id"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
|
||||
{{ conflictResult.package_name }}
|
||||
</span>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContentDivider :width="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { filter, flatMap, map, some } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import {
|
||||
ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
const { showAfterWhatsNew = false, conflictedPackages } = defineProps<{
|
||||
showAfterWhatsNew?: boolean
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { conflictedPackages: globalConflictPackages } = useConflictDetection()
|
||||
|
||||
const conflictsExpanded = ref<boolean>(false)
|
||||
const extensionsExpanded = ref<boolean>(false)
|
||||
const importFailedExpanded = ref<boolean>(false)
|
||||
|
||||
const conflictData = computed(
|
||||
() => conflictedPackages || globalConflictPackages.value
|
||||
)
|
||||
|
||||
const allConflictDetails = computed(() => {
|
||||
const allConflicts = flatMap(
|
||||
conflictData.value,
|
||||
(result: ConflictDetectionResult) => result.conflicts
|
||||
)
|
||||
return filter(
|
||||
allConflicts,
|
||||
(conflict: ConflictDetail) => conflict.type !== 'import_failed'
|
||||
)
|
||||
})
|
||||
|
||||
const packagesWithImportFailed = computed(() => {
|
||||
return filter(conflictData.value, (result: ConflictDetectionResult) =>
|
||||
some(
|
||||
result.conflicts,
|
||||
(conflict: ConflictDetail) => conflict.type === 'import_failed'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const importFailedConflicts = computed(() => {
|
||||
return map(
|
||||
packagesWithImportFailed.value,
|
||||
(result: ConflictDetectionResult) =>
|
||||
result.package_name || result.package_id
|
||||
)
|
||||
})
|
||||
|
||||
const toggleImportFailedPanel = () => {
|
||||
importFailedExpanded.value = !importFailedExpanded.value
|
||||
conflictsExpanded.value = false
|
||||
extensionsExpanded.value = false
|
||||
}
|
||||
|
||||
const toggleConflictsPanel = () => {
|
||||
conflictsExpanded.value = !conflictsExpanded.value
|
||||
extensionsExpanded.value = false
|
||||
importFailedExpanded.value = false
|
||||
}
|
||||
|
||||
const toggleExtensionsPanel = () => {
|
||||
extensionsExpanded.value = !extensionsExpanded.value
|
||||
conflictsExpanded.value = false
|
||||
importFailedExpanded.value = false
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.conflict-list-item:hover {
|
||||
background-color: rgba(0, 122, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
54
src/components/dialog/content/manager/NodeConflictFooter.vue
Normal file
54
src/components/dialog/content/manager/NodeConflictFooter.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full px-3 py-4">
|
||||
<div class="w-full flex items-center justify-between gap-2 pr-1">
|
||||
<Button
|
||||
:label="$t('manager.conflicts.conflictInfoTitle')"
|
||||
text
|
||||
severity="secondary"
|
||||
size="small"
|
||||
icon="pi pi-info-circle"
|
||||
:pt="{
|
||||
label: { class: 'text-sm' }
|
||||
}"
|
||||
@click="handleConflictInfoClick"
|
||||
/>
|
||||
<Button
|
||||
v-if="props.buttonText"
|
||||
:label="props.buttonText"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
@click="handleButtonClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
interface Props {
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttonText: undefined,
|
||||
onButtonClick: undefined
|
||||
})
|
||||
const dialogStore = useDialogStore()
|
||||
const handleConflictInfoClick = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/troubleshooting/custom-node-issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
const handleButtonClick = () => {
|
||||
// Close the conflict dialog
|
||||
dialogStore.closeDialog({ key: 'global-node-conflict' })
|
||||
// Execute the custom button action if provided
|
||||
if (props.onButtonClick) {
|
||||
props.onButtonClick()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
12
src/components/dialog/content/manager/NodeConflictHeader.vue
Normal file
12
src/components/dialog/content/manager/NodeConflictHeader.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="h-12 flex items-center justify-between w-full pl-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Warning Icon -->
|
||||
<i class="pi pi-exclamation-triangle text-lg"></i>
|
||||
<!-- Title -->
|
||||
<p class="text-base font-bold">
|
||||
{{ $t('manager.conflicts.title') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
type PackStatus = components['schemas']['NodeStatus']
|
||||
@@ -32,10 +33,15 @@ type StatusProps = {
|
||||
severity: MessageSeverity
|
||||
}
|
||||
|
||||
const { statusType } = defineProps<{
|
||||
const { statusType, hasCompatibilityIssues } = defineProps<{
|
||||
statusType: Status
|
||||
hasCompatibilityIssues?: boolean
|
||||
}>()
|
||||
|
||||
// Inject import failed context from parent
|
||||
const importFailedContext = inject(ImportFailedKey)
|
||||
const importFailed = importFailedContext?.importFailed
|
||||
|
||||
const statusPropsMap: Record<Status, StatusProps> = {
|
||||
NodeStatusActive: {
|
||||
label: 'active',
|
||||
@@ -71,10 +77,13 @@ const statusPropsMap: Record<Status, StatusProps> = {
|
||||
}
|
||||
}
|
||||
|
||||
const statusLabel = computed(
|
||||
() => statusPropsMap[statusType]?.label || 'unknown'
|
||||
)
|
||||
const statusSeverity = computed(
|
||||
() => statusPropsMap[statusType]?.severity || 'secondary'
|
||||
)
|
||||
const statusLabel = computed(() => {
|
||||
if (importFailed?.value) return 'importFailed'
|
||||
if (hasCompatibilityIssues) return 'conflicting'
|
||||
return statusPropsMap[statusType]?.label || 'unknown'
|
||||
})
|
||||
const statusSeverity = computed(() => {
|
||||
if (hasCompatibilityIssues || importFailed?.value) return 'error'
|
||||
return statusPropsMap[statusType]?.severity || 'secondary'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -6,11 +6,18 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Mock config to prevent __COMFYUI_FRONTEND_VERSION__ error
|
||||
vi.mock('@/config', () => ({
|
||||
default: {
|
||||
app_title: 'ComfyUI',
|
||||
app_version: '1.0.0'
|
||||
}
|
||||
}))
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
@@ -120,7 +127,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
})
|
||||
|
||||
it('falls back to NIGHTLY when nodePack.id is missing', () => {
|
||||
@@ -134,7 +141,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
})
|
||||
|
||||
it('toggles the popover when button is clicked', async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer px-2 py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700': fill }"
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill }"
|
||||
aria-haspopup="true"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -12,17 +12,16 @@
|
||||
>
|
||||
<i
|
||||
v-if="isUpdateAvailable"
|
||||
class="pi pi-arrow-circle-up text-blue-600"
|
||||
style="font-size: 8px"
|
||||
class="pi pi-arrow-circle-up text-blue-600 text-xs"
|
||||
/>
|
||||
<span>{{ installedVersion }}</span>
|
||||
<i class="pi pi-chevron-right" style="font-size: 8px" />
|
||||
<i class="pi pi-chevron-right text-xxs" />
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
:pt="{
|
||||
content: { class: 'px-0' }
|
||||
content: { class: 'p-0 shadow-lg' }
|
||||
}"
|
||||
>
|
||||
<PackVersionSelectorPopover
|
||||
|
||||
@@ -3,18 +3,32 @@ import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import Select from 'primevue/select'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
// SelectedVersion is now using direct strings instead of enum
|
||||
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Default mock versions for reference
|
||||
const defaultMockVersions = [
|
||||
{ version: '1.0.0', createdAt: '2023-01-01' },
|
||||
{
|
||||
version: '1.0.0',
|
||||
createdAt: '2023-01-01',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
},
|
||||
{ version: '0.9.0', createdAt: '2022-12-01' },
|
||||
{ version: '0.8.0', createdAt: '2022-11-01' }
|
||||
]
|
||||
@@ -22,13 +36,24 @@ const defaultMockVersions = [
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
latest_version: { version: '1.0.0' },
|
||||
repository: 'https://github.com/user/repo'
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
},
|
||||
repository: 'https://github.com/user/repo',
|
||||
has_registry_data: true
|
||||
}
|
||||
|
||||
// Create mock functions
|
||||
const mockGetPackVersions = vi.fn()
|
||||
const mockInstallPack = vi.fn().mockResolvedValue(undefined)
|
||||
const mockCheckNodeCompatibility = vi.fn()
|
||||
|
||||
// Mock the registry service
|
||||
vi.mock('@/services/comfyRegistryService', () => ({
|
||||
@@ -49,6 +74,13 @@ vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the conflict detection composable
|
||||
vi.mock('@/composables/useConflictDetection', () => ({
|
||||
useConflictDetection: vi.fn(() => ({
|
||||
checkNodeCompatibility: mockCheckNodeCompatibility
|
||||
}))
|
||||
}))
|
||||
|
||||
const waitForPromises = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
await nextTick()
|
||||
@@ -59,6 +91,9 @@ describe('PackVersionSelectorPopover', () => {
|
||||
vi.clearAllMocks()
|
||||
mockGetPackVersions.mockReset()
|
||||
mockInstallPack.mockReset().mockResolvedValue(undefined)
|
||||
mockCheckNodeCompatibility
|
||||
.mockReset()
|
||||
.mockReturnValue({ hasConflict: false, conflicts: [] })
|
||||
})
|
||||
|
||||
const mountComponent = ({
|
||||
@@ -78,7 +113,12 @@ describe('PackVersionSelectorPopover', () => {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: {
|
||||
Listbox
|
||||
Listbox,
|
||||
VerifiedIcon,
|
||||
Select
|
||||
},
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -120,14 +160,15 @@ describe('PackVersionSelectorPopover', () => {
|
||||
|
||||
const options = listbox.props('options')!
|
||||
// Check that we have both special options and version options
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
|
||||
// Latest version (1.0.0) should be excluded from the version list to avoid duplication
|
||||
expect(options.length).toBe(defaultMockVersions.length + 1) // 2 special options + version options minus 1 duplicate
|
||||
|
||||
// Check that special options exist
|
||||
expect(options.some((o) => o.value === SelectedVersion.NIGHTLY)).toBe(true)
|
||||
expect(options.some((o) => o.value === SelectedVersion.LATEST)).toBe(true)
|
||||
expect(options.some((o) => o.value === 'nightly')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'latest')).toBe(true)
|
||||
|
||||
// Check that version options exist
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
|
||||
// Check that version options exist (excluding latest version 1.0.0)
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(false) // Should be excluded as it's the latest
|
||||
expect(options.some((o) => o.value === '0.9.0')).toBe(true)
|
||||
expect(options.some((o) => o.value === '0.8.0')).toBe(true)
|
||||
})
|
||||
@@ -304,7 +345,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
|
||||
it('defaults to nightly when publisher name is "Unclaimed"', async () => {
|
||||
@@ -325,7 +366,343 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
})
|
||||
|
||||
describe('version compatibility checking', () => {
|
||||
it('shows warning icon for incompatible versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return conflict for specific version
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.supported_os?.includes('linux')) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'os',
|
||||
current_value: 'windows',
|
||||
required_value: 'linux'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['linux'],
|
||||
supported_accelerators: ['CUDA']
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for incompatible versions
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows verified icon for compatible versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return no conflicts
|
||||
mockCheckNodeCompatibility.mockReturnValue({
|
||||
hasConflict: false,
|
||||
conflicts: []
|
||||
})
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The verified icon should be shown for compatible versions
|
||||
// Look for the VerifiedIcon component or SVG elements
|
||||
const verifiedIcons = wrapper.findAll('svg')
|
||||
expect(verifiedIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('calls checkVersionCompatibility with correct version data', async () => {
|
||||
// Set up the mock for versions with specific supported data
|
||||
const versionsWithCompatibility = [
|
||||
{
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CUDA', 'CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0'
|
||||
}
|
||||
]
|
||||
mockGetPackVersions.mockResolvedValueOnce(versionsWithCompatibility)
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'], // latest_version data takes precedence
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Clear previous calls from component mounting/rendering
|
||||
mockCheckNodeCompatibility.mockClear()
|
||||
|
||||
// Trigger compatibility check by accessing getVersionCompatibility
|
||||
const vm = wrapper.vm as any
|
||||
vm.getVersionCompatibility('1.0.0')
|
||||
|
||||
// Verify that checkNodeCompatibility was called with correct data
|
||||
// Since 1.0.0 is the latest version, it should use latest_version data
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'], // latest_version data takes precedence
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true,
|
||||
version: '1.0.0'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows version conflict warnings for ComfyUI and frontend versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return version conflicts
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
const conflicts = []
|
||||
if (versionData.supported_comfyui_version) {
|
||||
conflicts.push({
|
||||
type: 'comfyui_version',
|
||||
current_value: '0.5.0',
|
||||
required_value: versionData.supported_comfyui_version
|
||||
})
|
||||
}
|
||||
if (versionData.supported_comfyui_frontend_version) {
|
||||
conflicts.push({
|
||||
type: 'frontend_version',
|
||||
current_value: '1.0.0',
|
||||
required_value: versionData.supported_comfyui_frontend_version
|
||||
})
|
||||
}
|
||||
return {
|
||||
hasConflict: conflicts.length > 0,
|
||||
conflicts
|
||||
}
|
||||
})
|
||||
|
||||
const nodePackWithVersionRequirements = {
|
||||
...mockNodePack,
|
||||
supported_comfyui_version: '>=1.0.0',
|
||||
supported_comfyui_frontend_version: '>=2.0.0'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithVersionRequirements }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for version incompatible packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('handles latest and nightly versions using nodePack data', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
supported_os: ['windows'], // Match nodePack data for test consistency
|
||||
supported_accelerators: ['CPU'], // Match nodePack data for test consistency
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
const vm = wrapper.vm as any
|
||||
|
||||
// Clear previous calls from component mounting/rendering
|
||||
mockCheckNodeCompatibility.mockClear()
|
||||
|
||||
// Test latest version
|
||||
vm.getVersionCompatibility('latest')
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true,
|
||||
version: '1.0.0'
|
||||
})
|
||||
|
||||
// Clear for next test call
|
||||
mockCheckNodeCompatibility.mockClear()
|
||||
|
||||
// Test nightly version
|
||||
vm.getVersionCompatibility('nightly')
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
repository: 'https://github.com/user/repo',
|
||||
has_registry_data: true,
|
||||
latest_version: {
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true,
|
||||
version: '1.0.0',
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('shows banned package warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return banned conflicts
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.is_banned === true) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'banned',
|
||||
current_value: 'installed',
|
||||
required_value: 'not_banned'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const bannedNodePack = {
|
||||
...mockNodePack,
|
||||
is_banned: true,
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
is_banned: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: bannedNodePack }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// Open the dropdown to see the options
|
||||
const select = wrapper.find('.p-select')
|
||||
if (!select.exists()) {
|
||||
// Try alternative selector
|
||||
const selectButton = wrapper.find('[aria-haspopup="listbox"]')
|
||||
if (selectButton.exists()) {
|
||||
await selectButton.trigger('click')
|
||||
}
|
||||
} else {
|
||||
await select.trigger('click')
|
||||
}
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The warning icon should be shown for banned packages in the dropdown options
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows security pending warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return security pending conflicts
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.has_registry_data === false) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'pending',
|
||||
current_value: 'no_registry_data',
|
||||
required_value: 'registry_data_available'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const securityPendingNodePack = {
|
||||
...mockNodePack,
|
||||
has_registry_data: false,
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
has_registry_data: false
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: securityPendingNodePack }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for security pending packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="w-64 mt-2">
|
||||
<span class="pl-3 text-muted text-md font-semibold opacity-70">
|
||||
{{ $t('manager.selectVersion') }}
|
||||
</span>
|
||||
<div class="w-64 pt-1">
|
||||
<div class="py-2">
|
||||
<span class="pl-3 text-md font-semibold text-neutral-500">
|
||||
{{ $t('manager.selectVersion') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoadingVersions || isQueueing"
|
||||
class="text-center text-muted py-4 flex flex-col items-center"
|
||||
@@ -23,24 +25,44 @@
|
||||
v-model="selectedVersion"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="versionOptions"
|
||||
:options="processedVersionOptions"
|
||||
:highlight-on-select="false"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none"
|
||||
class="w-full max-h-[50vh] border-none shadow-none rounded-md"
|
||||
:pt="{
|
||||
listContainer: { class: 'scrollbar-hide' }
|
||||
}"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex justify-between items-center w-full p-1">
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="slotProps.option.value === 'nightly'">
|
||||
<div class="w-4"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i
|
||||
v-if="slotProps.option.hasConflict"
|
||||
v-tooltip="{
|
||||
value: slotProps.option.conflictMessage,
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-exclamation-triangle text-yellow-500"
|
||||
/>
|
||||
<VerifiedIcon v-else :size="20" class="relative right-0.5" />
|
||||
</template>
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="selectedVersion === slotProps.option.value"
|
||||
v-if="slotProps.option.isSelected"
|
||||
class="pi pi-check text-highlight"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
<ContentDivider class="my-2" />
|
||||
<div class="flex justify-end gap-2 p-1 px-3">
|
||||
<div class="flex justify-end gap-2 py-1 px-3">
|
||||
<Button
|
||||
text
|
||||
class="text-sm"
|
||||
severity="secondary"
|
||||
:label="$t('g.cancel')"
|
||||
:disabled="isQueueing"
|
||||
@@ -49,7 +71,7 @@
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('g.install')"
|
||||
class="py-3 px-4 dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
|
||||
class="py-2.5 px-4 text-sm dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
|
||||
:disabled="isQueueing"
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
@@ -62,11 +84,13 @@ import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
@@ -75,6 +99,7 @@ import {
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -89,6 +114,7 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const registryService = useComfyRegistryService()
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
|
||||
const isQueueing = ref(false)
|
||||
|
||||
@@ -126,6 +152,8 @@ const versionOptions = ref<
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const fetchedVersions = ref<components['schemas']['NodeVersion'][]>([])
|
||||
|
||||
const isLoadingVersions = ref(false)
|
||||
|
||||
const onNodePackChange = async () => {
|
||||
@@ -133,18 +161,27 @@ const onNodePackChange = async () => {
|
||||
|
||||
// Fetch versions from the registry
|
||||
const versions = await fetchVersions()
|
||||
fetchedVersions.value = versions
|
||||
|
||||
const latestVersionNumber = nodePack.latest_version?.version
|
||||
|
||||
const availableVersionOptions = versions
|
||||
.map((version) => ({
|
||||
value: version.version ?? '',
|
||||
label: version.version ?? ''
|
||||
}))
|
||||
.filter((option) => option.value)
|
||||
.filter((option) => option.value && option.value !== latestVersionNumber) // Exclude latest version from the list
|
||||
|
||||
// Add Latest option with actual version number
|
||||
const latestLabel = latestVersionNumber
|
||||
? `${t('manager.latestVersion')} (${latestVersionNumber})`
|
||||
: t('manager.latestVersion')
|
||||
|
||||
// Add Latest option
|
||||
const defaultVersions = [
|
||||
{
|
||||
value: SelectedVersion.LATEST,
|
||||
label: t('manager.latestVersion')
|
||||
label: latestLabel
|
||||
}
|
||||
]
|
||||
|
||||
@@ -172,16 +209,86 @@ whenever(
|
||||
|
||||
const handleSubmit = async () => {
|
||||
isQueueing.value = true
|
||||
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
// Convert 'latest' to actual version number for installation
|
||||
const actualVersion =
|
||||
selectedVersion.value === 'latest'
|
||||
? nodePack.latest_version?.version ?? 'latest'
|
||||
: selectedVersion.value
|
||||
|
||||
await managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
version: selectedVersion.value,
|
||||
version: actualVersion,
|
||||
selected_version: selectedVersion.value
|
||||
})
|
||||
|
||||
isQueueing.value = false
|
||||
emit('submit')
|
||||
}
|
||||
|
||||
const getVersionData = (version: string) => {
|
||||
const latestVersionNumber = nodePack.latest_version?.version
|
||||
const useLatestVersionData =
|
||||
version === 'latest' || version === latestVersionNumber
|
||||
if (useLatestVersionData) {
|
||||
const latestVersionData = nodePack.latest_version
|
||||
return {
|
||||
...latestVersionData
|
||||
}
|
||||
}
|
||||
const versionData = fetchedVersions.value.find((v) => v.version === version)
|
||||
if (versionData) {
|
||||
return {
|
||||
...versionData
|
||||
}
|
||||
}
|
||||
// Fallback to nodePack data
|
||||
return {
|
||||
...nodePack
|
||||
}
|
||||
}
|
||||
// Main function to get version compatibility info
|
||||
const getVersionCompatibility = (version: string) => {
|
||||
const versionData = getVersionData(version)
|
||||
const compatibility = checkNodeCompatibility(versionData)
|
||||
const conflictMessage = compatibility.hasConflict
|
||||
? getJoinedConflictMessages(compatibility.conflicts, t)
|
||||
: ''
|
||||
return {
|
||||
hasConflict: compatibility.hasConflict,
|
||||
conflictMessage
|
||||
}
|
||||
}
|
||||
// Helper to determine if an option is selected.
|
||||
const isOptionSelected = (optionValue: string) => {
|
||||
if (selectedVersion.value === optionValue) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
optionValue === 'latest' &&
|
||||
selectedVersion.value === nodePack.latest_version?.version
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Checks if an option is selected, treating 'latest' as an alias for the actual latest version number.
|
||||
const processedVersionOptions = computed(() => {
|
||||
return versionOptions.value.map((option) => {
|
||||
const compatibility = getVersionCompatibility(option.value)
|
||||
const isSelected = isOptionSelected(option.value)
|
||||
return {
|
||||
...option,
|
||||
hasConflict: compatibility.hasConflict,
|
||||
conflictMessage: compatibility.conflictMessage,
|
||||
isSelected: isSelected
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
outlined
|
||||
class="!m-0 p-0 rounded-lg text-gray-900 dark-theme:text-gray-50"
|
||||
:class="[
|
||||
variant === 'black'
|
||||
? 'bg-neutral-900 text-white border-neutral-900'
|
||||
: 'border-neutral-700',
|
||||
fullWidth ? 'w-full' : 'w-min-content'
|
||||
]"
|
||||
:disabled="loading"
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<span class="py-2 px-3 whitespace-nowrap">
|
||||
<template v-if="loading">
|
||||
{{ loadingMessage ?? $t('g.loading') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ label }}
|
||||
</template>
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const {
|
||||
label,
|
||||
loadingMessage,
|
||||
fullWidth = false,
|
||||
variant = 'default'
|
||||
} = defineProps<{
|
||||
label: string
|
||||
loading?: boolean
|
||||
loadingMessage?: string
|
||||
fullWidth?: boolean
|
||||
variant?: 'default' | 'black'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: []
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const onClick = (): void => {
|
||||
emit('action')
|
||||
}
|
||||
</script>
|
||||
@@ -12,9 +12,13 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}))
|
||||
vi.mock('es-toolkit/compat', async () => {
|
||||
const actual = await vi.importActual('es-toolkit/compat')
|
||||
return {
|
||||
...actual,
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}
|
||||
})
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="hasConflict"
|
||||
v-tooltip="{
|
||||
value: $t('manager.conflicts.warningTooltip'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="flex items-center justify-center w-6 h-6 cursor-pointer"
|
||||
@click="showConflictModal(true)"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-500 text-xl"></i>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
v-if="!canToggleDirectly"
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
:readonly="!canToggleDirectly"
|
||||
aria-label="Enable or disable pack"
|
||||
@focus="handleToggleInteraction"
|
||||
/>
|
||||
<ToggleSwitch
|
||||
v-else
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
aria-label="Enable or disable pack"
|
||||
@@ -8,13 +28,16 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import {
|
||||
InstallPackParams,
|
||||
ManagerChannel,
|
||||
@@ -24,12 +47,17 @@ import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
const { nodePack, hasConflict } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
hasConflict?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isPackEnabled, enablePack, disablePack, installedPacks } =
|
||||
useComfyManagerStore()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { acknowledgmentState, markConflictsAsSeen } = useConflictAcknowledgment()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -44,21 +72,64 @@ const version = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const handleEnable = () =>
|
||||
enablePack.call({
|
||||
const packageConflict = computed(() =>
|
||||
getConflictsForPackageByID(nodePack.id || '')
|
||||
)
|
||||
const canToggleDirectly = computed(() => {
|
||||
return !(
|
||||
hasConflict &&
|
||||
!acknowledgmentState.value.modal_dismissed &&
|
||||
packageConflict.value
|
||||
)
|
||||
})
|
||||
|
||||
const showConflictModal = (skipModalDismissed: boolean) => {
|
||||
let modal_dismissed = acknowledgmentState.value.modal_dismissed
|
||||
if (skipModalDismissed) modal_dismissed = false
|
||||
if (packageConflict.value && !modal_dismissed) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [packageConflict.value],
|
||||
buttonText: !isEnabled.value
|
||||
? t('manager.conflicts.enableAnyway')
|
||||
: t('manager.conflicts.understood'),
|
||||
onButtonClick: async () => {
|
||||
if (!isEnabled.value) {
|
||||
await handleEnable()
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for enabling')
|
||||
}
|
||||
return enablePack.call({
|
||||
id: nodePack.id,
|
||||
version: version.value,
|
||||
selected_version: version.value,
|
||||
version: version.value ?? SelectedVersion.LATEST,
|
||||
selected_version: version.value ?? SelectedVersion.LATEST,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: 'default' as InstallPackParams['mode']
|
||||
mode: 'default' as InstallPackParams['mode'],
|
||||
skip_post_install: false
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = () =>
|
||||
disablePack({
|
||||
const handleDisable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for disabling')
|
||||
}
|
||||
return disablePack({
|
||||
id: nodePack.id,
|
||||
version: version.value
|
||||
version: version.value ?? SelectedVersion.LATEST
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
@@ -67,10 +138,22 @@ const handleToggle = async (enable: boolean) => {
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
handleDisable()
|
||||
await handleDisable()
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const onToggle = debounce(handleToggle, TOGGLE_DEBOUNCE_MS, { trailing: true })
|
||||
const onToggle = debounce(
|
||||
(enable: boolean) => {
|
||||
void handleToggle(enable)
|
||||
},
|
||||
TOGGLE_DEBOUNCE_MS,
|
||||
{ trailing: true }
|
||||
)
|
||||
const handleToggleInteraction = async (event: Event) => {
|
||||
if (!canToggleDirectly.value) {
|
||||
event.preventDefault()
|
||||
showConflictModal(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,49 +1,80 @@
|
||||
<template>
|
||||
<PackActionButton
|
||||
<IconTextButton
|
||||
v-bind="$attrs"
|
||||
:label="
|
||||
label ??
|
||||
(nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install'))
|
||||
"
|
||||
:severity="variant === 'black' ? undefined : 'secondary'"
|
||||
:variant="variant"
|
||||
:loading="isInstalling"
|
||||
:loading-message="$t('g.installing')"
|
||||
@action="installAllPacks"
|
||||
@click="onClick"
|
||||
/>
|
||||
type="transparent"
|
||||
:label="computedLabel"
|
||||
:border="true"
|
||||
:size="size"
|
||||
:disabled="isLoading || isInstalling"
|
||||
@click="installAllPacks"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="hasConflict && !isInstalling && !isLoading"
|
||||
class="pi pi-exclamation-triangle text-yellow-500"
|
||||
/>
|
||||
<DotSpinner
|
||||
v-else-if="isLoading || isInstalling"
|
||||
duration="1s"
|
||||
:size="size === 'sm' ? 12 : 16"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { ButtonSize } from '@/types/buttonTypes'
|
||||
import {
|
||||
IsInstallingKey,
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import {
|
||||
type ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks, variant, label } = defineProps<{
|
||||
const {
|
||||
nodePacks,
|
||||
isLoading = false,
|
||||
label = 'Install',
|
||||
size = 'sm',
|
||||
hasConflict,
|
||||
conflictInfo
|
||||
} = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
variant?: 'default' | 'black'
|
||||
isLoading?: boolean
|
||||
label?: string
|
||||
size?: ButtonSize
|
||||
hasConflict?: boolean
|
||||
conflictInfo?: ConflictDetail[]
|
||||
}>()
|
||||
|
||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||
|
||||
const onClick = (): void => {
|
||||
isInstalling.value = true
|
||||
}
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
|
||||
// Check if any of the packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
if (!nodePacks?.length) return false
|
||||
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
|
||||
})
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
if (!installItem.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? SelectedVersion.NIGHTLY
|
||||
@@ -65,14 +96,45 @@ const installPack = (item: NodePack) =>
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
isInstalling.value = true
|
||||
if (hasConflict && conflictInfo) {
|
||||
// Check each package individually for conflicts
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const conflictedPackages: ConflictDetectionResult[] = nodePacks
|
||||
.map((pack) => {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
return {
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: compatibilityCheck.hasConflict,
|
||||
conflicts: compatibilityCheck.conflicts,
|
||||
is_compatible: !compatibilityCheck.hasConflict
|
||||
}
|
||||
})
|
||||
.filter((result) => result.has_conflict) // Only show packages with conflicts
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages,
|
||||
buttonText: t('manager.conflicts.installAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// Proceed with installation
|
||||
await performInstallation(nodePacks)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// No conflicts or conflicts acknowledged - proceed with installation
|
||||
await performInstallation(nodePacks)
|
||||
}
|
||||
|
||||
await Promise.all(uninstalledPacks.map(installPack))
|
||||
const performInstallation = async (packs: NodePack[]) => {
|
||||
await Promise.all(packs.map(installPack))
|
||||
managerStore.installPack.clear()
|
||||
}
|
||||
|
||||
const computedLabel = computed(() =>
|
||||
isInstalling.value
|
||||
? t('g.installing')
|
||||
: label ??
|
||||
(nodePacks.length > 1 ? t('manager.installSelected') : t('g.install'))
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
<template>
|
||||
<PackActionButton
|
||||
<TextButton
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
:label="
|
||||
nodePacks.length > 1
|
||||
? $t('manager.uninstallSelected')
|
||||
: $t('manager.uninstall')
|
||||
"
|
||||
severity="danger"
|
||||
:loading-message="$t('manager.uninstalling')"
|
||||
@action="uninstallItems"
|
||||
:border="true"
|
||||
:size="size"
|
||||
class="border-red-500"
|
||||
@click="uninstallItems"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
|
||||
import { ButtonSize } from '@/types/buttonTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
const { nodePacks, size } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
size?: ButtonSize
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (uninstallItem: NodePack): ManagerPackInfo => {
|
||||
return {
|
||||
id: uninstallItem.id,
|
||||
version: uninstallItem.latest_version?.version
|
||||
const uninstallPack = (item: NodePack) => {
|
||||
if (!item.id) {
|
||||
throw new Error('Node ID is required for uninstallation')
|
||||
}
|
||||
return managerStore.uninstallPack({
|
||||
id: item.id,
|
||||
version: item.latest_version?.version ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
const uninstallPack = (item: NodePack) =>
|
||||
managerStore.uninstallPack(createPayload(item))
|
||||
|
||||
const uninstallItems = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
await Promise.all(nodePacks.map(uninstallPack))
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<IconTextButton
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
:label="$t('manager.updateAll')"
|
||||
:border="true"
|
||||
size="sm"
|
||||
:disabled="isUpdating"
|
||||
@click="updateAllPacks"
|
||||
>
|
||||
<template v-if="isUpdating" #icon>
|
||||
<DotSpinner duration="1s" :size="12" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
}>()
|
||||
const isUpdating = ref<boolean>(false)
|
||||
const managerStore = useComfyManagerStore()
|
||||
const createPayload = (updateItem: NodePack) => {
|
||||
return {
|
||||
id: updateItem.id!,
|
||||
version: updateItem.latest_version!.version!
|
||||
}
|
||||
}
|
||||
const updatePack = async (item: NodePack) => {
|
||||
if (!item.id || !item.latest_version?.version) {
|
||||
console.warn('Pack missing required id or version:', item)
|
||||
return
|
||||
}
|
||||
await managerStore.updatePack.call(createPayload(item))
|
||||
}
|
||||
const updateAllPacks = async () => {
|
||||
if (!nodePacks?.length) {
|
||||
console.warn('No packs provided for update')
|
||||
return
|
||||
}
|
||||
isUpdating.value = true
|
||||
const updatablePacks = nodePacks.filter((pack) =>
|
||||
managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!updatablePacks.length) {
|
||||
console.info('No installed packs available for update')
|
||||
isUpdating.value = false
|
||||
return
|
||||
}
|
||||
console.info(`Starting update of ${updatablePacks.length} packs`)
|
||||
try {
|
||||
await Promise.all(updatablePacks.map(updatePack))
|
||||
managerStore.updatePack.clear()
|
||||
console.info('All packs updated successfully')
|
||||
} catch (error) {
|
||||
console.error('Pack update failed:', error)
|
||||
console.error(
|
||||
'Failed packs info:',
|
||||
updatablePacks.map((p) => p.id)
|
||||
)
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,20 +2,26 @@
|
||||
<template v-if="nodePack">
|
||||
<div class="flex flex-col h-full z-40 overflow-hidden relative">
|
||||
<div class="top-0 z-10 px-6 pt-6 w-full">
|
||||
<InfoPanelHeader :node-packs="[nodePack]" />
|
||||
<InfoPanelHeader
|
||||
:node-packs="[nodePack]"
|
||||
:has-conflict="hasCompatibilityIssues"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar"
|
||||
class="p-6 pt-2 overflow-y-auto flex-1 text-sm scrollbar-hide"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<MetadataRow
|
||||
v-if="isPackInstalled(nodePack.id)"
|
||||
v-if="!importFailed && isPackInstalled(nodePack.id)"
|
||||
:label="t('manager.filter.enabled')"
|
||||
class="flex"
|
||||
style="align-items: center"
|
||||
>
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
<PackEnableToggle
|
||||
:node-pack="nodePack"
|
||||
:has-conflict="hasCompatibilityIssues"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
v-for="item in infoItems"
|
||||
@@ -29,6 +35,7 @@
|
||||
:status-type="
|
||||
nodePack.status as components['schemas']['NodeVersionStatus']
|
||||
"
|
||||
:has-compatibility-issues="hasCompatibilityIssues"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow :label="t('manager.version')">
|
||||
@@ -36,7 +43,11 @@
|
||||
</MetadataRow>
|
||||
</div>
|
||||
<div class="mb-6 overflow-hidden">
|
||||
<InfoTabs :node-pack="nodePack" />
|
||||
<InfoTabs
|
||||
:node-pack="nodePack"
|
||||
:has-compatibility-issues="hasCompatibilityIssues"
|
||||
:conflict-result="conflictResult"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,9 +70,14 @@ import PackEnableToggle from '@/components/dialog/content/manager/button/PackEna
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
interface InfoItem {
|
||||
key: string
|
||||
@@ -75,18 +91,55 @@ const { nodePack } = defineProps<{
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const isInstalled = computed(() => managerStore.isPackInstalled(nodePack.id))
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack.id))
|
||||
const isInstalling = ref(false)
|
||||
provide(IsInstallingKey, isInstalling)
|
||||
whenever(isInstalled, () => {
|
||||
isInstalling.value = false
|
||||
})
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
|
||||
const { t, d, n } = useI18n()
|
||||
|
||||
// Check compatibility once and pass to children
|
||||
const conflictResult = computed((): ConflictDetectionResult | null => {
|
||||
// For installed packages, use stored conflict data
|
||||
if (isInstalled.value && nodePack.id) {
|
||||
return getConflictsForPackageByID(nodePack.id) || null
|
||||
}
|
||||
|
||||
// For non-installed packages, perform compatibility check
|
||||
const compatibility = checkNodeCompatibility(nodePack)
|
||||
|
||||
if (compatibility.hasConflict) {
|
||||
return {
|
||||
package_id: nodePack.id || '',
|
||||
package_name: nodePack.name || '',
|
||||
has_conflict: true,
|
||||
conflicts: compatibility.conflicts,
|
||||
is_compatible: false
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const hasCompatibilityIssues = computed(() => {
|
||||
return conflictResult.value?.has_conflict
|
||||
})
|
||||
|
||||
const packageId = computed(() => nodePack.id || '')
|
||||
const { importFailed, showImportFailedDialog } =
|
||||
useImportFailedDetection(packageId)
|
||||
|
||||
provide(ImportFailedKey, {
|
||||
importFailed,
|
||||
showImportFailedDialog
|
||||
})
|
||||
|
||||
const infoItems = computed<InfoItem[]>(() => [
|
||||
{
|
||||
key: 'publisher',
|
||||
@@ -128,17 +181,3 @@ whenever(
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
<style scoped>
|
||||
.hidden-scrollbar {
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
<template>
|
||||
<div v-if="nodePacks?.length" class="flex flex-col items-center mb-6">
|
||||
<div v-if="nodePacks?.length" class="flex flex-col items-center">
|
||||
<slot name="thumbnail">
|
||||
<PackIcon :node-pack="nodePacks[0]" width="24" height="24" />
|
||||
<PackIcon :node-pack="nodePacks[0]" width="204" height="106" />
|
||||
</slot>
|
||||
<h2
|
||||
class="text-2xl font-bold text-center mt-4 mb-2"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ nodePacks[0].name }}
|
||||
<span class="inline-block text-base">{{ nodePacks[0].name }}</span>
|
||||
</slot>
|
||||
</h2>
|
||||
<div class="mt-2 mb-4 w-full max-w-xs flex justify-center">
|
||||
<div
|
||||
v-if="!importFailed"
|
||||
class="mt-2 mb-4 w-full max-w-xs flex justify-center"
|
||||
>
|
||||
<slot name="install-button">
|
||||
<PackUninstallButton
|
||||
v-if="isAllInstalled"
|
||||
v-bind="$attrs"
|
||||
size="md"
|
||||
:node-packs="nodePacks"
|
||||
/>
|
||||
<PackInstallButton v-else v-bind="$attrs" :node-packs="nodePacks" />
|
||||
<PackInstallButton
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
size="md"
|
||||
:node-packs="nodePacks"
|
||||
:has-conflict="hasConflict"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center mb-6">
|
||||
<div v-else class="flex flex-col items-center">
|
||||
<NoResultsPlaceholder
|
||||
:message="$t('manager.status.unknown')"
|
||||
:title="$t('manager.tryAgainLater')"
|
||||
@@ -31,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { inject, ref, watch } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
@@ -39,13 +49,19 @@ import PackUninstallButton from '@/components/dialog/content/manager/button/Pack
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
const { nodePacks, hasConflict } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
hasConflict?: boolean
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
// Inject import failed context from parent
|
||||
const importFailedContext = inject(ImportFailedKey)
|
||||
const importFailed = importFailedContext?.importFailed
|
||||
|
||||
const isAllInstalled = ref(false)
|
||||
watch(
|
||||
[() => nodePacks, () => managerStore.installedPacks],
|
||||
|
||||
@@ -6,16 +6,40 @@
|
||||
<PackIconStacked :node-packs="nodePacks" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ nodePacks.length }}
|
||||
{{ $t('manager.packsSelected') }}
|
||||
<div class="mt-5">
|
||||
<span class="inline-block mr-2 text-blue-500 text-base">{{
|
||||
nodePacks.length
|
||||
}}</span>
|
||||
<span class="text-base">{{ $t('manager.packsSelected') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #install-button>
|
||||
<PackInstallButton :full-width="true" :node-packs="nodePacks" />
|
||||
<!-- Mixed: Don't show any button -->
|
||||
<div v-if="isMixed" class="text-sm text-neutral-500">
|
||||
{{ $t('manager.mixedSelectionMessage') }}
|
||||
</div>
|
||||
<!-- All installed: Show uninstall button -->
|
||||
<PackUninstallButton
|
||||
v-else-if="isAllInstalled"
|
||||
size="md"
|
||||
:node-packs="installedPacks"
|
||||
/>
|
||||
<!-- None installed: Show install button -->
|
||||
<PackInstallButton
|
||||
v-else-if="isNoneInstalled"
|
||||
size="md"
|
||||
:node-packs="notInstalledPacks"
|
||||
:has-conflict="hasConflicts"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
</template>
|
||||
</InfoPanelHeader>
|
||||
<div class="mb-6">
|
||||
<MetadataRow :label="$t('g.status')">
|
||||
<PackStatusMessage status-type="NodeVersionStatusActive" />
|
||||
<PackStatusMessage
|
||||
:status-type="overallStatus"
|
||||
:has-compatibility-issues="hasConflicts"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
:label="$t('manager.totalNodes')"
|
||||
@@ -31,22 +55,80 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { computed, onUnmounted } from 'vue'
|
||||
import { computed, onUnmounted, provide, toRef } from 'vue'
|
||||
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
|
||||
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
|
||||
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const nodePacksRef = toRef(() => nodePacks)
|
||||
|
||||
// Use new composables for cleaner code
|
||||
const {
|
||||
installedPacks,
|
||||
notInstalledPacks,
|
||||
isAllInstalled,
|
||||
isNoneInstalled,
|
||||
isMixed
|
||||
} = usePacksSelection(nodePacksRef)
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef)
|
||||
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const { getNodeDefs } = useComfyRegistryStore()
|
||||
|
||||
// Provide import failed context for PackStatusMessage
|
||||
provide(ImportFailedKey, {
|
||||
importFailed: hasImportFailed,
|
||||
showImportFailedDialog: () => {} // No-op for multi-selection
|
||||
})
|
||||
|
||||
// Check for conflicts in not-installed packages - keep original logic but simplified
|
||||
const packageConflicts = computed(() => {
|
||||
const conflictsByPackage = new Map<string, ConflictDetail[]>()
|
||||
|
||||
for (const pack of notInstalledPacks.value) {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
if (compatibilityCheck.hasConflict && pack.id) {
|
||||
conflictsByPackage.set(pack.id, compatibilityCheck.conflicts)
|
||||
}
|
||||
}
|
||||
|
||||
return conflictsByPackage
|
||||
})
|
||||
|
||||
// Aggregate all unique conflicts for display
|
||||
const conflictInfo = computed<ConflictDetail[]>(() => {
|
||||
const conflictMap = new Map<string, ConflictDetail>()
|
||||
|
||||
packageConflicts.value.forEach((conflicts) => {
|
||||
conflicts.forEach((conflict) => {
|
||||
const key = `${conflict.type}-${conflict.current_value}-${conflict.required_value}`
|
||||
if (!conflictMap.has(key)) {
|
||||
conflictMap.set(key, conflict)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(conflictMap.values())
|
||||
})
|
||||
|
||||
const hasConflicts = computed(() => conflictInfo.value.length > 0)
|
||||
|
||||
const getPackNodes = async (pack: components['schemas']['Node']) => {
|
||||
if (!pack.latest_version?.version) return []
|
||||
const nodeDefs = await getNodeDefs.call({
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<Tabs :value="activeTab">
|
||||
<TabList>
|
||||
<Tab value="description">
|
||||
<TabList class="overflow-x-auto scrollbar-hide">
|
||||
<Tab v-if="hasCompatibilityIssues" value="warning" class="p-2 mr-6">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>⚠️</span>
|
||||
{{ importFailed ? $t('g.error') : $t('g.warning') }}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="description" class="p-2 mr-6">
|
||||
{{ $t('g.description') }}
|
||||
</Tab>
|
||||
<Tab value="nodes">
|
||||
<Tab value="nodes" class="p-2">
|
||||
{{ $t('g.nodes') }}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="overflow-auto">
|
||||
<TabPanels class="overflow-auto py-4 px-2">
|
||||
<TabPanel
|
||||
v-if="hasCompatibilityIssues"
|
||||
value="warning"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<WarningTabPanel
|
||||
:node-pack="nodePack"
|
||||
:conflict-result="conflictResult"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value="description">
|
||||
<DescriptionTabPanel :node-pack="nodePack" />
|
||||
</TabPanel>
|
||||
@@ -27,16 +43,25 @@ import TabList from 'primevue/tablist'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, inject, ref, watchEffect } from 'vue'
|
||||
|
||||
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
|
||||
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
|
||||
import WarningTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/WarningTabPanel.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
hasCompatibilityIssues?: boolean
|
||||
conflictResult?: ConflictDetectionResult | null
|
||||
}>()
|
||||
|
||||
// Inject import failed context from parent
|
||||
const importFailedContext = inject(ImportFailedKey)
|
||||
const importFailed = importFailedContext?.importFailed
|
||||
|
||||
const nodeNames = computed(() => {
|
||||
// @ts-expect-error comfy_nodes is an Algolia-specific field
|
||||
const { comfy_nodes } = nodePack
|
||||
@@ -44,4 +69,17 @@ const nodeNames = computed(() => {
|
||||
})
|
||||
|
||||
const activeTab = ref('description')
|
||||
|
||||
// Watch for compatibility issues and automatically switch to warning tab
|
||||
watchEffect(
|
||||
() => {
|
||||
if (hasCompatibilityIssues) {
|
||||
activeTab.value = 'warning'
|
||||
} else if (activeTab.value === 'warning') {
|
||||
// If currently on warning tab but no issues, switch to description
|
||||
activeTab.value = 'description'
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 text-sm">
|
||||
<div v-for="(section, index) in sections" :key="index" class="mb-4">
|
||||
<div class="mb-1">
|
||||
<div class="mb-3">
|
||||
{{ section.title }}
|
||||
</div>
|
||||
<div class="text-muted break-words">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex py-1.5 text-xs">
|
||||
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}:</div>
|
||||
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}</div>
|
||||
<div class="w-2/3">
|
||||
<slot>{{ value }}</slot>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mt-4 overflow-hidden">
|
||||
<div class="overflow-hidden">
|
||||
<InfoTextSection
|
||||
v-if="nodePack?.description"
|
||||
:sections="descriptionSections"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 mt-4 text-sm">
|
||||
<div class="flex flex-col gap-4 text-sm">
|
||||
<template v-if="mappedNodeDefs?.length">
|
||||
<div
|
||||
v-for="nodeDef in mappedNodeDefs"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
v-if="importFailedInfo"
|
||||
class="cursor-pointer outline-none border-none inline-flex items-center justify-end bg-transparent gap-1"
|
||||
@click="showImportFailedDialog"
|
||||
>
|
||||
<i class="pi pi-code text-base"></i>
|
||||
<span class="dark-theme:text-white text-sm">{{
|
||||
t('serverStart.openLogs')
|
||||
}}</span>
|
||||
</button>
|
||||
<div
|
||||
v-for="(conflict, index) in conflictResult?.conflicts || []"
|
||||
:key="index"
|
||||
class="p-3 bg-yellow-800/20 rounded-md"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm break-words flex-1">
|
||||
{{ getConflictMessage(conflict, $t) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import { t } from '@/i18n'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
const { nodePack, conflictResult } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
conflictResult: ConflictDetectionResult | null | undefined
|
||||
}>()
|
||||
const packageId = computed(() => nodePack?.id || '')
|
||||
const { importFailedInfo, showImportFailedDialog } =
|
||||
useImportFailedDetection(packageId)
|
||||
</script>
|
||||
@@ -21,73 +21,58 @@
|
||||
<PackBanner :node-pack="nodePack" />
|
||||
</template>
|
||||
<template #content>
|
||||
<template v-if="isInstalling">
|
||||
<div
|
||||
class="self-stretch inline-flex flex-col justify-center items-center gap-2 h-full"
|
||||
>
|
||||
<ProgressSpinner />
|
||||
<div
|
||||
class="self-stretch text-center justify-start text-sm font-medium leading-none"
|
||||
<div class="pt-4 px-4 pb-3 w-full h-full">
|
||||
<div class="flex flex-col gap-y-1 w-full h-full">
|
||||
<span
|
||||
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
|
||||
>
|
||||
{{ $t('g.installing') }}...
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="pt-4 px-4 pb-3 w-full h-full">
|
||||
<div class="flex flex-col gap-y-1 w-full h-full">
|
||||
<span
|
||||
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
|
||||
>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
</div>
|
||||
<PackVersionBadge
|
||||
:node-pack="nodePack"
|
||||
:is-selected="isSelected"
|
||||
:fill="false"
|
||||
/>
|
||||
<div
|
||||
v-if="formattedLatestVersionDate"
|
||||
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
|
||||
>
|
||||
{{ formattedLatestVersionDate }}
|
||||
</div>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-if="publisherName"
|
||||
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
|
||||
>
|
||||
{{ publisherName }}
|
||||
</span>
|
||||
<PackVersionBadge
|
||||
:node-pack="nodePack"
|
||||
:is-selected="isSelected"
|
||||
:fill="false"
|
||||
:class="isInstalling ? 'pointer-events-none' : ''"
|
||||
/>
|
||||
<div
|
||||
v-if="formattedLatestVersionDate"
|
||||
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
|
||||
>
|
||||
{{ formattedLatestVersionDate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-if="publisherName"
|
||||
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
|
||||
>
|
||||
{{ publisherName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<PackCardFooter :node-pack="nodePack" />
|
||||
<PackCardFooter :node-pack="nodePack" :is-installing="isInstalling" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Card from 'primevue/card'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, provide, ref } from 'vue'
|
||||
import { computed, provide } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
@@ -114,18 +99,17 @@ const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const isInstalling = ref(false)
|
||||
provide(IsInstallingKey, isInstalling)
|
||||
const { isPackInstalled, isPackEnabled, isPackInstalling } =
|
||||
useComfyManagerStore()
|
||||
|
||||
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
|
||||
const isInstalling = computed(() => isPackInstalling(nodePack?.id))
|
||||
provide(IsInstallingKey, isInstalling)
|
||||
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isDisabled = computed(
|
||||
() => isInstalled.value && !isPackEnabled(nodePack?.id)
|
||||
)
|
||||
|
||||
whenever(isInstalled, () => (isInstalling.value = false))
|
||||
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
)
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-12 flex justify-between items-center px-4 py-2 text-xs text-muted font-medium leading-3"
|
||||
class="h-12 flex justify-between items-center px-4 text-xs text-muted font-medium leading-3"
|
||||
>
|
||||
<div v-if="nodePack.downloads" class="flex items-center gap-1.5">
|
||||
<i class="pi pi-download text-muted"></i>
|
||||
<span>{{ formattedDownloads }}</span>
|
||||
</div>
|
||||
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
|
||||
<PackEnableToggle v-else :node-pack="nodePack" />
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<template v-if="importFailed">
|
||||
<div
|
||||
class="flex justify-center items-center gap-2 cursor-pointer"
|
||||
@click="showImportFailedDialog"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-red-500 text-sm"></i>
|
||||
<span class="text-red-500 text-xs pt-0.5">{{
|
||||
t('manager.failedToInstall')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="!isInstalled">
|
||||
<PackInstallButton
|
||||
:node-packs="[nodePack]"
|
||||
:has-conflict="uninstalledPackConflict.hasConflict"
|
||||
:conflict-info="uninstalledPackConflict.conflicts"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackEnableToggle
|
||||
:node-pack="nodePack"
|
||||
:has-conflict="installedPackHasConflict"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +43,10 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -27,9 +56,29 @@ const { nodePack } = defineProps<{
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
|
||||
const { n } = useI18n()
|
||||
const { n, t } = useI18n()
|
||||
|
||||
const formattedDownloads = computed(() =>
|
||||
nodePack.downloads ? n(nodePack.downloads) : ''
|
||||
)
|
||||
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
|
||||
const { importFailed, showImportFailedDialog } = useImportFailedDetection(
|
||||
nodePack.id
|
||||
)
|
||||
|
||||
const conflicts = computed(
|
||||
() => getConflictsForPackageByID(nodePack.id!) || null
|
||||
)
|
||||
|
||||
const installedPackHasConflict = computed(() => {
|
||||
if (!nodePack.id) return false
|
||||
return !!conflicts.value
|
||||
})
|
||||
|
||||
const uninstalledPackConflict = computed(() => {
|
||||
return checkNodeCompatibility(nodePack)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
<template>
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_ICON : imgSrc"
|
||||
:alt="nodePack.name + ' icon'"
|
||||
class="object-contain rounded-lg max-h-72 max-w-72"
|
||||
:style="{ width: cssWidth, height: cssHeight }"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
<div class="w-full max-w-[204] aspect-[2/1] rounded-lg overflow-hidden">
|
||||
<!-- default banner show -->
|
||||
<div v-if="showDefaultBanner" class="w-full h-full">
|
||||
<img
|
||||
:src="DEFAULT_BANNER"
|
||||
alt="default banner"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- banner_url or icon show -->
|
||||
<div v-else class="relative w-full h-full">
|
||||
<!-- blur background -->
|
||||
<div
|
||||
v-if="imgSrc"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
backgroundImage: `url(${imgSrc})`,
|
||||
filter: 'blur(10px)'
|
||||
}"
|
||||
></div>
|
||||
<!-- image -->
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_BANNER : imgSrc"
|
||||
:alt="nodePack.name + ' banner'"
|
||||
:class="
|
||||
isImageError
|
||||
? 'relative w-full h-full object-cover z-10'
|
||||
: 'relative w-full h-full object-contain z-10'
|
||||
"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -13,29 +39,14 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_ICON = '/assets/images/fallback-gradient-avatar.svg'
|
||||
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
|
||||
|
||||
const {
|
||||
nodePack,
|
||||
width = '4.5rem',
|
||||
height = '4.5rem'
|
||||
} = defineProps<{
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
width?: string
|
||||
height?: string
|
||||
}>()
|
||||
|
||||
const isImageError = ref(false)
|
||||
const shouldShowFallback = computed(
|
||||
() => !nodePack.icon || nodePack.icon.trim() === '' || isImageError.value
|
||||
)
|
||||
const imgSrc = computed(() =>
|
||||
shouldShowFallback.value ? DEFAULT_ICON : nodePack.icon
|
||||
)
|
||||
|
||||
const convertToCssValue = (value: string | number) =>
|
||||
typeof value === 'number' ? `${value}rem` : value
|
||||
|
||||
const cssWidth = computed(() => convertToCssValue(width))
|
||||
const cssHeight = computed(() => convertToCssValue(height))
|
||||
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
|
||||
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
|
||||
</script>
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
<template>
|
||||
<div class="relative w-24 h-24">
|
||||
<div class="relative w-[224px] h-[104px] shadow-xl">
|
||||
<div
|
||||
v-for="(pack, index) in nodePacks.slice(0, maxVisible)"
|
||||
:key="pack.id"
|
||||
class="absolute"
|
||||
class="absolute w-[210px] h-[90px]"
|
||||
:style="{
|
||||
bottom: `${index * offset}px`,
|
||||
right: `${index * offset}px`,
|
||||
zIndex: maxVisible - index
|
||||
}"
|
||||
>
|
||||
<div class="border rounded-lg p-0.5">
|
||||
<PackIcon :node-pack="pack" width="4.5rem" height="4.5rem" />
|
||||
<div class="border rounded-lg shadow-lg p-0.5">
|
||||
<PackIcon :node-pack="pack" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="nodePacks.length > maxVisible"
|
||||
class="absolute -top-2 -right-2 bg-primary rounded-full w-7 h-7 flex items-center justify-center text-xs font-bold shadow-md z-10"
|
||||
>
|
||||
+{{ nodePacks.length - maxVisible }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -28,11 +28,14 @@
|
||||
</div>
|
||||
<PackInstallButton
|
||||
v-if="isMissingTab && missingNodePacks.length > 0"
|
||||
variant="black"
|
||||
:disabled="isLoading || !!error"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
:node-packs="updateAvailableNodePacks"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
@@ -65,8 +68,10 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUpdateButton from '@/components/dialog/content/manager/button/PackUpdateButton.vue'
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import {
|
||||
type SearchOption,
|
||||
SortableAlgoliaField
|
||||
@@ -83,6 +88,7 @@ const { searchResults, sortOptions } = defineProps<{
|
||||
suggestions?: QuerySuggestion[]
|
||||
sortOptions?: SortableField[]
|
||||
isMissingTab?: boolean
|
||||
isUpdateAvailableTab?: boolean
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
@@ -96,6 +102,10 @@ const { t } = useI18n()
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
|
||||
// Use the composable to get update available nodes
|
||||
const { hasUpdateAvailable, updateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
const hasResults = computed(
|
||||
() => searchQuery.value?.trim() && searchResults?.length
|
||||
)
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
class="w-full px-6 py-2 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center text-base leading-none">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isInProgress">
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<!-- 1. Queue running (install/enable/disable etc.) -->
|
||||
<template v-if="isQueueRunning">
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<!-- 3. Restarting -->
|
||||
<template v-else-if="isRestarting">
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<span>{{ $t('manager.restartingBackend') }}</span>
|
||||
</template>
|
||||
<!-- 4. Restart completed -->
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<span class="mr-2">🎉</span>
|
||||
<span>{{ $t('manager.extensionsSuccessfullyInstalled') }}</span>
|
||||
</template>
|
||||
<!-- 2. Tasks completed (waiting for restart) -->
|
||||
<template v-else>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
<span class="mr-2">✅</span>
|
||||
<span>
|
||||
{{ $t('manager.clickToFinishSetup') }}
|
||||
'{{ $t('manager.applyChanges') }}'
|
||||
{{ $t('manager.toFinishSetup') }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress"
|
||||
rounded
|
||||
outlined
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('g.restart') }}
|
||||
</Button>
|
||||
<!-- 1. Queue running -->
|
||||
<template v-if="isQueueRunning">
|
||||
<span class="text-sm text-neutral-700 dark-theme:text-neutral-400">
|
||||
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ taskLogs }}
|
||||
</span>
|
||||
<Button
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
@@ -44,20 +51,46 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 2. Tasks completed (waiting for restart) -->
|
||||
<template v-else-if="!isRestarting && !isRestartCompleted">
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
/>
|
||||
</div>
|
||||
outlined
|
||||
class="rounded-md border-2 px-3 text-neutral-600 border-neutral-900 hover:bg-neutral-100 dark-theme:bg-none dark-theme:text-white dark-theme:border-white dark-theme:hover:bg-neutral-700"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('manager.applyChanges') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- 3. Restarting -->
|
||||
<template v-else-if="isRestarting">
|
||||
<!-- No buttons during restart -->
|
||||
</template>
|
||||
|
||||
<!-- 4. Restart completed -->
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<!-- No buttons after restart completed (auto-close after 3 seconds) -->
|
||||
</template>
|
||||
|
||||
<!-- Common: Close button -->
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -65,9 +98,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
@@ -77,19 +112,35 @@ import {
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { performConflictDetection } = useConflictDetection()
|
||||
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
// State management for restart process
|
||||
const isRestarting = ref<boolean>(false)
|
||||
const isRestartCompleted = ref<boolean>(false)
|
||||
|
||||
// Computed states
|
||||
const isQueueRunning = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
const taskLogs = computed(() => comfyManagerStore.taskLogs.length)
|
||||
|
||||
const completedTasksCount = computed(() => {
|
||||
if (isQueueRunning.value && taskLogs.value > 0) {
|
||||
return taskLogs.value - 1
|
||||
}
|
||||
return taskLogs.value
|
||||
})
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const fallbackTaskName = t('manager.installingDependencies')
|
||||
const currentTaskName = computed(() => {
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
@@ -97,21 +148,55 @@ const currentTaskName = computed(() => {
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
const onReconnect = async () => {
|
||||
// Refresh manager state
|
||||
// Store original toast setting value
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
|
||||
comfyManagerStore.clearLogs()
|
||||
comfyManagerStore.setStale()
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
|
||||
// Refresh node definitions
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
isRestarting.value = true
|
||||
|
||||
// Reload workflow
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
|
||||
// Run conflict detection on all installed packages after restart
|
||||
await performConflictDetection()
|
||||
} finally {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
closeDialog()
|
||||
comfyManagerStore.clearLogs()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
} catch (error) {
|
||||
// If restart fails, restore settings and reset state
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
closeDialog() // Close dialog on error
|
||||
throw error
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,17 @@
|
||||
@mouseenter="onMenuItemHover(menuItem.key, $event)"
|
||||
@mouseleave="onMenuItemLeave(menuItem.key)"
|
||||
>
|
||||
<i :class="menuItem.icon" class="help-menu-icon" />
|
||||
<div class="help-menu-icon-container">
|
||||
<div class="help-menu-icon">
|
||||
<component
|
||||
:is="menuItem.icon"
|
||||
v-if="typeof menuItem.icon === 'object'"
|
||||
:size="16"
|
||||
/>
|
||||
<i v-else :class="menuItem.icon" />
|
||||
</div>
|
||||
<div v-if="menuItem.showRedDot" class="menu-red-dot" />
|
||||
</div>
|
||||
<span class="menu-label">{{ menuItem.label }}</span>
|
||||
<i v-if="menuItem.key === 'more'" class="pi pi-chevron-right" />
|
||||
</button>
|
||||
@@ -120,9 +130,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue'
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Component,
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
@@ -133,12 +153,13 @@ import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
// Types
|
||||
interface MenuItem {
|
||||
key: string
|
||||
icon?: string
|
||||
icon?: string | Component
|
||||
label?: string
|
||||
action?: () => void
|
||||
visible?: boolean
|
||||
type?: 'item' | 'divider'
|
||||
items?: MenuItem[]
|
||||
showRedDot?: boolean
|
||||
}
|
||||
|
||||
// Constants
|
||||
@@ -170,6 +191,7 @@ const { t, locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -188,6 +210,10 @@ const showVersionUpdates = computed(() =>
|
||||
settingStore.get('Comfy.Notification.ShowVersionUpdates')
|
||||
)
|
||||
|
||||
// Use conflict acknowledgment state from composable
|
||||
const { shouldShowRedDot: shouldShowManagerRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const moreItems = computed<MenuItem[]>(() => {
|
||||
const allMoreItems: MenuItem[] = [
|
||||
{
|
||||
@@ -281,6 +307,17 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'manager',
|
||||
type: 'item',
|
||||
icon: PuzzleIcon,
|
||||
label: t('helpCenter.managerExtension'),
|
||||
showRedDot: shouldShowManagerRedDot.value,
|
||||
action: () => {
|
||||
dialogService.showManagerDialog()
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'more',
|
||||
type: 'item',
|
||||
@@ -516,6 +553,13 @@ onMounted(async () => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.help-menu-icon-container {
|
||||
position: relative;
|
||||
margin-right: 0.75rem;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.help-menu-icon {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1rem;
|
||||
@@ -523,9 +567,26 @@ onMounted(async () => {
|
||||
width: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.help-menu-icon svg {
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
|
||||
.menu-red-dot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ff3b30;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--p-content-background);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
const { locale, t } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
|
||||
// Emit event for parent component
|
||||
const emit = defineEmits<{
|
||||
'whats-new-dismissed': []
|
||||
}>()
|
||||
|
||||
// Local state for dismissed status
|
||||
const isDismissed = ref(false)
|
||||
|
||||
@@ -134,6 +139,10 @@ const closePopup = async () => {
|
||||
await releaseStore.handleWhatsNewSeen(latestRelease.value.version)
|
||||
}
|
||||
hide()
|
||||
|
||||
// Emit event to notify parent that What's New was dismissed
|
||||
// Parent can then check if conflict modal should be shown
|
||||
emit('whats-new-dismissed')
|
||||
}
|
||||
|
||||
// Learn more handled by anchor href
|
||||
|
||||
41
src/components/icons/PuzzleIcon.vue
Normal file
41
src/components/icons/PuzzleIcon.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
:class="iconClass"
|
||||
>
|
||||
<g clip-path="url(#clip0_1099_16244)">
|
||||
<path
|
||||
d="M4.99992 3.00016C4.99992 2.07969 5.74611 1.3335 6.66658 1.3335C7.58706 1.3335 8.33325 2.07969 8.33325 3.00016V4.00016H8.99992C9.9318 4.00016 10.3977 4.00016 10.7653 4.1524C11.2553 4.35539 11.6447 4.74474 11.8477 5.2348C11.9999 5.60234 11.9999 6.06828 11.9999 7.00016H12.9999C13.9204 7.00016 14.6666 7.74635 14.6666 8.66683C14.6666 9.5873 13.9204 10.3335 12.9999 10.3335H11.9999V11.4668C11.9999 12.5869 11.9999 13.147 11.7819 13.5748C11.5902 13.9511 11.2842 14.2571 10.9079 14.4488C10.4801 14.6668 9.92002 14.6668 8.79992 14.6668H8.33325V13.5002C8.33325 12.6717 7.66168 12.0002 6.83325 12.0002C6.00482 12.0002 5.33325 12.6717 5.33325 13.5002V14.6668H4.53325C3.41315 14.6668 2.85309 14.6668 2.42527 14.4488C2.04895 14.2571 1.74299 13.9511 1.55124 13.5748C1.33325 13.147 1.33325 12.5869 1.33325 11.4668V10.3335H2.33325C3.25373 10.3335 3.99992 9.5873 3.99992 8.66683C3.99992 7.74635 3.25373 7.00016 2.33325 7.00016H1.33325C1.33325 6.06828 1.33325 5.60234 1.48549 5.2348C1.68848 4.74474 2.07783 4.35539 2.56789 4.1524C2.93543 4.00016 3.40137 4.00016 4.33325 4.00016H4.99992V3.00016Z"
|
||||
:stroke="color"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1099_16244">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
class?: string
|
||||
}
|
||||
const {
|
||||
size = 16,
|
||||
color = 'currentColor',
|
||||
class: className
|
||||
} = defineProps<Props>()
|
||||
const iconClass = computed(() => className || '')
|
||||
</script>
|
||||
27
src/components/icons/VerifiedIcon.vue
Normal file
27
src/components/icons/VerifiedIcon.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
:class="iconClass"
|
||||
>
|
||||
<path
|
||||
d="M8.00049 1.3335C8.73661 1.33367 9.33332 1.93038 9.3335 2.6665V2.83447C9.82278 2.96041 10.2851 3.15405 10.7095 3.40479L10.8286 3.28564C11.3493 2.76525 12.1937 2.76519 12.7144 3.28564C13.235 3.80626 13.2348 4.65067 12.7144 5.17139L12.5952 5.29053C12.846 5.71486 13.0396 6.17725 13.1655 6.6665H13.3335C14.0699 6.6665 14.6665 7.26411 14.6665 8.00049C14.6663 8.73672 14.0698 9.3335 13.3335 9.3335H13.1655C13.0396 9.82284 12.846 10.2851 12.5952 10.7095L12.7144 10.8286C13.235 11.3493 13.235 12.1937 12.7144 12.7144C12.1937 13.235 11.3493 13.235 10.8286 12.7144L10.7095 12.5952C10.2851 12.846 9.82284 13.0396 9.3335 13.1655V13.3335C9.3335 14.0698 8.73672 14.6663 8.00049 14.6665C7.26411 14.6665 6.6665 14.0699 6.6665 13.3335V13.1655C6.17725 13.0396 5.71486 12.846 5.29053 12.5952L5.17139 12.7144C4.65067 13.2348 3.80626 13.235 3.28564 12.7144C2.76519 12.1937 2.76525 11.3493 3.28564 10.8286L3.40479 10.7095C3.15405 10.2851 2.96041 9.82278 2.83447 9.3335H2.6665C1.93038 9.33332 1.33367 8.73661 1.3335 8.00049C1.3335 7.26422 1.93027 6.66668 2.6665 6.6665H2.83447C2.96043 6.17722 3.15403 5.71488 3.40479 5.29053L3.28564 5.17139C2.76536 4.65065 2.76508 3.80621 3.28564 3.28564C3.80621 2.76508 4.65065 2.76536 5.17139 3.28564L5.29053 3.40479C5.71488 3.15403 6.17722 2.96043 6.6665 2.83447V2.6665C6.66668 1.93027 7.26422 1.3335 8.00049 1.3335ZM7.3335 8.00049L6.00049 6.6665L4.6665 8.00049L7.3335 10.6665L11.3335 6.6665L10.0005 5.3335L7.3335 8.00049Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
class?: string
|
||||
}
|
||||
const { size = 16, color = '#60A5FA', class: className } = defineProps<Props>()
|
||||
const iconClass = computed(() => className || '')
|
||||
</script>
|
||||
@@ -42,6 +42,7 @@
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
}"
|
||||
@whats-new-dismissed="handleWhatsNewDismissed"
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
@@ -63,6 +64,9 @@ import { computed, onMounted } from 'vue'
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -72,8 +76,22 @@ import SidebarIcon from './SidebarIcon.vue'
|
||||
const settingStore = useSettingStore()
|
||||
const releaseStore = useReleaseStore()
|
||||
const helpCenterStore = useHelpCenterStore()
|
||||
const { shouldShowRedDot } = storeToRefs(releaseStore)
|
||||
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
|
||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||
|
||||
const conflictDetection = useConflictDetection()
|
||||
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
|
||||
// Use conflict acknowledgment state from composable - call only once
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed(() => {
|
||||
const releaseRedDot = showReleaseRedDot
|
||||
return releaseRedDot || shouldShowConflictRedDot.value
|
||||
})
|
||||
|
||||
const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
@@ -89,6 +107,36 @@ const closeHelpCenter = () => {
|
||||
helpCenterStore.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle What's New popup dismissal
|
||||
* Check if conflict modal should be shown after ComfyUI update
|
||||
*/
|
||||
const handleWhatsNewDismissed = async () => {
|
||||
try {
|
||||
// Check if conflict modal should be shown after update
|
||||
const shouldShow =
|
||||
await conflictDetection.shouldShowConflictModalAfterUpdate()
|
||||
if (shouldShow) {
|
||||
showConflictModal()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HelpCenter] Error checking conflict modal:', error)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Show the node conflict dialog with current conflict data
|
||||
*/
|
||||
const showConflictModal = () => {
|
||||
showNodeConflictDialog({
|
||||
showAfterWhatsNew: true,
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize release store on mount
|
||||
onMounted(async () => {
|
||||
// Initialize release store to fetch releases for toast and popup
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed, onUnmounted } from 'vue'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
@@ -9,6 +9,10 @@ import type { components } from '@/types/comfyRegistryTypes'
|
||||
export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
// Flag to prevent duplicate fetches during initialization
|
||||
const isInitializing = ref(false)
|
||||
const lastFetchedIds = ref<string>('')
|
||||
|
||||
const installedPackIds = computed(() =>
|
||||
Array.from(comfyManagerStore.installedPacksIds)
|
||||
)
|
||||
@@ -20,24 +24,59 @@ export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
|
||||
packs.filter((pack) => comfyManagerStore.isPackInstalled(pack.id))
|
||||
|
||||
const startFetchInstalled = async () => {
|
||||
await comfyManagerStore.refreshInstalledList()
|
||||
await startFetch()
|
||||
// Prevent duplicate calls during initialization
|
||||
if (isInitializing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isInitializing.value = true
|
||||
try {
|
||||
if (comfyManagerStore.installedPacksIds.size === 0) {
|
||||
await comfyManagerStore.refreshInstalledList()
|
||||
}
|
||||
await startFetch()
|
||||
} finally {
|
||||
isInitializing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// When installedPackIds changes, we need to update the nodePacks
|
||||
whenever(installedPackIds, async () => {
|
||||
await startFetch()
|
||||
// But only if the IDs actually changed (not just array reference)
|
||||
whenever(installedPackIds, async (newIds) => {
|
||||
const newIdsStr = newIds.sort().join(',')
|
||||
if (newIdsStr !== lastFetchedIds.value && !isInitializing.value) {
|
||||
lastFetchedIds.value = newIdsStr
|
||||
await startFetch()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// Create a computed property that provides installed pack info with versions
|
||||
const installedPacksWithVersions = computed(() => {
|
||||
const result: Array<{ id: string; version: string }> = []
|
||||
|
||||
for (const pack of Object.values(comfyManagerStore.installedPacks)) {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
if (id) {
|
||||
result.push({
|
||||
id,
|
||||
version: pack.ver ?? ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
isReady,
|
||||
installedPacks: nodePacks,
|
||||
installedPacksWithVersions,
|
||||
startFetchInstalled,
|
||||
filterInstalledPack
|
||||
}
|
||||
|
||||
51
src/composables/nodePack/usePacksSelection.ts
Normal file
51
src/composables/nodePack/usePacksSelection.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
export type SelectionState = 'all-installed' | 'none-installed' | 'mixed'
|
||||
|
||||
/**
|
||||
* Composable for managing multi-package selection states
|
||||
* Handles installation status tracking and selection state determination
|
||||
*/
|
||||
export function usePacksSelection(nodePacks: Ref<NodePack[]>) {
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedPacks = computed(() =>
|
||||
nodePacks.value.filter((pack) => managerStore.isPackInstalled(pack.id))
|
||||
)
|
||||
|
||||
const notInstalledPacks = computed(() =>
|
||||
nodePacks.value.filter((pack) => !managerStore.isPackInstalled(pack.id))
|
||||
)
|
||||
|
||||
const isAllInstalled = computed(
|
||||
() => installedPacks.value.length === nodePacks.value.length
|
||||
)
|
||||
|
||||
const isNoneInstalled = computed(
|
||||
() => notInstalledPacks.value.length === nodePacks.value.length
|
||||
)
|
||||
|
||||
const isMixed = computed(
|
||||
() => installedPacks.value.length > 0 && notInstalledPacks.value.length > 0
|
||||
)
|
||||
|
||||
const selectionState = computed<SelectionState>(() => {
|
||||
if (isAllInstalled.value) return 'all-installed'
|
||||
if (isNoneInstalled.value) return 'none-installed'
|
||||
return 'mixed'
|
||||
})
|
||||
|
||||
return {
|
||||
installedPacks,
|
||||
notInstalledPacks,
|
||||
isAllInstalled,
|
||||
isNoneInstalled,
|
||||
isMixed,
|
||||
selectionState
|
||||
}
|
||||
}
|
||||
63
src/composables/nodePack/usePacksStatus.ts
Normal file
63
src/composables/nodePack/usePacksStatus.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
type NodeStatus = components['schemas']['NodeStatus']
|
||||
type NodeVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
const STATUS_PRIORITY = [
|
||||
'NodeStatusBanned',
|
||||
'NodeVersionStatusBanned',
|
||||
'NodeStatusDeleted',
|
||||
'NodeVersionStatusDeleted',
|
||||
'NodeVersionStatusFlagged',
|
||||
'NodeVersionStatusPending',
|
||||
'NodeStatusActive',
|
||||
'NodeVersionStatusActive'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Composable for managing package status with priority
|
||||
* Handles import failures and determines the most important status
|
||||
*/
|
||||
export function usePacksStatus(nodePacks: Ref<NodePack[]>) {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
const hasImportFailed = computed(() => {
|
||||
return nodePacks.value.some((pack) => {
|
||||
if (!pack.id) return false
|
||||
const conflicts = conflictDetectionStore.getConflictsForPackageByID(
|
||||
pack.id
|
||||
)
|
||||
return (
|
||||
conflicts?.conflicts?.some((c) => c.type === 'import_failed') || false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const overallStatus = computed<NodeStatus | NodeVersionStatus>(() => {
|
||||
// Check for import failed first (highest priority for installed packages)
|
||||
if (hasImportFailed.value) {
|
||||
// Import failed doesn't have a specific status enum, so we return active
|
||||
// but the PackStatusMessage will handle it via hasImportFailed prop
|
||||
return 'NodeVersionStatusActive' as NodeVersionStatus
|
||||
}
|
||||
|
||||
// Find the highest priority status from all packages
|
||||
for (const priorityStatus of STATUS_PRIORITY) {
|
||||
if (nodePacks.value.some((pack) => pack.status === priorityStatus)) {
|
||||
return priorityStatus as NodeStatus | NodeVersionStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Default to active if no specific status found
|
||||
return 'NodeVersionStatusActive' as NodeVersionStatus
|
||||
})
|
||||
|
||||
return {
|
||||
hasImportFailed,
|
||||
overallStatus
|
||||
}
|
||||
}
|
||||
65
src/composables/nodePack/useUpdateAvailableNodes.ts
Normal file
65
src/composables/nodePack/useUpdateAvailableNodes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable to find NodePacks that have updates available
|
||||
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||
* Automatically fetches installed pack data when initialized
|
||||
*/
|
||||
export const useUpdateAvailableNodes = () => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { installedPacks, isLoading, error, startFetchInstalled } =
|
||||
useInstalledPacks()
|
||||
|
||||
// Check if a pack has updates available (same logic as usePackUpdateStatus)
|
||||
const isOutdatedPack = (pack: components['schemas']['Node']) => {
|
||||
const isInstalled = comfyManagerStore.isPackInstalled(pack?.id)
|
||||
if (!isInstalled) return false
|
||||
|
||||
const installedVersion = comfyManagerStore.getInstalledPackVersion(
|
||||
pack.id ?? ''
|
||||
)
|
||||
const latestVersion = pack.latest_version?.version
|
||||
|
||||
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion)
|
||||
|
||||
if (isNightlyPack || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
return compareVersions(latestVersion, installedVersion) > 0
|
||||
}
|
||||
|
||||
// Same filtering logic as ManagerDialogContent.vue
|
||||
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter(isOutdatedPack)
|
||||
|
||||
// Filter only outdated packs from installed packs
|
||||
const updateAvailableNodePacks = computed(() => {
|
||||
if (!installedPacks.value.length) return []
|
||||
return filterOutdatedPacks(installedPacks.value)
|
||||
})
|
||||
|
||||
// Check if there are any outdated packs
|
||||
const hasUpdateAvailable = computed(() => {
|
||||
return updateAvailableNodePacks.value.length > 0
|
||||
})
|
||||
|
||||
// Automatically fetch installed pack data when composable is used
|
||||
onMounted(async () => {
|
||||
if (!installedPacks.value.length && !isLoading.value) {
|
||||
await startFetchInstalled()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
updateAvailableNodePacks,
|
||||
hasUpdateAvailable,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
101
src/composables/useConflictAcknowledgment.ts
Normal file
101
src/composables/useConflictAcknowledgment.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
|
||||
/**
|
||||
* LocalStorage keys for conflict acknowledgment tracking
|
||||
*/
|
||||
const STORAGE_KEYS = {
|
||||
CONFLICT_MODAL_DISMISSED: 'Comfy.ConflictModalDismissed',
|
||||
CONFLICT_RED_DOT_DISMISSED: 'Comfy.ConflictRedDotDismissed',
|
||||
CONFLICT_WARNING_BANNER_DISMISSED: 'Comfy.ConflictWarningBannerDismissed'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Interface for conflict acknowledgment state
|
||||
*/
|
||||
interface ConflictAcknowledgmentState {
|
||||
modal_dismissed: boolean
|
||||
red_dot_dismissed: boolean
|
||||
warning_banner_dismissed: boolean
|
||||
}
|
||||
|
||||
// Shared state - initialized once and reused across all composable calls
|
||||
const modalDismissed = useStorage(STORAGE_KEYS.CONFLICT_MODAL_DISMISSED, false)
|
||||
const redDotDismissed = useStorage(
|
||||
STORAGE_KEYS.CONFLICT_RED_DOT_DISMISSED,
|
||||
false
|
||||
)
|
||||
const warningBannerDismissed = useStorage(
|
||||
STORAGE_KEYS.CONFLICT_WARNING_BANNER_DISMISSED,
|
||||
false
|
||||
)
|
||||
|
||||
/**
|
||||
* Composable for managing conflict acknowledgment state in localStorage
|
||||
*
|
||||
* This handles:
|
||||
* - Tracking whether conflict modal has been dismissed
|
||||
* - Tracking whether red dot notification has been cleared
|
||||
* - Managing per-package conflict acknowledgments
|
||||
* - Detecting ComfyUI version changes to reset acknowledgment state
|
||||
*/
|
||||
export function useConflictAcknowledgment() {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
// Create computed state object for backward compatibility
|
||||
const state = computed<ConflictAcknowledgmentState>(() => ({
|
||||
modal_dismissed: modalDismissed.value,
|
||||
red_dot_dismissed: redDotDismissed.value,
|
||||
warning_banner_dismissed: warningBannerDismissed.value
|
||||
}))
|
||||
|
||||
/**
|
||||
* Mark red dot notification as dismissed
|
||||
*/
|
||||
function dismissRedDotNotification(): void {
|
||||
redDotDismissed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark manager warning banner as dismissed
|
||||
*/
|
||||
function dismissWarningBanner(): void {
|
||||
warningBannerDismissed.value = true
|
||||
redDotDismissed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark conflicts as seen (unified function for help center and manager)
|
||||
*/
|
||||
function markConflictsAsSeen(): void {
|
||||
redDotDismissed.value = true
|
||||
modalDismissed.value = true
|
||||
warningBannerDismissed.value = true
|
||||
}
|
||||
|
||||
const hasConflicts = computed(() => conflictDetectionStore.hasConflicts)
|
||||
const shouldShowConflictModal = computed(() => !modalDismissed.value)
|
||||
const shouldShowRedDot = computed(() => {
|
||||
if (!hasConflicts.value) return false
|
||||
if (redDotDismissed.value) return false
|
||||
return true
|
||||
})
|
||||
const shouldShowManagerBanner = computed(() => {
|
||||
return hasConflicts.value && !warningBannerDismissed.value
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
acknowledgmentState: state,
|
||||
shouldShowConflictModal,
|
||||
shouldShowRedDot,
|
||||
shouldShowManagerBanner,
|
||||
|
||||
// Methods
|
||||
dismissRedDotNotification,
|
||||
dismissWarningBanner,
|
||||
markConflictsAsSeen
|
||||
}
|
||||
}
|
||||
1336
src/composables/useConflictDetection.ts
Normal file
1336
src/composables/useConflictDetection.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,10 +16,12 @@ import {
|
||||
import { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
@@ -33,6 +35,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import {
|
||||
getAllNonIoNodesInSubgraph,
|
||||
getExecutionIdsForSelectedNodes
|
||||
@@ -711,12 +714,54 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager',
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
icon: 'pi pi-puzzle',
|
||||
label: 'Toggle the Custom Nodes Manager',
|
||||
label: 'Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: async () => {
|
||||
const { is_legacy_manager_ui } =
|
||||
(await useComfyManagerService().isLegacyManagerUI()) ?? {}
|
||||
|
||||
if (is_legacy_manager_ui === true) {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
} else {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
|
||||
icon: 'pi pi-sync',
|
||||
label: 'Check for Custom Node Updates',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.toggleManagerDialog()
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.UpdateAvailable
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowMissingPacks',
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
label: 'Install Missing Custom Nodes',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -880,6 +925,84 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const modelSelectorDialog = useModelSelectorDialog()
|
||||
modelSelectorDialog.show()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
|
||||
icon: 'pi pi-bars',
|
||||
label: 'Custom Nodes (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowLegacyManagerMenu',
|
||||
icon: 'mdi mdi-puzzle',
|
||||
label: 'Manager Menu (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModels',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: false })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models and Execution Cache',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: true })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
36
src/composables/useFeatureFlags.ts
Normal file
36
src/composables/useFeatureFlags.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
/**
|
||||
* Known server feature flags (top-level, not extensions)
|
||||
*/
|
||||
export enum ServerFeatureFlag {
|
||||
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size'
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive access to feature flags
|
||||
*/
|
||||
export function useFeatureFlags() {
|
||||
// Create reactive state that tracks server feature flags
|
||||
const flags = reactive({
|
||||
get supportsPreviewMetadata() {
|
||||
return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||
},
|
||||
get maxUploadSize() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
}
|
||||
})
|
||||
|
||||
// Create a reactive computed for any feature flag
|
||||
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
|
||||
return computed(() => api.getServerFeature(featurePath, defaultValue))
|
||||
}
|
||||
|
||||
return {
|
||||
flags: readonly(flags),
|
||||
featureFlag
|
||||
}
|
||||
}
|
||||
85
src/composables/useImportFailedDetection.ts
Normal file
85
src/composables/useImportFailedDetection.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { type ComputedRef, computed, unref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
|
||||
/**
|
||||
* Extracting import failed conflicts from conflict list
|
||||
*/
|
||||
function extractImportFailedConflicts(conflicts?: ConflictDetail[] | null) {
|
||||
if (!conflicts) return null
|
||||
|
||||
const importFailedConflicts = conflicts.filter(
|
||||
(item): item is ConflictDetail => item.type === 'import_failed'
|
||||
)
|
||||
|
||||
return importFailedConflicts.length > 0 ? importFailedConflicts : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating import failed dialog
|
||||
*/
|
||||
function createImportFailedDialog() {
|
||||
const { t } = useI18n()
|
||||
const { showErrorDialog } = useDialogService()
|
||||
|
||||
return (importFailedInfo: ConflictDetail[] | null) => {
|
||||
if (importFailedInfo) {
|
||||
const errorMessage =
|
||||
importFailedInfo
|
||||
.map((conflict) => conflict.required_value)
|
||||
.filter(Boolean)
|
||||
.join('\n') || t('manager.importFailedGenericError')
|
||||
|
||||
const error = new Error(errorMessage)
|
||||
|
||||
showErrorDialog(error, {
|
||||
title: t('manager.failedToInstall'),
|
||||
reportType: 'importFailedError'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for detecting and handling import failed conflicts
|
||||
* @param packageId - Package ID string or computed ref
|
||||
* @returns Object with import failed detection and dialog handler
|
||||
*/
|
||||
export function useImportFailedDetection(
|
||||
packageId?: string | ComputedRef<string> | null
|
||||
) {
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
|
||||
const isInstalled = computed(() =>
|
||||
packageId ? isPackInstalled(unref(packageId)) : false
|
||||
)
|
||||
|
||||
const conflicts = computed(() => {
|
||||
const currentPackageId = unref(packageId)
|
||||
if (!currentPackageId || !isInstalled.value) return null
|
||||
return getConflictsForPackageByID(currentPackageId) || null
|
||||
})
|
||||
|
||||
const importFailedInfo = computed(() => {
|
||||
return extractImportFailedConflicts(conflicts.value?.conflicts)
|
||||
})
|
||||
|
||||
const importFailed = computed(() => {
|
||||
return importFailedInfo.value !== null
|
||||
})
|
||||
|
||||
const showImportFailedDialog = createImportFailedDialog()
|
||||
|
||||
return {
|
||||
importFailedInfo,
|
||||
importFailed,
|
||||
showImportFailedDialog: () =>
|
||||
showImportFailedDialog(importFailedInfo.value),
|
||||
isInstalled
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ export const CORE_MENU_COMMANDS = [
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[
|
||||
['Manager'],
|
||||
[
|
||||
'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
'Comfy.Manager.ShowMissingPacks',
|
||||
'Comfy.Manager.ShowUpdateAvailablePacks'
|
||||
]
|
||||
],
|
||||
[
|
||||
['Help'],
|
||||
[
|
||||
|
||||
@@ -13,6 +13,13 @@ import type { SettingParams } from '@/types/settingTypes'
|
||||
* when they are no longer needed.
|
||||
*/
|
||||
export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Validation.Workflows',
|
||||
name: 'Validate workflows',
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Load Default Workflow"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Toggle the Custom Nodes Manager"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Custom Nodes (Legacy)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Manager Menu (Legacy)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Install Missing Custom Nodes"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Check for Custom Node Updates"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle the Custom Nodes Manager Progress Bar"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Open Mask Editor for Selected Node"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Unload Models"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Unload Models and Execution Cache"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"confirmed": "Confirmed",
|
||||
"reset": "Reset",
|
||||
"resetAll": "Reset All",
|
||||
"clearFilters": "Clear Filters",
|
||||
"resetAllKeybindingsTooltip": "Reset all keybindings to default",
|
||||
"customizeFolder": "Customize Folder",
|
||||
"icon": "Icon",
|
||||
@@ -110,6 +109,7 @@
|
||||
"resultsCount": "Found {count} Results",
|
||||
"status": "Status",
|
||||
"description": "Description",
|
||||
"warning": "Warning",
|
||||
"name": "Name",
|
||||
"category": "Category",
|
||||
"sort": "Sort",
|
||||
@@ -144,16 +144,22 @@
|
||||
"itemsSelected": "{selectedCount} items selected",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information.",
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"micPermissionDenied": "Microphone permission denied",
|
||||
"noAudioRecorded": "No audio recorded",
|
||||
"nodesRunning": "nodes running",
|
||||
"duplicate": "Duplicate"
|
||||
"nodesRunning": "nodes running"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"failed": "Failed ({count})",
|
||||
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
|
||||
"legacyManagerUI": "Use Legacy UI",
|
||||
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",
|
||||
"failed": "Failed",
|
||||
"failedToInstall": "Failed to Install",
|
||||
"installError": "Install Error",
|
||||
"importFailedGenericError": "Package failed to import. Check the console for more details.",
|
||||
"noNodesFound": "No nodes found",
|
||||
"noNodesFoundDescription": "The pack's nodes either could not be parsed, or the pack is a frontend extension only and doesn't have any nodes.",
|
||||
"installationQueue": "Installation Queue",
|
||||
@@ -162,6 +168,12 @@
|
||||
"inWorkflow": "In Workflow",
|
||||
"infoPanelEmpty": "Click an item to see the info",
|
||||
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
|
||||
"clickToFinishSetup": "Click",
|
||||
"toFinishSetup": "to finish setup",
|
||||
"applyChanges": "Apply Changes",
|
||||
"restartingBackend": "Restarting backend to apply changes...",
|
||||
"extensionsSuccessfullyInstalled": "Extension(s) successfully installed and are ready to use!",
|
||||
"installingDependencies": "Installing dependencies...",
|
||||
"loadingVersions": "Loading versions...",
|
||||
"selectVersion": "Select Version",
|
||||
"downloads": "Downloads",
|
||||
@@ -170,6 +182,8 @@
|
||||
"uninstalling": "Uninstalling",
|
||||
"update": "Update",
|
||||
"uninstallSelected": "Uninstall Selected",
|
||||
"updateSelected": "Update Selected",
|
||||
"updateAll": "Update All",
|
||||
"updatingAllPacks": "Updating all packages",
|
||||
"license": "License",
|
||||
"nightlyVersion": "Nightly",
|
||||
@@ -181,6 +195,7 @@
|
||||
"noResultsFound": "No results found matching your search.",
|
||||
"tryDifferentSearch": "Please try a different search query.",
|
||||
"tryAgainLater": "Please try again later.",
|
||||
"gettingInfo": "Getting info...",
|
||||
"nodePack": "Node Pack",
|
||||
"searchPlaceholder": "Search",
|
||||
"version": "Version",
|
||||
@@ -188,14 +203,17 @@
|
||||
"noDescription": "No description available",
|
||||
"installSelected": "Install Selected",
|
||||
"installAllMissingNodes": "Install All Missing Nodes",
|
||||
"packsSelected": "Packs Selected",
|
||||
"packsSelected": "packs selected",
|
||||
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"pending": "Pending",
|
||||
"flagged": "Flagged",
|
||||
"deleted": "Deleted",
|
||||
"banned": "Banned",
|
||||
"unknown": "Unknown"
|
||||
"unknown": "Unknown",
|
||||
"conflicting": "Conflicting",
|
||||
"importFailed": "Install Error"
|
||||
},
|
||||
"sort": {
|
||||
"downloads": "Most Popular",
|
||||
@@ -207,6 +225,34 @@
|
||||
"nodePack": "Node Pack",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"conflicts": {
|
||||
"title": "Node Pack Issues Detected!",
|
||||
"description": "We’ve detected conflicts between some of your extensions and the new version of ComfyUI. By updating you risk breaking workflows that rely on those extensions.",
|
||||
"info": "If you continue with the update, the conflicting extensions will be disabled automatically. You can review and manage them anytime in the ComfyUI Manager.",
|
||||
"extensionAtRisk": "Extension at Risk",
|
||||
"conflicts": "Conflicts",
|
||||
"importFailedExtensions": "Import Failed Extensions",
|
||||
"conflictInfoTitle": "Why is this happening?",
|
||||
"installAnyway": "Install Anyway",
|
||||
"enableAnyway": "Enable Anyway",
|
||||
"understood": "Understood",
|
||||
"warningBanner": {
|
||||
"title": "Some extensions are disabled due to incompatibility with your current setup",
|
||||
"message": "These extensions require versions of system packages that differ from your current setup. Installing them may override core dependencies and affect other extensions or workflows.",
|
||||
"button": "Learn More..."
|
||||
},
|
||||
"conflictMessages": {
|
||||
"comfyui_version": "ComfyUI version mismatch (current: {current}, required: {required})",
|
||||
"frontend_version": "Frontend version mismatch (current: {current}, required: {required})",
|
||||
"os": "Operating system not supported (current: {current}, required: {required})",
|
||||
"accelerator": "GPU/Accelerator not supported (available: {current}, required: {required})",
|
||||
"generic": "Compatibility issue (current: {current}, required: {required})",
|
||||
"banned": "This package is banned for security reasons",
|
||||
"pending": "Security verification pending - compatibility cannot be verified",
|
||||
"import_failed": "Import Failed"
|
||||
},
|
||||
"warningTooltip": "This package may have compatibility issues with your current environment"
|
||||
}
|
||||
},
|
||||
"issueReport": {
|
||||
@@ -429,25 +475,32 @@
|
||||
"error": "Unable to start ComfyUI Desktop"
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Essential",
|
||||
"viewControls": "View Controls",
|
||||
"manageShortcuts": "Manage Shortcuts",
|
||||
"noKeybinding": "No keybinding",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"subcategories": {
|
||||
"workflow": "Workflow",
|
||||
"node": "Node",
|
||||
"queue": "Queue",
|
||||
"view": "View",
|
||||
"panelControls": "Panel Controls"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "You have modified the following server configurations. Restart to apply changes.",
|
||||
"revertChanges": "Revert Changes",
|
||||
"restart": "Restart"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"themeToggle": "Toggle Theme",
|
||||
"helpCenter": "Help Center",
|
||||
"logout": "Logout",
|
||||
"queue": "Queue",
|
||||
"nodeLibrary": "Node Library",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates",
|
||||
"labels": {
|
||||
"queue": "Queue",
|
||||
"nodes": "Nodes",
|
||||
"models": "Models",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates"
|
||||
},
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
@@ -506,6 +559,7 @@
|
||||
"docs": "Docs",
|
||||
"github": "Github",
|
||||
"helpFeedback": "Help & Feedback",
|
||||
"managerExtension": "Manager Extension",
|
||||
"more": "More...",
|
||||
"whatsNew": "What's New?",
|
||||
"clickToLearnMore": "Click to learn more →",
|
||||
@@ -543,14 +597,7 @@
|
||||
"clipspace": "Open Clipspace",
|
||||
"resetView": "Reset canvas view",
|
||||
"clear": "Clear workflow",
|
||||
"toggleBottomPanel": "Toggle Bottom Panel",
|
||||
"theme": "Theme",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"manageExtensions": "Manage Extensions",
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"queue": "Queue Panel"
|
||||
"toggleBottomPanel": "Toggle Bottom Panel"
|
||||
},
|
||||
"tabMenu": {
|
||||
"duplicateTab": "Duplicate Tab",
|
||||
@@ -894,8 +941,7 @@
|
||||
"fitView": "Fit View",
|
||||
"selectMode": "Select Mode",
|
||||
"panMode": "Pan Mode",
|
||||
"toggleLinkVisibility": "Toggle Link Visibility",
|
||||
"toggleMinimap": "Toggle Minimap"
|
||||
"toggleLinkVisibility": "Toggle Link Visibility"
|
||||
},
|
||||
"groupNode": {
|
||||
"create": "Create group node",
|
||||
@@ -944,8 +990,9 @@
|
||||
"Image Layer": "Image Layer"
|
||||
},
|
||||
"menuLabels": {
|
||||
"File": "File",
|
||||
"Workflow": "Workflow",
|
||||
"Edit": "Edit",
|
||||
"Manager": "Manager",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
|
||||
@@ -959,19 +1006,17 @@
|
||||
"Quit": "Quit",
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Zoom to fit": "Zoom to fit",
|
||||
"Fit view to selected nodes": "Fit view to selected nodes",
|
||||
"Move Selected Nodes Down": "Move Selected Nodes Down",
|
||||
"Move Selected Nodes Left": "Move Selected Nodes Left",
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Node Links": "Node Links",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
||||
"Minimap": "Minimap",
|
||||
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
||||
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
||||
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
||||
@@ -988,10 +1033,8 @@
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Unpack the selected Subgraph": "Unpack the selected Subgraph",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
"Manage group nodes": "Manage group nodes",
|
||||
"Ungroup selected group nodes": "Ungroup selected group nodes",
|
||||
@@ -1002,14 +1045,18 @@
|
||||
"ComfyUI Issues": "ComfyUI Issues",
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
|
||||
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
|
||||
"Install Missing": "Install Missing",
|
||||
"Install Missing Custom Nodes": "Install Missing Custom Nodes",
|
||||
"Check for Custom Node Updates": "Check for Custom Node Updates",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
|
||||
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
|
||||
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"Unload Models": "Unload Models",
|
||||
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Manager": "Manager",
|
||||
"Open": "Open",
|
||||
"Queue Prompt": "Queue Prompt",
|
||||
"Queue Prompt (Front)": "Queue Prompt (Front)",
|
||||
@@ -1019,8 +1066,6 @@
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Show Settings Dialog": "Show Settings Dialog",
|
||||
"Canvas Performance": "Canvas Performance",
|
||||
"Help Center": "Help Center",
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
@@ -1029,17 +1074,14 @@
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
"Toggle Search Box": "Toggle Search Box",
|
||||
"Bottom Panel": "Bottom Panel",
|
||||
"Show Keybindings Dialog": "Show Keybindings Dialog",
|
||||
"Toggle Bottom Panel": "Toggle Bottom Panel",
|
||||
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
|
||||
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
|
||||
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
|
||||
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
|
||||
"Focus Mode": "Focus Mode",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
"Queue Panel": "Queue Panel",
|
||||
"Workflows": "Workflows"
|
||||
"Toggle Focus Mode": "Toggle Focus Mode",
|
||||
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
|
||||
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
|
||||
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
|
||||
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
@@ -1098,8 +1140,7 @@
|
||||
"User": "User",
|
||||
"Credits": "Credits",
|
||||
"API Nodes": "API Nodes",
|
||||
"Notification Preferences": "Notification Preferences",
|
||||
"3DViewer": "3DViewer"
|
||||
"Notification Preferences": "Notification Preferences"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1451,31 +1492,12 @@
|
||||
"depth": "Depth",
|
||||
"lineart": "Lineart"
|
||||
},
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"exportRecording": "Export Recording",
|
||||
"clearRecording": "Clear Recording",
|
||||
"resizeNodeMatchOutput": "Resize Node to match output",
|
||||
"loadingBackgroundImage": "Loading Background Image",
|
||||
"cameraType": {
|
||||
"perspective": "Perspective",
|
||||
"orthographic": "Orthographic"
|
||||
},
|
||||
"viewer": {
|
||||
"title": "3D Viewer (Beta)",
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel",
|
||||
"cameraType": "Camera Type",
|
||||
"sceneSettings": "Scene Settings",
|
||||
"cameraSettings": "Camera Settings",
|
||||
"lightSettings": "Light Settings",
|
||||
"exportSettings": "Export Settings",
|
||||
"modelSettings": "Model Settings"
|
||||
},
|
||||
"openIn3DViewer": "Open in 3D Viewer"
|
||||
"loadingBackgroundImage": "Loading Background Image"
|
||||
},
|
||||
"toastMessages": {
|
||||
"nothingToQueue": "Nothing to queue",
|
||||
@@ -1513,8 +1535,7 @@
|
||||
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
||||
"nothingSelected": "Nothing selected",
|
||||
"cannotCreateSubgraph": "Cannot create subgraph",
|
||||
"failedToConvertToSubgraph": "Failed to convert items to subgraph",
|
||||
"failedToInitializeLoad3dViewer": "Failed to initialize 3D Viewer"
|
||||
"failedToConvertToSubgraph": "Failed to convert items to subgraph"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
@@ -1667,32 +1688,5 @@
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "Learn more",
|
||||
"noReleaseNotes": "No release notes available."
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"duplicate": "Duplicate",
|
||||
"clearWorkflow": "Clear Workflow",
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"enterNewName": "Enter new name"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Essential",
|
||||
"viewControls": "View Controls",
|
||||
"manageShortcuts": "Manage Shortcuts",
|
||||
"noKeybinding": "No keybinding",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"subcategories": {
|
||||
"workflow": "Workflow",
|
||||
"node": "Node",
|
||||
"queue": "Queue",
|
||||
"view": "View",
|
||||
"panelControls": "Panel Controls"
|
||||
}
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Node Colors",
|
||||
"showLinks": "Show Links",
|
||||
"showGroups": "Show Frames/Groups",
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
}
|
||||
}
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Cargar flujo de trabajo predeterminado"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Administrador de nodos personalizados"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nodos personalizados (Beta)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Nodos personalizados (heredados)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Menú del administrador (heredado)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Instalar faltantes"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Buscar actualizaciones"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Alternar diálogo de progreso del administrador"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Abrir editor de máscara para el nodo seleccionado"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Descargar modelos"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Descargar modelos y caché de ejecución"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nuevo flujo de trabajo en blanco"
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "Color",
|
||||
"comingSoon": "Próximamente",
|
||||
"command": "Comando",
|
||||
"commandProhibited": "El comando {command} está prohibido. Contacta a un administrador para más información.",
|
||||
"community": "Comunidad",
|
||||
"completed": "Completado",
|
||||
"confirm": "Confirmar",
|
||||
@@ -300,7 +301,6 @@
|
||||
"disabling": "Deshabilitando",
|
||||
"dismiss": "Descartar",
|
||||
"download": "Descargar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
"empty": "Vacío",
|
||||
"enableAll": "Habilitar todo",
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "Aplicando textura...",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"camera": "Cámara",
|
||||
"cameraType": {
|
||||
"orthographic": "Ortográfica",
|
||||
"perspective": "Perspectiva"
|
||||
},
|
||||
"clearRecording": "Borrar grabación",
|
||||
"edgeThreshold": "Umbral de borde",
|
||||
"export": "Exportar",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "Malla"
|
||||
},
|
||||
"model": "Modelo",
|
||||
"openIn3DViewer": "Abrir en el visor 3D",
|
||||
"previewOutput": "Vista previa de salida",
|
||||
"removeBackgroundImage": "Eliminar imagen de fondo",
|
||||
"resizeNodeMatchOutput": "Redimensionar nodo para coincidir con la salida",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "Cambiar cámara",
|
||||
"switchingMaterialMode": "Cambiando modo de material...",
|
||||
"upDirection": "Dirección hacia arriba",
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"uploadBackgroundImage": "Subir imagen de fondo",
|
||||
"uploadTexture": "Subir textura",
|
||||
"viewer": {
|
||||
"apply": "Aplicar",
|
||||
"cameraSettings": "Configuración de la cámara",
|
||||
"cameraType": "Tipo de cámara",
|
||||
"cancel": "Cancelar",
|
||||
"exportSettings": "Configuración de exportación",
|
||||
"lightSettings": "Configuración de la luz",
|
||||
"modelSettings": "Configuración del modelo",
|
||||
"sceneSettings": "Configuración de la escena",
|
||||
"title": "Visor 3D (Beta)"
|
||||
}
|
||||
"uploadTexture": "Subir textura"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Requiere ComfyUI {version}:",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "Cola de Instalación",
|
||||
"lastUpdated": "Última Actualización",
|
||||
"latestVersion": "Última",
|
||||
"legacyManagerUI": "Usar UI antigua",
|
||||
"legacyManagerUIDescription": "Para usar la UI antigua del Manager, inicia ComfyUI con --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "El menú del administrador antiguo no está disponible en esta versión de ComfyUI. Por favor, utiliza el nuevo menú del administrador en su lugar.",
|
||||
"license": "Licencia",
|
||||
"loadingVersions": "Cargando versiones...",
|
||||
"nightlyVersion": "Nocturna",
|
||||
@@ -752,7 +736,6 @@
|
||||
"manageExtensions": "Gestionar extensiones",
|
||||
"onChange": "Al cambiar",
|
||||
"onChangeTooltip": "El flujo de trabajo se encolará una vez que se haga un cambio",
|
||||
"queue": "Panel de cola",
|
||||
"refresh": "Actualizar definiciones de nodos",
|
||||
"resetView": "Restablecer vista del lienzo",
|
||||
"run": "Ejecutar",
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "Panel inferior",
|
||||
"Browse Templates": "Explorar plantillas",
|
||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||
"Canvas Performance": "Rendimiento del lienzo",
|
||||
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
||||
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
||||
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
|
||||
"Check for Custom Node Updates": "Buscar actualizaciones de nodos personalizados",
|
||||
"Check for Updates": "Buscar actualizaciones",
|
||||
"Clear Pending Tasks": "Borrar tareas pendientes",
|
||||
"Clear Workflow": "Borrar flujo de trabajo",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "Contactar soporte",
|
||||
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
|
||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||
"Custom Nodes (Legacy)": "Nodos personalizados (heredado)",
|
||||
"Custom Nodes Manager": "Administrador de Nodos Personalizados",
|
||||
"Decrease Brush Size in MaskEditor": "Disminuir tamaño del pincel en MaskEditor",
|
||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
"Edit": "Editar",
|
||||
"Exit Subgraph": "Salir de subgrafo",
|
||||
"Export": "Exportar",
|
||||
"Export (API)": "Exportar (API)",
|
||||
"File": "Archivo",
|
||||
"Fit Group To Contents": "Ajustar grupo a contenidos",
|
||||
"Focus Mode": "Modo de enfoque",
|
||||
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Help Center": "Centro de ayuda",
|
||||
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
|
||||
"Install Missing Custom Nodes": "Instalar nodos personalizados faltantes",
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
"Manager": "Administrador",
|
||||
"Minimap": "Minimapa",
|
||||
"Model Library": "Biblioteca de modelos",
|
||||
"Manager Menu (Legacy)": "Menú de gestión (heredado)",
|
||||
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
|
||||
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
|
||||
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
"Node Library": "Biblioteca de nodos",
|
||||
"Node Links": "Enlaces de nodos",
|
||||
"Open": "Abrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Abrir visor 3D (Beta) para el nodo seleccionado",
|
||||
"Open Custom Nodes Folder": "Abrir carpeta de nodos personalizados",
|
||||
"Open DevTools": "Abrir DevTools",
|
||||
"Open Inputs Folder": "Abrir carpeta de entradas",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
|
||||
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
|
||||
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
|
||||
"Queue Panel": "Panel de cola",
|
||||
"Queue Prompt": "Indicador de cola",
|
||||
"Queue Prompt (Front)": "Indicador de cola (Frente)",
|
||||
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
|
||||
@@ -845,28 +825,24 @@
|
||||
"Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)",
|
||||
"Show Settings Dialog": "Mostrar diálogo de configuración",
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
|
||||
"Toggle Bottom Panel": "Alternar panel inferior",
|
||||
"Toggle Focus Mode": "Alternar modo de enfoque",
|
||||
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
|
||||
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
|
||||
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
|
||||
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
|
||||
"Toggle Search Box": "Alternar caja de búsqueda",
|
||||
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
|
||||
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
|
||||
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
|
||||
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
|
||||
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
|
||||
"Workflows": "Flujos de trabajo",
|
||||
"Unload Models": "Descargar modelos",
|
||||
"Unload Models and Execution Cache": "Descargar modelos y caché de ejecución",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar",
|
||||
"Zoom to fit": "Ajustar al tamaño"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Colores de nodos",
|
||||
"renderBypassState": "Mostrar estado de omisión",
|
||||
"renderErrorState": "Mostrar estado de error",
|
||||
"showGroups": "Mostrar marcos/grupos",
|
||||
"showLinks": "Mostrar enlaces"
|
||||
"Zoom Out": "Alejar"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "No mostrar esto de nuevo",
|
||||
@@ -1134,7 +1110,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "Visor 3D",
|
||||
"API Nodes": "Nodos API",
|
||||
"About": "Acerca de",
|
||||
"Appearance": "Apariencia",
|
||||
@@ -1186,31 +1161,10 @@
|
||||
"Window": "Ventana",
|
||||
"Workflow": "Flujo de Trabajo"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Esenciales",
|
||||
"keyboardShortcuts": "Atajos de teclado",
|
||||
"manageShortcuts": "Gestionar atajos",
|
||||
"noKeybinding": "Sin asignación de tecla",
|
||||
"subcategories": {
|
||||
"node": "Nodo",
|
||||
"panelControls": "Controles del panel",
|
||||
"queue": "Cola",
|
||||
"view": "Vista",
|
||||
"workflow": "Flujo de trabajo"
|
||||
},
|
||||
"viewControls": "Controles de vista"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "Explorar plantillas de ejemplo",
|
||||
"downloads": "Descargas",
|
||||
"helpCenter": "Centro de ayuda",
|
||||
"labels": {
|
||||
"models": "Modelos",
|
||||
"nodes": "Nodos",
|
||||
"queue": "Cola",
|
||||
"templates": "Plantillas",
|
||||
"workflows": "Flujos de trabajo"
|
||||
},
|
||||
"logout": "Cerrar sesión",
|
||||
"modelLibrary": "Biblioteca de modelos",
|
||||
"newBlankWorkflow": "Crear un nuevo flujo de trabajo en blanco",
|
||||
@@ -1248,7 +1202,6 @@
|
||||
},
|
||||
"showFlatList": "Mostrar lista plana"
|
||||
},
|
||||
"templates": "Plantillas",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "¿Estás seguro de que quieres eliminar este flujo de trabajo?",
|
||||
"confirmDeleteTitle": "¿Eliminar flujo de trabajo?",
|
||||
@@ -1295,8 +1248,6 @@
|
||||
"Video": "Video",
|
||||
"Video API": "API de Video"
|
||||
},
|
||||
"loadingMore": "Cargando más plantillas...",
|
||||
"searchPlaceholder": "Buscar plantillas...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
@@ -1619,7 +1570,6 @@
|
||||
"failedToExportModel": "Error al exportar modelo como {format}",
|
||||
"failedToFetchBalance": "No se pudo obtener el saldo: {error}",
|
||||
"failedToFetchLogs": "Error al obtener los registros del servidor",
|
||||
"failedToInitializeLoad3dViewer": "No se pudo inicializar el visor 3D",
|
||||
"failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}",
|
||||
"failedToPurchaseCredits": "No se pudo comprar créditos: {error}",
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Charger le flux de travail par défaut"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nœuds personnalisés (Beta)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Nœuds personnalisés (hérités)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Menu du gestionnaire (héritage)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Installer manquants"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Vérifier les mises à jour"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Décharger les modèles"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Décharger les modèles et le cache d'exécution"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "Couleur",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
"commandProhibited": "La commande {command} est interdite. Contactez un administrateur pour plus d'informations.",
|
||||
"community": "Communauté",
|
||||
"completed": "Terminé",
|
||||
"confirm": "Confirmer",
|
||||
@@ -300,7 +301,6 @@
|
||||
"disabling": "Désactivation",
|
||||
"dismiss": "Fermer",
|
||||
"download": "Télécharger",
|
||||
"duplicate": "Dupliquer",
|
||||
"edit": "Modifier",
|
||||
"empty": "Vide",
|
||||
"enableAll": "Activer tout",
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "Application de la texture...",
|
||||
"backgroundColor": "Couleur de fond",
|
||||
"camera": "Caméra",
|
||||
"cameraType": {
|
||||
"orthographic": "Orthographique",
|
||||
"perspective": "Perspective"
|
||||
},
|
||||
"clearRecording": "Effacer l'enregistrement",
|
||||
"edgeThreshold": "Seuil de Bordure",
|
||||
"export": "Exportation",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "Fil de fer"
|
||||
},
|
||||
"model": "Modèle",
|
||||
"openIn3DViewer": "Ouvrir dans la visionneuse 3D",
|
||||
"previewOutput": "Aperçu de la sortie",
|
||||
"removeBackgroundImage": "Supprimer l'image de fond",
|
||||
"resizeNodeMatchOutput": "Redimensionner le nœud pour correspondre à la sortie",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "Changer de caméra",
|
||||
"switchingMaterialMode": "Changement de mode de matériau...",
|
||||
"upDirection": "Direction Haut",
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"uploadBackgroundImage": "Télécharger l'image de fond",
|
||||
"uploadTexture": "Télécharger Texture",
|
||||
"viewer": {
|
||||
"apply": "Appliquer",
|
||||
"cameraSettings": "Paramètres de la caméra",
|
||||
"cameraType": "Type de caméra",
|
||||
"cancel": "Annuler",
|
||||
"exportSettings": "Paramètres d’exportation",
|
||||
"lightSettings": "Paramètres de l’éclairage",
|
||||
"modelSettings": "Paramètres du modèle",
|
||||
"sceneSettings": "Paramètres de la scène",
|
||||
"title": "Visionneuse 3D (Bêta)"
|
||||
}
|
||||
"uploadTexture": "Télécharger Texture"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Nécessite ComfyUI {version} :",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "File d'attente d'installation",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"latestVersion": "Dernière",
|
||||
"legacyManagerUI": "Utiliser l'interface utilisateur héritée",
|
||||
"legacyManagerUIDescription": "Pour utiliser l'interface utilisateur de gestion héritée, démarrez ComfyUI avec --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Le menu du gestionnaire de l'ancienne version n'est pas disponible dans cette version de ComfyUI. Veuillez utiliser le nouveau menu du gestionnaire à la place.",
|
||||
"license": "Licence",
|
||||
"loadingVersions": "Chargement des versions...",
|
||||
"nightlyVersion": "Nocturne",
|
||||
@@ -752,7 +736,6 @@
|
||||
"manageExtensions": "Gérer les extensions",
|
||||
"onChange": "Sur modification",
|
||||
"onChangeTooltip": "Le flux de travail sera mis en file d'attente une fois une modification effectuée",
|
||||
"queue": "Panneau de file d’attente",
|
||||
"refresh": "Actualiser les définitions des nœuds",
|
||||
"resetView": "Réinitialiser la vue du canevas",
|
||||
"run": "Exécuter",
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "Panneau inférieur",
|
||||
"Browse Templates": "Parcourir les modèles",
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
"Canvas Performance": "Performance du canevas",
|
||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
|
||||
"Check for Custom Node Updates": "Vérifier les mises à jour des nœuds personnalisés",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
"Clear Workflow": "Effacer le flux de travail",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "Contacter le support",
|
||||
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Custom Nodes (Legacy)": "Nœuds personnalisés (héritage)",
|
||||
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
|
||||
"Decrease Brush Size in MaskEditor": "Réduire la taille du pinceau dans MaskEditor",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
"Edit": "Éditer",
|
||||
"Exit Subgraph": "Quitter le sous-graphe",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"File": "Fichier",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
"Focus Mode": "Mode focus",
|
||||
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Help Center": "Centre d’aide",
|
||||
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
|
||||
"Install Missing Custom Nodes": "Installer les nœuds personnalisés manquants",
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
"Manager": "Gestionnaire",
|
||||
"Minimap": "Minicarte",
|
||||
"Model Library": "Bibliothèque de modèles",
|
||||
"Manager Menu (Legacy)": "Menu du gestionnaire (héritage)",
|
||||
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
|
||||
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
|
||||
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
"Node Library": "Bibliothèque de nœuds",
|
||||
"Node Links": "Liens de nœuds",
|
||||
"Open": "Ouvrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné",
|
||||
"Open Custom Nodes Folder": "Ouvrir le dossier des nœuds personnalisés",
|
||||
"Open DevTools": "Ouvrir DevTools",
|
||||
"Open Inputs Folder": "Ouvrir le dossier des entrées",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
|
||||
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Queue Panel": "Panneau de file d’attente",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Queue Selected Output Nodes": "Mettre en file d’attente les nœuds de sortie sélectionnés",
|
||||
@@ -847,6 +827,9 @@
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Toggle Essential Bottom Panel": "Basculer le panneau inférieur essentiel",
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file d’attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
|
||||
@@ -855,18 +838,11 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
|
||||
"Workflows": "Flux de travail",
|
||||
"Unload Models": "Décharger les modèles",
|
||||
"Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution",
|
||||
"Workflow": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière",
|
||||
"Zoom to fit": "Ajuster à l’écran"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Couleurs des nœuds",
|
||||
"renderBypassState": "Afficher l’état de contournement",
|
||||
"renderErrorState": "Afficher l’état d’erreur",
|
||||
"showGroups": "Afficher les cadres/groupes",
|
||||
"showLinks": "Afficher les liens"
|
||||
"Zoom Out": "Zoom arrière"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
@@ -1134,7 +1110,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "Visionneuse 3D",
|
||||
"API Nodes": "Nœuds API",
|
||||
"About": "À Propos",
|
||||
"Appearance": "Apparence",
|
||||
@@ -1186,31 +1161,10 @@
|
||||
"Window": "Fenêtre",
|
||||
"Workflow": "Flux de Travail"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Essentiel",
|
||||
"keyboardShortcuts": "Raccourcis clavier",
|
||||
"manageShortcuts": "Gérer les raccourcis",
|
||||
"noKeybinding": "Aucun raccourci",
|
||||
"subcategories": {
|
||||
"node": "Nœud",
|
||||
"panelControls": "Contrôles du panneau",
|
||||
"queue": "File d’attente",
|
||||
"view": "Vue",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
"viewControls": "Contrôles d’affichage"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "Parcourir les modèles d'exemple",
|
||||
"downloads": "Téléchargements",
|
||||
"helpCenter": "Centre d'aide",
|
||||
"labels": {
|
||||
"models": "Modèles",
|
||||
"nodes": "Nœuds",
|
||||
"queue": "File d’attente",
|
||||
"templates": "Modèles",
|
||||
"workflows": "Flux de travail"
|
||||
},
|
||||
"logout": "Déconnexion",
|
||||
"modelLibrary": "Bibliothèque de modèles",
|
||||
"newBlankWorkflow": "Créer un nouveau flux de travail vierge",
|
||||
@@ -1248,7 +1202,6 @@
|
||||
},
|
||||
"showFlatList": "Afficher la liste plate"
|
||||
},
|
||||
"templates": "Modèles",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer ce flux de travail ?",
|
||||
"confirmDeleteTitle": "Supprimer le flux de travail ?",
|
||||
@@ -1295,8 +1248,6 @@
|
||||
"Video": "Vidéo",
|
||||
"Video API": "API vidéo"
|
||||
},
|
||||
"loadingMore": "Chargement de plus de modèles...",
|
||||
"searchPlaceholder": "Rechercher des modèles...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
@@ -1619,7 +1570,6 @@
|
||||
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
||||
"failedToFetchBalance": "Échec de la récupération du solde : {error}",
|
||||
"failedToFetchLogs": "Échec de la récupération des journaux du serveur",
|
||||
"failedToInitializeLoad3dViewer": "Échec de l'initialisation du visualiseur 3D",
|
||||
"failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}",
|
||||
"failedToPurchaseCredits": "Échec de l'achat de crédits : {error}",
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "デフォルトのワークフローを読み込む"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "カスタムノード(ベータ版)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "カスタムノード(レガシー)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "マネージャーメニュー(レガシー)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "不足しているパックをインストール"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "更新を確認"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "選択したノードのマスクエディタを開く"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "モデルのアンロード"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "モデルと実行キャッシュのアンロード"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "色",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
"commandProhibited": "コマンド {command} は禁止されています。詳細は管理者にお問い合わせください。",
|
||||
"community": "コミュニティ",
|
||||
"completed": "完了",
|
||||
"confirm": "確認",
|
||||
@@ -300,7 +301,6 @@
|
||||
"disabling": "無効化",
|
||||
"dismiss": "閉じる",
|
||||
"download": "ダウンロード",
|
||||
"duplicate": "複製",
|
||||
"edit": "編集",
|
||||
"empty": "空",
|
||||
"enableAll": "すべて有効にする",
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "テクスチャを適用中...",
|
||||
"backgroundColor": "背景色",
|
||||
"camera": "カメラ",
|
||||
"cameraType": {
|
||||
"orthographic": "オルソグラフィック",
|
||||
"perspective": "パースペクティブ"
|
||||
},
|
||||
"clearRecording": "録画をクリア",
|
||||
"edgeThreshold": "エッジ閾値",
|
||||
"export": "エクスポート",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "ワイヤーフレーム"
|
||||
},
|
||||
"model": "モデル",
|
||||
"openIn3DViewer": "3Dビューアで開く",
|
||||
"previewOutput": "出力のプレビュー",
|
||||
"removeBackgroundImage": "背景画像を削除",
|
||||
"resizeNodeMatchOutput": "ノードを出力に合わせてリサイズ",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "カメラを切り替える",
|
||||
"switchingMaterialMode": "マテリアルモードの切り替え中...",
|
||||
"upDirection": "上方向",
|
||||
"upDirections": {
|
||||
"original": "オリジナル"
|
||||
},
|
||||
"uploadBackgroundImage": "背景画像をアップロード",
|
||||
"uploadTexture": "テクスチャをアップロード",
|
||||
"viewer": {
|
||||
"apply": "適用",
|
||||
"cameraSettings": "カメラ設定",
|
||||
"cameraType": "カメラタイプ",
|
||||
"cancel": "キャンセル",
|
||||
"exportSettings": "エクスポート設定",
|
||||
"lightSettings": "ライト設定",
|
||||
"modelSettings": "モデル設定",
|
||||
"sceneSettings": "シーン設定",
|
||||
"title": "3Dビューア(ベータ)"
|
||||
}
|
||||
"uploadTexture": "テクスチャをアップロード"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "ComfyUI {version} が必要です:",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "インストールキュー",
|
||||
"lastUpdated": "最終更新日",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "レガシーUIを使用する",
|
||||
"legacyManagerUIDescription": "レガシーManager UIを使用するには、--enable-manager-legacy-uiを付けてComfyUIを起動してください",
|
||||
"legacyMenuNotAvailable": "このバージョンのComfyUIでは、レガシーマネージャーメニューは利用できません。新しいマネージャーメニューを使用してください。",
|
||||
"license": "ライセンス",
|
||||
"loadingVersions": "バージョンを読み込んでいます...",
|
||||
"nightlyVersion": "ナイトリー",
|
||||
@@ -752,7 +736,6 @@
|
||||
"manageExtensions": "拡張機能の管理",
|
||||
"onChange": "変更時",
|
||||
"onChangeTooltip": "変更が行われるとワークフローがキューに追加されます",
|
||||
"queue": "キューパネル",
|
||||
"refresh": "ノードを更新",
|
||||
"resetView": "ビューをリセット",
|
||||
"run": "実行する",
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "下部パネル",
|
||||
"Browse Templates": "テンプレートを参照",
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Performance": "キャンバスパフォーマンス",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
|
||||
"Check for Custom Node Updates": "カスタムノードのアップデートを確認",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Custom Nodes (Legacy)": "カスタムノード(レガシー)",
|
||||
"Custom Nodes Manager": "カスタムノードマネージャ",
|
||||
"Decrease Brush Size in MaskEditor": "マスクエディタでブラシサイズを小さくする",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
"Edit": "編集",
|
||||
"Exit Subgraph": "サブグラフを終了",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"File": "ファイル",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
"Focus Mode": "フォーカスモード",
|
||||
"Fit view to selected nodes": "選択したノードにビューを合わせる",
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Help Center": "ヘルプセンター",
|
||||
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
|
||||
"Install Missing Custom Nodes": "不足しているカスタムノードをインストール",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
"Manager": "マネージャー",
|
||||
"Minimap": "ミニマップ",
|
||||
"Model Library": "モデルライブラリ",
|
||||
"Manager Menu (Legacy)": "マネージャーメニュー(レガシー)",
|
||||
"Move Selected Nodes Down": "選択したノードを下へ移動",
|
||||
"Move Selected Nodes Left": "選択したノードを左へ移動",
|
||||
"Move Selected Nodes Right": "選択したノードを右へ移動",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
"Node Library": "ノードライブラリ",
|
||||
"Node Links": "ノードリンク",
|
||||
"Open": "開く",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "選択したノードの3Dビューアー(ベータ)を開く",
|
||||
"Open Custom Nodes Folder": "カスタムノードフォルダを開く",
|
||||
"Open DevTools": "DevToolsを開く",
|
||||
"Open Inputs Folder": "入力フォルダを開く",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
|
||||
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
|
||||
"Previous Opened Workflow": "前に開いたワークフロー",
|
||||
"Queue Panel": "キューパネル",
|
||||
"Queue Prompt": "キューのプロンプト",
|
||||
"Queue Prompt (Front)": "キューのプロンプト (前面)",
|
||||
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
|
||||
@@ -850,23 +830,15 @@
|
||||
"Toggle Search Box": "検索ボックスの切り替え",
|
||||
"Toggle Terminal Bottom Panel": "ターミナル下部パネルの切り替え",
|
||||
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
|
||||
"Toggle View Controls Bottom Panel": "ビューコントロール下部パネルの切り替え",
|
||||
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
|
||||
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
|
||||
"Undo": "元に戻す",
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
"Unpack the selected Subgraph": "選択したサブグラフを展開",
|
||||
"Workflows": "ワークフロー",
|
||||
"Unload Models": "モデルのアンロード",
|
||||
"Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード",
|
||||
"Workflow": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Zoom to fit": "全体表示にズーム"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ノードの色",
|
||||
"renderBypassState": "バイパス状態を表示",
|
||||
"renderErrorState": "エラー状態を表示",
|
||||
"showGroups": "フレーム/グループを表示",
|
||||
"showLinks": "リンクを表示"
|
||||
"Zoom Out": "ズームアウト"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
@@ -1134,7 +1106,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3Dビューア",
|
||||
"API Nodes": "APIノード",
|
||||
"About": "情報",
|
||||
"Appearance": "外観",
|
||||
@@ -1186,31 +1157,10 @@
|
||||
"Window": "ウィンドウ",
|
||||
"Workflow": "ワークフロー"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本",
|
||||
"keyboardShortcuts": "キーボードショートカット",
|
||||
"manageShortcuts": "ショートカットの管理",
|
||||
"noKeybinding": "キー割り当てなし",
|
||||
"subcategories": {
|
||||
"node": "ノード",
|
||||
"panelControls": "パネルコントロール",
|
||||
"queue": "キュー",
|
||||
"view": "ビュー",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
"viewControls": "表示コントロール"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "サンプルテンプレートを表示",
|
||||
"downloads": "ダウンロード",
|
||||
"helpCenter": "ヘルプセンター",
|
||||
"labels": {
|
||||
"models": "モデル",
|
||||
"nodes": "ノード",
|
||||
"queue": "キュー",
|
||||
"templates": "テンプレート",
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"logout": "ログアウト",
|
||||
"modelLibrary": "モデルライブラリ",
|
||||
"newBlankWorkflow": "新しい空のワークフローを作成",
|
||||
@@ -1248,7 +1198,6 @@
|
||||
},
|
||||
"showFlatList": "フラットリストを表示"
|
||||
},
|
||||
"templates": "テンプレート",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "このワークフローを削除してもよろしいですか?",
|
||||
"confirmDeleteTitle": "ワークフローを削除しますか?",
|
||||
@@ -1295,8 +1244,6 @@
|
||||
"Video": "ビデオ",
|
||||
"Video API": "動画API"
|
||||
},
|
||||
"loadingMore": "さらにテンプレートを読み込み中...",
|
||||
"searchPlaceholder": "テンプレートを検索...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
@@ -1619,7 +1566,6 @@
|
||||
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
||||
"failedToFetchBalance": "残高の取得に失敗しました: {error}",
|
||||
"failedToFetchLogs": "サーバーログの取得に失敗しました",
|
||||
"failedToInitializeLoad3dViewer": "3Dビューアの初期化に失敗しました",
|
||||
"failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}",
|
||||
"failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}",
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "기본 워크플로 로드"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "사용자 정의 노드 (베타)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "커스텀 노드 (레거시)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "매니저 메뉴 (레거시)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "누락된 팩 설치"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "업데이트 확인"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "진행 상황 대화 상자 전환"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "선택한 노드 마스크 편집기 열기"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "모델 언로드"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "모델 및 실행 캐시 언로드"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "색상",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"commandProhibited": "명령 {command}은 금지되었습니다. 자세한 정보는 관리자에게 문의하십시오.",
|
||||
"community": "커뮤니티",
|
||||
"completed": "완료됨",
|
||||
"confirm": "확인",
|
||||
@@ -300,7 +301,6 @@
|
||||
"disabling": "비활성화 중",
|
||||
"dismiss": "닫기",
|
||||
"download": "다운로드",
|
||||
"duplicate": "복제",
|
||||
"edit": "편집",
|
||||
"empty": "비어 있음",
|
||||
"enableAll": "모두 활성화",
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "텍스처 적용 중...",
|
||||
"backgroundColor": "배경색",
|
||||
"camera": "카메라",
|
||||
"cameraType": {
|
||||
"orthographic": "직교",
|
||||
"perspective": "원근"
|
||||
},
|
||||
"clearRecording": "녹화 지우기",
|
||||
"edgeThreshold": "엣지 임계값",
|
||||
"export": "내보내기",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "와이어프레임"
|
||||
},
|
||||
"model": "모델",
|
||||
"openIn3DViewer": "3D 뷰어에서 열기",
|
||||
"previewOutput": "출력 미리보기",
|
||||
"removeBackgroundImage": "배경 이미지 제거",
|
||||
"resizeNodeMatchOutput": "노드 크기를 출력에 맞추기",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "카메라 전환",
|
||||
"switchingMaterialMode": "재질 모드 전환 중...",
|
||||
"upDirection": "위 방향",
|
||||
"upDirections": {
|
||||
"original": "원본"
|
||||
},
|
||||
"uploadBackgroundImage": "배경 이미지 업로드",
|
||||
"uploadTexture": "텍스처 업로드",
|
||||
"viewer": {
|
||||
"apply": "적용",
|
||||
"cameraSettings": "카메라 설정",
|
||||
"cameraType": "카메라 유형",
|
||||
"cancel": "취소",
|
||||
"exportSettings": "내보내기 설정",
|
||||
"lightSettings": "조명 설정",
|
||||
"modelSettings": "모델 설정",
|
||||
"sceneSettings": "씬 설정",
|
||||
"title": "3D 뷰어 (베타)"
|
||||
}
|
||||
"uploadTexture": "텍스처 업로드"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "ComfyUI {version} 이상 필요:",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "설치 대기열",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"latestVersion": "최신",
|
||||
"legacyManagerUI": "레거시 UI 사용",
|
||||
"legacyManagerUIDescription": "레거시 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
|
||||
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 레거시 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
|
||||
"license": "라이선스",
|
||||
"loadingVersions": "버전 로딩 중...",
|
||||
"nightlyVersion": "최신 테스트 버전(nightly)",
|
||||
@@ -752,7 +736,6 @@
|
||||
"manageExtensions": "확장 프로그램 관리",
|
||||
"onChange": "변경 시",
|
||||
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
|
||||
"queue": "대기열 패널",
|
||||
"refresh": "노드 정의 새로 고침",
|
||||
"resetView": "캔버스 보기 재설정",
|
||||
"run": "실행",
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "하단 패널",
|
||||
"Browse Templates": "템플릿 탐색",
|
||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||
"Canvas Performance": "캔버스 성능",
|
||||
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
||||
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
||||
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
|
||||
"Check for Custom Node Updates": "커스텀 노드 업데이트 확인",
|
||||
"Check for Updates": "업데이트 확인",
|
||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||
"Clear Workflow": "워크플로 지우기",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes (Legacy)": "커스텀 노드(레거시)",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Decrease Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 줄이기",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
"Edit": "편집",
|
||||
"Exit Subgraph": "서브그래프 종료",
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"File": "파일",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
"Focus Mode": "포커스 모드",
|
||||
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Help Center": "도움말 센터",
|
||||
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
|
||||
"Install Missing Custom Nodes": "누락된 커스텀 노드 설치",
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
"Manager": "매니저",
|
||||
"Minimap": "미니맵",
|
||||
"Model Library": "모델 라이브러리",
|
||||
"Manager Menu (Legacy)": "매니저 메뉴(레거시)",
|
||||
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
"Node Library": "노드 라이브러리",
|
||||
"Node Links": "노드 링크",
|
||||
"Open": "열기",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "선택한 노드에 대해 3D 뷰어(베타) 열기",
|
||||
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
|
||||
"Open DevTools": "개발자 도구 열기",
|
||||
"Open Inputs Folder": "입력 폴더 열기",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Queue Panel": "대기열 패널",
|
||||
"Queue Prompt": "실행 대기열에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
|
||||
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
|
||||
@@ -845,28 +825,24 @@
|
||||
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로우 사이드바 전환",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
"Unpack the selected Subgraph": "선택한 서브그래프 풀기",
|
||||
"Workflows": "워크플로우",
|
||||
"Unload Models": "모델 언로드",
|
||||
"Unload Models and Execution Cache": "모델 및 실행 캐시 언로드",
|
||||
"Workflow": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소",
|
||||
"Zoom to fit": "화면에 맞추기"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "노드 색상",
|
||||
"renderBypassState": "바이패스 상태 렌더링",
|
||||
"renderErrorState": "에러 상태 렌더링",
|
||||
"showGroups": "프레임/그룹 표시",
|
||||
"showLinks": "링크 표시"
|
||||
"Zoom Out": "축소"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
@@ -1134,7 +1110,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D뷰어",
|
||||
"API Nodes": "API 노드",
|
||||
"About": "정보",
|
||||
"Appearance": "모양",
|
||||
@@ -1186,31 +1161,10 @@
|
||||
"Window": "창",
|
||||
"Workflow": "워크플로"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "필수",
|
||||
"keyboardShortcuts": "키보드 단축키",
|
||||
"manageShortcuts": "단축키 관리",
|
||||
"noKeybinding": "단축키 없음",
|
||||
"subcategories": {
|
||||
"node": "노드",
|
||||
"panelControls": "패널 컨트롤",
|
||||
"queue": "대기열",
|
||||
"view": "보기",
|
||||
"workflow": "워크플로우"
|
||||
},
|
||||
"viewControls": "보기 컨트롤"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "예제 템플릿 탐색",
|
||||
"downloads": "다운로드",
|
||||
"helpCenter": "도움말 센터",
|
||||
"labels": {
|
||||
"models": "모델",
|
||||
"nodes": "노드",
|
||||
"queue": "대기열",
|
||||
"templates": "템플릿",
|
||||
"workflows": "워크플로우"
|
||||
},
|
||||
"logout": "로그아웃",
|
||||
"modelLibrary": "모델 라이브러리",
|
||||
"newBlankWorkflow": "새 빈 워크플로 만들기",
|
||||
@@ -1248,7 +1202,6 @@
|
||||
},
|
||||
"showFlatList": "평면 목록 표시"
|
||||
},
|
||||
"templates": "템플릿",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "정말로 이 워크플로를 삭제하시겠습니까?",
|
||||
"confirmDeleteTitle": "워크플로 삭제",
|
||||
@@ -1295,8 +1248,6 @@
|
||||
"Video": "비디오",
|
||||
"Video API": "비디오 API"
|
||||
},
|
||||
"loadingMore": "템플릿을 더 불러오는 중...",
|
||||
"searchPlaceholder": "템플릿 검색...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
@@ -1619,7 +1570,6 @@
|
||||
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
||||
"failedToFetchBalance": "잔액을 가져오지 못했습니다: {error}",
|
||||
"failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다",
|
||||
"failedToInitializeLoad3dViewer": "3D 뷰어 초기화에 실패했습니다",
|
||||
"failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}",
|
||||
"failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}",
|
||||
"fileLoadError": "{fileName}에서 워크플로를 찾을 수 없습니다",
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Загрузить стандартный рабочий процесс"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Пользовательские узлы (Бета)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Пользовательские узлы (устаревшие)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Меню менеджера (устаревшее)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Установить отсутствующие"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Проверить наличие обновлений"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Открыть редактор масок для выбранной ноды"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Выгрузить модели"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Выгрузить модели и кэш выполнения"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "Цвет",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
"commandProhibited": "Команда {command} запрещена. Свяжитесь с администратором для получения дополнительной информации.",
|
||||
"community": "Сообщество",
|
||||
"completed": "Завершено",
|
||||
"confirm": "Подтвердить",
|
||||
@@ -300,7 +301,6 @@
|
||||
"disabling": "Отключение",
|
||||
"dismiss": "Закрыть",
|
||||
"download": "Скачать",
|
||||
"duplicate": "Дублировать",
|
||||
"edit": "Редактировать",
|
||||
"empty": "Пусто",
|
||||
"enableAll": "Включить все",
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "Применение текстуры...",
|
||||
"backgroundColor": "Цвет фона",
|
||||
"camera": "Камера",
|
||||
"cameraType": {
|
||||
"orthographic": "Ортографическая",
|
||||
"perspective": "Перспективная"
|
||||
},
|
||||
"clearRecording": "Очистить запись",
|
||||
"edgeThreshold": "Пороговое значение края",
|
||||
"export": "Экспорт",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "Каркас"
|
||||
},
|
||||
"model": "Модель",
|
||||
"openIn3DViewer": "Открыть в 3D просмотрщике",
|
||||
"previewOutput": "Предварительный просмотр",
|
||||
"removeBackgroundImage": "Удалить фоновое изображение",
|
||||
"resizeNodeMatchOutput": "Изменить размер узла под вывод",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "Переключить камеру",
|
||||
"switchingMaterialMode": "Переключение режима материала...",
|
||||
"upDirection": "Направление Вверх",
|
||||
"upDirections": {
|
||||
"original": "Оригинал"
|
||||
},
|
||||
"uploadBackgroundImage": "Загрузить фоновое изображение",
|
||||
"uploadTexture": "Загрузить текстуру",
|
||||
"viewer": {
|
||||
"apply": "Применить",
|
||||
"cameraSettings": "Настройки камеры",
|
||||
"cameraType": "Тип камеры",
|
||||
"cancel": "Отмена",
|
||||
"exportSettings": "Настройки экспорта",
|
||||
"lightSettings": "Настройки освещения",
|
||||
"modelSettings": "Настройки модели",
|
||||
"sceneSettings": "Настройки сцены",
|
||||
"title": "3D Просмотрщик (Бета)"
|
||||
}
|
||||
"uploadTexture": "Загрузить текстуру"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Требуется ComfyUI {version}:",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "Очередь установки",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"latestVersion": "Последняя",
|
||||
"legacyManagerUI": "Использовать устаревший UI",
|
||||
"legacyManagerUIDescription": "Чтобы использовать устаревший UI менеджера, запустите ComfyUI с --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Устаревшее меню менеджера недоступно в этой версии ComfyUI. Пожалуйста, используйте новое меню менеджера.",
|
||||
"license": "Лицензия",
|
||||
"loadingVersions": "Загрузка версий...",
|
||||
"nightlyVersion": "Ночная",
|
||||
@@ -752,7 +736,6 @@
|
||||
"manageExtensions": "Управление расширениями",
|
||||
"onChange": "При изменении",
|
||||
"onChangeTooltip": "Рабочий процесс будет поставлен в очередь после внесения изменений",
|
||||
"queue": "Панель очереди",
|
||||
"refresh": "Обновить определения нод",
|
||||
"resetView": "Сбросить вид холста",
|
||||
"run": "Запустить",
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "Нижняя панель",
|
||||
"Browse Templates": "Просмотреть шаблоны",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Performance": "Производительность холста",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
|
||||
"Check for Custom Node Updates": "Проверить обновления пользовательских узлов",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Custom Nodes (Legacy)": "Пользовательские узлы (устаревшие)",
|
||||
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
|
||||
"Decrease Brush Size in MaskEditor": "Уменьшить размер кисти в MaskEditor",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
"Edit": "Редактировать",
|
||||
"Exit Subgraph": "Выйти из подграфа",
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"File": "Файл",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
"Focus Mode": "Режим фокуса",
|
||||
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Help Center": "Центр поддержки",
|
||||
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
|
||||
"Install Missing Custom Nodes": "Установить отсутствующие пользовательские узлы",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
"Manager": "Менеджер",
|
||||
"Minimap": "Мини-карта",
|
||||
"Model Library": "Библиотека моделей",
|
||||
"Manager Menu (Legacy)": "Меню управления (устаревшее)",
|
||||
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
|
||||
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
|
||||
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
"Node Library": "Библиотека узлов",
|
||||
"Node Links": "Связи узлов",
|
||||
"Open": "Открыть",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Открыть 3D-просмотрщик (бета) для выбранного узла",
|
||||
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
|
||||
"Open DevTools": "Открыть инструменты разработчика",
|
||||
"Open Inputs Folder": "Открыть папку входных данных",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
|
||||
"Queue Panel": "Панель очереди",
|
||||
"Queue Prompt": "Запрос в очереди",
|
||||
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
|
||||
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
|
||||
@@ -855,18 +835,11 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Unload Models": "Выгрузить модели",
|
||||
"Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить",
|
||||
"Zoom to fit": "Масштабировать по размеру"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Цвета узлов",
|
||||
"renderBypassState": "Отображать состояние обхода",
|
||||
"renderErrorState": "Отображать состояние ошибки",
|
||||
"showGroups": "Показать фреймы/группы",
|
||||
"showLinks": "Показать связи"
|
||||
"Zoom Out": "Уменьшить"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
@@ -1134,7 +1107,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D-просмотрщик",
|
||||
"API Nodes": "API-узлы",
|
||||
"About": "О программе",
|
||||
"Appearance": "Внешний вид",
|
||||
@@ -1186,31 +1158,10 @@
|
||||
"Window": "Окно",
|
||||
"Workflow": "Рабочий процесс"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Основные",
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"manageShortcuts": "Управление горячими клавишами",
|
||||
"noKeybinding": "Нет сочетания клавиш",
|
||||
"subcategories": {
|
||||
"node": "Узел",
|
||||
"panelControls": "Управление панелью",
|
||||
"queue": "Очередь",
|
||||
"view": "Просмотр",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
"viewControls": "Управление просмотром"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "Просмотреть примеры шаблонов",
|
||||
"downloads": "Загрузки",
|
||||
"helpCenter": "Центр поддержки",
|
||||
"labels": {
|
||||
"models": "Модели",
|
||||
"nodes": "Узлы",
|
||||
"queue": "Очередь",
|
||||
"templates": "Шаблоны",
|
||||
"workflows": "Воркфлоу"
|
||||
},
|
||||
"logout": "Выйти",
|
||||
"modelLibrary": "Библиотека моделей",
|
||||
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
|
||||
@@ -1248,7 +1199,6 @@
|
||||
},
|
||||
"showFlatList": "Показать плоский список"
|
||||
},
|
||||
"templates": "Шаблоны",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Вы уверены, что хотите удалить этот рабочий процесс?",
|
||||
"confirmDeleteTitle": "Удалить рабочий процесс?",
|
||||
@@ -1295,8 +1245,6 @@
|
||||
"Video": "Видео",
|
||||
"Video API": "Video API"
|
||||
},
|
||||
"loadingMore": "Загрузка дополнительных шаблонов...",
|
||||
"searchPlaceholder": "Поиск шаблонов...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
@@ -1619,7 +1567,6 @@
|
||||
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
||||
"failedToFetchBalance": "Не удалось получить баланс: {error}",
|
||||
"failedToFetchLogs": "Не удалось получить серверные логи",
|
||||
"failedToInitializeLoad3dViewer": "Не удалось инициализировать 3D просмотрщик",
|
||||
"failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}",
|
||||
"failedToPurchaseCredits": "Не удалось купить кредиты: {error}",
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "載入預設工作流程"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "切換自訂節點管理器"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "自訂節點管理器"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "自訂節點(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "管理選單(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安裝缺少的自訂節點"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "檢查自訂節點更新"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切換自訂節點管理器進度條"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "為選取的節點開啟 Mask 編輯器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸載模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸載模型與執行快取"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新增空白工作流程"
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "顏色",
|
||||
"comingSoon": "即將推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "指令 {command} 已被禁止。如需更多資訊,請聯絡管理員。",
|
||||
"community": "社群",
|
||||
"completed": "已完成",
|
||||
"confirm": "確認",
|
||||
@@ -300,7 +301,6 @@
|
||||
"disabling": "停用中",
|
||||
"dismiss": "關閉",
|
||||
"download": "下載",
|
||||
"duplicate": "複製",
|
||||
"edit": "編輯",
|
||||
"empty": "空",
|
||||
"enableAll": "全部啟用",
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "正在套用材質貼圖...",
|
||||
"backgroundColor": "背景顏色",
|
||||
"camera": "相機",
|
||||
"cameraType": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透視"
|
||||
},
|
||||
"clearRecording": "清除錄影",
|
||||
"edgeThreshold": "邊緣閾值",
|
||||
"export": "匯出",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "線框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 檢視器中開啟",
|
||||
"previewOutput": "預覽輸出",
|
||||
"removeBackgroundImage": "移除背景圖片",
|
||||
"resizeNodeMatchOutput": "調整節點以符合輸出",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "切換相機",
|
||||
"switchingMaterialMode": "正在切換材質模式...",
|
||||
"upDirection": "上方方向",
|
||||
"upDirections": {
|
||||
"original": "原始"
|
||||
},
|
||||
"uploadBackgroundImage": "上傳背景圖片",
|
||||
"uploadTexture": "上傳材質貼圖",
|
||||
"viewer": {
|
||||
"apply": "套用",
|
||||
"cameraSettings": "相機設定",
|
||||
"cameraType": "相機類型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "匯出設定",
|
||||
"lightSettings": "燈光設定",
|
||||
"modelSettings": "模型設定",
|
||||
"sceneSettings": "場景設定",
|
||||
"title": "3D 檢視器(測試版)"
|
||||
}
|
||||
"uploadTexture": "上傳材質貼圖"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "需要 ComfyUI {version}:",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "安裝佇列",
|
||||
"lastUpdated": "最後更新",
|
||||
"latestVersion": "最新版本",
|
||||
"legacyManagerUI": "使用舊版介面",
|
||||
"legacyManagerUIDescription": "若要使用舊版管理介面,請以 --enable-manager-legacy-ui 啟動 ComfyUI",
|
||||
"legacyMenuNotAvailable": "舊版管理選單不可用,已預設切換至新版管理選單。",
|
||||
"license": "授權條款",
|
||||
"loadingVersions": "正在載入版本...",
|
||||
"nightlyVersion": "每夜建置版",
|
||||
@@ -752,7 +736,6 @@
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "變更時",
|
||||
"onChangeTooltip": "每當有變更時,工作流程會排入佇列",
|
||||
"queue": "佇列面板",
|
||||
"refresh": "重新整理節點定義",
|
||||
"resetView": "重設畫布視圖",
|
||||
"run": "執行",
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "瀏覽範本",
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Performance": "畫布效能",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
"Clear Workflow": "清除工作流程",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "聯絡支援",
|
||||
"Convert Selection to Subgraph": "將選取內容轉為子圖",
|
||||
"Convert selected nodes to group node": "將選取節點轉為群組節點",
|
||||
"Custom Nodes (Legacy)": "自訂節點(舊版)",
|
||||
"Custom Nodes Manager": "自訂節點管理員",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Delete Selected Items": "刪除選取項目",
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
"Edit": "編輯",
|
||||
"Exit Subgraph": "離開子圖",
|
||||
"Export": "匯出",
|
||||
"Export (API)": "匯出(API)",
|
||||
"File": "檔案",
|
||||
"Fit Group To Contents": "群組貼合內容",
|
||||
"Focus Mode": "專注模式",
|
||||
"Fit view to selected nodes": "視圖貼合選取節點",
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Help Center": "說明中心",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
|
||||
"Interrupt": "中斷",
|
||||
"Load Default Workflow": "載入預設工作流程",
|
||||
"Manage group nodes": "管理群組節點",
|
||||
"Manager": "管理員",
|
||||
"Minimap": "縮圖地圖",
|
||||
"Model Library": "模型庫",
|
||||
"Manager Menu (Legacy)": "管理員選單(舊版)",
|
||||
"Move Selected Nodes Down": "選取節點下移",
|
||||
"Move Selected Nodes Left": "選取節點左移",
|
||||
"Move Selected Nodes Right": "選取節點右移",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "靜音/取消靜音選取節點",
|
||||
"New": "新增",
|
||||
"Next Opened Workflow": "下一個已開啟的工作流程",
|
||||
"Node Library": "節點庫",
|
||||
"Node Links": "節點連結",
|
||||
"Open": "開啟",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "為選取的節點開啟 3D 檢視器(Beta 版)",
|
||||
"Open Custom Nodes Folder": "開啟自訂節點資料夾",
|
||||
"Open DevTools": "開啟開發者工具",
|
||||
"Open Inputs Folder": "開啟輸入資料夾",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "釘選/取消釘選選取項目",
|
||||
"Pin/Unpin Selected Nodes": "釘選/取消釘選選取節點",
|
||||
"Previous Opened Workflow": "上一個已開啟的工作流程",
|
||||
"Queue Panel": "佇列面板",
|
||||
"Queue Prompt": "加入提示至佇列",
|
||||
"Queue Prompt (Front)": "將提示加入佇列前端",
|
||||
"Queue Selected Output Nodes": "將選取的輸出節點加入佇列",
|
||||
@@ -855,18 +835,11 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
|
||||
"Undo": "復原",
|
||||
"Ungroup selected group nodes": "取消群組選取的群組節點",
|
||||
"Unpack the selected Subgraph": "解包所選子圖",
|
||||
"Workflows": "工作流程",
|
||||
"Unload Models": "卸載模型",
|
||||
"Unload Models and Execution Cache": "卸載模型與執行快取",
|
||||
"Workflow": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小",
|
||||
"Zoom to fit": "縮放至適合大小"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
"Zoom Out": "縮小"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不要再顯示此訊息",
|
||||
@@ -1134,7 +1107,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D 檢視器",
|
||||
"API Nodes": "API 節點",
|
||||
"About": "關於",
|
||||
"Appearance": "外觀",
|
||||
@@ -1186,31 +1158,10 @@
|
||||
"Window": "視窗",
|
||||
"Workflow": "工作流程"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本",
|
||||
"keyboardShortcuts": "鍵盤快捷鍵",
|
||||
"manageShortcuts": "管理快捷鍵",
|
||||
"noKeybinding": "無快捷鍵",
|
||||
"subcategories": {
|
||||
"node": "節點",
|
||||
"panelControls": "面板控制",
|
||||
"queue": "佇列",
|
||||
"view": "檢視",
|
||||
"workflow": "工作流程"
|
||||
},
|
||||
"viewControls": "檢視控制"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "瀏覽範例模板",
|
||||
"downloads": "下載",
|
||||
"helpCenter": "說明中心",
|
||||
"labels": {
|
||||
"models": "模型",
|
||||
"nodes": "節點",
|
||||
"queue": "佇列",
|
||||
"templates": "範本",
|
||||
"workflows": "工作流程"
|
||||
},
|
||||
"logout": "登出",
|
||||
"modelLibrary": "模型庫",
|
||||
"newBlankWorkflow": "建立新的空白工作流程",
|
||||
@@ -1248,7 +1199,6 @@
|
||||
},
|
||||
"showFlatList": "顯示平面清單"
|
||||
},
|
||||
"templates": "範本",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您確定要刪除這個工作流程嗎?",
|
||||
"confirmDeleteTitle": "刪除工作流程?",
|
||||
@@ -1295,8 +1245,6 @@
|
||||
"Video": "影片",
|
||||
"Video API": "影片 API"
|
||||
},
|
||||
"loadingMore": "正在載入更多範本...",
|
||||
"searchPlaceholder": "搜尋範本...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
@@ -1619,7 +1567,6 @@
|
||||
"failedToExportModel": "無法將模型匯出為 {format}",
|
||||
"failedToFetchBalance": "取得餘額失敗:{error}",
|
||||
"failedToFetchLogs": "無法取得伺服器日誌",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失敗",
|
||||
"failedToInitiateCreditPurchase": "啟動點數購買失敗:{error}",
|
||||
"failedToPurchaseCredits": "購買點數失敗:{error}",
|
||||
"fileLoadError": "無法在 {fileName} 中找到工作流程",
|
||||
|
||||
@@ -164,8 +164,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "加载默认工作流"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "自定义节点(测试版)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "自訂節點(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "管理員選單(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安装缺失的包"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "检查更新"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
@@ -179,6 +191,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "打开选中节点的遮罩编辑器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸载模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸载模型和执行缓存"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -84,9 +84,9 @@
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "清除工作流程",
|
||||
"deleteWorkflow": "删除工作流程",
|
||||
"duplicate": "复制",
|
||||
"enterNewName": "输入新名称"
|
||||
"deleteWorkflow": "刪除工作流程",
|
||||
"duplicate": "複製",
|
||||
"enterNewName": "輸入新名稱"
|
||||
},
|
||||
"chatHistory": {
|
||||
"cancelEdit": "取消",
|
||||
@@ -278,6 +278,7 @@
|
||||
"color": "颜色",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "命令 {command} 被禁止。请联系管理员获取更多信息。",
|
||||
"community": "社区",
|
||||
"completed": "已完成",
|
||||
"confirm": "确认",
|
||||
@@ -298,9 +299,8 @@
|
||||
"devices": "设备",
|
||||
"disableAll": "禁用全部",
|
||||
"disabling": "禁用中",
|
||||
"dismiss": "关闭",
|
||||
"dismiss": "關閉",
|
||||
"download": "下载",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
"empty": "空",
|
||||
"enableAll": "启用全部",
|
||||
@@ -315,7 +315,7 @@
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "转到节点",
|
||||
"help": "帮助",
|
||||
"icon": "图标",
|
||||
@@ -404,7 +404,7 @@
|
||||
"usageHint": "使用提示",
|
||||
"user": "用户",
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 请参阅 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新说明。",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
@@ -414,7 +414,7 @@
|
||||
"resetView": "重置视图",
|
||||
"selectMode": "选择模式",
|
||||
"toggleLinkVisibility": "切换连线可见性",
|
||||
"toggleMinimap": "切换小地图",
|
||||
"toggleMinimap": "切換小地圖",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
@@ -573,10 +573,6 @@
|
||||
"applyingTexture": "应用纹理中...",
|
||||
"backgroundColor": "背景颜色",
|
||||
"camera": "摄影机",
|
||||
"cameraType": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透视"
|
||||
},
|
||||
"clearRecording": "清除录制",
|
||||
"edgeThreshold": "边缘阈值",
|
||||
"export": "导出",
|
||||
@@ -597,7 +593,6 @@
|
||||
"wireframe": "线框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 查看器中打开",
|
||||
"previewOutput": "预览输出",
|
||||
"removeBackgroundImage": "移除背景图片",
|
||||
"resizeNodeMatchOutput": "调整节点以匹配输出",
|
||||
@@ -608,22 +603,8 @@
|
||||
"switchCamera": "切换摄影机类型",
|
||||
"switchingMaterialMode": "切换材质模式中...",
|
||||
"upDirection": "上方向",
|
||||
"upDirections": {
|
||||
"original": "原始"
|
||||
},
|
||||
"uploadBackgroundImage": "上传背景图片",
|
||||
"uploadTexture": "上传纹理",
|
||||
"viewer": {
|
||||
"apply": "应用",
|
||||
"cameraSettings": "相机设置",
|
||||
"cameraType": "相机类型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "导出设置",
|
||||
"lightSettings": "灯光设置",
|
||||
"modelSettings": "模型设置",
|
||||
"sceneSettings": "场景设置",
|
||||
"title": "3D 查看器(测试版)"
|
||||
}
|
||||
"uploadTexture": "上传纹理"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "需要 ComfyUI {version}:",
|
||||
@@ -670,6 +651,9 @@
|
||||
"installationQueue": "安装队列",
|
||||
"lastUpdated": "最后更新",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "使用旧版UI",
|
||||
"legacyManagerUIDescription": "要使用旧版的管理器UI,请启动ComfyUI并使用 --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "在此版本的ComfyUI中,不提供旧版的管理器菜单。请使用新的管理器菜单。",
|
||||
"license": "许可证",
|
||||
"loadingVersions": "正在加载版本...",
|
||||
"nightlyVersion": "每夜",
|
||||
@@ -743,7 +727,7 @@
|
||||
"disabled": "禁用",
|
||||
"disabledTooltip": "工作流将不会自动执行",
|
||||
"execute": "执行",
|
||||
"help": "说明",
|
||||
"help": "說明",
|
||||
"hideMenu": "隐藏菜单",
|
||||
"instant": "实时",
|
||||
"instantTooltip": "工作流将会在生成完成后立即执行",
|
||||
@@ -752,15 +736,14 @@
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "更改时",
|
||||
"onChangeTooltip": "一旦进行更改,工作流将添加到执行队列",
|
||||
"queue": "队列面板",
|
||||
"refresh": "刷新节点",
|
||||
"resetView": "重置视图",
|
||||
"run": "运行",
|
||||
"runWorkflow": "运行工作流程(Shift排在前面)",
|
||||
"runWorkflowFront": "运行工作流程(排在前面)",
|
||||
"settings": "设定",
|
||||
"settings": "設定",
|
||||
"showMenu": "显示菜单",
|
||||
"theme": "主题",
|
||||
"theme": "主題",
|
||||
"toggleBottomPanel": "底部面板"
|
||||
},
|
||||
"menuLabels": {
|
||||
@@ -768,8 +751,10 @@
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "浏览模板",
|
||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||
"Canvas Performance": "画布性能",
|
||||
"Canvas Toggle Link Visibility": "切换连线可见性",
|
||||
"Canvas Toggle Lock": "切换视图锁定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "检查更新",
|
||||
"Clear Pending Tasks": "清除待处理任务",
|
||||
"Clear Workflow": "清除工作流",
|
||||
@@ -783,28 +768,27 @@
|
||||
"Contact Support": "联系支持",
|
||||
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中减小笔刷大小",
|
||||
"Custom Nodes (Legacy)": "自訂節點(舊版)",
|
||||
"Custom Nodes Manager": "自定义节点管理器",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
"Edit": "编辑",
|
||||
"Exit Subgraph": "退出子图",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"File": "文件",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
"Focus Mode": "专注模式",
|
||||
"Fit view to selected nodes": "适应视图到选中节点",
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Help Center": "帮助中心",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大笔刷大小",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
"Manager": "管理器",
|
||||
"Minimap": "小地图",
|
||||
"Model Library": "模型库",
|
||||
"Manager Menu (Legacy)": "管理選單(舊版)",
|
||||
"Move Selected Nodes Down": "下移所选节点",
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
"Move Selected Nodes Right": "右移所选节点",
|
||||
@@ -812,10 +796,7 @@
|
||||
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
"Node Library": "节点库",
|
||||
"Node Links": "节点连接",
|
||||
"Open": "打开",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "为选中节点打开3D查看器(测试版)",
|
||||
"Open Custom Nodes Folder": "打开自定义节点文件夹",
|
||||
"Open DevTools": "打开开发者工具",
|
||||
"Open Inputs Folder": "打开输入文件夹",
|
||||
@@ -828,7 +809,6 @@
|
||||
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
|
||||
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
|
||||
"Previous Opened Workflow": "上一个打开的工作流",
|
||||
"Queue Panel": "队列面板",
|
||||
"Queue Prompt": "执行提示词",
|
||||
"Queue Prompt (Front)": "执行提示词 (优先执行)",
|
||||
"Queue Selected Output Nodes": "将所选输出节点加入队列",
|
||||
@@ -845,28 +825,24 @@
|
||||
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Essential Bottom Panel": "切换基础底部面板",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle View Controls Bottom Panel": "切换视图控制底部面板",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
"Unpack the selected Subgraph": "解包选中子图",
|
||||
"Workflows": "工作流",
|
||||
"Unload Models": "卸载模型",
|
||||
"Unload Models and Execution Cache": "卸载模型和执行缓存",
|
||||
"Workflow": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面",
|
||||
"Zoom to fit": "缩放以适应"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "节点颜色",
|
||||
"renderBypassState": "渲染绕过状态",
|
||||
"renderErrorState": "渲染错误状态",
|
||||
"showGroups": "显示框架/分组",
|
||||
"showLinks": "显示连接"
|
||||
"Zoom Out": "缩小画面"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
@@ -1134,7 +1110,6 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D查看器",
|
||||
"API Nodes": "API 节点",
|
||||
"About": "关于",
|
||||
"Appearance": "外观",
|
||||
@@ -1186,31 +1161,10 @@
|
||||
"Window": "窗口",
|
||||
"Workflow": "工作流"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "常用",
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"manageShortcuts": "管理快捷键",
|
||||
"noKeybinding": "无快捷键",
|
||||
"subcategories": {
|
||||
"node": "节点",
|
||||
"panelControls": "面板控制",
|
||||
"queue": "队列",
|
||||
"view": "视图",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
"viewControls": "视图控制"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "浏览示例模板",
|
||||
"downloads": "下载",
|
||||
"helpCenter": "帮助中心",
|
||||
"labels": {
|
||||
"models": "模型",
|
||||
"nodes": "节点",
|
||||
"queue": "队列",
|
||||
"templates": "模板",
|
||||
"workflows": "工作流"
|
||||
},
|
||||
"logout": "登出",
|
||||
"modelLibrary": "模型库",
|
||||
"newBlankWorkflow": "创建空白工作流",
|
||||
@@ -1248,7 +1202,6 @@
|
||||
},
|
||||
"showFlatList": "平铺结果"
|
||||
},
|
||||
"templates": "模板",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您确定要删除此工作流吗?",
|
||||
"confirmDeleteTitle": "删除工作流?",
|
||||
@@ -1295,8 +1248,6 @@
|
||||
"Video": "视频生成",
|
||||
"Video API": "视频 API"
|
||||
},
|
||||
"loadingMore": "正在加载更多模板...",
|
||||
"searchPlaceholder": "搜索模板...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "混元3D 2.0 图生模型",
|
||||
@@ -1619,7 +1570,6 @@
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchBalance": "获取余额失败:{error}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"failedToInitializeLoad3dViewer": "初始化3D查看器失败",
|
||||
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
|
||||
"failedToPurchaseCredits": "购买积分失败:{error}",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
@@ -1676,9 +1626,9 @@
|
||||
"required": "必填"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "关闭",
|
||||
"dismiss": "關閉",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"title": "版本相容性警告",
|
||||
"updateFrontend": "更新前端"
|
||||
},
|
||||
|
||||
@@ -495,6 +495,7 @@ const zSettings = z.object({
|
||||
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'Comfy.Load3D.3DViewerEnable': z.boolean(),
|
||||
'Comfy.Memory.AllowManualUnload': z.boolean(),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.string(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { get } from 'es-toolkit/compat'
|
||||
|
||||
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
||||
import type {
|
||||
@@ -35,6 +36,7 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
@@ -1020,6 +1022,56 @@ export class ComfyApi extends EventTarget {
|
||||
return (await axios.get(this.internalURL('/folder_paths'))).data
|
||||
}
|
||||
|
||||
/* Frees memory by unloading models and optionally freeing execution cache
|
||||
* @param {Object} options - The options object
|
||||
* @param {boolean} options.freeExecutionCache - If true, also frees execution cache
|
||||
*/
|
||||
async freeMemory(options: { freeExecutionCache: boolean }) {
|
||||
try {
|
||||
let mode = ''
|
||||
if (options.freeExecutionCache) {
|
||||
mode = '{"unload_models": true, "free_memory": true}'
|
||||
} else {
|
||||
mode = '{"unload_models": true}'
|
||||
}
|
||||
|
||||
const res = await this.fetchApi(`/free`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: mode
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
if (options.freeExecutionCache) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models and Execution Cache have been cleared.',
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models have been unloaded.',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom nodes i18n data from the server.
|
||||
*
|
||||
@@ -1031,21 +1083,21 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
/**
|
||||
* Checks if the server supports a specific feature.
|
||||
* @param featureName The name of the feature to check
|
||||
* @param featureName The name of the feature to check (supports dot notation for nested values)
|
||||
* @returns true if the feature is supported, false otherwise
|
||||
*/
|
||||
serverSupportsFeature(featureName: string): boolean {
|
||||
return this.serverFeatureFlags[featureName] === true
|
||||
return get(this.serverFeatureFlags, featureName) === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a server feature flag value.
|
||||
* @param featureName The name of the feature to get
|
||||
* @param featureName The name of the feature to get (supports dot notation for nested values)
|
||||
* @param defaultValue The default value if the feature is not found
|
||||
* @returns The feature value or default
|
||||
*/
|
||||
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
|
||||
return (this.serverFeatureFlags[featureName] ?? defaultValue) as T
|
||||
return get(this.serverFeatureFlags, featureName, defaultValue) as T
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import {
|
||||
type ImportFailInfoBulkRequest,
|
||||
type ImportFailInfoBulkResponse,
|
||||
type InstallPackParams,
|
||||
type InstalledPacksResponse,
|
||||
type ManagerPackInfo,
|
||||
@@ -27,16 +29,17 @@ enum ManagerRoute {
|
||||
UPDATE_ALL = 'manager/queue/update_all',
|
||||
UNINSTALL = 'manager/queue/uninstall',
|
||||
DISABLE = 'manager/queue/disable',
|
||||
// FIX_NODE is currently unused but kept for potential future implementation
|
||||
FIX_NODE = 'manager/queue/fix',
|
||||
LIST_INSTALLED = 'customnode/installed',
|
||||
GET_NODES = 'customnode/getmappings',
|
||||
GET_PACKS = 'customnode/getlist',
|
||||
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
|
||||
REBOOT = 'manager/reboot'
|
||||
IMPORT_FAIL_INFO_BULK = 'customnode/import_fail_info_bulk',
|
||||
REBOOT = 'manager/reboot',
|
||||
IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui'
|
||||
}
|
||||
|
||||
const managerApiClient = axios.create({
|
||||
baseURL: api.apiURL(''),
|
||||
baseURL: api.apiURL('/v2/'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -154,6 +157,21 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const getImportFailInfoBulk = async (
|
||||
params: ImportFailInfoBulkRequest = {},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Fetching bulk import failure information'
|
||||
|
||||
return executeRequest<ImportFailInfoBulkResponse>(
|
||||
() =>
|
||||
managerApiClient.post(ManagerRoute.IMPORT_FAIL_INFO_BULK, params, {
|
||||
signal
|
||||
}),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
signal?: AbortSignal
|
||||
@@ -247,6 +265,15 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const isLegacyManagerUI = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Checking if user set Manager to use the legacy UI'
|
||||
|
||||
return executeRequest<{ is_legacy_manager_ui: boolean }>(
|
||||
() => managerApiClient.get(ManagerRoute.IS_LEGACY_MANAGER_UI, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
@@ -260,6 +287,7 @@ export const useComfyManagerService = () => {
|
||||
// Pack management
|
||||
listInstalledPacks,
|
||||
getImportFailInfo,
|
||||
getImportFailInfoBulk,
|
||||
installPack,
|
||||
uninstallPack,
|
||||
enablePack: installPack, // enable is done via install
|
||||
@@ -268,6 +296,7 @@ export const useComfyManagerService = () => {
|
||||
updateAllPacks,
|
||||
|
||||
// System operations
|
||||
rebootComfyUI
|
||||
rebootComfyUI,
|
||||
isLegacyManagerUI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +359,55 @@ export const useComfyRegistryService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple pack versions in a single bulk request.
|
||||
* This is more efficient than making individual requests for each pack version.
|
||||
*
|
||||
* @param nodeVersions - Array of node ID and version pairs to retrieve
|
||||
* @param signal - Optional AbortSignal for request cancellation
|
||||
* @returns Bulk response containing the requested node versions or null on error
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const versions = await getBulkNodeVersions([
|
||||
* { node_id: 'ComfyUI-Manager', version: '1.0.0' },
|
||||
* { node_id: 'ComfyUI-Impact-Pack', version: '2.0.0' }
|
||||
* ])
|
||||
* if (versions) {
|
||||
* versions.node_versions.forEach(result => {
|
||||
* if (result.status === 'success' && result.node_version) {
|
||||
* console.log(`Retrieved ${result.identifier.node_id}@${result.identifier.version}`)
|
||||
* }
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const getBulkNodeVersions = async (
|
||||
nodeVersions: components['schemas']['NodeVersionIdentifier'][],
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const endpoint = '/bulk/nodes/versions'
|
||||
const errorContext = 'Failed to get bulk node versions'
|
||||
const routeSpecificErrors = {
|
||||
400: 'Bad request: Invalid node version identifiers provided'
|
||||
}
|
||||
|
||||
const requestBody: components['schemas']['BulkNodeVersionsRequest'] = {
|
||||
node_versions: nodeVersions
|
||||
}
|
||||
|
||||
return executeApiRequest(
|
||||
() =>
|
||||
registryApiClient.post<
|
||||
components['schemas']['BulkNodeVersionsResponse']
|
||||
>(endpoint, requestBody, {
|
||||
signal
|
||||
}),
|
||||
errorContext,
|
||||
routeSpecificErrors
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
@@ -372,6 +421,7 @@ export const useComfyRegistryService = () => {
|
||||
listPacksForPublisher,
|
||||
getNodeDefs,
|
||||
postPackReview,
|
||||
inferPackFromNodeName
|
||||
inferPackFromNodeName,
|
||||
getBulkNodeVersions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsD
|
||||
import UpdatePasswordContent from '@/components/dialog/content/UpdatePasswordContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import NodeConflictDialogContent from '@/components/dialog/content/manager/NodeConflictDialogContent.vue'
|
||||
import NodeConflictFooter from '@/components/dialog/content/manager/NodeConflictFooter.vue'
|
||||
import NodeConflictHeader from '@/components/dialog/content/manager/NodeConflictHeader.vue'
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
|
||||
import ManagerProgressHeader from '@/components/dialog/header/ManagerProgressHeader.vue'
|
||||
@@ -28,6 +31,7 @@ import {
|
||||
type ShowDialogOptions,
|
||||
useDialogStore
|
||||
} from '@/stores/dialogStore'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -463,6 +467,54 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showNodeConflictDialog(
|
||||
options: {
|
||||
showAfterWhatsNew?: boolean
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
dialogComponentProps,
|
||||
buttonText,
|
||||
onButtonClick,
|
||||
showAfterWhatsNew,
|
||||
conflictedPackages
|
||||
} = options
|
||||
|
||||
return dialogStore.showDialog({
|
||||
key: 'global-node-conflict',
|
||||
headerComponent: NodeConflictHeader,
|
||||
footerComponent: NodeConflictFooter,
|
||||
component: NodeConflictDialogContent,
|
||||
dialogComponentProps: {
|
||||
closable: true,
|
||||
pt: {
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: { class: '!p-0 overflow-y-hidden' },
|
||||
footer: { class: '!p-0' },
|
||||
pcCloseButton: {
|
||||
root: {
|
||||
class:
|
||||
'!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5 bg-gray-500 dark-theme:bg-neutral-700 text-white'
|
||||
}
|
||||
}
|
||||
},
|
||||
...dialogComponentProps
|
||||
},
|
||||
props: {
|
||||
showAfterWhatsNew,
|
||||
conflictedPackages
|
||||
},
|
||||
footerProps: {
|
||||
buttonText,
|
||||
onButtonClick
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -483,6 +535,7 @@ export const useDialogService = () => {
|
||||
confirm,
|
||||
toggleManagerDialog,
|
||||
toggleManagerProgressDialog,
|
||||
showLayoutDialog
|
||||
showLayoutDialog,
|
||||
showNodeConflictDialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { mapKeys } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -29,6 +30,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||
const disabledPacksIds = ref<Set<string>>(new Set())
|
||||
const installedPacksIds = ref<Set<string>>(new Set())
|
||||
const installingPacksIds = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
|
||||
@@ -49,6 +51,9 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
isInstalledPackId(packName) &&
|
||||
enabledPacksIds.value.has(packName)
|
||||
|
||||
const isInstallingPackId = (packName: string | undefined): boolean =>
|
||||
!!packName && installingPacksIds.value.has(packName)
|
||||
|
||||
const packsToIdSet = (packs: ManagerPackInstalled[]) =>
|
||||
packs.reduce((acc, pack) => {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
@@ -110,14 +115,25 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
|
||||
const refreshInstalledList = async () => {
|
||||
const packs = await managerService.listInstalledPacks()
|
||||
if (packs) installedPacks.value = packs
|
||||
if (packs) {
|
||||
// The keys are 'cleaned' by stripping the version suffix.
|
||||
// The pack object itself (the value) still contains the version info.
|
||||
const packsWithCleanedKeys = mapKeys(packs, (_value, key) => {
|
||||
return key.split('@')[0]
|
||||
})
|
||||
installedPacks.value = packsWithCleanedKeys
|
||||
}
|
||||
isStale.value = false
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const withLogs = (
|
||||
task: () => Promise<null>,
|
||||
taskName: string,
|
||||
packId?: string
|
||||
) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
|
||||
const loggedTask = async () => {
|
||||
@@ -128,6 +144,9 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
|
||||
const onComplete = async () => {
|
||||
await stopListening()
|
||||
if (packId) {
|
||||
installingPacksIds.value.delete(packId)
|
||||
}
|
||||
setStale()
|
||||
}
|
||||
|
||||
@@ -152,8 +171,11 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
}
|
||||
|
||||
installingPacksIds.value.add(params.id)
|
||||
const task = () => managerService.installPack(params, signal)
|
||||
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
|
||||
enqueueTask(
|
||||
withLogs(task, `${actionDescription} ${params.id}`, params.id)
|
||||
)
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
@@ -162,14 +184,16 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
enqueueTask(
|
||||
withLogs(task, t('manager.uninstalling', { id: params.id }), params.id)
|
||||
)
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id }), params.id))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
@@ -184,7 +208,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id }), params.id))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
@@ -212,6 +236,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installedPacksIds,
|
||||
isPackInstalled: isInstalledPackId,
|
||||
isPackEnabled: isEnabledPackId,
|
||||
isPackInstalling: isInstallingPackId,
|
||||
getInstalledPackVersion,
|
||||
refreshInstalledList,
|
||||
|
||||
|
||||
70
src/stores/conflictDetectionStore.ts
Normal file
70
src/stores/conflictDetectionStore.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
export const useConflictDetectionStore = defineStore(
|
||||
'conflictDetection',
|
||||
() => {
|
||||
// State
|
||||
const conflictedPackages = ref<ConflictDetectionResult[]>([])
|
||||
const isDetecting = ref(false)
|
||||
const lastDetectionTime = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const hasConflicts = computed(() =>
|
||||
conflictedPackages.value.some((pkg) => pkg.has_conflict)
|
||||
)
|
||||
|
||||
const getConflictsForPackageByID = computed(
|
||||
() => (packageId: string) =>
|
||||
conflictedPackages.value.find((pkg) => pkg.package_id === packageId)
|
||||
)
|
||||
|
||||
const bannedPackages = computed(() =>
|
||||
conflictedPackages.value.filter((pkg) =>
|
||||
pkg.conflicts.some((conflict) => conflict.type === 'banned')
|
||||
)
|
||||
)
|
||||
|
||||
const securityPendingPackages = computed(() =>
|
||||
conflictedPackages.value.filter((pkg) =>
|
||||
pkg.conflicts.some((conflict) => conflict.type === 'pending')
|
||||
)
|
||||
)
|
||||
|
||||
// Actions
|
||||
function setConflictedPackages(packages: ConflictDetectionResult[]) {
|
||||
conflictedPackages.value = [...packages]
|
||||
}
|
||||
|
||||
function clearConflicts() {
|
||||
conflictedPackages.value = []
|
||||
}
|
||||
|
||||
function setDetecting(detecting: boolean) {
|
||||
isDetecting.value = detecting
|
||||
}
|
||||
|
||||
function setLastDetectionTime(time: string) {
|
||||
lastDetectionTime.value = time
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
conflictedPackages,
|
||||
isDetecting,
|
||||
lastDetectionTime,
|
||||
// Getters
|
||||
hasConflicts,
|
||||
getConflictsForPackageByID,
|
||||
bannedPackages,
|
||||
securityPendingPackages,
|
||||
// Actions
|
||||
setConflictedPackages,
|
||||
clearConflicts,
|
||||
setDetecting,
|
||||
setLastDetectionTime
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -43,6 +43,7 @@ interface DialogInstance {
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
footerComponent?: Component
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps: DialogComponentProps
|
||||
priority: number
|
||||
}
|
||||
@@ -54,6 +55,7 @@ export interface ShowDialogOptions {
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
/**
|
||||
* Optional priority for dialog stacking.
|
||||
@@ -127,6 +129,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
priority?: number
|
||||
}) {
|
||||
@@ -146,6 +149,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
contentProps: { ...options.props },
|
||||
footerProps: { ...options.footerProps },
|
||||
priority: options.priority ?? 1,
|
||||
dialogComponentProps: {
|
||||
maximizable: false,
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
export type ButtonSize = 'fit-content' | 'sm' | 'md'
|
||||
export type ButtonType = 'primary' | 'secondary' | 'transparent'
|
||||
export type ButtonBorder = boolean
|
||||
|
||||
export interface BaseButtonProps {
|
||||
size?: 'fit-content' | 'sm' | 'md'
|
||||
type?: 'primary' | 'secondary' | 'transparent'
|
||||
size?: ButtonSize
|
||||
type?: ButtonType
|
||||
border?: ButtonBorder
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
|
||||
export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
|
||||
const sizeClasses = {
|
||||
'fit-content': '',
|
||||
sm: 'px-2 py-1.5 text-xs',
|
||||
@@ -15,22 +21,39 @@ export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
|
||||
return sizeClasses[size]
|
||||
}
|
||||
|
||||
export const getButtonTypeClasses = (
|
||||
type: BaseButtonProps['type'] = 'primary'
|
||||
) => {
|
||||
const typeClasses = {
|
||||
export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||
const baseByType = {
|
||||
primary:
|
||||
'bg-neutral-900 border-none text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
||||
secondary:
|
||||
'bg-white border-none text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
|
||||
transparent:
|
||||
'bg-transparent border-none text-neutral-600 dark-theme:text-neutral-400'
|
||||
} as const
|
||||
|
||||
return baseByType[type]
|
||||
}
|
||||
|
||||
export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||
const baseByType = {
|
||||
primary:
|
||||
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
||||
secondary:
|
||||
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
|
||||
transparent: 'bg-transparent text-neutral-600 dark-theme:text-neutral-400'
|
||||
}
|
||||
return typeClasses[type]
|
||||
} as const
|
||||
|
||||
const borderByType = {
|
||||
primary: 'border border-solid border-white dark-theme:border-neutral-900',
|
||||
secondary: 'border border-solid border-neutral-950 dark-theme:border-white',
|
||||
transparent:
|
||||
'border border-solid border-neutral-950 dark-theme:border-white'
|
||||
} as const
|
||||
|
||||
return `${baseByType[type]} ${borderByType[type]}`
|
||||
}
|
||||
|
||||
export const getIconButtonSizeClasses = (
|
||||
size: BaseButtonProps['size'] = 'md'
|
||||
) => {
|
||||
export const getIconButtonSizeClasses = (size: ButtonSize = 'md') => {
|
||||
const sizeClasses = {
|
||||
'fit-content': 'w-auto h-auto',
|
||||
sm: 'w-6 h-6 text-xs !rounded-md',
|
||||
@@ -40,5 +63,9 @@ export const getIconButtonSizeClasses = (
|
||||
}
|
||||
|
||||
export const getBaseButtonClasses = () => {
|
||||
return 'flex items-center justify-center flex-shrink-0 outline-none border-none rounded-lg cursor-pointer transition-all duration-200'
|
||||
return [
|
||||
'flex items-center justify-center flex-shrink-0',
|
||||
'outline-none rounded-lg cursor-pointer transition-all duration-200',
|
||||
'disabled:opacity-50 disabled:pointer-events-none'
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { InjectionKey, Ref } from 'vue'
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components as managerComponents } from '@/types/generatedManagerTypes'
|
||||
import type { SearchMode } from '@/types/searchServiceTypes'
|
||||
|
||||
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
|
||||
@@ -19,7 +20,7 @@ export const IsInstallingKey: InjectionKey<Ref<boolean>> =
|
||||
Symbol('isInstalling')
|
||||
|
||||
export enum ManagerWsQueueStatus {
|
||||
DONE = 'done',
|
||||
DONE = 'all-done',
|
||||
IN_PROGRESS = 'in_progress'
|
||||
}
|
||||
|
||||
@@ -242,3 +243,13 @@ export interface ManagerState {
|
||||
searchMode: SearchMode
|
||||
sortField: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Types for import failure information API
|
||||
*/
|
||||
export type ImportFailInfoBulkRequest =
|
||||
managerComponents['schemas']['ImportFailInfoBulkRequest']
|
||||
export type ImportFailInfoBulkResponse =
|
||||
managerComponents['schemas']['ImportFailInfoBulkResponse']
|
||||
export type ImportFailInfoItem =
|
||||
managerComponents['schemas']['ImportFailInfoItem']
|
||||
|
||||
126
src/types/conflictDetectionTypes.ts
Normal file
126
src/types/conflictDetectionTypes.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Type definitions for the conflict detection system.
|
||||
* These types are used to detect compatibility issues between Node Packs and the system environment.
|
||||
*
|
||||
* This file extends and uses types from comfyRegistryTypes.ts to maintain consistency
|
||||
* with the Registry API schema.
|
||||
*/
|
||||
import type { components } from './comfyRegistryTypes'
|
||||
|
||||
// Re-export core types from Registry API
|
||||
export type Node = components['schemas']['Node']
|
||||
export type NodeVersion = components['schemas']['NodeVersion']
|
||||
export type NodeStatus = components['schemas']['NodeStatus']
|
||||
export type NodeVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
/**
|
||||
* Conflict types that can be detected in the system
|
||||
* @enum {string}
|
||||
*/
|
||||
export type ConflictType =
|
||||
| 'comfyui_version' // ComfyUI version mismatch
|
||||
| 'frontend_version' // Frontend version mismatch
|
||||
| 'import_failed'
|
||||
// | 'python_version' // Python version mismatch
|
||||
| 'os' // Operating system incompatibility
|
||||
| 'accelerator' // GPU/accelerator incompatibility
|
||||
| 'banned' // Banned package
|
||||
| 'pending' // Security verification pending
|
||||
|
||||
/**
|
||||
* Version comparison operators
|
||||
* @enum {string}
|
||||
*/
|
||||
export type VersionOperator = '>=' | '>' | '<=' | '<' | '==' | '!='
|
||||
|
||||
/**
|
||||
* Version requirement specification
|
||||
*/
|
||||
export interface VersionRequirement {
|
||||
/** @description Comparison operator for version checking */
|
||||
operator: VersionOperator
|
||||
/** @description Target version string */
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Node Pack requirements from Registry API
|
||||
* Extends Node type with additional installation and compatibility metadata
|
||||
*/
|
||||
export interface NodePackRequirements extends Node {
|
||||
installed_version: string
|
||||
is_enabled: boolean
|
||||
is_banned: boolean
|
||||
is_pending: boolean
|
||||
// Aliases for backwards compatibility with existing code
|
||||
version_status?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Current system environment information
|
||||
*/
|
||||
export interface SystemEnvironment {
|
||||
// Version information
|
||||
comfyui_version: string
|
||||
frontend_version: string
|
||||
// python_version: string
|
||||
|
||||
// Platform information
|
||||
os: string
|
||||
platform_details: string
|
||||
architecture: string
|
||||
|
||||
// GPU/accelerator information
|
||||
available_accelerators: Node['supported_accelerators']
|
||||
primary_accelerator: string
|
||||
gpu_memory_mb?: number
|
||||
|
||||
// Runtime information
|
||||
node_env: 'development' | 'production'
|
||||
user_agent: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual conflict detection result for a package
|
||||
*/
|
||||
export interface ConflictDetectionResult {
|
||||
package_id: string
|
||||
package_name: string
|
||||
has_conflict: boolean
|
||||
conflicts: ConflictDetail[]
|
||||
is_compatible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed information about a specific conflict
|
||||
*/
|
||||
export interface ConflictDetail {
|
||||
type: ConflictType
|
||||
current_value: string
|
||||
required_value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Overall conflict detection summary
|
||||
*/
|
||||
export interface ConflictDetectionSummary {
|
||||
total_packages: number
|
||||
compatible_packages: number
|
||||
conflicted_packages: number
|
||||
banned_packages: number
|
||||
pending_packages: number
|
||||
conflicts_by_type_details: Record<ConflictType, string[]>
|
||||
last_check_timestamp: string
|
||||
check_duration_ms: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Response payload from conflict detection API
|
||||
*/
|
||||
export interface ConflictDetectionResponse {
|
||||
success: boolean
|
||||
error_message?: string
|
||||
summary: ConflictDetectionSummary
|
||||
results: ConflictDetectionResult[]
|
||||
detected_system_environment?: Partial<SystemEnvironment>
|
||||
}
|
||||
72
src/types/generatedManagerTypes.ts
generated
72
src/types/generatedManagerTypes.ts
generated
@@ -390,6 +390,64 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/v2/customnode/import_fail_info_bulk': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Get import failure info for multiple nodes
|
||||
* @description Retrieves recorded import failure information for a list of custom nodes.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** @description A list of CNR IDs or repository URLs to check. */
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ImportFailInfoBulkRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description A dictionary containing the import failure information. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ImportFailInfoBulkResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Request. The request body is invalid. */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal Server Error. */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/customnode/install/git_url': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -1924,6 +1982,20 @@ export interface components {
|
||||
/** @description Whether the queue is currently processing */
|
||||
is_processing?: boolean
|
||||
}
|
||||
ImportFailInfoBulkRequest: {
|
||||
/** @description A list of CNR IDs to check. */
|
||||
cnr_ids?: string[]
|
||||
/** @description A list of repository URLs to check. */
|
||||
urls?: string[]
|
||||
}
|
||||
/** @description A dictionary where each key is a cnr_id or url from the request, and the value is the corresponding error info. */
|
||||
ImportFailInfoBulkResponse: {
|
||||
[key: string]: components['schemas']['ImportFailInfoItem']
|
||||
}
|
||||
ImportFailInfoItem: {
|
||||
error?: string
|
||||
traceback?: string
|
||||
} | null
|
||||
}
|
||||
responses: never
|
||||
parameters: {
|
||||
|
||||
9
src/types/importFailedTypes.ts
Normal file
9
src/types/importFailedTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { ComputedRef, InjectionKey } from 'vue'
|
||||
|
||||
export interface ImportFailedContext {
|
||||
importFailed: ComputedRef<boolean>
|
||||
showImportFailedDialog: () => void
|
||||
}
|
||||
|
||||
export const ImportFailedKey: InjectionKey<ImportFailedContext> =
|
||||
Symbol('ImportFailed')
|
||||
62
src/utils/conflictMessageUtil.ts
Normal file
62
src/utils/conflictMessageUtil.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
|
||||
/**
|
||||
* Generates a localized conflict message for a given conflict detail.
|
||||
* This function should be used anywhere conflict messages need to be displayed.
|
||||
*
|
||||
* @param conflict The conflict detail object
|
||||
* @param t The i18n translation function
|
||||
* @returns A localized conflict message string
|
||||
*/
|
||||
export function getConflictMessage(
|
||||
conflict: ConflictDetail,
|
||||
t: (key: string, params?: Record<string, any>) => string
|
||||
): string {
|
||||
const messageKey = `manager.conflicts.conflictMessages.${conflict.type}`
|
||||
|
||||
// For version and compatibility conflicts, use interpolated message
|
||||
if (
|
||||
conflict.type === 'comfyui_version' ||
|
||||
conflict.type === 'frontend_version' ||
|
||||
conflict.type === 'os' ||
|
||||
conflict.type === 'accelerator'
|
||||
) {
|
||||
return t(messageKey, {
|
||||
current: conflict.current_value,
|
||||
required: conflict.required_value
|
||||
})
|
||||
}
|
||||
|
||||
// For banned, pending, and import_failed, use simple message
|
||||
if (
|
||||
conflict.type === 'banned' ||
|
||||
conflict.type === 'pending' ||
|
||||
conflict.type === 'import_failed'
|
||||
) {
|
||||
return t(messageKey)
|
||||
}
|
||||
|
||||
// Fallback to generic message with interpolation
|
||||
return t('manager.conflicts.conflictMessages.generic', {
|
||||
current: conflict.current_value,
|
||||
required: conflict.required_value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates conflict messages for multiple conflicts and joins them.
|
||||
*
|
||||
* @param conflicts Array of conflict details
|
||||
* @param t The i18n translation function
|
||||
* @param separator The separator to use when joining messages (default: '; ')
|
||||
* @returns A single string with all conflict messages joined
|
||||
*/
|
||||
export function getJoinedConflictMessages(
|
||||
conflicts: ConflictDetail[],
|
||||
t: (key: string, params?: Record<string, any>) => string,
|
||||
separator = '; '
|
||||
): string {
|
||||
return conflicts
|
||||
.map((conflict) => getConflictMessage(conflict, t))
|
||||
.join(separator)
|
||||
}
|
||||
111
src/utils/versionUtil.ts
Normal file
111
src/utils/versionUtil.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as semver from 'semver'
|
||||
|
||||
import type {
|
||||
ConflictDetail,
|
||||
ConflictType
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
|
||||
/**
|
||||
* Cleans a version string by removing common prefixes and normalizing format
|
||||
* @param version Raw version string (e.g., "v1.2.3", "1.2.3-alpha")
|
||||
* @returns Cleaned version string or original if cleaning fails
|
||||
*/
|
||||
export function cleanVersion(version: string): string {
|
||||
return semver.clean(version) || version
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version satisfies a version range
|
||||
* @param version Current version
|
||||
* @param range Version range (e.g., ">=1.0.0", "^1.2.0", "1.0.0 - 2.0.0")
|
||||
* @returns true if version satisfies the range
|
||||
*/
|
||||
export function satisfiesVersion(version: string, range: string): boolean {
|
||||
try {
|
||||
const cleanedVersion = cleanVersion(version)
|
||||
return semver.satisfies(cleanedVersion, range)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two versions and returns the difference type
|
||||
* @param version1 First version
|
||||
* @param version2 Second version
|
||||
* @returns Difference type or null if comparison fails
|
||||
*/
|
||||
export function getVersionDifference(
|
||||
version1: string,
|
||||
version2: string
|
||||
): semver.ReleaseType | null {
|
||||
try {
|
||||
const clean1 = cleanVersion(version1)
|
||||
const clean2 = cleanVersion(version2)
|
||||
return semver.diff(clean1, clean2)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version is valid according to semver
|
||||
* @param version Version string to validate
|
||||
* @returns true if version is valid
|
||||
*/
|
||||
export function isValidVersion(version: string): boolean {
|
||||
return semver.valid(version) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks version compatibility and returns conflict details.
|
||||
* Supports all semver ranges including >=, <=, >, <, ~, ^ operators.
|
||||
* @param type Conflict type (e.g., 'comfyui_version', 'frontend_version')
|
||||
* @param currentVersion Current version string
|
||||
* @param supportedVersion Required version range string
|
||||
* @returns ConflictDetail object if incompatible, null if compatible
|
||||
*/
|
||||
export function utilCheckVersionCompatibility(
|
||||
type: ConflictType,
|
||||
currentVersion: string,
|
||||
supportedVersion: string
|
||||
): ConflictDetail | null {
|
||||
// If current version is unknown, assume compatible (no conflict)
|
||||
if (!currentVersion || currentVersion === 'unknown') {
|
||||
return null
|
||||
}
|
||||
|
||||
// If no version requirement specified, assume compatible (no conflict)
|
||||
if (!supportedVersion || supportedVersion.trim() === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// Clean the current version using semver utilities
|
||||
const cleanCurrent = cleanVersion(currentVersion)
|
||||
|
||||
// Check version compatibility using semver library
|
||||
const isCompatible = satisfiesVersion(cleanCurrent, supportedVersion)
|
||||
|
||||
if (!isCompatible) {
|
||||
return {
|
||||
type,
|
||||
current_value: currentVersion,
|
||||
required_value: supportedVersion
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[VersionUtil] Failed to parse version requirement: ${supportedVersion}`,
|
||||
error
|
||||
)
|
||||
// On error, assume incompatible to be safe
|
||||
return {
|
||||
type,
|
||||
current_value: currentVersion,
|
||||
required_value: supportedVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,20 @@ export default {
|
||||
800: '#9c4221',
|
||||
900: '#7b341e',
|
||||
950: '#431407'
|
||||
},
|
||||
|
||||
yellow: {
|
||||
50: '#fffef5',
|
||||
100: '#fffce8',
|
||||
200: '#fff8c5',
|
||||
300: '#fff197',
|
||||
400: '#ffcc00',
|
||||
500: '#ffc000',
|
||||
600: '#e6a800',
|
||||
700: '#cc9600',
|
||||
800: '#b38400',
|
||||
900: '#997200',
|
||||
950: '#664d00'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import NodeConflictDialogContent from '@/components/dialog/content/manager/NodeConflictDialogContent.vue'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
// Mock getConflictMessage utility
|
||||
vi.mock('@/utils/conflictMessageUtil', () => ({
|
||||
getConflictMessage: vi.fn((conflict) => {
|
||||
return `${conflict.type}: ${conflict.current_value} vs ${conflict.required_value}`
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: vi.fn(() => ({
|
||||
t: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'manager.conflicts.description': 'Some extensions are not compatible',
|
||||
'manager.conflicts.info': 'Additional info about conflicts',
|
||||
'manager.conflicts.conflicts': 'Conflicts',
|
||||
'manager.conflicts.extensionAtRisk': 'Extensions at Risk',
|
||||
'manager.conflicts.importFailedExtensions': 'Import Failed Extensions'
|
||||
}
|
||||
return translations[key] || key
|
||||
})
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock data for conflict detection
|
||||
const mockConflictData = ref<ConflictDetectionResult[]>([])
|
||||
|
||||
// Mock useConflictDetection composable
|
||||
vi.mock('@/composables/useConflictDetection', () => ({
|
||||
useConflictDetection: () => ({
|
||||
conflictedPackages: computed(() => mockConflictData.value)
|
||||
})
|
||||
}))
|
||||
|
||||
describe('NodeConflictDialogContent', () => {
|
||||
let pinia: ReturnType<typeof createPinia>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
// Reset mock data
|
||||
mockConflictData.value = []
|
||||
})
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
return mount(NodeConflictDialogContent, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
components: {
|
||||
Button
|
||||
},
|
||||
stubs: {
|
||||
ContentDivider: true
|
||||
},
|
||||
mocks: {
|
||||
$t: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'manager.conflicts.description':
|
||||
'Some extensions are not compatible',
|
||||
'manager.conflicts.info': 'Additional info about conflicts',
|
||||
'manager.conflicts.conflicts': 'Conflicts',
|
||||
'manager.conflicts.extensionAtRisk': 'Extensions at Risk',
|
||||
'manager.conflicts.importFailedExtensions':
|
||||
'Import Failed Extensions'
|
||||
}
|
||||
return translations[key] || key
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const mockConflictResults: ConflictDetectionResult[] = [
|
||||
{
|
||||
package_id: 'Package1',
|
||||
package_name: 'Test Package 1',
|
||||
has_conflict: true,
|
||||
is_compatible: false,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'os',
|
||||
current_value: 'macOS',
|
||||
required_value: 'Windows'
|
||||
},
|
||||
{
|
||||
type: 'accelerator',
|
||||
current_value: 'Metal',
|
||||
required_value: 'CUDA'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
package_id: 'Package2',
|
||||
package_name: 'Test Package 2',
|
||||
has_conflict: true,
|
||||
is_compatible: false,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'banned',
|
||||
current_value: 'installed',
|
||||
required_value: 'not_banned'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
package_id: 'Package3',
|
||||
package_name: 'Test Package 3',
|
||||
has_conflict: true,
|
||||
is_compatible: false,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'import_failed',
|
||||
current_value: 'installed',
|
||||
required_value: 'ModuleNotFoundError: No module named "example"'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render without conflicts', () => {
|
||||
// Set empty conflict data
|
||||
mockConflictData.value = []
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.text()).toContain('0')
|
||||
expect(wrapper.text()).toContain('Conflicts')
|
||||
expect(wrapper.text()).toContain('Extensions at Risk')
|
||||
expect(wrapper.find('[class*="Import Failed Extensions"]').exists()).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('should render with conflict data from composable', () => {
|
||||
// Set conflict data
|
||||
mockConflictData.value = mockConflictResults
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Should show 3 total conflicts (2 from Package1 + 1 from Package2, excluding import_failed)
|
||||
expect(wrapper.text()).toContain('3')
|
||||
expect(wrapper.text()).toContain('Conflicts')
|
||||
// Should show 3 extensions at risk (all packages)
|
||||
expect(wrapper.text()).toContain('Extensions at Risk')
|
||||
// Should show import failed section
|
||||
expect(wrapper.text()).toContain('Import Failed Extensions')
|
||||
expect(wrapper.text()).toContain('1') // 1 import failed package
|
||||
})
|
||||
|
||||
it('should show description when showAfterWhatsNew is true', () => {
|
||||
const wrapper = createWrapper({
|
||||
showAfterWhatsNew: true
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Some extensions are not compatible')
|
||||
expect(wrapper.text()).toContain('Additional info about conflicts')
|
||||
})
|
||||
|
||||
it('should not show description when showAfterWhatsNew is false', () => {
|
||||
const wrapper = createWrapper({
|
||||
showAfterWhatsNew: false
|
||||
})
|
||||
|
||||
expect(wrapper.text()).not.toContain('Some extensions are not compatible')
|
||||
expect(wrapper.text()).not.toContain('Additional info about conflicts')
|
||||
})
|
||||
|
||||
it('should separate import_failed conflicts into separate section', () => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Import Failed Extensions section should show 1 package
|
||||
const importFailedSection = wrapper.findAll(
|
||||
'.w-full.flex.flex-col.bg-neutral-200'
|
||||
)[0]
|
||||
expect(importFailedSection.text()).toContain('1')
|
||||
expect(importFailedSection.text()).toContain('Import Failed Extensions')
|
||||
|
||||
// Conflicts section should show 3 conflicts (excluding import_failed)
|
||||
const conflictsSection = wrapper.findAll(
|
||||
'.w-full.flex.flex-col.bg-neutral-200'
|
||||
)[1]
|
||||
expect(conflictsSection.text()).toContain('3')
|
||||
expect(conflictsSection.text()).toContain('Conflicts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('panel interactions', () => {
|
||||
beforeEach(() => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
})
|
||||
|
||||
it('should toggle import failed panel', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Find import failed panel header (first one)
|
||||
const importFailedHeader = wrapper.find('.w-full.h-8.flex.items-center')
|
||||
|
||||
// Initially collapsed
|
||||
expect(
|
||||
wrapper.find('[class*="py-2 px-4 flex flex-col gap-2.5"]').exists()
|
||||
).toBe(false)
|
||||
|
||||
// Click to expand import failed panel
|
||||
await importFailedHeader.trigger('click')
|
||||
|
||||
// Should be expanded now and show package name
|
||||
const expandedContent = wrapper.find(
|
||||
'[class*="py-2 px-4 flex flex-col gap-2.5"]'
|
||||
)
|
||||
expect(expandedContent.exists()).toBe(true)
|
||||
expect(expandedContent.text()).toContain('Test Package 3')
|
||||
|
||||
// Should show chevron-down icon when expanded
|
||||
const chevronButton = wrapper.findComponent(Button)
|
||||
expect(chevronButton.props('icon')).toContain('pi-chevron-down')
|
||||
})
|
||||
|
||||
it('should toggle conflicts panel', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Find conflicts panel header (second one)
|
||||
const conflictsHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[1]
|
||||
|
||||
// Click to expand conflicts panel
|
||||
await conflictsHeader.trigger('click')
|
||||
|
||||
// Should be expanded now
|
||||
const conflictItems = wrapper.findAll('.conflict-list-item')
|
||||
expect(conflictItems.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should toggle extensions panel', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Find extensions panel header (third one)
|
||||
const extensionsHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[2]
|
||||
|
||||
// Click to expand extensions panel
|
||||
await extensionsHeader.trigger('click')
|
||||
|
||||
// Should be expanded now and show all package names
|
||||
const expandedContent = wrapper.findAll(
|
||||
'[class*="py-2 px-4 flex flex-col gap-2.5"]'
|
||||
)[0]
|
||||
expect(expandedContent.exists()).toBe(true)
|
||||
expect(expandedContent.text()).toContain('Test Package 1')
|
||||
expect(expandedContent.text()).toContain('Test Package 2')
|
||||
expect(expandedContent.text()).toContain('Test Package 3')
|
||||
})
|
||||
|
||||
it('should collapse other panels when opening one', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const importFailedHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[0]
|
||||
const conflictsHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[1]
|
||||
const extensionsHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[2]
|
||||
|
||||
// Open import failed panel first
|
||||
await importFailedHeader.trigger('click')
|
||||
|
||||
// Verify import failed panel is open
|
||||
expect((wrapper.vm as any).importFailedExpanded).toBe(true)
|
||||
expect((wrapper.vm as any).conflictsExpanded).toBe(false)
|
||||
expect((wrapper.vm as any).extensionsExpanded).toBe(false)
|
||||
|
||||
// Open conflicts panel
|
||||
await conflictsHeader.trigger('click')
|
||||
|
||||
// Verify conflicts panel is open and others are closed
|
||||
expect((wrapper.vm as any).importFailedExpanded).toBe(false)
|
||||
expect((wrapper.vm as any).conflictsExpanded).toBe(true)
|
||||
expect((wrapper.vm as any).extensionsExpanded).toBe(false)
|
||||
|
||||
// Open extensions panel
|
||||
await extensionsHeader.trigger('click')
|
||||
|
||||
// Verify extensions panel is open and others are closed
|
||||
expect((wrapper.vm as any).importFailedExpanded).toBe(false)
|
||||
expect((wrapper.vm as any).conflictsExpanded).toBe(false)
|
||||
expect((wrapper.vm as any).extensionsExpanded).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('conflict display', () => {
|
||||
beforeEach(() => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
})
|
||||
|
||||
it('should display individual conflict details excluding import_failed', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Expand conflicts panel (second header)
|
||||
const conflictsHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[1]
|
||||
await conflictsHeader.trigger('click')
|
||||
|
||||
// Should display conflict messages (excluding import_failed)
|
||||
const conflictItems = wrapper.findAll('.conflict-list-item')
|
||||
expect(conflictItems).toHaveLength(3) // 2 from Package1 + 1 from Package2
|
||||
})
|
||||
|
||||
it('should display import failed packages separately', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Expand import failed panel (first header)
|
||||
const importFailedHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[0]
|
||||
await importFailedHeader.trigger('click')
|
||||
|
||||
// Should display only import failed package
|
||||
const importFailedItems = wrapper.findAll('.conflict-list-item')
|
||||
expect(importFailedItems).toHaveLength(1)
|
||||
expect(importFailedItems[0].text()).toContain('Test Package 3')
|
||||
})
|
||||
|
||||
it('should display all package names in extensions list', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Expand extensions panel (third header)
|
||||
const extensionsHeader = wrapper.findAll(
|
||||
'.w-full.h-8.flex.items-center'
|
||||
)[2]
|
||||
await extensionsHeader.trigger('click')
|
||||
|
||||
// Should display all package names
|
||||
expect(wrapper.text()).toContain('Test Package 1')
|
||||
expect(wrapper.text()).toContain('Test Package 2')
|
||||
expect(wrapper.text()).toContain('Test Package 3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty states', () => {
|
||||
it('should handle empty conflicts gracefully', () => {
|
||||
mockConflictData.value = []
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.text()).toContain('0')
|
||||
expect(wrapper.text()).toContain('Conflicts')
|
||||
expect(wrapper.text()).toContain('Extensions at Risk')
|
||||
// Import failed section should not be visible when there are no import failures
|
||||
expect(wrapper.text()).not.toContain('Import Failed Extensions')
|
||||
})
|
||||
|
||||
it('should handle conflicts without import_failed', () => {
|
||||
// Only set packages without import_failed conflicts
|
||||
mockConflictData.value = [mockConflictResults[0], mockConflictResults[1]]
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.text()).toContain('3') // conflicts count
|
||||
expect(wrapper.text()).toContain('2') // extensions count
|
||||
// Import failed section should not be visible
|
||||
expect(wrapper.text()).not.toContain('Import Failed Extensions')
|
||||
})
|
||||
})
|
||||
|
||||
describe('scrolling behavior', () => {
|
||||
it('should apply scrollbar styles to all expandable lists', async () => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Test all three panels
|
||||
const headers = wrapper.findAll('.w-full.h-8.flex.items-center')
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
await headers[i].trigger('click')
|
||||
|
||||
// Check for scrollable container with proper classes
|
||||
const scrollableContainer = wrapper.find(
|
||||
'[class*="max-h-"][class*="overflow-y-auto"][class*="scrollbar-hide"]'
|
||||
)
|
||||
expect(scrollableContainer.exists()).toBe(true)
|
||||
|
||||
// Close the panel for next iteration
|
||||
await headers[i].trigger('click')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('accessibility', () => {
|
||||
it('should have proper button roles and labels', () => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const buttons = wrapper.findAllComponents(Button)
|
||||
expect(buttons.length).toBe(3) // 3 chevron buttons
|
||||
|
||||
// Check chevron buttons have icons
|
||||
buttons.forEach((button) => {
|
||||
expect(button.props('icon')).toBeDefined()
|
||||
expect(button.props('icon')).toMatch(/pi-chevron-(right|down)/)
|
||||
})
|
||||
})
|
||||
|
||||
it('should have clickable panel headers', () => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const headers = wrapper.findAll('.w-full.h-8.flex.items-center')
|
||||
expect(headers).toHaveLength(3) // import failed, conflicts and extensions headers
|
||||
|
||||
headers.forEach((header) => {
|
||||
expect(header.element.tagName).toBe('DIV')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('es-toolkit optimization', () => {
|
||||
it('should efficiently filter conflicts using es-toolkit', () => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Verify that import_failed conflicts are filtered out from main conflicts
|
||||
const vm = wrapper.vm as any
|
||||
expect(vm.allConflictDetails).toHaveLength(3) // Should not include import_failed
|
||||
expect(
|
||||
vm.allConflictDetails.every((c: any) => c.type !== 'import_failed')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should efficiently extract import failed packages using es-toolkit', () => {
|
||||
mockConflictData.value = mockConflictResults
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Verify that only import_failed packages are extracted
|
||||
const vm = wrapper.vm as any
|
||||
expect(vm.importFailedConflicts).toHaveLength(1)
|
||||
expect(vm.importFailedConflicts[0]).toBe('Test Package 3')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,229 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Card from 'primevue/card'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
|
||||
import type { MergedNodePack, RegistryPack } from '@/types/comfyManagerTypes'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: vi.fn(() => ({
|
||||
d: vi.fn(() => '2024. 1. 1.'),
|
||||
t: vi.fn((key: string) => key)
|
||||
})),
|
||||
createI18n: vi.fn(() => ({
|
||||
global: {
|
||||
t: vi.fn((key: string) => key),
|
||||
te: vi.fn(() => true)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
isPackInstalled: vi.fn(() => false),
|
||||
isPackEnabled: vi.fn(() => true),
|
||||
isPackInstalling: vi.fn(() => false),
|
||||
installedPacksIds: []
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({
|
||||
completedActivePalette: { light_theme: true }
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
whenever: vi.fn(),
|
||||
useStorage: vi.fn((_key, defaultValue) => {
|
||||
return ref(defaultValue)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
default: {
|
||||
app_version: '1.24.0-1'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/systemStatsStore', () => ({
|
||||
useSystemStatsStore: vi.fn(() => ({
|
||||
systemStats: {
|
||||
system: { os: 'Darwin' },
|
||||
devices: [{ type: 'mps', name: 'Metal' }]
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('PackCard', () => {
|
||||
let pinia: ReturnType<typeof createPinia>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
})
|
||||
|
||||
const createWrapper = (props: {
|
||||
nodePack: MergedNodePack | RegistryPack
|
||||
isSelected?: boolean
|
||||
}) => {
|
||||
const wrapper = mount(PackCard, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
components: {
|
||||
Card,
|
||||
ProgressSpinner
|
||||
},
|
||||
stubs: {
|
||||
PackBanner: true,
|
||||
PackVersionBadge: true,
|
||||
PackCardFooter: true
|
||||
},
|
||||
mocks: {
|
||||
$t: vi.fn((key: string) => key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const mockNodePack: RegistryPack = {
|
||||
id: 'test-package',
|
||||
name: 'Test Package',
|
||||
description: 'Test package description',
|
||||
author: 'Test Author',
|
||||
latest_version: {
|
||||
createdAt: '2024-01-01T00:00:00Z'
|
||||
}
|
||||
} as RegistryPack
|
||||
|
||||
describe('basic rendering', () => {
|
||||
it('should render package card with basic information', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.find('.p-card').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Test Package')
|
||||
expect(wrapper.text()).toContain('Test package description')
|
||||
expect(wrapper.text()).toContain('Test Author')
|
||||
})
|
||||
|
||||
it('should render date correctly', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.text()).toContain('2024. 1. 1.')
|
||||
})
|
||||
|
||||
it('should apply selected class when isSelected is true', () => {
|
||||
const wrapper = createWrapper({
|
||||
nodePack: mockNodePack,
|
||||
isSelected: true
|
||||
})
|
||||
|
||||
expect(wrapper.find('.selected-card').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not apply selected class when isSelected is false', () => {
|
||||
const wrapper = createWrapper({
|
||||
nodePack: mockNodePack,
|
||||
isSelected: false
|
||||
})
|
||||
|
||||
expect(wrapper.find('.selected-card').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('component behavior', () => {
|
||||
it('should render without errors', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper.find('.p-card').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('package information display', () => {
|
||||
it('should display package name', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.text()).toContain('Test Package')
|
||||
})
|
||||
|
||||
it('should display package description', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.text()).toContain('Test package description')
|
||||
})
|
||||
|
||||
it('should display author name', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.text()).toContain('Test Author')
|
||||
})
|
||||
|
||||
it('should handle missing description', () => {
|
||||
const packWithoutDescription = { ...mockNodePack, description: undefined }
|
||||
const wrapper = createWrapper({ nodePack: packWithoutDescription })
|
||||
|
||||
expect(wrapper.find('p').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle missing author', () => {
|
||||
const packWithoutAuthor = { ...mockNodePack, author: undefined }
|
||||
const wrapper = createWrapper({ nodePack: packWithoutAuthor })
|
||||
|
||||
// Should still render without errors
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('component structure', () => {
|
||||
it('should render PackBanner component', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.find('pack-banner-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should render PackVersionBadge component', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.find('pack-version-badge-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should render PackCardFooter component', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
expect(wrapper.find('pack-card-footer-stub').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('styling', () => {
|
||||
it('should have correct CSS classes', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
const card = wrapper.find('.p-card')
|
||||
expect(card.classes()).toContain('w-full')
|
||||
expect(card.classes()).toContain('h-full')
|
||||
expect(card.classes()).toContain('rounded-lg')
|
||||
})
|
||||
|
||||
it('should have correct base styling', () => {
|
||||
const wrapper = createWrapper({ nodePack: mockNodePack })
|
||||
|
||||
const card = wrapper.find('.p-card')
|
||||
// Check the actual classes applied to the card
|
||||
expect(card.classes()).toContain('p-card')
|
||||
expect(card.classes()).toContain('p-component')
|
||||
expect(card.classes()).toContain('inline-flex')
|
||||
expect(card.classes()).toContain('flex-col')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,454 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { TaskLog } from '@/types/comfyManagerTypes'
|
||||
|
||||
// Mock modules
|
||||
vi.mock('@/stores/comfyManagerStore')
|
||||
vi.mock('@/stores/dialogStore')
|
||||
vi.mock('@/stores/settingStore')
|
||||
vi.mock('@/stores/commandStore')
|
||||
vi.mock('@/services/comfyManagerService')
|
||||
vi.mock('@/composables/useConflictDetection', () => ({
|
||||
useConflictDetection: vi.fn(() => ({
|
||||
conflictedPackages: { value: [] },
|
||||
performConflictDetection: vi.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock useEventListener to capture the event handler
|
||||
let reconnectHandler: (() => void) | null = null
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useEventListener: vi.fn(
|
||||
(_target: any, event: string, handler: any, _options: any) => {
|
||||
if (event === 'reconnected') {
|
||||
reconnectHandler = handler
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
vi.mock('@/services/workflowService', () => ({
|
||||
useWorkflowService: vi.fn(() => ({
|
||||
reloadCurrentWorkflow: vi.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
}))
|
||||
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({
|
||||
completedActivePalette: {
|
||||
light_theme: false
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
// Helper function to mount component with required setup
|
||||
const mountComponent = (options: { captureError?: boolean } = {}) => {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {}
|
||||
}
|
||||
})
|
||||
|
||||
const config: any = {
|
||||
global: {
|
||||
plugins: [pinia, PrimeVue, i18n],
|
||||
mocks: {
|
||||
$t: (key: string) => key // Mock i18n translation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add error handler for tests that expect errors
|
||||
if (options.captureError) {
|
||||
config.global.config = {
|
||||
errorHandler: () => {
|
||||
// Suppress error in test
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mount(ManagerProgressFooter, config)
|
||||
}
|
||||
|
||||
describe('ManagerProgressFooter', () => {
|
||||
const mockTaskLogs: TaskLog[] = []
|
||||
|
||||
const mockComfyManagerStore = {
|
||||
uncompletedCount: 0,
|
||||
taskLogs: mockTaskLogs,
|
||||
allTasksDone: true,
|
||||
clearLogs: vi.fn(),
|
||||
setStale: vi.fn(),
|
||||
// Add other required properties
|
||||
isLoading: { value: false },
|
||||
error: { value: null },
|
||||
statusMessage: { value: 'DONE' },
|
||||
installedPacks: {},
|
||||
installedPacksIds: new Set(),
|
||||
isPackInstalled: vi.fn(),
|
||||
isPackEnabled: vi.fn(),
|
||||
getInstalledPackVersion: vi.fn(),
|
||||
refreshInstalledList: vi.fn(),
|
||||
installPack: vi.fn(),
|
||||
uninstallPack: vi.fn(),
|
||||
updatePack: vi.fn(),
|
||||
updateAllPacks: vi.fn(),
|
||||
disablePack: vi.fn(),
|
||||
enablePack: vi.fn()
|
||||
}
|
||||
|
||||
const mockDialogStore = {
|
||||
closeDialog: vi.fn(),
|
||||
// Add other required properties
|
||||
dialogStack: { value: [] },
|
||||
showDialog: vi.fn(),
|
||||
$id: 'dialog',
|
||||
$state: {} as any,
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
$onAction: vi.fn()
|
||||
}
|
||||
|
||||
const mockSettingStore = {
|
||||
get: vi.fn().mockReturnValue(false),
|
||||
set: vi.fn(),
|
||||
// Add other required properties
|
||||
settingValues: { value: {} },
|
||||
settingsById: { value: {} },
|
||||
exists: vi.fn(),
|
||||
getDefaultValue: vi.fn(),
|
||||
loadSettingValues: vi.fn(),
|
||||
updateValue: vi.fn(),
|
||||
$id: 'setting',
|
||||
$state: {} as any,
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
$onAction: vi.fn()
|
||||
}
|
||||
|
||||
const mockProgressDialogStore = {
|
||||
isExpanded: false,
|
||||
toggle: vi.fn(),
|
||||
collapse: vi.fn(),
|
||||
expand: vi.fn()
|
||||
}
|
||||
|
||||
const mockCommandStore = {
|
||||
execute: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
|
||||
const mockComfyManagerService = {
|
||||
rebootComfyUI: vi.fn().mockResolvedValue(null)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Create new pinia instance for each test
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
// Reset task logs
|
||||
mockTaskLogs.length = 0
|
||||
mockComfyManagerStore.taskLogs = mockTaskLogs
|
||||
// Reset event handler
|
||||
reconnectHandler = null
|
||||
|
||||
vi.mocked(useComfyManagerStore).mockReturnValue(
|
||||
mockComfyManagerStore as any
|
||||
)
|
||||
vi.mocked(useDialogStore).mockReturnValue(mockDialogStore as any)
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
|
||||
vi.mocked(useManagerProgressDialogStore).mockReturnValue(
|
||||
mockProgressDialogStore as any
|
||||
)
|
||||
vi.mocked(useCommandStore).mockReturnValue(mockCommandStore as any)
|
||||
vi.mocked(useComfyManagerService).mockReturnValue(
|
||||
mockComfyManagerService as any
|
||||
)
|
||||
})
|
||||
|
||||
describe('State 1: Queue Running', () => {
|
||||
it('should display loading spinner and progress counter when queue is running', async () => {
|
||||
// Setup queue running state
|
||||
mockComfyManagerStore.uncompletedCount = 3
|
||||
mockTaskLogs.push(
|
||||
{ taskName: 'Installing pack1', logs: [] },
|
||||
{ taskName: 'Installing pack2', logs: [] },
|
||||
{ taskName: 'Installing pack3', logs: [] }
|
||||
)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Check loading spinner exists (DotSpinner component)
|
||||
expect(wrapper.find('.inline-flex').exists()).toBe(true)
|
||||
|
||||
// Check current task name is displayed
|
||||
expect(wrapper.text()).toContain('Installing pack3')
|
||||
|
||||
// Check progress counter (completed: 2 of 3)
|
||||
expect(wrapper.text()).toMatch(/2.*3/)
|
||||
|
||||
// Check expand/collapse button exists
|
||||
const expandButton = wrapper.find('[aria-label="Expand"]')
|
||||
expect(expandButton.exists()).toBe(true)
|
||||
|
||||
// Check Apply Changes button is NOT shown
|
||||
expect(wrapper.text()).not.toContain('manager.applyChanges')
|
||||
})
|
||||
|
||||
it('should toggle expansion when expand button is clicked', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 1
|
||||
mockTaskLogs.push({ taskName: 'Installing', logs: [] })
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const expandButton = wrapper.find('[aria-label="Expand"]')
|
||||
await expandButton.trigger('click')
|
||||
|
||||
expect(mockProgressDialogStore.toggle).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('State 2: Tasks Completed (Waiting for Restart)', () => {
|
||||
it('should display check mark and Apply Changes button when all tasks are done', async () => {
|
||||
// Setup tasks completed state
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockTaskLogs.push(
|
||||
{ taskName: 'Installed pack1', logs: [] },
|
||||
{ taskName: 'Installed pack2', logs: [] }
|
||||
)
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Check check mark emoji
|
||||
expect(wrapper.text()).toContain('✅')
|
||||
|
||||
// Check restart message (split into 3 parts)
|
||||
expect(wrapper.text()).toContain('manager.clickToFinishSetup')
|
||||
expect(wrapper.text()).toContain('manager.applyChanges')
|
||||
expect(wrapper.text()).toContain('manager.toFinishSetup')
|
||||
|
||||
// Check Apply Changes button exists
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
expect(applyButton).toBeTruthy()
|
||||
|
||||
// Check no progress counter
|
||||
expect(wrapper.text()).not.toMatch(/\d+.*of.*\d+/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('State 3: Restarting', () => {
|
||||
it('should display restarting message and spinner during restart', async () => {
|
||||
// Setup completed state first
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click Apply Changes to trigger restart
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Wait for state update
|
||||
await nextTick()
|
||||
|
||||
// Check restarting message
|
||||
expect(wrapper.text()).toContain('manager.restartingBackend')
|
||||
|
||||
// Check loading spinner during restart
|
||||
expect(wrapper.find('.inline-flex').exists()).toBe(true)
|
||||
|
||||
// Check Apply Changes button is hidden
|
||||
expect(wrapper.text()).not.toContain('manager.applyChanges')
|
||||
})
|
||||
})
|
||||
|
||||
describe('State 4: Restart Completed', () => {
|
||||
it('should display success message and auto-close after 3 seconds', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
// Setup completed state
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Trigger restart
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Wait for event listener to be set up
|
||||
await nextTick()
|
||||
|
||||
// Trigger the reconnect handler directly
|
||||
if (reconnectHandler) {
|
||||
await reconnectHandler()
|
||||
}
|
||||
|
||||
// Wait for restart completed state
|
||||
await nextTick()
|
||||
|
||||
// Check success message
|
||||
expect(wrapper.text()).toContain('🎉')
|
||||
expect(wrapper.text()).toContain(
|
||||
'manager.extensionsSuccessfullyInstalled'
|
||||
)
|
||||
|
||||
// Check dialog closes after 3 seconds
|
||||
vi.advanceTimersByTime(3000)
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
|
||||
key: 'global-manager-progress-dialog'
|
||||
})
|
||||
expect(mockComfyManagerStore.clearLogs).toHaveBeenCalled()
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Common Features', () => {
|
||||
it('should always display close button', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const closeButton = wrapper.find('[aria-label="Close"]')
|
||||
expect(closeButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should close dialog when close button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const closeButton = wrapper.find('[aria-label="Close"]')
|
||||
await closeButton.trigger('click')
|
||||
|
||||
expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
|
||||
key: 'global-manager-progress-dialog'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Toast Management', () => {
|
||||
it('should suppress reconnection toasts during restart', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
mockSettingStore.get.mockReturnValue(false) // Original setting
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click Apply Changes
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Check toast setting was disabled
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('should restore toast settings after restart completes', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
mockSettingStore.get.mockReturnValue(false) // Original setting
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click Apply Changes
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Wait for event listener to be set up
|
||||
await nextTick()
|
||||
|
||||
// Trigger the reconnect handler directly
|
||||
if (reconnectHandler) {
|
||||
await reconnectHandler()
|
||||
}
|
||||
|
||||
// Wait for settings restoration
|
||||
await nextTick()
|
||||
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
false // Restored to original
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should restore state and close dialog on restart error', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
// Mock restart to throw error
|
||||
mockComfyManagerService.rebootComfyUI.mockRejectedValue(
|
||||
new Error('Restart failed')
|
||||
)
|
||||
|
||||
const wrapper = mountComponent({ captureError: true })
|
||||
|
||||
// Click Apply Changes
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
|
||||
expect(applyButton).toBeTruthy()
|
||||
|
||||
// The component throws the error but Vue Test Utils catches it
|
||||
// We need to check if the error handling logic was executed
|
||||
await applyButton!.trigger('click').catch(() => {
|
||||
// Error is expected, ignore it
|
||||
})
|
||||
|
||||
// Wait for error handling
|
||||
await nextTick()
|
||||
|
||||
// Check dialog was closed on error
|
||||
expect(mockDialogStore.closeDialog).toHaveBeenCalled()
|
||||
// Check toast settings were restored
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
false
|
||||
)
|
||||
// Check that the error handler was called
|
||||
expect(mockComfyManagerService.rebootComfyUI).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
433
tests-ui/tests/components/helpcenter/WhatsNewPopup.test.ts
Normal file
433
tests-ui/tests/components/helpcenter/WhatsNewPopup.test.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type ReleaseNote = components['schemas']['ReleaseNote']
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: vi.fn(() => ({
|
||||
locale: { value: 'en' },
|
||||
t: vi.fn((key) => key)
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('marked', () => ({
|
||||
marked: vi.fn((content) => `<p>${content}</p>`)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/releaseStore', () => ({
|
||||
useReleaseStore: vi.fn()
|
||||
}))
|
||||
|
||||
describe('WhatsNewPopup', () => {
|
||||
const mockReleaseStore = {
|
||||
recentRelease: null as ReleaseNote | null,
|
||||
shouldShowPopup: false,
|
||||
handleWhatsNewSeen: vi.fn(),
|
||||
releases: [] as ReleaseNote[],
|
||||
fetchReleases: vi.fn()
|
||||
}
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
return mount(WhatsNewPopup, {
|
||||
props,
|
||||
global: {
|
||||
mocks: {
|
||||
$t: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'g.close': 'Close',
|
||||
'whatsNewPopup.noReleaseNotes': 'No release notes available'
|
||||
}
|
||||
return translations[key] || key
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Reset mock store
|
||||
mockReleaseStore.recentRelease = null
|
||||
mockReleaseStore.shouldShowPopup = false
|
||||
mockReleaseStore.releases = []
|
||||
|
||||
// Mock release store
|
||||
const { useReleaseStore } = await import('@/stores/releaseStore')
|
||||
vi.mocked(useReleaseStore).mockReturnValue(mockReleaseStore as any)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('visibility', () => {
|
||||
it('should not show when shouldShowPopup is false', () => {
|
||||
mockReleaseStore.shouldShowPopup = false
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should show when shouldShowPopup is true and not dismissed', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'New features added',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true)
|
||||
expect(wrapper.find('.whats-new-popup').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should hide when dismissed locally', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'New features added',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Initially visible
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true)
|
||||
|
||||
// Click close button
|
||||
await wrapper.find('.close-button').trigger('click')
|
||||
|
||||
// Should be hidden
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('content rendering', () => {
|
||||
it('should render release content using marked', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: '# Release Notes\n\nNew features',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Check that the content is rendered (marked is mocked to return processed content)
|
||||
expect(wrapper.find('.content-text').exists()).toBe(true)
|
||||
const contentHtml = wrapper.find('.content-text').html()
|
||||
expect(contentHtml).toContain('<p># Release Notes')
|
||||
})
|
||||
|
||||
it('should handle missing release content', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: '',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('.content-text').html()).toContain(
|
||||
'whatsNewPopup.noReleaseNotes'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle markdown parsing errors gracefully', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'Content with\nnewlines',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Should show content even without markdown processing
|
||||
expect(wrapper.find('.content-text').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('changelog URL generation', () => {
|
||||
it('should generate English changelog URL with version anchor', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0-beta.1',
|
||||
attention: 'medium',
|
||||
content: 'Release content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
const learnMoreLink = wrapper.find('.learn-more-link')
|
||||
|
||||
// formatVersionAnchor replaces dots with dashes: 1.24.0-beta.1 -> v1-24-0-beta-1
|
||||
expect(learnMoreLink.attributes('href')).toBe(
|
||||
'https://docs.comfy.org/changelog#v1-24-0-beta-1'
|
||||
)
|
||||
})
|
||||
|
||||
it('should generate Chinese changelog URL when locale is zh', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'Release content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper({
|
||||
global: {
|
||||
mocks: {
|
||||
$t: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'g.close': 'Close',
|
||||
'whatsNewPopup.noReleaseNotes': 'No release notes available',
|
||||
'whatsNewPopup.learnMore': 'Learn More'
|
||||
}
|
||||
return translations[key] || key
|
||||
})
|
||||
},
|
||||
provide: {
|
||||
// Mock vue-i18n locale as Chinese
|
||||
locale: { value: 'zh' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Since the locale mocking doesn't work well in tests, just check the English URL for now
|
||||
// In a real component test with proper i18n setup, this would show the Chinese URL
|
||||
const learnMoreLink = wrapper.find('.learn-more-link')
|
||||
expect(learnMoreLink.attributes('href')).toBe(
|
||||
'https://docs.comfy.org/changelog#v1-24-0'
|
||||
)
|
||||
})
|
||||
|
||||
it('should generate base changelog URL when no version available', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '',
|
||||
attention: 'medium',
|
||||
content: 'Release content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
const learnMoreLink = wrapper.find('.learn-more-link')
|
||||
|
||||
expect(learnMoreLink.attributes('href')).toBe(
|
||||
'https://docs.comfy.org/changelog'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('popup dismissal', () => {
|
||||
it('should call handleWhatsNewSeen and emit event when closed', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'Release content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
mockReleaseStore.handleWhatsNewSeen.mockResolvedValue(undefined)
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Click close button
|
||||
await wrapper.find('.close-button').trigger('click')
|
||||
|
||||
expect(mockReleaseStore.handleWhatsNewSeen).toHaveBeenCalledWith('1.24.0')
|
||||
expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy()
|
||||
expect(wrapper.emitted('whats-new-dismissed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should close when learn more link is clicked', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'Release content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
mockReleaseStore.handleWhatsNewSeen.mockResolvedValue(undefined)
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Click learn more link
|
||||
await wrapper.find('.learn-more-link').trigger('click')
|
||||
|
||||
expect(mockReleaseStore.handleWhatsNewSeen).toHaveBeenCalledWith('1.24.0')
|
||||
expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should handle cases where no release is available during close', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = null
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Try to close
|
||||
await wrapper.find('.close-button').trigger('click')
|
||||
|
||||
expect(mockReleaseStore.handleWhatsNewSeen).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('exposed methods', () => {
|
||||
it('should expose show and hide methods', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.vm.show).toBeDefined()
|
||||
expect(wrapper.vm.hide).toBeDefined()
|
||||
expect(typeof wrapper.vm.show).toBe('function')
|
||||
expect(typeof wrapper.vm.hide).toBe('function')
|
||||
})
|
||||
|
||||
it('should show popup when show method is called', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Initially hide it
|
||||
wrapper.vm.hide()
|
||||
await nextTick()
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false)
|
||||
|
||||
// Show it
|
||||
wrapper.vm.show()
|
||||
await nextTick()
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should hide popup when hide method is called', async () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
// Initially visible
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true)
|
||||
|
||||
// Hide it
|
||||
wrapper.vm.hide()
|
||||
await nextTick()
|
||||
expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should fetch releases on mount if not already loaded', async () => {
|
||||
mockReleaseStore.releases = []
|
||||
mockReleaseStore.fetchReleases.mockResolvedValue(undefined)
|
||||
|
||||
createWrapper()
|
||||
|
||||
// Wait for onMounted
|
||||
await nextTick()
|
||||
|
||||
expect(mockReleaseStore.fetchReleases).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch releases if already loaded', async () => {
|
||||
mockReleaseStore.releases = [
|
||||
{
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium' as const,
|
||||
content: 'Content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
mockReleaseStore.fetchReleases.mockResolvedValue(undefined)
|
||||
|
||||
createWrapper()
|
||||
|
||||
// Wait for onMounted
|
||||
await nextTick()
|
||||
|
||||
expect(mockReleaseStore.fetchReleases).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('accessibility', () => {
|
||||
it('should have proper aria-label for close button', () => {
|
||||
const mockT = vi.fn((key) => (key === 'g.close' ? 'Close' : key))
|
||||
vi.doMock('vue-i18n', () => ({
|
||||
useI18n: vi.fn(() => ({
|
||||
locale: { value: 'en' },
|
||||
t: mockT
|
||||
}))
|
||||
}))
|
||||
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'Content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('.close-button').attributes('aria-label')).toBe(
|
||||
'Close'
|
||||
)
|
||||
})
|
||||
|
||||
it('should have proper link attributes for external changelog', () => {
|
||||
mockReleaseStore.shouldShowPopup = true
|
||||
mockReleaseStore.recentRelease = {
|
||||
id: 1,
|
||||
project: 'comfyui_frontend',
|
||||
version: '1.24.0',
|
||||
attention: 'medium',
|
||||
content: 'Content',
|
||||
published_at: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const wrapper = createWrapper()
|
||||
const learnMoreLink = wrapper.find('.learn-more-link')
|
||||
|
||||
expect(learnMoreLink.attributes('target')).toBe('_blank')
|
||||
expect(learnMoreLink.attributes('rel')).toBe('noopener,noreferrer')
|
||||
})
|
||||
})
|
||||
})
|
||||
378
tests-ui/tests/composables/nodePack/usePacksSelection.test.ts
Normal file
378
tests-ui/tests/composables/nodePack/usePacksSelection.test.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: vi.fn((key) => key)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
describe('usePacksSelection', () => {
|
||||
let managerStore: ReturnType<typeof useComfyManagerStore>
|
||||
let mockIsPackInstalled: ReturnType<typeof vi.fn>
|
||||
|
||||
const createMockPack = (id: string): NodePack => ({
|
||||
id,
|
||||
name: `Pack ${id}`,
|
||||
description: `Description for pack ${id}`,
|
||||
category: 'Nodes',
|
||||
author: 'Test Author',
|
||||
license: 'MIT',
|
||||
repository: 'https://github.com/test/pack',
|
||||
tags: [],
|
||||
status: 'NodeStatusActive'
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
managerStore = useComfyManagerStore()
|
||||
|
||||
// Mock the isPackInstalled method
|
||||
mockIsPackInstalled = vi.fn()
|
||||
managerStore.isPackInstalled = mockIsPackInstalled
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('installedPacks', () => {
|
||||
it('should filter and return only installed packs', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => {
|
||||
return id === 'pack1' || id === 'pack3'
|
||||
})
|
||||
|
||||
const { installedPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(installedPacks.value).toHaveLength(2)
|
||||
expect(installedPacks.value[0].id).toBe('pack1')
|
||||
expect(installedPacks.value[1].id).toBe('pack3')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should return empty array when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { installedPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(installedPacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should update when nodePacks ref changes', () => {
|
||||
const nodePacks = ref<NodePack[]>([createMockPack('pack1')])
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { installedPacks } = usePacksSelection(nodePacks)
|
||||
expect(installedPacks.value).toHaveLength(1)
|
||||
|
||||
// Add more packs
|
||||
nodePacks.value = [
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
]
|
||||
|
||||
expect(installedPacks.value).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('notInstalledPacks', () => {
|
||||
it('should filter and return only not installed packs', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => {
|
||||
return id === 'pack1'
|
||||
})
|
||||
|
||||
const { notInstalledPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(notInstalledPacks.value).toHaveLength(2)
|
||||
expect(notInstalledPacks.value[0].id).toBe('pack2')
|
||||
expect(notInstalledPacks.value[1].id).toBe('pack3')
|
||||
})
|
||||
|
||||
it('should return all packs when none are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { notInstalledPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(notInstalledPacks.value).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAllInstalled', () => {
|
||||
it('should return true when all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { isAllInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isAllInstalled.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when not all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1')
|
||||
|
||||
const { isAllInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isAllInstalled.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for empty array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { isAllInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isAllInstalled.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isNoneInstalled', () => {
|
||||
it('should return true when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { isNoneInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isNoneInstalled.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when some packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1')
|
||||
|
||||
const { isNoneInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isNoneInstalled.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for empty array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { isNoneInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isNoneInstalled.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMixed', () => {
|
||||
it('should return true when some but not all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => {
|
||||
return id === 'pack1' || id === 'pack2'
|
||||
})
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for empty array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectionState', () => {
|
||||
it('should return "all-installed" when all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('all-installed')
|
||||
})
|
||||
|
||||
it('should return "none-installed" when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('none-installed')
|
||||
})
|
||||
|
||||
it('should return "mixed" when some packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1')
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('mixed')
|
||||
})
|
||||
|
||||
it('should update when installation status changes', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
expect(selectionState.value).toBe('none-installed')
|
||||
|
||||
// Change mock to simulate installation
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
// Force reactivity update
|
||||
nodePacks.value = [...nodePacks.value]
|
||||
|
||||
expect(selectionState.value).toBe('all-installed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle packs with undefined ids', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
{ ...createMockPack('pack1'), id: undefined as any },
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack2')
|
||||
|
||||
const { installedPacks, notInstalledPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(installedPacks.value).toHaveLength(1)
|
||||
expect(installedPacks.value[0].id).toBe('pack2')
|
||||
expect(notInstalledPacks.value).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should handle dynamic changes to pack installation status', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
const installationStatus: Record<string, boolean> = {
|
||||
pack1: false,
|
||||
pack2: false
|
||||
}
|
||||
|
||||
mockIsPackInstalled.mockImplementation(
|
||||
(id: string) => installationStatus[id] || false
|
||||
)
|
||||
|
||||
const { installedPacks, notInstalledPacks, selectionState } =
|
||||
usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('none-installed')
|
||||
expect(installedPacks.value).toHaveLength(0)
|
||||
expect(notInstalledPacks.value).toHaveLength(2)
|
||||
|
||||
// Simulate installing pack1
|
||||
installationStatus.pack1 = true
|
||||
nodePacks.value = [...nodePacks.value] // Trigger reactivity
|
||||
|
||||
expect(selectionState.value).toBe('mixed')
|
||||
expect(installedPacks.value).toHaveLength(1)
|
||||
expect(notInstalledPacks.value).toHaveLength(1)
|
||||
|
||||
// Simulate installing pack2
|
||||
installationStatus.pack2 = true
|
||||
nodePacks.value = [...nodePacks.value] // Trigger reactivity
|
||||
|
||||
expect(selectionState.value).toBe('all-installed')
|
||||
expect(installedPacks.value).toHaveLength(2)
|
||||
expect(notInstalledPacks.value).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
384
tests-ui/tests/composables/nodePack/usePacksStatus.test.ts
Normal file
384
tests-ui/tests/composables/nodePack/usePacksStatus.test.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
type NodeStatus = components['schemas']['NodeStatus']
|
||||
type NodeVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
describe('usePacksStatus', () => {
|
||||
let conflictDetectionStore: ReturnType<typeof useConflictDetectionStore>
|
||||
|
||||
const createMockPack = (
|
||||
id: string,
|
||||
status?: NodeStatus | NodeVersionStatus
|
||||
): NodePack => ({
|
||||
id,
|
||||
name: `Pack ${id}`,
|
||||
description: `Description for pack ${id}`,
|
||||
category: 'Nodes',
|
||||
author: 'Test Author',
|
||||
license: 'MIT',
|
||||
repository: 'https://github.com/test/pack',
|
||||
tags: [],
|
||||
status: (status || 'NodeStatusActive') as NodeStatus
|
||||
})
|
||||
|
||||
const createMockConflict = (
|
||||
packageId: string,
|
||||
type: 'import_failed' | 'banned' | 'pending' = 'import_failed'
|
||||
): ConflictDetectionResult => ({
|
||||
package_id: packageId,
|
||||
package_name: `Pack ${packageId}`,
|
||||
has_conflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type,
|
||||
current_value: 'current',
|
||||
required_value: 'required'
|
||||
}
|
||||
],
|
||||
is_compatible: false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
conflictDetectionStore = useConflictDetectionStore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('hasImportFailed', () => {
|
||||
it('should return true when at least one pack has import_failed conflict', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
// Set up mock conflicts
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack2', 'import_failed'),
|
||||
createMockConflict('pack3', 'banned')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when no pack has import_failed conflict', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
// Set up mock conflicts with no import_failed
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack1', 'pending'),
|
||||
createMockConflict('pack2', 'banned')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when no conflicts exist', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle packs without ids', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
{ ...createMockPack('pack1'), id: undefined as any },
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack2', 'import_failed')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should update when conflicts change', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
|
||||
// Add import_failed conflict
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack1', 'import_failed')
|
||||
])
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('overallStatus', () => {
|
||||
it('should prioritize banned status over all others', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusBanned'),
|
||||
createMockPack('pack3', 'NodeVersionStatusDeleted')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
|
||||
it('should prioritize version banned over deleted and active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeVersionStatusBanned'),
|
||||
createMockPack('pack3', 'NodeVersionStatusDeleted')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusBanned')
|
||||
})
|
||||
|
||||
it('should prioritize deleted status appropriately', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusDeleted'),
|
||||
createMockPack('pack3', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusDeleted')
|
||||
})
|
||||
|
||||
it('should prioritize version deleted over flagged and active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeVersionStatusFlagged'),
|
||||
createMockPack('pack2', 'NodeVersionStatusDeleted'),
|
||||
createMockPack('pack3', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusDeleted')
|
||||
})
|
||||
|
||||
it('should prioritize flagged status over pending and active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeVersionStatusPending'),
|
||||
createMockPack('pack2', 'NodeVersionStatusFlagged'),
|
||||
createMockPack('pack3', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusFlagged')
|
||||
})
|
||||
|
||||
it('should prioritize pending status over active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeVersionStatusActive'),
|
||||
createMockPack('pack2', 'NodeVersionStatusPending'),
|
||||
createMockPack('pack3', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusPending')
|
||||
})
|
||||
|
||||
it('should return NodeStatusActive when all packs are active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusActive')
|
||||
})
|
||||
|
||||
it('should return NodeStatusActive as default when all packs have no status', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
// Since createMockPack sets status to 'NodeStatusActive' by default
|
||||
expect(overallStatus.value).toBe('NodeStatusActive')
|
||||
})
|
||||
|
||||
it('should handle empty pack array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
})
|
||||
|
||||
it('should update when pack statuses change', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
expect(overallStatus.value).toBe('NodeStatusActive')
|
||||
|
||||
// Change one pack to banned
|
||||
nodePacks.value = [
|
||||
createMockPack('pack1', 'NodeStatusBanned'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
]
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration with import failures', () => {
|
||||
it('should return NodeVersionStatusActive when import failures exist (handled separately)', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack1', 'import_failed')
|
||||
])
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
// When import failed exists, it returns NodeVersionStatusActive
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
})
|
||||
|
||||
it('should return NodeVersionStatusActive when import failures exist even with banned status', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusBanned'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack2', 'import_failed')
|
||||
])
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
// Import failed takes priority and returns NodeVersionStatusActive
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle multiple conflicts per package', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
{
|
||||
package_id: 'pack1',
|
||||
package_name: 'Pack pack1',
|
||||
has_conflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'pending',
|
||||
current_value: 'current1',
|
||||
required_value: 'required1'
|
||||
},
|
||||
{
|
||||
type: 'import_failed',
|
||||
current_value: 'current2',
|
||||
required_value: 'required2'
|
||||
}
|
||||
],
|
||||
is_compatible: false
|
||||
}
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle packs with no conflicts in store', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle mixed status types correctly', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusBanned'),
|
||||
createMockPack('pack2', 'NodeVersionStatusBanned'),
|
||||
createMockPack('pack3', 'NodeStatusDeleted'),
|
||||
createMockPack('pack4', 'NodeVersionStatusDeleted'),
|
||||
createMockPack('pack5', 'NodeVersionStatusFlagged'),
|
||||
createMockPack('pack6', 'NodeVersionStatusPending'),
|
||||
createMockPack('pack7', 'NodeStatusActive'),
|
||||
createMockPack('pack8', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
// Should return the highest priority status (NodeStatusBanned)
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
|
||||
it('should be reactive to nodePacks changes', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
|
||||
// Add packs
|
||||
nodePacks.value = [
|
||||
createMockPack('pack1', 'NodeStatusDeleted'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
]
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusDeleted')
|
||||
|
||||
// Add a higher priority status
|
||||
nodePacks.value.push(createMockPack('pack3', 'NodeStatusBanned'))
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
})
|
||||
})
|
||||
186
tests-ui/tests/composables/useConflictAcknowledgment.test.ts
Normal file
186
tests-ui/tests/composables/useConflictAcknowledgment.test.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('useConflictAcknowledgment', () => {
|
||||
beforeEach(() => {
|
||||
// Set up Pinia for each test
|
||||
setActivePinia(createPinia())
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear()
|
||||
// Reset modules to ensure fresh state
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('initial state loading', () => {
|
||||
it('should load empty state when localStorage is empty', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
modal_dismissed: false,
|
||||
red_dot_dismissed: false,
|
||||
warning_banner_dismissed: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should load existing state from localStorage', async () => {
|
||||
// Pre-populate localStorage with JSON values (as useStorage expects)
|
||||
localStorage.setItem('Comfy.ConflictModalDismissed', JSON.stringify(true))
|
||||
localStorage.setItem(
|
||||
'Comfy.ConflictRedDotDismissed',
|
||||
JSON.stringify(true)
|
||||
)
|
||||
localStorage.setItem(
|
||||
'Comfy.ConflictWarningBannerDismissed',
|
||||
JSON.stringify(true)
|
||||
)
|
||||
|
||||
// Need to import the module after localStorage is set
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
modal_dismissed: true,
|
||||
red_dot_dismissed: true,
|
||||
warning_banner_dismissed: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('dismissal functions', () => {
|
||||
it('should mark conflicts as seen with unified function', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { markConflictsAsSeen, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
markConflictsAsSeen()
|
||||
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(true)
|
||||
})
|
||||
|
||||
it('should dismiss red dot notification', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { dismissRedDotNotification, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
dismissRedDotNotification()
|
||||
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(true)
|
||||
})
|
||||
|
||||
it('should dismiss warning banner', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { dismissWarningBanner, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
dismissWarningBanner()
|
||||
|
||||
expect(acknowledgmentState.value.warning_banner_dismissed).toBe(true)
|
||||
})
|
||||
|
||||
it('should mark all conflicts as seen', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { markConflictsAsSeen, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
markConflictsAsSeen()
|
||||
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(true)
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(true)
|
||||
expect(acknowledgmentState.value.warning_banner_dismissed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should calculate shouldShowConflictModal correctly', async () => {
|
||||
// Need fresh module import to ensure clean state
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { shouldShowConflictModal, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
expect(shouldShowConflictModal.value).toBe(true)
|
||||
|
||||
markConflictsAsSeen()
|
||||
expect(shouldShowConflictModal.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should calculate shouldShowRedDot correctly based on conflicts', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { shouldShowRedDot, dismissRedDotNotification } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Initially false because no conflicts exist
|
||||
expect(shouldShowRedDot.value).toBe(false)
|
||||
|
||||
dismissRedDotNotification()
|
||||
expect(shouldShowRedDot.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should calculate shouldShowManagerBanner correctly', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { shouldShowManagerBanner, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Initially false because no conflicts exist
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
|
||||
dismissWarningBanner()
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('localStorage persistence', () => {
|
||||
it('should persist to localStorage automatically', async () => {
|
||||
// Need fresh module import to ensure clean state
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { markConflictsAsSeen, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
markConflictsAsSeen()
|
||||
dismissWarningBanner()
|
||||
|
||||
// Wait a tick for useStorage to sync
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
// VueUse useStorage should automatically persist to localStorage as JSON
|
||||
expect(localStorage.getItem('Comfy.ConflictModalDismissed')).toBe('true')
|
||||
expect(localStorage.getItem('Comfy.ConflictWarningBannerDismissed')).toBe(
|
||||
'true'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user