Compare commits

..

137 Commits

Author SHA1 Message Date
Terry Jia
e6a98e3286 export generateUUID 2025-04-17 21:59:47 -04:00
Chenlei Hu
9ce3cccfd4 [API Nodes] Wire password login (#3469) 2025-04-15 19:41:22 -04:00
Chenlei Hu
9935b322f0 [API Nodes] Signin/Signup dialog (#3466)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-15 17:37:53 -04:00
Comfy Org PR Bot
60dd242b23 [chore] Update Comfy Registry API types from comfy-api@b664e39 (#3465)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-15 17:16:02 -04:00
Christian Byrne
cec0dcbccd [Api Node] Firebase auth and user auth store (#3467) 2025-04-15 17:15:51 -04:00
Chenlei Hu
907632a250 Use bundler moduleResolution mode in tsconfig (#3464) 2025-04-15 11:17:07 -04:00
Comfy Org PR Bot
45c450cdb9 [chore] Update litegraph to 0.13.3 (#3463)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-15 11:03:32 -04:00
Chenlei Hu
ca85b2b144 [DevExperience] Add recommended extensions to .vscode (#3459) 2025-04-15 10:27:43 -04:00
Comfy Org PR Bot
1f28e6ef33 1.17.0 (#3458)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-14 21:06:33 -04:00
Comfy Org PR Bot
fee444c64b [chore] Update litegraph to 0.13.2 (#3460)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-14 21:06:23 -04:00
Chenlei Hu
851739a768 [API Node] Sign in required dialog (#3457)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-14 17:49:17 -04:00
Benjamin Lu
1631665efb Fix minor typo (#3456)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-14 13:24:13 -04:00
Terry Jia
1a066c7062 [3d] move default values of showPreview, showGrid, cameraType to settings panel (#3443)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-13 19:44:47 -04:00
Chenlei Hu
e45f5bdebb [lint] Fix remaining vue lint warnings (#3435) 2025-04-12 22:29:29 -04:00
Dr.Lt.Data
c270e7734a refine locales/ko (#3434) 2025-04-12 22:19:11 -04:00
Chenlei Hu
8d7a21e008 [lint] Add eslint-plugin-prettier (#3433) 2025-04-12 22:12:50 -04:00
Comfy Org PR Bot
29e63baca6 1.16.7 (#3430)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-12 21:22:02 -04:00
Benjamin Lu
b22713daf0 Add source prop to commands (#3429)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-12 21:20:53 -04:00
Benjamin Lu
c8b8953e0a Add reset individual keybind button (#3423)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-12 16:59:00 -04:00
Benjamin Lu
731ce8599d Allow scrolling on .settings-content (#3427)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-12 16:58:08 -04:00
filtered
ec8e55c1c1 Allow zooming inside multi-line string widgets (#3422)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-12 13:13:00 -04:00
dependabot[bot]
04d38f2538 Bump vite from 5.4.17 to 5.4.18 (#3426)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-12 12:19:10 -04:00
Chenlei Hu
1c41db75f8 [Type] Enable strict schema type for setting entries (#3425) 2025-04-12 11:34:02 -04:00
Christian Byrne
c7a7397000 [Types] Make more types available directly in @comfyorg/comfyui-frontend-types package (#3418) 2025-04-12 11:33:34 -04:00
Terry Jia
e660e1d678 [3d] add support to output camera state (#3421) 2025-04-12 11:27:05 -04:00
Christian Byrne
fb19752389 [Types] Adds missing settings fields types (#3417) 2025-04-12 11:12:07 -04:00
Christian Byrne
d098d6ae4e [Types] Move enum to types file (#3416) 2025-04-12 11:11:41 -04:00
Christian Byrne
e4a5355f58 [Manager] Fix loading state on uninstall button (#3413) 2025-04-11 22:14:49 -04:00
Comfy Org PR Bot
42c004d41d 1.16.6 (#3410)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-11 20:33:25 -04:00
Benjamin Lu
009c389607 Rename reset keybindings to reset all keybindings (#3411)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-11 20:01:28 -04:00
Christian Byrne
b449dbd26b [Manager] Allowing changing sort field of registry search results (#3409)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-11 18:45:21 -04:00
Christian Byrne
67835edfca [Manager] Preview the individual nodes for packs on the registry (#3408)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-11 16:18:41 -04:00
filtered
60c0ce228a Generate a new workflow id when using "save as" (#3407) 2025-04-11 15:43:10 -04:00
Benjamin Lu
1990f25638 Allow keybind overwriting (#3393)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-11 14:28:36 -04:00
Comfy Org PR Bot
30c473db77 [chore] Update Comfy Registry API types from comfy-api@a28605f (#3405)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-11 14:02:37 -04:00
Chenlei Hu
2371288fed [Bug] Fix widgets values migration (#3404) 2025-04-11 13:44:14 -04:00
Christian Byrne
2337fe6f8e [Manager] Fix hot reload after install of missing node pack (#3397)
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-04-11 12:55:02 -04:00
Benjamin Lu
25e6386b2a Do not trigger autosave on workflows that are not persisted (#3400)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-11 12:53:49 -04:00
Chenlei Hu
a03841cb1a [lint] Enable plugin vue recommended rules (#3403) 2025-04-11 12:53:20 -04:00
Chenlei Hu
dc5d7ea1be [lint] Enforce @typescript-eslint/no-floating-promises (#3402) 2025-04-11 12:19:22 -04:00
Christian Byrne
59e20964a0 [Manager] Add fallback to infer node pack when metadata is missing from workflow (#3396) 2025-04-11 09:50:30 -04:00
Comfy Org PR Bot
8f00d8ca6a 1.16.5 (#3394)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-10 21:29:17 -04:00
Christian Byrne
05e0036898 [Manager] Button to open manager from Missing Nodes dialog (#3395) 2025-04-10 21:28:42 -04:00
Benjamin Lu
9e7690405a keep user input on top of selection toolbox (#3389)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-10 20:50:19 -04:00
Christian Byrne
d687ea2cde [Manager] Fix infinite fetch attempts when response is empty (#3392) 2025-04-10 16:39:54 -07:00
Chenlei Hu
c801a0c854 Attach frontend version metadata on workflow export (#3386) 2025-04-10 14:19:54 -04:00
Chenlei Hu
615c183059 Disable reroute migration (2/2) (#3385) 2025-04-10 14:06:09 -04:00
Chenlei Hu
27c8389b9f [Cleanup] Remove ComfyApp.serializeGraph (#3383) 2025-04-10 11:50:27 -04:00
Chenlei Hu
261f671ef0 Disable native reroute migration (#3379)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-10 08:50:07 -04:00
Benjamin Lu
22ae30132c Add tag pi-language icon for Comfy.Locale setting (#3378)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-09 22:47:40 -04:00
Chenlei Hu
7d3bf372b0 [Bug] Apply link fixes regardless of fixed state (#3376) 2025-04-09 20:28:49 -04:00
Comfy Org PR Bot
cd35373c25 1.16.4 (#3372)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-09 19:41:01 -04:00
Chenlei Hu
a500a96c4a [TS] Fix widget GET_CONFIG on loading primitive nodes (#3374) 2025-04-09 17:48:28 -04:00
Chenlei Hu
dc9ea44f3a Prevent widget value serialization for media preview widgets (#3370) 2025-04-09 17:01:02 -04:00
Christian Byrne
2dc33b1eb9 [Manager] Remove outdated comment (#3371) 2025-04-09 16:51:16 -04:00
Comfy Org PR Bot
ed8f9a5a4f [chore] Update litegraph to 0.13.1 (#3369)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-09 16:45:31 -04:00
Chenlei Hu
6e72e1924e Update ComfyUI_devtools version in CI (#3368) 2025-04-09 14:59:07 -04:00
Christian Byrne
f7854a4e0b [Manager] Fix node pack icons not displaying in search results (#3366) 2025-04-09 13:51:47 -04:00
Chenlei Hu
05023b7889 Migrate defaultInput widget option (#3364)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-09 13:51:34 -04:00
Chenlei Hu
609496957b [Cleanup] Remove WidgetOptions.forceInput (#3362) 2025-04-09 11:36:35 -04:00
Chenlei Hu
a879f413bb [Cleanup] Remove WidgetOptions.inputIsOptional (#3361) 2025-04-09 10:45:51 -04:00
Comfy Org PR Bot
21d679a662 1.16.3 (#3357)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-08 23:05:29 -04:00
Chenlei Hu
34f9603961 [Feature Request]: Add resize keybinding (#3356) (#3358)
Co-authored-by: Emanuel F. <70411130+efrancisworks@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-08 23:05:21 -04:00
Christian Byrne
cf27a896f3 [Manager] Handle display of git hash versions for NIGHTLY node packs (#3359) 2025-04-08 23:00:36 -04:00
Chenlei Hu
e9a98161ca [Bug] Fix converted widget compression on export (#3354) 2025-04-08 20:20:19 -04:00
Christian Byrne
fa132e4106 Don't translate 'Stable Zero' (#3352) 2025-04-08 19:51:11 -04:00
Chenlei Hu
a489c19b07 Upstream rgthree's link fixer (#3350) 2025-04-08 18:32:43 -04:00
Comfy Org PR Bot
46af2f03f3 1.16.2 (#3345)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-08 11:08:53 -04:00
Chenlei Hu
3a1c95fb10 [Bug] Fix input link slots (#3349)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-08 11:08:42 -04:00
Comfy Org PR Bot
7a6f0e210e 1.16.1 (#3342)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-07 22:22:45 -04:00
Chenlei Hu
ac3bd7a848 [Test] Add test on dynamically added inputs (#3344)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-07 22:22:32 -04:00
Comfy Org PR Bot
77b5e487cf [chore] Update litegraph to 0.13.0 (#3343)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-07 20:04:18 -04:00
catboxanon
a7a8459e18 Add option to disable workflow persistence (#3341)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-04-07 17:06:41 -04:00
dependabot[bot]
65c9c264c6 Bump vite from 5.4.16 to 5.4.17 (#3338)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 21:37:16 -04:00
Comfy Org PR Bot
8ea070df12 1.16.0 (#3334)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-06 21:37:08 -04:00
Chenlei Hu
2c02d4ebb3 Migrate forceInput widgets_values (#3337)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-06 21:27:42 -04:00
Benjamin Lu
a2b3048b94 Clarify Playwright setup README and add note to .env_example (#3336)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-06 21:00:14 -04:00
Christian Byrne
549a42716f [Manager] Add tab for outdated node packs (has update available) (#3255)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-06 18:49:40 -04:00
Benjamin Lu
fa75614dc3 Add autosave feature (#3330)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-06 18:48:00 -04:00
Chenlei Hu
ac53296b2e Support associated socket for widgets (#3326)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-06 11:50:21 -04:00
Comfy Org PR Bot
6eb2b76621 [chore] Update litegraph to 0.12.0 (#3335)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-06 11:31:42 -04:00
Christian Byrne
9dd3b9fff5 [3d] Add translations for material modes controls (#3325)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-04-05 22:29:46 -04:00
Chenlei Hu
785cad70ba [Cleanup] Remove extra.info from workflow schema (#3332) 2025-04-05 21:43:09 -04:00
Comfy Org PR Bot
026f076b8a 1.15.12 (#3331)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-05 21:22:08 -04:00
Christian Byrne
65f1561ec6 Add option to disable reconnecting toasts (#3327) 2025-04-05 18:50:25 -04:00
Benjamin Lu
bb094cf0ae Scroll to active offscreen tab when opened (#3320)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-05 16:06:45 -04:00
Christian Byrne
ec684ee6b8 [Manager] Fix primevue severity in status messages (#3324) 2025-04-05 16:04:30 -04:00
Comfy Org PR Bot
3978613f14 1.15.11 (#3322)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-05 17:56:18 +11:00
Comfy Org PR Bot
0a40e07f7e [chore] Update litegraph to 0.11.10 (#3321)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-04-05 17:42:24 +11:00
Comfy Org PR Bot
577af51ff8 1.15.10 (#3319)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-03 22:31:57 -04:00
Chenlei Hu
df7c7383e2 Only show reroute migration dialog when native reroute is not present (#3318) 2025-04-03 22:08:40 -04:00
Comfy Org PR Bot
1279f30f5a [chore] Update litegraph to 0.11.9 (#3316)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-03 14:49:35 -04:00
Benjamin Lu
9ab4b549c0 Enable double clicking keybind row to edit bind (#2924) (#3315)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-03 14:04:02 -04:00
Comfy Org PR Bot
10de4e5445 1.15.9 (#3314)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-02 19:35:18 -04:00
Chenlei Hu
30420f2c0a [TS] Add null checks to widgetInputs.ts (#3312) 2025-04-02 15:48:46 -04:00
Chenlei Hu
39c3a57c11 [Cleanup] Remove handling of legacy slot widget config (#3311) 2025-04-02 13:58:06 -04:00
Chenlei Hu
6d09b7165f [TS] Fix event type for executing listener (#3310) 2025-04-02 11:16:36 -04:00
Chenlei Hu
8fc6840434 [Bug] Fix progress bar display on last output node (#3309) 2025-04-02 11:07:25 -04:00
Chenlei Hu
db575425fe [nit] Show error message from response (#3308) 2025-04-02 10:39:10 -04:00
Laurent Erignoux
ccb71bf1a3 Ensures we clean missing local storage comfy UserId (#3289) 2025-04-01 22:42:00 -04:00
Chenlei Hu
733d71aaac [Refactor] Split node constructor logic (#3307) 2025-04-01 20:48:32 -04:00
Comfy Org PR Bot
e059b9b82f 1.15.8 (#3306)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-01 16:47:22 -04:00
Chenlei Hu
cfaf769a65 [TS] Properly type slot widget (#3305) 2025-04-01 15:13:46 -04:00
Chenlei Hu
b80e0e1a3c [Performance] Avoid layout thrashing (#3302) 2025-04-01 14:05:06 -04:00
Chenlei Hu
7b7d9905a7 Expose currently active color palette (#3304) 2025-04-01 14:04:57 -04:00
Chenlei Hu
594fc5945c Fix workflow persistence (#3303) 2025-04-01 13:52:04 -04:00
Chenlei Hu
e5abf765bd [Performance] Avoid per-frame workflow persistence (#3301) 2025-04-01 11:31:28 -04:00
dependabot[bot]
712c127bb5 Bump vite from 5.4.15 to 5.4.16 (#3295)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 00:11:55 -04:00
Chenlei Hu
854501ef27 Show object widget values as string on missing nodes (#3294)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-31 20:55:50 -04:00
Comfy Org PR Bot
aea4493b4d 1.15.7 (#3293)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-31 20:07:19 -04:00
Chenlei Hu
df47226fd4 Add Reroute SplineOffset setting (#3292)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-31 20:05:38 -04:00
Chenlei Hu
f26f5f25bb Add widget to node with missing definition (#3291)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-31 14:23:44 -04:00
Comfy Org PR Bot
284902cabe 1.15.6 (#3287)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-30 21:48:18 -04:00
Chenlei Hu
58dec5ea42 Add reroute migration toast (#3286)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-30 21:48:10 -04:00
Chenlei Hu
7e76665a22 Revert "Migrate legacy reroute to litegraph native reroute (#3151)" (#3285)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-30 21:19:33 -04:00
Chenlei Hu
cb06d96930 [Refactor] Use NodeSlot.hasErrors API (#3284)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-30 20:10:28 -04:00
Benjamin Lu
b01ddb6aff Make entire result image preview clickable (#3279)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-03-30 19:06:29 -04:00
Chenlei Hu
10bed33383 [Refactor] Use LGraphNode.progress API (#3281) 2025-03-30 18:13:31 -04:00
Comfy Org PR Bot
a57e60d60a [chore] Update litegraph to 0.11.7 (#3280)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-30 17:59:20 -04:00
Chenlei Hu
8c789bd05d [Refactor] Use litegraph LGraphNode.strokeStyles API (#3278) 2025-03-30 12:05:45 -04:00
Chenlei Hu
28def833f9 [TS] Fix node constructor signature (#3276) 2025-03-29 20:55:11 -04:00
Chenlei Hu
fcc22f06ac [Refactor/TS] Simplify node filter logic (#3275) 2025-03-29 13:00:18 -04:00
Chenlei Hu
3922a5882b [Refactor] Extract fuse search class as a separate file (#3274) 2025-03-29 12:04:29 -04:00
Comfy Org PR Bot
4a40e83b98 1.15.5 (#3268)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-28 21:20:27 -04:00
Chenlei Hu
21e0caa1b1 [Bug] Fix undo of colorization via selection toolbox (#3267) 2025-03-28 21:00:43 -04:00
Chenlei Hu
04af8cda4d Use new error dialog for queue prompt errors (#3266)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-28 13:51:00 -04:00
Chenlei Hu
504b717575 [Refactor] Unify error dialog component (#3265)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-28 11:53:29 -04:00
Chenlei Hu
62fdcd4949 [Refactor] Extract error report generation logic (#3263) 2025-03-28 10:50:27 -04:00
Yiximail
cb7adaef9b maskeditor pen input support for windows (#3201) 2025-03-28 13:58:53 +01:00
Comfy Org PR Bot
6aad5222ab 1.15.4 (#3261)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-27 22:23:15 -04:00
Chenlei Hu
690326c374 [Reroute] Migrate floating link (#3260) 2025-03-27 22:13:16 -04:00
Comfy Org PR Bot
25ce267b2e [chore] Update litegraph to 0.11.5 (#3258)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-03-28 09:09:32 +11:00
Comfy Org PR Bot
78e3a20773 [chore] Update litegraph to 0.11.4 (#3257)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-03-27 14:24:19 -04:00
Chenlei Hu
56dbcbbd22 [Bug] Fix convert dom widget placeholder render (#3256) 2025-03-27 14:09:30 -04:00
Christian Byrne
4bfc8e9e33 [Manager] Fetch lists of node packs in single request (#3250) 2025-03-27 11:49:05 -04:00
Chenlei Hu
6e72207927 [Bug] Fix this binding in useChainCallback (#3252) 2025-03-27 11:38:52 -04:00
Christian Byrne
71968ae133 Translate action history items (#3249)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-27 11:13:24 -04:00
321 changed files with 12025 additions and 3440 deletions

View File

@@ -8,6 +8,15 @@ const vue3CompositionApiBestPractices = [
"Use watch and watchEffect for side effects",
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
"Utilize provide/inject for dependency injection",
"Use vue 3.5 style of default prop declaration. Example:
const { nodes, showTotal = true } = defineProps<{
nodes: ApiNodeCost[]
showTotal?: boolean
}>()
",
"Organize vue component in <template> <script> <style> order",
]
// Folder structure
@@ -40,4 +49,6 @@ const additionalInstructions = `
7. Implement proper error handling
8. Follow Vue 3 style guide and naming conventions
9. Use Vite for fast development and building
10. Use vue-i18n in composition API for any string literals. Place new translation
entries in src/locales/en/main.json.
`;

View File

@@ -1,4 +1,5 @@
# Local development playwright target
# Note: Don't add a trailing / after the port
PLAYWRIGHT_TEST_URL=http://localhost:5173
# PLAYWRIGHT_TEST_URL=http://localhost:8188

View File

@@ -30,7 +30,7 @@ jobs:
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be'
ref: '49c8220be49120dbaff85f32813d854d6dff2d05'
- uses: actions/setup-node@v4
with:

View File

@@ -10,7 +10,7 @@ module.exports = defineConfig({
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, controlnet, lora.
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
`

25
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"recommendations": [
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"figma.figma-vscode-extension",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"hbenl.vscode-test-explorer",
"lokalise.i18n-ally",
"ms-playwright.playwright",
"vitest.explorer",
"vue.volar",
"sonarsource.sonarlint-vscode",
"deque-systems.vscode-axe-linter",
"kisstkondoros.vscode-codemetrics",
"donjayamanne.githistory",
"wix.vscode-import-cost",
"prograhammer.tslint-vue",
"antfu.vite"
]
}

View File

@@ -9,15 +9,26 @@ If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directo
## Setup
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing.
### ComfyUI devtools
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
### Node.js & Playwright Prerequisites
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
```bash
npx playwright install chromium --with-deps
```
### Environment Variables
Ensure the environment variables in `.env` are set correctly according to your setup.
The `.env` file will not exist until you create it yourself.
A template with helpful information can be found in `.env_example`.
### Multiple Tests
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
## Running Tests
There are two ways to run the tests:
@@ -34,8 +45,6 @@ There are two ways to run the tests:
```
This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
To run the same test multiple times in Playwright's UI mode, you must launch the main ComfyUI process with the `--multi-user` flag.
![Playwright UI Mode](https://github.com/user-attachments/assets/6a1ebef0-90eb-4157-8694-f5ee94d03755)
## Screenshot Expectations

View File

@@ -0,0 +1,126 @@
{
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
"revision": 0,
"last_node_id": 5,
"last_link_id": 3,
"nodes": [
{
"id": 4,
"type": "KSampler",
"pos": [
867.4669799804688,
347.22369384765625
],
"size": [
315,
262
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
},
{
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": 3
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 5,
"type": "PrimitiveInt",
"pos": [
443.0852355957031,
441.131591796875
],
"size": [
315,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "PrimitiveInt"
},
"widgets_values": [
0,
"randomize"
]
}
],
"links": [
[
3,
5,
0,
4,
5,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.9487171000000016,
"offset": [
-325.57196748514497,
-168.13150517966463
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,53 @@
{
"id": "9bcb9451-8319-492a-88d4-fb711d8c3d25",
"revision": 0,
"last_node_id": 6,
"last_link_id": 0,
"nodes": [
{
"id": 6,
"type": "DevToolsNodeWithDefaultInput",
"pos": [
8.39722728729248,
29.727279663085938
],
"size": [
315,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "float_input",
"shape": 7,
"type": "FLOAT",
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithDefaultInput"
},
"widgets_values": [
0,
1,
0
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 2.1600300525920346,
"offset": [
63.071794466403446,
75.18055335968394
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,82 @@
{
"last_node_id": 9,
"last_link_id": 13,
"nodes": [
{
"id": 3,
"type": "KSampler",
"pos": [
0,
30
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
} ,
{
"name": "dynamic_input",
"type": "FLOAT",
"link": null,
"_meta": "Dynamically added input via frontend JS logic"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,74 @@
{
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
"revision": 0,
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "CLIPTextEncode",
"pos": [904, 466],
"size": [400, 200],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 1
},
{
"name": "clip",
"type": "CLIP",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": null
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [""]
},
{
"id": 2,
"type": "PrimitiveString",
"pos": [556.8589477539062, 472.94342041015625],
"size": [315, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [1]
}
],
"properties": {
"Node name for S&R": "PrimitiveString"
},
"widgets_values": ["foo"]
}
],
"links": [[1, 2, 0, 1, 0, "STRING"]],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.7715610000000013,
"offset": [-388.521484375, -162.31336975097656]
}
},
"version": 0.4
}

View File

@@ -51,7 +51,10 @@
0.85,
false,
false,
""
"",
{
"foo": "bar"
}
]
}
],

File diff suppressed because it is too large Load Diff

View File

@@ -412,7 +412,7 @@ export class ComfyPage {
}
async getVisibleToastCount() {
return await this.page.locator('.p-toast:visible').count()
return await this.page.locator('.p-toast-message:visible').count()
}
async clickTextEncodeNode1() {

View File

@@ -81,7 +81,7 @@ export class NodeWidgetReference {
if (!widget) throw new Error(`Widget ${index} not found.`)
const [x, y, w, h] = node.getBounding()
return window['app'].canvas.ds.convertOffsetToCanvas([
return window['app'].canvasPosToClientPos([
x + w / 2,
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
])
@@ -94,6 +94,36 @@ export class NodeWidgetReference {
}
}
/**
* @returns The position of the widget's associated socket
*/
async getSocketPosition(): Promise<Position> {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
const slot = node.inputs.find(
(slot) => slot.widget?.name === widget.name
)
if (!slot) throw new Error(`Socket ${widget.name} not found.`)
const [x, y] = node.getBounding()
return window['app'].canvasPosToClientPos([
x + slot.pos[0],
y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT']
])
},
[this.node.id, this.index] as const
)
return {
x: pos[0],
y: pos[1]
}
}
async click() {
await this.node.comfyPage.canvas.click({
position: await this.getPosition()
@@ -250,7 +280,7 @@ export class NodeReference {
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
await this.comfyPage.dragAndDrop(
await originSlot.getPosition(),
await targetWidget.getPosition()
await targetWidget.getSocketPosition()
)
return originSlot
}

View File

@@ -32,7 +32,7 @@ test.describe('Keybindings', () => {
})
await comfyPage.executeCommand('TestCommand')
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
expect(await comfyPage.getToastErrorCount()).toBe(1)
})
test('Should handle async command errors', async ({ comfyPage }) => {
@@ -45,6 +45,6 @@ test.describe('Keybindings', () => {
})
await comfyPage.executeCommand('TestCommand')
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
expect(await comfyPage.getToastErrorCount()).toBe(1)
})
})

View File

@@ -323,7 +323,21 @@ test.describe('Error dialog', () => {
await comfyPage.loadWorkflow('default')
const errorDialog = comfyPage.page.locator('.error-dialog-content')
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
})
test('Should display an error dialog when prompt execution fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(async () => {
const app = window['app']
app.api.queuePrompt = () => {
throw new Error('Error on queuePrompt!')
}
await app.queuePrompt(0)
})
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
})
})

View File

@@ -0,0 +1,20 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Execution', () => {
test('Report error on unconnected slot', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
await comfyPage.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -0,0 +1,21 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Graph', () => {
// Should be able to fix link input slot index after swap the input order
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
test('Fix link input slots', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('input_order_swap')
expect(
await comfyPage.page.evaluate(() => {
return window['app'].graph.links.get(1)?.target_slot
})
).toBe(1)
})
test('Validate workflow links', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('bad_link')
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -25,6 +25,11 @@ test.describe('Optional input', () => {
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
})
test('Default input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default_input')
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
})
test('Only optional inputs', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('only_optional_inputs')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
@@ -67,4 +72,10 @@ test.describe('Optional input', () => {
'missing_nodes_converted_widget.png'
)
})
test('dynamically added input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('dynamically_added_input')
await expect(comfyPage.canvas).toHaveScreenshot(
'dynamically_added_input.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -43,4 +43,15 @@ test.describe('Primitive Node', () => {
'static_primitive_connected.png'
)
})
test('Report missing nodes when connect to missing node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'primitive/primitive_node_connect_missing_node'
)
// Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
await expect(missingNodesWarning).toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -39,6 +39,10 @@ test.describe('Reroute Node', () => {
})
test.describe('LiteGraph Native Reroute Node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -88,63 +88,6 @@ test.describe('Node Right Click Menu', () => {
)
})
test.describe('Widget conversion', () => {
const convertibleWidgetTypes = ['text', 'string', 'number', 'toggle']
test('Can convert widget to input', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Convert Widget to Input').click()
await comfyPage.nextFrame()
// The submenu has an identical entry as the base menu - use last
await comfyPage.page.getByText('Convert width to input').last().click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-widget-converted.png'
)
})
test('Can convert widget without submenu', async ({ comfyPage }) => {
// Right-click the width widget
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Convert width to input').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-widget-converted.png'
)
})
convertibleWidgetTypes.forEach((widgetType) => {
test(`Can convert ${widgetType} widget to input`, async ({
comfyPage
}) => {
const nodeType = 'KSampler'
// To avoid needing multiple clicks, disable nesting of conversion options
await comfyPage.setSetting('Comfy.NodeInputConversionSubmenus', false)
// Add the widget using the node's `addWidget` method
await comfyPage.page.evaluate(
([nodeType, widgetType]) => {
const node = window['app'].graph.nodes.find(
(n) => n.type === nodeType
)
node.addWidget(widgetType, widgetType, 'defaultValue', () => {}, {})
},
[nodeType, widgetType]
)
// Verify the context menu includes the conversion option
const node = (await comfyPage.getNodeRefsByType(nodeType))[0]
const menuOptions = await node.getContextMenuOptionNames()
expect(menuOptions.includes(`Convert ${widgetType} to input`)).toBe(
true
)
})
})
})
test('Can pin and unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -246,5 +246,24 @@ test.describe('Selection Toolbox', () => {
)
await expect(colorPickerButton).toHaveCSS('color', BLUE_COLOR)
})
test('colorization via color picker can be undone', async ({
comfyPage
}) => {
// Select a node and color it
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
// Undo the colorization
await comfyPage.page.keyboard.press('Control+Z')
await comfyPage.nextFrame()
// Node should be uncolored again
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
expect(await selectedNode.getProperty('color')).toBeUndefined()
})
})
})

View File

@@ -192,3 +192,19 @@ test.describe('Load audio widget', () => {
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
})
})
test.describe('Unserialized widgets', () => {
test('Unserialized widgets values do not mark graph as modified', async ({
comfyPage
}) => {
// Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized)
await comfyPage.loadWorkflow('widgets/load_image_widget')
// Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts`
await comfyPage.page.mouse.move(10, 10)
await comfyPage.page.mouse.click(10, 10)
// Expect the graph to not be modified
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,4 +1,5 @@
import pluginJs from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
@@ -20,21 +21,26 @@ export default [
globals: {
...globals.browser,
__COMFYUI_FRONTEND_VERSION__: 'readonly'
},
parser: tseslint.parser,
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2020,
sourceType: 'module',
extraFileExtensions: ['.vue']
}
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
...pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
{
files: ['src/**/*.vue'],
languageOptions: { parserOptions: { parser: tseslint.parser } }
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off'
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
@@ -42,10 +48,12 @@ export default [
'unused-imports': unusedImports
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
'unused-imports/no-unused-imports': 'error'
'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off'
}
}
]

1281
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.15.3",
"version": "1.17.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -44,6 +44,8 @@
"autoprefixer": "^10.4.19",
"chalk": "^5.3.0",
"eslint": "^9.12.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-vue": "^9.27.0",
"fs-extra": "^11.2.0",
@@ -60,7 +62,7 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.15",
"vite": "^5.4.18",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
@@ -71,7 +73,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.11.3",
"@comfyorg/litegraph": "^0.13.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -89,6 +91,7 @@
"algoliasearch": "^5.21.0",
"axios": "^1.8.2",
"dotenv": "^16.4.5",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"jsondiffpatch": "^0.6.0",
"lodash": "^4.17.21",
@@ -101,6 +104,7 @@
"vue": "^3.5.13",
"vue-i18n": "^9.14.3",
"vue-router": "^4.4.3",
"vuefire": "^3.2.1",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -398,6 +398,7 @@ button.comfy-queue-btn {
.graphdialog {
min-height: 1em;
background-color: var(--comfy-menu-bg);
z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */
}
.graphdialog .name {

View File

@@ -17,7 +17,9 @@ const TITLE_SUFFIX = ' - ComfyUI'
const executionStore = useExecutionStore()
const executionText = computed(() =>
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
executionStore.isIdle
? ''
: `[${Math.round(executionStore.executionProgress * 100)}%]`
)
const settingStore = useSettingStore()
@@ -41,7 +43,7 @@ const workflowNameText = computed(() => {
const nodeExecutionTitle = computed(() =>
executionStore.executingNode && executionStore.executingNodeProgress
? `${executionText.value}[${executionStore.executingNodeProgress}%] ${executionStore.executingNode.type}`
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
: ''
)

View File

@@ -1,19 +1,19 @@
<template>
<Splitter
:key="activeSidebarTabId ?? undefined"
class="splitter-overlay-root splitter-overlay"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:key="activeSidebarTabId ?? undefined"
:stateKey="activeSidebarTabId ?? undefined"
stateStorage="local"
:state-key="activeSidebarTabId ?? undefined"
state-storage="local"
>
<SplitterPanel
class="side-bar-panel"
:minSize="10"
:size="20"
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'left'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel"></slot>
<slot name="side-bar-panel" />
</SplitterPanel>
<SplitterPanel :size="100">
@@ -21,26 +21,26 @@
class="splitter-overlay max-w-full"
layout="vertical"
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
stateKey="bottom-panel-splitter"
stateStorage="local"
state-key="bottom-panel-splitter"
state-storage="local"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel"></slot>
<slot name="graph-canvas-panel" />
</SplitterPanel>
<SplitterPanel class="bottom-panel" v-show="bottomPanelVisible">
<slot name="bottom-panel"></slot>
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
<slot name="bottom-panel" />
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
class="side-bar-panel"
:minSize="10"
:size="20"
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'right'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel"></slot>
<slot name="side-bar-panel" />
</SplitterPanel>
</Splitter>
</template>

View File

@@ -5,11 +5,11 @@
:style="positionCSS"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"

View File

@@ -1,19 +1,19 @@
<template>
<div
class="batch-count"
v-tooltip.bottom="{
value: $t('menu.batchCount'),
showDelay: 600
}"
class="batch-count"
:aria-label="$t('menu.batchCount')"
>
<InputNumber
class="w-14"
v-model="batchCount"
class="w-14"
:min="minQueueCount"
:max="maxQueueCount"
fluid
showButtons
show-buttons
:pt="{
incrementButton: {
class: 'w-6',

View File

@@ -4,9 +4,8 @@
:style="style"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
>
<div class="actionbar-content flex items-center select-none" ref="panelRef">
<span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef">
</span>
<div ref="panelRef" class="actionbar-content flex items-center select-none">
<span ref="dragHandleRef" class="drag-handle cursor-move mr-2 p-0!" />
<ComfyQueueButton />
</div>
</Panel>
@@ -89,9 +88,9 @@ const setInitialPosition = () => {
}
}
onMounted(setInitialPosition)
watch(visible, (newVisible) => {
watch(visible, async (newVisible) => {
if (newVisible) {
nextTick(setInitialPosition)
await nextTick(setInitialPosition)
}
})

View File

@@ -1,19 +1,19 @@
<template>
<div class="queue-button-group flex">
<SplitButton
class="comfyui-queue-button"
:label="activeQueueModeMenuItem.label"
severity="primary"
size="small"
@click="queuePrompt"
:model="queueModeMenuItems"
data-testid="queue-button"
v-tooltip.bottom="{
value: workspaceStore.shiftDown
? $t('menu.runWorkflowFront')
: $t('menu.runWorkflow'),
showDelay: 600
}"
class="comfyui-queue-button"
:label="activeQueueModeMenuItem.label"
severity="primary"
size="small"
:model="queueModeMenuItems"
data-testid="queue-button"
@click="queuePrompt"
>
<template #icon>
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
@@ -23,15 +23,15 @@
</template>
<template #item="{ item }">
<Button
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
:label="String(item.label)"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"
text
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
/>
</template>
</SplitButton>
@@ -48,8 +48,7 @@
text
:aria-label="$t('menu.interrupt')"
@click="() => commandStore.execute('Comfy.Interrupt')"
>
</Button>
/>
<Button
v-tooltip.bottom="{
value: $t('sideToolbar.queueTab.clearPendingTasks'),
@@ -135,12 +134,12 @@ const hasPendingTasks = computed(
)
const commandStore = useCommandStore()
const queuePrompt = (e: Event) => {
const queuePrompt = async (e: Event) => {
const commandId =
'shiftKey' in e && e.shiftKey
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
commandStore.execute(commandId)
await commandStore.execute(commandId)
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col h-full">
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
<TabList pt:tabList="border-none">
<TabList pt:tab-list="border-none">
<div class="w-full flex justify-between">
<div class="tabs-container">
<Tab

View File

@@ -1,7 +1,7 @@
<template>
<div class="relative overflow-hidden h-full w-full bg-black" ref="rootEl">
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
<div class="p-terminal rounded-none h-full w-full p-2">
<div class="h-full terminal-host" ref="terminalEl"></div>
<div ref="terminalEl" class="h-full terminal-host" />
</div>
</div>
</template>

View File

@@ -24,17 +24,17 @@ const terminalCreated = (
root,
autoRows: true,
autoCols: true,
onResize: () => {
onResize: async () => {
// If we aren't visible, don't resize
if (!terminal.element?.offsetParent) return
terminalApi.resize(terminal.cols, terminal.rows)
await terminalApi.resize(terminal.cols, terminal.rows)
}
})
onMounted(async () => {
offData = terminal.onData(async (message: string) => {
terminalApi.write(message)
await terminalApi.write(message)
})
offOutput = terminalApi.onOutput((message) => {

View File

@@ -1,6 +1,8 @@
<template>
<div class="bg-black h-full w-full">
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>
<ProgressSpinner
v-else-if="loading"
class="relative inset-0 flex justify-center items-center h-full z-10"
@@ -57,7 +59,7 @@ const terminalCreated = (
if (!clientId.value) {
await until(clientId).not.toBeNull()
}
api.subscribeLogs(true)
await api.subscribeLogs(true)
api.addEventListener('logs', logReceived)
}
@@ -76,9 +78,9 @@ const terminalCreated = (
loading.value = false
})
onUnmounted(() => {
onUnmounted(async () => {
if (api.clientId) {
api.subscribeLogs(false)
await api.subscribeLogs(false)
}
api.removeEventListener('logs', logReceived)
})

View File

@@ -0,0 +1,75 @@
<template>
<div class="flex flex-col gap-3 h-full">
<div class="flex justify-between text-xs">
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
</div>
<ScrollPanel class="flex-grow h-0">
<div class="flex flex-col gap-2">
<div
v-for="node in nodes"
:key="node.name"
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
>
<div class="flex items-center gap-2">
<span class="text-base font-medium leading-tight">{{
node.name
}}</span>
</div>
<div class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<span class="text-base font-medium leading-tight">
{{ node.cost.toFixed(costPrecision) }}
</span>
</div>
</div>
</div>
</ScrollPanel>
<template v-if="showTotal && nodes.length > 1">
<Divider class="my-2" />
<div class="flex justify-between items-center border-t px-3">
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
<div class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-yellow-500 p-1"
/>
<span>{{ totalCost.toFixed(costPrecision) }}</span>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import Tag from 'primevue/tag'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ApiNodeCost } from '@/types/apiNodeTypes'
const { t } = useI18n()
const {
nodes,
showTotal = true,
costPrecision = 3
} = defineProps<{
nodes: ApiNodeCost[]
showTotal?: boolean
costPrecision?: number
}>()
const totalCost = computed(() =>
nodes.reduce((sum, node) => sum + node.cost, 0)
)
</script>

View File

@@ -5,8 +5,8 @@
<SelectButton
v-model="selectedColorOption"
:options="colorOptionsWithCustom"
optionLabel="name"
dataKey="value"
option-label="name"
data-key="value"
:allow-empty="false"
>
<template #option="slotProps">
@@ -18,8 +18,8 @@
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
></div>
<i v-else class="pi pi-palette text-lg"></i>
/>
<i v-else class="pi pi-palette text-lg" />
</template>
</SelectButton>
<ColorPicker

View File

@@ -8,22 +8,22 @@
<img
v-if="contain"
:src="src"
@error="handleImageError"
:data-test="src"
class="comfy-image-blur"
:style="{ 'background-image': `url(${src})` }"
:alt="alt"
@error="handleImageError"
/>
<img
:src="src"
@error="handleImageError"
class="comfy-image-main"
:class="classProp"
:alt="alt"
@error="handleImageError"
/>
</span>
<div v-if="imageBroken" class="broken-image-placeholder">
<i class="pi pi-image"></i>
<i class="pi pi-image" />
<span>{{ $t('g.imageFailedToLoad') }}</span>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<div ref="container"></div>
<div ref="container" />
</template>
<script setup lang="ts">

View File

@@ -6,14 +6,14 @@
<SelectButton
v-model="selectedIcon"
:options="iconOptions"
optionLabel="name"
dataKey="value"
option-label="name"
data-key="value"
>
<template #option="slotProps">
<i
:class="['pi', slotProps.option.value, 'mr-2']"
:style="{ color: finalColor }"
></i>
/>
</template>
</SelectButton>
</div>
@@ -30,14 +30,14 @@
<Button
:label="$t('g.reset')"
icon="pi pi-refresh"
@click="resetCustomization"
class="p-button-text"
@click="resetCustomization"
/>
<Button
:label="$t('g.confirm')"
icon="pi pi-check"
@click="confirmCustomization"
autofocus
@click="confirmCustomization"
/>
</template>
</Dialog>

View File

@@ -1,7 +1,9 @@
<template>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in deviceColumns" :key="col.field">
<div class="font-medium">{{ col.header }}</div>
<div class="font-medium">
{{ col.header }}
</div>
<div>
{{ formatValue(props.device[col.field], col.field) }}
</div>

View File

@@ -6,19 +6,19 @@
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
<InputText
v-else
ref="inputRef"
v-model:modelValue="inputValue"
v-focus
type="text"
size="small"
fluid
v-model:modelValue="inputValue"
ref="inputRef"
@keyup.enter="blurInputElement"
@click.stop
:pt="{
root: {
onBlur: finishEditing
}
}"
v-focus
@keyup.enter="blurInputElement"
@click.stop
/>
</div>
</template>
@@ -45,10 +45,10 @@ const finishEditing = () => {
}
watch(
() => isEditing,
(newVal) => {
async (newVal) => {
if (newVal) {
inputValue.value = modelValue
nextTick(() => {
await nextTick(() => {
if (!inputRef.value) return
const fileName = inputValue.value.includes('.')
? inputValue.value.split('.').slice(0, -1).join('.')

View File

@@ -2,7 +2,7 @@
<template>
<div class="flex flex-col">
<div class="flex flex-row items-center gap-2">
<i class="pi pi-check text-green-500" v-if="status === 'completed'" />
<i v-if="status === 'completed'" class="pi pi-check text-green-500" />
<div class="file-info">
<div class="file-details">
<span class="file-type" :title="hint">{{ label }}</span>
@@ -14,20 +14,20 @@
<div class="file-action">
<Button
v-if="status === null || status === 'error'"
class="file-action-button"
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="!!props.error"
@click="triggerDownload"
v-if="status === null || status === 'error'"
icon="pi pi-download"
@click="triggerDownload"
/>
</div>
</div>
<div
class="flex flex-row items-center gap-2"
v-if="status === 'in_progress' || status === 'paused'"
class="flex flex-row items-center gap-2"
>
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
@@ -39,36 +39,36 @@
/>
<Button
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
@click="triggerPauseDownload"
v-if="status === 'in_progress'"
icon="pi pi-pause-circle"
v-tooltip.top="t('electronFileDownload.pause')"
/>
<Button
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
@click="triggerResumeDownload"
icon="pi pi-pause-circle"
@click="triggerPauseDownload"
/>
<Button
v-if="status === 'paused'"
icon="pi pi-play-circle"
v-tooltip.top="t('electronFileDownload.resume')"
/>
<Button
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
@click="triggerCancelDownload"
icon="pi pi-play-circle"
@click="triggerResumeDownload"
/>
<Button
v-tooltip.top="t('electronFileDownload.cancel')"
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
icon="pi pi-times-circle"
severity="danger"
v-tooltip.top="t('electronFileDownload.cancel')"
@click="triggerCancelDownload"
/>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<component v-if="extension.type === 'vue'" :is="extension.component" />
<component :is="extension.component" v-if="extension.type === 'vue'" />
<div
v-else
:ref="
@@ -11,7 +11,7 @@
)
}
"
></div>
/>
</template>
<script setup lang="ts">

View File

@@ -10,7 +10,7 @@
:src="modelValue"
class="max-w-full max-h-full object-contain"
/>
<i v-else class="pi pi-image text-gray-400 text-xl"></i>
<i v-else class="pi pi-image text-gray-400 text-xl" />
</div>
<div class="flex flex-col gap-2">

View File

@@ -3,26 +3,26 @@
<div class="flex flex-row items-center gap-2">
<div class="form-label flex flex-grow items-center">
<span
:id="`${props.id}-label`"
class="text-muted"
:class="props.labelClass"
:id="`${props.id}-label`"
>
<slot name="name-prefix"></slot>
<slot name="name-prefix" />
{{ props.item.name }}
<i
v-if="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
v-tooltip="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
/>
<slot name="name-suffix"></slot>
<slot name="name-suffix" />
</span>
</div>
<div class="form-input flex justify-end">
<component
:is="markRaw(getFormComponent(props.item))"
:id="props.id"
:aria-labelledby="`${props.id}-label`"
v-model:modelValue="formValue"
:aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)"
/>
</div>

View File

@@ -1,26 +1,26 @@
<template>
<div class="input-knob flex flex-row items-center gap-2">
<Knob
:modelValue="modelValue"
@update:modelValue="updateValue"
:valueTemplate="displayValue"
:model-value="modelValue"
:value-template="displayValue"
class="knob-part"
:class="knobClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="updateValue"
/>
<InputNumber
:modelValue="modelValue"
@update:modelValue="updateValue"
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allowEmpty="false"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>

View File

@@ -1,25 +1,25 @@
<template>
<div class="input-slider flex flex-row items-center gap-2">
<Slider
:modelValue="modelValue"
@update:modelValue="(value) => updateValue(value as number)"
:model-value="modelValue"
class="slider-part"
:class="sliderClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="(value) => updateValue(value as number)"
/>
<InputNumber
:modelValue="modelValue"
@update:modelValue="updateValue"
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allowEmpty="false"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>

View File

@@ -3,14 +3,16 @@
<Card>
<template #content>
<div class="flex flex-col items-center">
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem"></i>
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem" />
<h3>{{ title }}</h3>
<p class="whitespace-pre-line text-center">{{ message }}</p>
<p class="whitespace-pre-line text-center">
{{ message }}
</p>
<Button
v-if="buttonLabel"
:label="buttonLabel"
@click="$emit('action')"
class="p-button-text"
@click="$emit('action')"
/>
</div>
</template>

View File

@@ -22,7 +22,7 @@
class="p-button-icon pi pi-refresh transition-all"
:class="{ 'opacity-0': active }"
data-pc-section="icon"
></span>
/>
<span class="p-button-label" data-pc-section="label">&nbsp;</span>
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
</Button>
@@ -32,7 +32,7 @@
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
import { VueSeverity } from '@/types/primeVueTypes'
import { PrimeVueSeverity } from '@/types/primeVueTypes'
const {
disabled,
@@ -41,7 +41,7 @@ const {
} = defineProps<{
disabled?: boolean
outlined?: boolean
severity?: VueSeverity
severity?: PrimeVueSeverity
}>()
// Model

View File

@@ -11,9 +11,9 @@
/>
<InputText
class="search-box-input w-full"
@input="handleInput"
:modelValue="modelValue"
:model-value="modelValue"
:placeholder="placeholder"
@input="handleInput"
/>
<InputIcon v-if="!modelValue" :class="icon" />
<Button
@@ -26,8 +26,8 @@
/>
</IconField>
<div
class="search-filters pt-2 flex flex-wrap gap-2"
v-if="filters?.length"
class="search-filters pt-2 flex flex-wrap gap-2"
>
<SearchFilterChip
v-for="filter in filters"

View File

@@ -1,10 +1,14 @@
<template>
<div class="system-stats">
<div class="mb-6">
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.systemInfo') }}</h2>
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.systemInfo') }}
</h2>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in systemColumns" :key="col.field">
<div class="font-medium">{{ col.header }}</div>
<div class="font-medium">
{{ col.header }}
</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
</template>
</div>
@@ -13,7 +17,9 @@
<Divider />
<div>
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.devices') }}</h2>
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"

View File

@@ -1,11 +1,11 @@
<template>
<Tree
class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class"
v-model:expandedKeys="expandedKeys"
v-model:selectionKeys="selectionKeys"
class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class"
:value="renderedRoot.children"
selectionMode="single"
selection-mode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ context }) => ({
@@ -186,9 +186,9 @@ const menuItems = computed<MenuItem[]>(() =>
{
label: t('g.delete'),
icon: 'pi pi-trash',
command: () => {
command: async () => {
if (menuTargetNode.value) {
deleteCommand(menuTargetNode.value)
await deleteCommand(menuTargetNode.value)
}
},
visible: menuTargetNode.value?.handleDelete !== undefined,

View File

@@ -1,5 +1,6 @@
<template>
<div
ref="container"
:class="[
'tree-node',
{
@@ -8,17 +9,16 @@
'tree-leaf': props.node.leaf
}
]"
ref="container"
>
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="props.node"></slot>
<slot name="before-label" :node="props.node" />
<EditableText
:modelValue="node.label"
:isEditing="isEditing"
:model-value="node.label"
:is-editing="isEditing"
@edit="handleRename"
/>
<slot name="after-label" :node="props.node"></slot>
<slot name="after-label" :node="props.node" />
</span>
<Badge
v-if="showNodeBadgeText"
@@ -30,7 +30,7 @@
<div
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node"></slot>
<slot name="actions" :node="props.node" />
</div>
</div>
</template>

View File

@@ -3,7 +3,7 @@
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
<div :style="gridStyle">
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
<slot name="item" :item="item"> </slot>
<slot name="item" :item="item" />
</div>
</div>
<div

View File

@@ -60,7 +60,7 @@ describe('UrlInput', () => {
})
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
@@ -74,7 +74,7 @@ describe('UrlInput', () => {
validateUrlFn: () => Promise.resolve(true)
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
@@ -88,7 +88,7 @@ describe('UrlInput', () => {
validateUrlFn: () => Promise.resolve(false)
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
@@ -141,14 +141,14 @@ describe('UrlInput', () => {
}
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
// Trigger multiple validations in quick succession
wrapper.find('.pi-spinner').trigger('click')
wrapper.find('.pi-spinner').trigger('click')
wrapper.find('.pi-spinner').trigger('click')
await wrapper.find('.pi-spinner').trigger('click')
await wrapper.find('.pi-spinner').trigger('click')
await wrapper.find('.pi-spinner').trigger('click')
await nextTick()
await nextTick()

View File

@@ -13,11 +13,13 @@
>
<template #header>
<component
v-if="item.headerComponent"
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">{{ item.title || ' ' }}</h3>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
</template>
<component
@@ -26,7 +28,7 @@
:maximized="item.dialogComponentProps.maximized"
/>
<template #footer v-if="item.footerComponent">
<template v-if="item.footerComponent" #footer>
<component :is="item.footerComponent" />
</template>
</Dialog>

View File

@@ -0,0 +1,43 @@
<!-- Prompt user that the workflow contains API nodes that needs login to run -->
<template>
<div class="flex flex-col gap-4 max-w-96 h-110 p-2">
<div class="text-2xl font-medium mb-2">
{{ t('apiNodesSignInDialog.title') }}
</div>
<div class="text-base mb-4">
{{ t('apiNodesSignInDialog.message') }}
</div>
<ApiNodesCostBreakdown :nodes="apiNodes" :show-total="true" />
<div class="flex justify-between items-center">
<Button :label="t('g.learnMore')" link />
<div class="flex gap-2">
<Button
:label="t('g.cancel')"
outlined
severity="secondary"
@click="onCancel?.()"
/>
<Button :label="t('g.login')" @click="onLogin?.()" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import ApiNodesCostBreakdown from '@/components/common/ApiNodesCostBreakdown.vue'
import type { ApiNodeCost } from '@/types/apiNodeTypes'
const { t } = useI18n()
const { apiNodes, onLogin, onCancel } = defineProps<{
apiNodes: ApiNodeCost[]
onLogin?: () => void
onCancel?: () => void
}>()
</script>

View File

@@ -2,7 +2,9 @@
<section class="prompt-dialog-content flex flex-col gap-6 m-2 mt-4">
<span>{{ message }}</span>
<ul v-if="itemList?.length" class="pl-4 m-0 flex flex-col gap-2">
<li v-for="item of itemList" :key="item">{{ item }}</li>
<li v-for="item of itemList" :key="item">
{{ item }}
</li>
</ul>
<Message
v-if="hint"
@@ -18,53 +20,53 @@
:label="$t('g.cancel')"
icon="pi pi-undo"
severity="secondary"
@click="onCancel"
autofocus
@click="onCancel"
/>
<Button
v-if="type === 'default'"
:label="$t('g.confirm')"
severity="primary"
@click="onConfirm"
icon="pi pi-check"
@click="onConfirm"
/>
<Button
v-else-if="type === 'delete'"
:label="$t('g.delete')"
severity="danger"
@click="onConfirm"
icon="pi pi-trash"
@click="onConfirm"
/>
<Button
v-else-if="type === 'overwrite'"
:label="$t('g.overwrite')"
severity="warn"
@click="onConfirm"
icon="pi pi-save"
@click="onConfirm"
/>
<template v-else-if="type === 'dirtyClose'">
<Button
:label="$t('g.no')"
severity="secondary"
@click="onDeny"
icon="pi pi-times"
@click="onDeny"
/>
<Button :label="$t('g.save')" @click="onConfirm" icon="pi pi-save" />
<Button :label="$t('g.save')" icon="pi pi-save" @click="onConfirm" />
</template>
<Button
v-else-if="type === 'reinstall'"
:label="$t('desktopMenu.reinstall')"
severity="warn"
@click="onConfirm"
icon="pi pi-eraser"
@click="onConfirm"
/>
<!-- Invalid - just show a close button. -->
<Button
v-else
:label="$t('g.close')"
severity="primary"
@click="onCancel"
icon="pi pi-times"
@click="onCancel"
/>
</div>
</section>

View File

@@ -1,80 +1,159 @@
<template>
<div class="error-dialog-content flex flex-col gap-4">
<div class="comfy-error-report flex flex-col gap-4">
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="title"
:message="errorMessage"
:message="error.exceptionMessage"
/>
<pre
class="stack-trace p-5 text-neutral-400 text-xs max-h-[50vh] overflow-auto bg-black/20"
>
{{ stackTrace }}
</pre>
<template v-if="extensionFile">
<template v-if="error.extensionFile">
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
<br />
<span class="font-bold">{{ extensionFile }}</span>
<span class="font-bold">{{ error.extensionFile }}</span>
</template>
<Button
v-show="!sendReportOpen"
text
fluid
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
<div class="flex gap-2 justify-center">
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
</div>
<template v-if="reportOpen">
<Divider />
<ScrollPanel class="w-full h-[400px] max-w-[80vw]">
<pre class="whitespace-pre-wrap break-words">{{ reportContent }}</pre>
</ScrollPanel>
<Divider />
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:error-type="errorType"
:extra-fields="[
{
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => stackTrace
}
]"
:title="$t('issueReport.submitErrorReport')"
:error-type="error.reportType ?? 'unknownError'"
:extra-fields="[stackTraceField]"
:tags="{
exceptionMessage: errorMessage,
extensionFile: extensionFile ?? 'UNKNOWN'
exceptionMessage: error.exceptionMessage,
nodeType: error.nodeType ?? 'UNKNOWN'
}"
:title="t('issueReport.submitErrorReport')"
/>
<div class="flex gap-4 justify-end">
<FindIssueButton
:error-message="error.exceptionMessage"
:repo-owner="repoOwner"
:repo-name="repoName"
/>
<Button
v-if="reportOpen"
:label="$t('g.copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed, ref } from 'vue'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { ReportField } from '@/types/issueReportTypes'
import {
type ErrorReportData,
generateErrorReport
} from '@/utils/errorReportUtil'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const { t } = useI18n()
const {
title: _title,
errorMessage,
stackTrace: _stackTrace,
extensionFile,
errorType = 'frontendError'
} = defineProps<{
title?: string
errorMessage: string
stackTrace?: string
extensionFile?: string
errorType?: string
const { error } = defineProps<{
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
/**
* The type of error report to submit.
* @default 'unknownError'
*/
reportType?: string
/**
* The file name of the extension that caused the error.
*/
extensionFile?: string
}
}>()
const title = computed(() => _title ?? t('errorDialog.defaultTitle'))
const stackTrace = computed(() => _stackTrace ?? t('errorDialog.noStackTrace'))
const repoOwner = 'comfyanonymous'
const repoName = 'ComfyUI'
const reportContent = ref('')
const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const sendReportOpen = ref(false)
function showSendReport() {
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const systemStatsStore = useSystemStatsStore()
const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => error.traceback
}
})
onMounted(async () => {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
}
try {
const [logs] = await Promise.all([api.getLogs()])
reportContent.value = generateErrorReport({
systemStats: systemStatsStore.systemStats!,
serverLogs: logs,
workflow: app.graph.serialize(),
exceptionType: error.exceptionType,
exceptionMessage: error.exceptionMessage,
traceback: error.traceback,
nodeId: error.nodeId,
nodeType: error.nodeType
})
} catch (error) {
console.error('Error fetching logs:', error)
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('toastMessages.failedToFetchLogs'),
life: 5000
})
}
})
const { copyToClipboard } = useCopyToClipboard()
const copyReportToClipboard = async () => {
await copyToClipboard(reportContent.value)
}
</script>

View File

@@ -1,196 +0,0 @@
<template>
<NoResultsPlaceholder
icon="pi pi-exclamation-circle"
:title="props.error.node_type"
:message="props.error.exception_message"
/>
<div class="comfy-error-report">
<div class="flex gap-2 justify-center">
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
</div>
<template v-if="reportOpen">
<Divider />
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
<pre class="wrapper-pre">{{ reportContent }}</pre>
</ScrollPanel>
<Divider />
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
error-type="graphExecutionError"
:extra-fields="[stackTraceField]"
:tags="{
exceptionMessage: props.error.exception_message,
nodeType: props.error.node_type
}"
/>
<div class="action-container">
<FindIssueButton
:errorMessage="props.error.exception_message"
:repoOwner="repoOwner"
:repoName="repoName"
/>
<Button
v-if="reportOpen"
:label="$t('g.copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import type { ExecutionErrorWsMessage, SystemStats } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { ReportField } from '@/types/issueReportTypes'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const props = defineProps<{
error: ExecutionErrorWsMessage
}>()
const repoOwner = 'comfyanonymous'
const repoName = 'ComfyUI'
const reportContent = ref('')
const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const sendReportOpen = ref(false)
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => props.error.traceback?.join('\n')
}
})
onMounted(async () => {
try {
const [systemStats, logs] = await Promise.all([
api.getSystemStats(),
api.getLogs()
])
generateReport(systemStats, logs)
} catch (error) {
console.error('Error fetching system stats or logs:', error)
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to fetch system information',
life: 5000
})
}
})
const generateReport = (systemStats: SystemStats, logs: string) => {
// The default JSON workflow has about 3000 characters.
const MAX_JSON_LENGTH = 20000
const workflowJSONString = JSON.stringify(app.graph.serialize())
const workflowText =
workflowJSONString.length > MAX_JSON_LENGTH
? 'Workflow too large. Please manually upload the workflow from local file system.'
: workflowJSONString
reportContent.value = `
# ComfyUI Error Report
## Error Details
- **Node ID:** ${props.error.node_id}
- **Node Type:** ${props.error.node_type}
- **Exception Type:** ${props.error.exception_type}
- **Exception Message:** ${props.error.exception_message}
## Stack Trace
\`\`\`
${props.error.traceback.join('\n')}
\`\`\`
## System Information
- **ComfyUI Version:** ${systemStats.system.comfyui_version}
- **Arguments:** ${systemStats.system.argv.join(' ')}
- **OS:** ${systemStats.system.os}
- **Python Version:** ${systemStats.system.python_version}
- **Embedded Python:** ${systemStats.system.embedded_python}
- **PyTorch Version:** ${systemStats.system.pytorch_version}
## Devices
${systemStats.devices
.map(
(device) => `
- **Name:** ${device.name}
- **Type:** ${device.type}
- **VRAM Total:** ${device.vram_total}
- **VRAM Free:** ${device.vram_free}
- **Torch VRAM Total:** ${device.torch_vram_total}
- **Torch VRAM Free:** ${device.torch_vram_free}
`
)
.join('\n')}
## Logs
\`\`\`
${logs}
\`\`\`
## Attached Workflow
Please make sure that workflow does not contain any sensitive information such as API keys or passwords.
\`\`\`
${workflowText}
\`\`\`
## Additional Context
(Please add any additional context or steps to reproduce the error here)
`
}
const { copyToClipboard } = useCopyToClipboard()
const copyReportToClipboard = async () => {
await copyToClipboard(reportContent.value)
}
</script>
<style scoped>
.comfy-error-report {
display: flex;
flex-direction: column;
gap: 1rem;
}
.action-container {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.wrapper-pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

View File

@@ -8,7 +8,9 @@
>
<template #header>
<header class="flex flex-col items-center w-full">
<h2 id="issue-report-title" class="text-4xl">{{ title }}</h2>
<h2 id="issue-report-title" class="text-4xl">
{{ title }}
</h2>
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
</header>
</template>

View File

@@ -7,8 +7,8 @@
/>
<ListBox
:options="uniqueNodes"
optionLabel="label"
scrollHeight="100%"
option-label="label"
scroll-height="100%"
class="comfy-missing-nodes"
:pt="{
list: { class: 'border-none' }
@@ -22,14 +22,17 @@
}}</span>
<Button
v-if="slotProps.option.action"
@click="slotProps.option.action.callback"
:label="slotProps.option.action.text"
size="small"
outlined
@click="slotProps.option.action.callback"
/>
</div>
</template>
</ListBox>
<div class="flex justify-end py-3">
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
</template>
<script setup lang="ts">
@@ -38,7 +41,9 @@ import ListBox from 'primevue/listbox'
import { computed } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { useDialogService } from '@/services/dialogService'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
@@ -64,6 +69,12 @@ const uniqueNodes = computed(() => {
return { label: node }
})
})
const openManager = () => {
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
}
</script>
<style scoped>

View File

@@ -129,9 +129,12 @@ const missingModels = computed(() => {
})
})
onBeforeUnmount(() => {
onBeforeUnmount(async () => {
if (doNotAskAgain.value) {
useSettingStore().set('Comfy.Workflow.ShowMissingModelsWarning', false)
await useSettingStore().set(
'Comfy.Workflow.ShowMissingModelsWarning',
false
)
}
})
</script>

View File

@@ -4,13 +4,15 @@
<InputText
ref="inputRef"
v-model="inputValue"
autofocus
@keyup.enter="onConfirm"
@focus="selectAllText"
autofocus
/>
<label>{{ message }}</label>
</FloatLabel>
<Button @click="onConfirm">{{ $t('g.confirm') }}</Button>
<Button @click="onConfirm">
{{ $t('g.confirm') }}
</Button>
</div>
</template>

View File

@@ -2,18 +2,18 @@
<div class="settings-container">
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-48 2xl:w-64">
<SearchBox
class="settings-search-box w-full mb-2"
v-model:modelValue="searchQuery"
@search="handleSearch"
class="settings-search-box w-full mb-2"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
@search="handleSearch"
/>
<Listbox
v-model="activeCategory"
:options="categories"
optionLabel="translatedLabel"
scrollHeight="100%"
:optionDisabled="
option-label="translatedLabel"
scroll-height="100%"
:option-disabled="
(option: SettingTreeNode) =>
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
"
@@ -25,7 +25,7 @@
<Tabs :value="tabValue" :lazy="true" class="settings-content h-full w-full">
<TabPanels class="settings-tab-panels h-full w-full pr-0">
<PanelTemplate value="Search Results">
<SettingsPanel :settingGroups="searchResults" />
<SettingsPanel :setting-groups="searchResults" />
</PanelTemplate>
<PanelTemplate
@@ -38,7 +38,7 @@
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
</template>
<SettingsPanel :settingGroups="sortedGroups(category)" />
<SettingsPanel :setting-groups="sortedGroups(category)" />
</PanelTemplate>
<AboutPanel />
@@ -293,6 +293,10 @@ watch(activeCategory, (_, oldValue) => {
overflow: hidden;
}
.settings-content {
overflow-x: auto;
}
@media (max-width: 768px) {
.settings-container {
flex-direction: column;

View File

@@ -0,0 +1,124 @@
<template>
<div class="w-96 p-2">
<!-- Header -->
<div class="flex flex-col gap-4 mb-8">
<h1 class="text-2xl font-medium leading-normal my-0">
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
</h1>
<p class="text-base my-0">
<span class="text-muted">{{
isSignIn
? t('auth.login.newUser')
: t('auth.signup.alreadyHaveAccount')
}}</span>
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
}}</span>
</p>
</div>
<!-- Form -->
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
<SignUpForm v-else @submit="signInWithEmail" />
<!-- Divider -->
<Divider align="center" layout="horizontal" class="my-8">
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
</Divider>
<!-- Social Login Buttons -->
<div class="flex flex-col gap-6">
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGoogle')
: t('auth.signup.signUpWithGoogle')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGithub')
: t('auth.signup.signUpWithGithub')
}}
</Button>
</div>
<!-- Terms -->
<p class="text-xs text-muted mt-8">
{{ t('auth.login.termsText') }}
<span class="text-blue-500 cursor-pointer">{{
t('auth.login.termsLink')
}}</span>
{{ t('auth.login.andText') }}
<span class="text-blue-500 cursor-pointer">{{
t('auth.login.privacyLink')
}}</span
>.
</p>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { SignInData, SignUpData } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import SignInForm from './signin/SignInForm.vue'
import SignUpForm from './signin/SignUpForm.vue'
const { t } = useI18n()
const { onSuccess } = defineProps<{
onSuccess: () => void
}>()
const firebaseAuthStore = useFirebaseAuthStore()
const isSignIn = ref(true)
const toggleState = () => {
isSignIn.value = !isSignIn.value
}
const signInWithGoogle = () => {
// Implement Google login
console.log(isSignIn.value)
console.log('Google login clicked')
onSuccess()
}
const signInWithGithub = () => {
// Implement Github login
console.log(isSignIn.value)
console.log('Github login clicked')
onSuccess()
}
const signInWithEmail = async (values: SignInData | SignUpData) => {
const { email, password } = values
if (isSignIn.value) {
await firebaseAuthStore.login(email, password)
} else {
await firebaseAuthStore.register(email, password)
}
onSuccess()
}
</script>

View File

@@ -1,11 +1,10 @@
<template>
<Button
@click="openGitHubIssues"
:label="$t('g.findIssues')"
severity="secondary"
icon="pi pi-github"
>
</Button>
@click="openGitHubIssues"
/>
</template>
<script setup lang="ts">

View File

@@ -1,8 +1,8 @@
<template>
<Form
v-slot="$form"
@submit="submit"
:resolver="zodResolver(issueReportSchema)"
@submit="submit"
>
<Panel :pt="$attrs.pt as any">
<template #header>
@@ -33,15 +33,15 @@
>
<Checkbox
v-bind="$field"
:inputId="field.value"
:value="field.value"
v-model="selection"
:input-id="field.value"
:value="field.value"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
<FormField class="mb-4" v-slot="$field" name="details">
<FormField v-slot="$field" class="mb-4" name="details">
<Textarea
v-bind="$field"
class="w-full"
@@ -83,9 +83,9 @@
>
<Checkbox
v-bind="$field"
:inputId="checkbox.value"
:value="checkbox.value"
v-model="contactPrefs"
:input-id="checkbox.value"
:value="checkbox.value"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
@@ -101,7 +101,6 @@
<script setup lang="ts">
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'

View File

@@ -14,8 +14,8 @@
<div class="flex flex-1 relative overflow-hidden">
<ManagerNavSidebar
v-if="isSideNavOpen"
:tabs="tabs"
v-model:selectedTab="selectedTab"
:tabs="tabs"
/>
<div
class="flex-1 overflow-auto pr-80"
@@ -29,7 +29,8 @@
<RegistrySearchBar
v-model:searchQuery="searchQuery"
v-model:searchMode="searchMode"
:searchResults="searchResults"
v-model:sortField="sortField"
:search-results="searchResults"
:suggestions="suggestions"
/>
<div class="flex-1 overflow-auto">
@@ -56,16 +57,16 @@
<VirtualGrid
:items="resultsWithKeys"
:buffer-rows="3"
:gridStyle="GRID_STYLE"
:grid-style="GRID_STYLE"
@approach-end="onApproachEnd"
>
<template #item="{ item }">
<PackCard
@click.stop="(event) => selectNodePack(item, event)"
:node-pack="item"
:is-selected="
selectedNodePacks.some((pack) => pack.id === item.id)
"
@click.stop="(event) => selectNodePack(item, event)"
/>
</template>
</VirtualGrid>
@@ -107,19 +108,18 @@ import RegistrySearchBar from '@/components/dialog/content/manager/registrySearc
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
enum ManagerTab {
All = 'all',
Installed = 'installed',
Workflow = 'workflow',
Missing = 'missing'
}
const { initialTab = ManagerTab.All } = defineProps<{
initialTab: ManagerTab
}>()
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
@@ -150,9 +150,16 @@ const tabs = ref<TabItem[]>([
id: ManagerTab.Missing,
label: t('g.missing'),
icon: 'pi-exclamation-circle'
},
{
id: ManagerTab.UpdateAvailable,
label: t('g.updateAvailable'),
icon: 'pi-sync'
}
])
const selectedTab = ref<TabItem>(tabs.value[0])
const selectedTab = ref<TabItem>(
tabs.value.find((tab) => tab.id === initialTab) || tabs.value[0]
)
const {
searchQuery,
@@ -160,6 +167,7 @@ const {
isLoading: isSearchLoading,
searchResults,
searchMode,
sortField,
suggestions
} = useRegistrySearch()
pageNumber.value = 0
@@ -178,90 +186,140 @@ const {
startFetchInstalled,
filterInstalledPack,
installedPacks,
isLoading: isLoadingInstalled
isLoading: isLoadingInstalled,
isReady: installedPacksReady
} = useInstalledPacks()
const {
startFetchWorkflowPacks,
filterWorkflowPack,
workflowPacks,
isLoading: isLoadingWorkflow
isLoading: isLoadingWorkflow,
isReady: workflowPacksReady
} = useWorkflowPacks()
const getInstalledResults = () => {
if (isEmptySearch.value) {
startFetchInstalled()
return installedPacks.value
} else {
return filterInstalledPack(searchResults.value)
}
}
const getInWorkflowResults = () => {
if (isEmptySearch.value) {
startFetchWorkflowPacks()
return workflowPacks.value
} else {
return filterWorkflowPack(searchResults.value)
}
}
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
const setMissingPacks = () => {
displayPacks.value = filterMissingPacks(workflowPacks.value)
}
const isUpdateAvailableTab = computed(
() => selectedTab.value?.id === ManagerTab.UpdateAvailable
)
const isInstalledTab = computed(
() => selectedTab.value?.id === ManagerTab.Installed
)
const isMissingTab = computed(
() => selectedTab.value?.id === ManagerTab.Missing
)
const isWorkflowTab = computed(
() => selectedTab.value?.id === ManagerTab.Workflow
)
const isAllTab = computed(() => selectedTab.value?.id === ManagerTab.All)
const getMissingPacks = () => {
if (isEmptySearch.value) {
startFetchWorkflowPacks()
whenever(() => workflowPacks.value.length, setMissingPacks, {
immediate: true,
once: true
})
return filterMissingPacks(workflowPacks.value)
} else {
return filterMissingPacks(filterWorkflowPack(searchResults.value))
}
const isOutdatedPack = (pack: components['schemas']['Node']) => {
const { isUpdateAvailable } = usePackUpdateStatus(pack)
return isUpdateAvailable.value === true
}
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
packs.filter(isOutdatedPack)
const onTabChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
displayPacks.value = getInstalledResults()
break
case ManagerTab.Workflow:
displayPacks.value = getInWorkflowResults()
break
case ManagerTab.Missing:
displayPacks.value = getMissingPacks()
break
default:
displayPacks.value = searchResults.value
}
}
watch(
[isUpdateAvailableTab, installedPacks],
async () => {
if (!isUpdateAvailableTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
} else if (!installedPacks.value.length) {
await startFetchInstalled()
} else {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
}
},
{ immediate: true }
)
watch(
[isInstalledTab, installedPacks],
async () => {
if (!isInstalledTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterInstalledPack(searchResults.value)
} else if (
!installedPacks.value.length &&
!installedPacksReady.value &&
!isLoadingInstalled.value
) {
await startFetchInstalled()
} else {
displayPacks.value = installedPacks.value
}
},
{ immediate: true }
)
watch(
[isMissingTab, isWorkflowTab, workflowPacks, installedPacks],
async () => {
if (!isWorkflowTab.value && !isMissingTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = isMissingTab.value
? filterMissingPacks(filterWorkflowPack(searchResults.value))
: filterWorkflowPack(searchResults.value)
} else if (
!workflowPacks.value.length &&
!isLoadingWorkflow.value &&
!workflowPacksReady.value
) {
await startFetchWorkflowPacks()
if (isMissingTab.value) {
await startFetchInstalled()
}
} else {
displayPacks.value = isMissingTab.value
? filterMissingPacks(workflowPacks.value)
: workflowPacks.value
}
},
{ immediate: true }
)
watch([isAllTab, searchResults], () => {
if (!isAllTab.value) return
displayPacks.value = searchResults.value
})
const onResultsChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
displayPacks.value = filterInstalledPack(searchResults.value)
displayPacks.value = isEmptySearch.value
? installedPacks.value
: filterInstalledPack(searchResults.value)
break
case ManagerTab.Workflow:
displayPacks.value = filterWorkflowPack(searchResults.value)
displayPacks.value = isEmptySearch.value
? workflowPacks.value
: filterWorkflowPack(searchResults.value)
break
case ManagerTab.Missing:
displayPacks.value = filterMissingPacks(
filterWorkflowPack(searchResults.value)
)
if (!isEmptySearch.value) {
displayPacks.value = filterMissingPacks(
filterWorkflowPack(searchResults.value)
)
}
break
case ManagerTab.UpdateAvailable:
displayPacks.value = isEmptySearch.value
? filterOutdatedPacks(installedPacks.value)
: filterOutdatedPacks(searchResults.value)
break
default:
displayPacks.value = searchResults.value
}
}
whenever(selectedTab, onTabChange)
watch(searchResults, onResultsChange, { flush: 'pre' })
watch(searchResults, onResultsChange, { flush: 'post' })
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
const isLoading = computed(() => {

View File

@@ -6,8 +6,8 @@
<Listbox
v-model="selectedTab"
:options="tabs"
optionLabel="label"
listStyle="max-height:unset"
option-label="label"
list-style="max-height:unset"
class="w-full border-0 bg-transparent shadow-none"
:pt="{
list: { class: 'p-5' },
@@ -17,7 +17,7 @@
>
<template #option="slotProps">
<div class="text-left flex items-center">
<i :class="['pi', slotProps.option.icon, 'mr-3']"></i>
<i :class="['pi', slotProps.option.icon, 'mr-3']" />
<span class="text-lg">{{ slotProps.option.label }}</span>
</div>
</template>

View File

@@ -10,7 +10,7 @@
<i
class="pi pi-circle-fill mr-1.5 text-[0.6rem] p-0"
:style="{ opacity: 0.8 }"
></i>
/>
{{ $t(`manager.status.${statusLabel}`) }}
</Message>
</template>
@@ -20,15 +20,16 @@ import Message from 'primevue/message'
import { computed } from 'vue'
import { components } from '@/types/comfyRegistryTypes'
import { VueSeverity } from '@/types/primeVueTypes'
type PackVersionStatus = components['schemas']['NodeVersionStatus']
type PackStatus = components['schemas']['NodeStatus']
type Status = PackVersionStatus | PackStatus
type MessageProps = InstanceType<typeof Message>['$props']
type MessageSeverity = MessageProps['severity']
type StatusProps = {
label: string
severity: VueSeverity
severity: MessageSeverity
}
const { statusType } = defineProps<{
@@ -46,7 +47,7 @@ const statusPropsMap: Record<Status, StatusProps> = {
},
NodeStatusBanned: {
label: 'banned',
severity: 'danger'
severity: 'error'
},
NodeVersionStatusActive: {
label: 'active',
@@ -62,11 +63,11 @@ const statusPropsMap: Record<Status, StatusProps> = {
},
NodeVersionStatusFlagged: {
label: 'flagged',
severity: 'danger'
severity: 'error'
},
NodeVersionStatusBanned: {
label: 'banned',
severity: 'danger'
severity: 'error'
}
}

View File

@@ -39,6 +39,9 @@ import PackVersionSelectorPopover from '@/components/dialog/content/manager/Pack
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { SelectedVersion } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import { isSemVer } from '@/utils/formatUtil'
const TRUNCATED_HASH_LENGTH = 7
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
@@ -50,11 +53,13 @@ const managerStore = useComfyManagerStore()
const installedVersion = computed(() => {
if (!nodePack.id) return SelectedVersion.NIGHTLY
return (
const version =
managerStore.installedPacks[nodePack.id]?.ver ??
nodePack.latest_version?.version ??
SelectedVersion.NIGHTLY
)
// If Git hash, truncate to 7 characters
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
})
const toggleVersionSelector = (event: Event) => {

View File

@@ -75,6 +75,7 @@ import {
SelectedVersion
} from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import { isSemVer } from '@/utils/formatUtil'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
@@ -93,12 +94,26 @@ const isQueueing = ref(false)
const selectedVersion = ref<string>(SelectedVersion.LATEST)
onMounted(() => {
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
selectedVersion.value =
nodePack.publisher?.name === 'Unclaimed'
? SelectedVersion.NIGHTLY
: nodePack.latest_version?.version ?? SelectedVersion.NIGHTLY
// Use NIGHTLY when version is a Git hash
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
})
const getInitialSelectedVersion = () => {
if (!nodePack.id) return
// If unclaimed, set selected version to nightly
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
// If node pack is installed, set selected version to the installed version
if (managerStore.isPackInstalled(nodePack.id))
return managerStore.getInstalledPackVersion(nodePack.id)
// If node pack is not installed, set selected version to latest
return nodePack.latest_version?.version
}
const fetchVersions = async () => {
if (!nodePack?.id) return []
return (await registryService.getPackVersions(nodePack.id)) || []

View File

@@ -43,7 +43,9 @@ vi.mock('@/stores/comfyManagerStore', () => ({
installPack: {
call: mockInstallPack,
clear: vi.fn()
}
},
isPackInstalled: vi.fn(() => false),
getInstalledPackVersion: vi.fn(() => undefined)
}))
}))

View File

@@ -6,12 +6,12 @@
'w-full': fullWidth,
'w-min-content': !fullWidth
}"
:disabled="isInstalling"
:disabled="loading"
v-bind="$attrs"
@click="onClick"
>
<span class="py-2.5 px-3">
<template v-if="isInstalling">
<template v-if="loading">
{{ loadingMessage ?? $t('g.loading') }}
</template>
<template v-else>
@@ -23,9 +23,6 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { inject, ref } from 'vue'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
const {
label,
@@ -33,6 +30,7 @@ const {
fullWidth = false
} = defineProps<{
label: string
loading?: boolean
loadingMessage?: string
fullWidth?: boolean
}>()
@@ -45,10 +43,7 @@ defineOptions({
inheritAttrs: false
})
const isInstalling = inject(IsInstallingKey, ref(false))
const onClick = (): void => {
isInstalling.value = true
emit('action')
}
</script>

View File

@@ -5,8 +5,10 @@
nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install')
"
severity="secondary"
:loading="isInstalling"
:loading-message="$t('g.installing')"
@action="installAllPacks"
@click="onClick"
/>
</template>
@@ -31,6 +33,10 @@ const { nodePacks } = defineProps<{
const isInstalling = inject(IsInstallingKey, ref(false))
const onClick = (): void => {
isInstalling.value = true
}
const managerStore = useComfyManagerStore()
const createPayload = (installItem: NodePack) => {

Some files were not shown because too many files have changed in this diff Show More