Compare commits

...

156 Commits

Author SHA1 Message Date
bymyself
289817402c await toolbox commands added by extensions 2025-04-11 07:38:09 -07:00
bymyself
cb8ada3723 await queue prompt 2025-04-11 07:37:51 -07:00
bymyself
3d18bf75d9 await queue button actions 2025-04-11 07:32:14 -07:00
bymyself
ad0d1833d2 await command executions 2025-04-11 07:30:39 -07: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
Comfy Org PR Bot
4702cd18ce 1.15.3 (#3246)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-26 16:33:13 -04:00
Chenlei Hu
00d281c7fa [Test] Add playwright tests (#3245)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-26 16:32:42 -04:00
Chenlei Hu
3e25e08b10 [Bug] Fix broken output link id during reroute migration (#3244) 2025-03-26 16:00:54 -04:00
Comfy Org PR Bot
1d66d6d7d3 [chore] Update litegraph to 0.11.3 (#3242)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-26 15:25:17 -04:00
dependabot[bot]
cae5bbe86f Bump vite from 5.4.14 to 5.4.15 (#3243)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 15:25:08 -04:00
Christian Byrne
4d35d937cf [Manager] Fix build env vars (#3238) 2025-03-26 11:03:03 -04:00
Comfy Org PR Bot
60afa5cf6c [chore] Update Comfy Registry API types from comfy-api@d7be0da (#3239)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-03-26 02:48:38 -07:00
Comfy Org PR Bot
a1a33c8c9b 1.15.2 (#3237)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-25 22:07:08 -04:00
Chenlei Hu
9988fb8f1e [Bug] Fix selection toolbox select+drag (#3235)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-25 15:27:25 -04:00
Chenlei Hu
0518b170d3 [i18n] Add spanish translation (#3233)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-25 14:34:44 -04:00
Chenlei Hu
562cd7ea70 [TS] Fix type errors in app.ts (#3232) 2025-03-25 13:02:55 -04:00
Chenlei Hu
d3c64d404b [Bug] Fix selection toolbox position on pasted node (#3231) 2025-03-25 10:51:04 -04:00
Chenlei Hu
24dcaa7f72 [i18n] Translate toast messages (#3228)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 22:22:17 -04:00
Comfy Org PR Bot
6c18781663 1.15.1 (#3227)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-24 22:01:15 -04:00
Chenlei Hu
b6988e8d5c Report file load error via toast (#3226)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 22:01:07 -04:00
Chenlei Hu
ae64721555 Implement load workflow error dialog in Vue (#3225)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 21:00:50 -04:00
Terry Jia
abe65e58a0 [3d] add support to upload texture (#3224)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 16:58:45 -04:00
Comfy Org PR Bot
a1cfb68116 [chore] Update litegraph to 0.11.2 (#3223)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 16:09:40 -04:00
MohammadAboulEla
5bee36a73e Enable/Disable the drawing of image size (#3200) 2025-03-24 13:47:55 -04:00
Chenlei Hu
a4b0f5ab5e [Bug] Fix widget placeholder rendering (#3215)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-23 23:02:35 -04:00
filtered
f8a2c90138 Fix drag new link from reroute to widget (#3214) 2025-03-23 22:29:51 -04:00
Chenlei Hu
27c252f74a Render placeholder rect for DOM widgets when zoomed out (#3213)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-23 21:35:58 -04:00
Comfy Org PR Bot
7e26cffb26 [chore] Update litegraph to 0.11.1 (#3212)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-23 21:02:31 -04:00
niboshi
cb8354bfce Show error message on drop error (#3189) 2025-03-23 20:52:55 -04:00
Comfy Org PR Bot
d5ebd7b7cb [chore] Update litegraph to 0.11.0 (#3211)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-03-24 11:36:24 +11:00
Comfy Org PR Bot
845d045991 1.15.0 (#3210)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-23 19:13:09 -04:00
Chenlei Hu
c3154fe297 [Reroute] Clean floating reroutes in reroute migration (#3209) 2025-03-23 18:40:39 -04:00
Christian Byrne
f90d61fad5 [Style] Remove box shadow on template dialog sidenav (#3208) 2025-03-23 16:14:34 -04:00
Christian Byrne
17834459a1 Fix parsing workflows from 3d outputs on Windows (#3196) 2025-03-23 11:42:57 -07:00
Chenlei Hu
564c4d557f Migrate legacy reroute to litegraph native reroute (#3151)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-22 18:56:35 -04:00
filtered
f852639758 Fix slot types added multiple times (#3194) 2025-03-23 09:01:12 +11:00
Comfy Org PR Bot
eae538b08e [chore] Update litegraph to 0.11.0-2 (#3193)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-03-23 08:01:25 +11:00
Dr.Lt.Data
d23108433e refine locales/ko (#3185) 2025-03-22 15:06:10 -04:00
Comfy Org PR Bot
e6e7449ece [chore] Update litegraph to 0.11.0-1 (#3190)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-03-23 03:49:44 +11:00
Comfy Org PR Bot
22a1200bdf [chore] Update litegraph to 0.11.0-0 (#3183)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-03-22 10:08:41 +11:00
Chenlei Hu
ee20b63bc1 Remove unused plugin vite-plugin-static-copy (#3182) 2025-03-21 14:11:01 -04:00
Comfy Org PR Bot
0752e8b986 1.14.5 (#3181)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-21 11:07:21 -04:00
Christian Byrne
b234a68cf8 [Manager] Get node pack info on select (#3177) 2025-03-21 11:05:09 -04:00
Terry Jia
0863fda6a4 [3d] add support to export different formats (#3176)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-21 11:04:39 -04:00
Christian Byrne
8530406c3e [Manager] Set max size on custom node pack icons (#3178) 2025-03-21 11:04:23 -04:00
Christian Byrne
f7bfb6ec57 Fix Workflow Validation error when node pack 'unknown' version (#3179) 2025-03-21 11:04:06 -04:00
Comfy Org PR Bot
830933e78f Update locales for node definitions (#3174)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-20 21:44:46 -04:00
Comfy Org PR Bot
65693ed2be 1.14.4 (#3173)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-20 21:36:22 -04:00
Chenlei Hu
ed153dccd9 [Test] Fix flaky playwright tests (#3170) 2025-03-20 21:33:05 -04:00
Christian Byrne
fc7ed1bf09 Add Hunyuan3d template workflow titles i18 fields (#3171)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-03-20 21:31:42 -04:00
Christian Byrne
4dad89369a Load workflows from GLTF files (#3169)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-20 20:55:51 -04:00
Chenlei Hu
5b730517a3 [Test] Sync workflow instead of full page reload (#3168) 2025-03-20 20:42:12 -04:00
Chenlei Hu
af0bf05883 [TS] Add null checks to TreeExplorer.vue (#3166) 2025-03-20 13:08:22 -04:00
Chenlei Hu
d9e62ff860 [Vue] Use Vue 3.5 syntax for prop default value (#3165) 2025-03-20 12:40:49 -04:00
Chenlei Hu
d9ae6cb395 [TS] Use custom TreeNode type (#3164) 2025-03-20 12:03:47 -04:00
Chenlei Hu
b162963593 [TS] Fix TreeExplorerNode types (#3163) 2025-03-20 11:47:04 -04:00
Christian Byrne
c34cc301f1 [Manager] Allow cancelling registry requests by route (#3158) 2025-03-20 10:30:00 -04:00
Terry Jia
afdb94f12f [3d] refactor legacy code by using new vue style (#3161) 2025-03-20 10:28:35 -04:00
Dr.Lt.Data
cc8dc3dbfb refine locales/ko (#3162) 2025-03-20 10:27:55 -04:00
filtered
42d99fc37e Track floating link changes in undo/redo history (#3160) 2025-03-20 22:19:31 +11:00
244 changed files with 18052 additions and 2437 deletions

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

@@ -9,8 +9,8 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, controlnet, lora.
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
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.
`

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

@@ -0,0 +1,104 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 304.3653259277344,
"1": 42.15586471557617
},
"size": [
315,
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
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 1,
"type": "PrimitiveInt",
"pos": {
"0": 14,
"1": 43
},
"size": [
203.1999969482422,
40.368401303242536
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "value",
"type": "INT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Int"
},
"widgets_values": [10]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

Binary file not shown.

View File

@@ -214,6 +214,10 @@ export class ComfyPage {
`Failed to setup workflows directory: ${await resp.text()}`
)
}
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}
async setupUser(username: string) {
@@ -408,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() {
@@ -478,6 +482,7 @@ export class ComfyPage {
if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.webm')) return 'video/webm'
if (fileName.endsWith('.json')) return 'application/json'
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
return 'application/octet-stream'
}
@@ -584,11 +589,20 @@ export class ComfyPage {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
async connectEdge() {
await this.dragAndDrop(
this.loadCheckpointNodeClipOutputSlot,
this.clipTextEncodeNode1InputSlot
)
async connectEdge(
options: {
reverse?: boolean
} = {}
) {
const { reverse = false } = options
const start = reverse
? this.clipTextEncodeNode1InputSlot
: this.loadCheckpointNodeClipOutputSlot
const end = reverse
? this.loadCheckpointNodeClipOutputSlot
: this.clipTextEncodeNode1InputSlot
await this.dragAndDrop(start, end)
}
async adjustWidgetValue() {

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)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -309,3 +309,35 @@ test.describe('Feedback dialog', () => {
await expect(feedbackHeader).not.toBeVisible()
})
})
test.describe('Error dialog', () => {
test('Should display an error dialog when graph configure fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
const graph = window['graph']
graph.configure = () => {
throw new Error('Error on configure!')
}
})
await comfyPage.loadWorkflow('default')
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

@@ -5,8 +5,8 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('DOM Widget', () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('collapsed_multiline')
expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible()
})
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {

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)
})
})

View File

@@ -91,15 +91,20 @@ test.describe('Node Interaction', () => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
})
test('Can disconnect/connect edge', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge()
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
// Test both directions of edge connection.
;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => {
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge({ reverse })
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -8,7 +8,8 @@ test.describe('Load Workflow in Media', () => {
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow.webm'
'workflow.webm',
'workflow.glb'
].forEach(async (fileName) => {
test(`Load workflow in ${fileName}`, async ({ comfyPage }) => {
await comfyPage.dragAndDropFile(fileName)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 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: 102 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -5,14 +5,14 @@ import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Primitive Node', () => {
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive_node')
await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
})
// When link is dropped on widget, it should automatically convert the widget
// to input.
test('Can connect to widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive_node_unconnected')
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
// Connect the output of the primitive node to the input of first widget of the ksampler node
@@ -23,7 +23,9 @@ test.describe('Primitive Node', () => {
})
test('Can connect to dom widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive_node_unconnected_dom_widget')
await comfyPage.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget'
)
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2)
await primitiveNode.connectWidget(0, clipEncoderNode, 0)
@@ -31,4 +33,25 @@ test.describe('Primitive Node', () => {
'primitive_node_connected_dom_widget.png'
)
})
test('Can connect to static primitive node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'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.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -321,6 +321,7 @@ test.describe('Remote COMBO Widget', () => {
// Click refresh button
await clickRefreshButton(comfyPage, nodeName)
await comfyPage.page.waitForTimeout(200)
// Verify the selected value of the widget is the first option in the refreshed list
const refreshedValue = await getWidgetValue(comfyPage, nodeName)

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: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 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

@@ -30,6 +30,46 @@ test.describe('Selection Toolbox', () => {
).toBeVisible()
})
test('shows at correct position when node is pasted', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.selectNodes(['KSampler'])
await comfyPage.ctrlC()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV()
const overlayContainer = comfyPage.page.locator(
'.selection-overlay-container'
)
await expect(overlayContainer).toBeVisible()
// Verify the absolute position
const boundingBox = await overlayContainer.boundingBox()
expect(boundingBox).not.toBeNull()
// 10px offset for the pasted node
expect(Math.round(boundingBox!.x)).toBeCloseTo(90, -1) // Allow ~10px tolerance
// 30px offset of node title height
expect(Math.round(boundingBox!.y)).toBeCloseTo(60, -1)
})
test('hide when select and drag happen at the same time', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodePos = await node.getPosition()
// Drag on the title of the node
await comfyPage.page.mouse.move(nodePos.x + 100, nodePos.y - 15)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('.selection-overlay-container')
).not.toBeVisible()
})
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])
@@ -206,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

@@ -34,7 +34,6 @@ test.describe('Workflows sidebar', () => {
'workflow1.json': 'default.json',
'workflow2.json': 'default.json'
})
await comfyPage.setup()
const tab = comfyPage.menu.workflowsTab
await tab.open()
@@ -77,7 +76,6 @@ test.describe('Workflows sidebar', () => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'single_ksampler.json'
})
await comfyPage.setup()
const tab = comfyPage.menu.workflowsTab
await tab.open()
@@ -101,7 +99,6 @@ test.describe('Workflows sidebar', () => {
'bar.json': 'default.json'
}
})
await comfyPage.setup()
const tab = comfyPage.menu.workflowsTab
await tab.open()
@@ -230,6 +227,7 @@ test.describe('Workflows sidebar', () => {
await topbar.saveWorkflowAs('workflow1.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.page.waitForTimeout(200)
// The old workflow1.json should be deleted and the new one should be saved.
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow2.json',
@@ -323,7 +321,7 @@ test.describe('Workflows sidebar', () => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
await comfyPage.setup()
await comfyPage.menu.workflowsTab.open()
const nodeCount = await comfyPage.getGraphNodesCount()

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

2
global.d.ts vendored
View File

@@ -1,6 +1,8 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
interface Navigator {
/**

39
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.14.3",
"version": "1.16.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.14.3",
"version": "1.16.5",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.10.9",
"@comfyorg/litegraph": "^0.13.1",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -77,9 +77,8 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.14",
"vite": "^5.4.17",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
@@ -479,9 +478,9 @@
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.10.9",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.10.9.tgz",
"integrity": "sha512-ubGozxdDIVNL/MYvfCAXgiaqBfIODtp0jZeN9uzWrdHwqUy9ZkLt/7/q7G4nGpNcEoShbMu7EK4VPH3WRmNQ7A==",
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.1.tgz",
"integrity": "sha512-tAlAdeBpvBch5Wj7MghoJvJkGwdeHG3825LqDMkKynYVXfiMcrTtM+LWRkH58JlbEEocD+1fdWKLMZ+lJc9m0A==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
@@ -12560,9 +12559,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"version": "5.4.17",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz",
"integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12723,24 +12722,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/vite-plugin-static-copy": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-1.0.6.tgz",
"integrity": "sha512-3uSvsMwDVFZRitqoWHj0t4137Kz7UynnJeq1EZlRW7e25h2068fyIZX4ORCCOAkfp1FklGxJNVJBkBOD+PZIew==",
"dev": true,
"dependencies": {
"chokidar": "^3.5.3",
"fast-glob": "^3.2.11",
"fs-extra": "^11.1.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0"
}
},
"node_modules/vitest": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.14.3",
"version": "1.16.5",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -60,9 +60,8 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.14",
"vite": "^5.4.17",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
@@ -72,7 +71,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.10.9",
"@comfyorg/litegraph": "^0.13.1",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

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,7 +1,6 @@
<template>
<div
class="batch-count"
:class="props.class"
v-tooltip.bottom="{
value: $t('menu.batchCount'),
showDelay: 600
@@ -41,14 +40,6 @@ import { computed } from 'vue'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
interface Props {
class?: string
}
const props = withDefaults(defineProps<Props>(), {
class: ''
})
const queueSettingsStore = useQueueSettingsStore()
const { batchCount } = storeToRefs(queueSettingsStore)
const minQueueCount = 1

View File

@@ -47,7 +47,7 @@
:disabled="!executingPrompt"
text
:aria-label="$t('menu.interrupt')"
@click="() => commandStore.execute('Comfy.Interrupt')"
@click="async () => await commandStore.execute('Comfy.Interrupt')"
>
</Button>
<Button
@@ -61,9 +61,9 @@
text
:aria-label="$t('sideToolbar.queueTab.clearPendingTasks')"
@click="
() => {
async () => {
if (queueCountStore.count.value > 1) {
commandStore.execute('Comfy.ClearPendingTasks')
await commandStore.execute('Comfy.ClearPendingTasks')
}
queueMode = 'disabled'
}
@@ -135,12 +135,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

@@ -18,7 +18,7 @@
:src="src"
@error="handleImageError"
class="comfy-image-main"
:class="[...classArray]"
:class="classProp"
:alt="alt"
/>
</span>
@@ -29,37 +29,24 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ref } from 'vue'
const props = withDefaults(
defineProps<{
src: string
class?: string | string[] | object
contain: boolean
alt?: string
}>(),
{
contain: false,
alt: 'Image content'
}
)
const {
src,
class: classProp,
contain = false,
alt = 'Image content'
} = defineProps<{
src: string
class?: any
contain?: boolean
alt?: string
}>()
const imageBroken = ref(false)
const handleImageError = () => {
imageBroken.value = true
}
const classArray = computed(() => {
if (Array.isArray(props.class)) {
return props.class
} else if (typeof props.class === 'string') {
return props.class.split(' ')
} else if (typeof props.class === 'object') {
// @ts-expect-error fixme ts strict error
return Object.keys(props.class).filter((key) => props.class[key])
}
return []
})
</script>
<style scoped>

View File

@@ -1,6 +1,6 @@
<template>
<div class="editable-text">
<span v-if="!props.isEditing">
<span v-if="!isEditing">
{{ modelValue }}
</span>
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
@@ -27,17 +27,13 @@
import InputText from 'primevue/inputtext'
import { nextTick, ref, watch } from 'vue'
interface EditableTextProps {
const { modelValue, isEditing = false } = defineProps<{
modelValue: string
isEditing?: boolean
}
const props = withDefaults(defineProps<EditableTextProps>(), {
isEditing: false
})
}>()
const emit = defineEmits(['update:modelValue', 'edit'])
const inputValue = ref<string>(props.modelValue)
const inputValue = ref<string>(modelValue)
const inputRef = ref<InstanceType<typeof InputText> | undefined>()
const blurInputElement = () => {
@@ -48,10 +44,10 @@ const finishEditing = () => {
emit('edit', inputValue.value)
}
watch(
() => props.isEditing,
() => isEditing,
(newVal) => {
if (newVal) {
inputValue.value = props.modelValue
inputValue.value = modelValue
nextTick(() => {
if (!inputRef.value) return
const fileName = inputValue.value.includes('.')

View File

@@ -13,9 +13,9 @@
<template>
<Button
class="relative p-button-icon-only"
:outlined="props.outlined"
:severity="props.severity"
:disabled="active || props.disabled"
:outlined="outlined"
:severity="severity"
:disabled="active || disabled"
@click="(event) => $emit('refresh', event)"
>
<span
@@ -32,18 +32,17 @@
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
import { VueSeverity } from '@/types/primeVueTypes'
import { PrimeVueSeverity } from '@/types/primeVueTypes'
// Properties
interface Props {
outlined?: boolean
const {
disabled,
outlined = true,
severity = 'secondary'
} = defineProps<{
disabled?: boolean
severity?: VueSeverity
}
const props = withDefaults(defineProps<Props>(), {
outlined: true,
severity: 'secondary'
})
outlined?: boolean
severity?: PrimeVueSeverity
}>()
// Model
const active = defineModel<boolean>({ required: true })

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex items-center" :class="props.class">
<div class="flex items-center">
<span v-if="position === 'left'" class="mr-2 shrink-0">{{ text }}</span>
<Divider :align="align" :type="type" :layout="layout" class="flex-grow" />
<span v-if="position === 'right'" class="ml-2 shrink-0">{{ text }}</span>
@@ -9,19 +9,17 @@
<script setup lang="ts">
import Divider from 'primevue/divider'
interface Props {
const {
text,
position = 'left',
align = 'center',
type = 'solid',
layout = 'horizontal'
} = defineProps<{
text: string
class?: string
position?: 'left' | 'right'
align?: 'left' | 'center' | 'right' | 'top' | 'bottom'
type?: 'solid' | 'dashed' | 'dotted'
layout?: 'horizontal' | 'vertical'
}
const props = withDefaults(defineProps<Props>(), {
position: 'left',
align: 'center',
type: 'solid',
layout: 'horizontal'
})
}>()
</script>

View File

@@ -170,32 +170,38 @@ const deleteCommand = async (node: RenderedTreeExplorerNode) => {
await node.handleDelete?.()
emit('nodeDelete', node)
}
// @ts-expect-error fixme ts strict error
const menuItems = computed<MenuItem[]>(() =>
[
getAddFolderMenuItem(menuTargetNode.value),
{
label: t('g.rename'),
icon: 'pi pi-file-edit',
// @ts-expect-error fixme ts strict error
command: () => renameCommand(menuTargetNode.value),
command: () => {
if (menuTargetNode.value) {
renameCommand(menuTargetNode.value)
}
},
visible: menuTargetNode.value?.handleRename !== undefined
},
{
label: t('g.delete'),
icon: 'pi pi-trash',
// @ts-expect-error fixme ts strict error
command: () => deleteCommand(menuTargetNode.value),
command: () => {
if (menuTargetNode.value) {
deleteCommand(menuTargetNode.value)
}
},
visible: menuTargetNode.value?.handleDelete !== undefined,
isAsync: true // The delete command can be async
},
...extraMenuItems.value
].map((menuItem) => ({
].map((menuItem: MenuItem) => ({
...menuItem,
// @ts-expect-error fixme ts strict error
command: wrapCommandWithErrorHandler(menuItem.command, {
isAsync: menuItem.isAsync ?? false
})
command: menuItem.command
? wrapCommandWithErrorHandler(menuItem.command, {
isAsync: menuItem.isAsync ?? false
})
: undefined
}))
)
@@ -223,15 +229,15 @@ const wrapCommandWithErrorHandler = (
}
defineExpose({
renameCommand,
deleteCommand,
/**
* The command to add a folder to a node via the context menu
* @param targetNodeKey - The key of the node where the folder will be added under
*/
addFolderCommand: (targetNodeKey: string) => {
// @ts-expect-error fixme ts strict error
addFolderCommand(findNodeByKey(renderedRoot.value, targetNodeKey))
const targetNode = findNodeByKey(renderedRoot.value, targetNodeKey)
if (targetNode) {
addFolderCommand(targetNode)
}
}
})
</script>

View File

@@ -0,0 +1,159 @@
<template>
<div class="comfy-error-report flex flex-col gap-4">
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="title"
:message="error.exceptionMessage"
/>
<template v-if="error.extensionFile">
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
<br />
<span class="font-bold">{{ error.extensionFile }}</span>
</template>
<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"
:title="$t('issueReport.submitErrorReport')"
:error-type="error.reportType ?? 'unknownError'"
:extra-fields="[stackTraceField]"
:tags="{
exceptionMessage: error.exceptionMessage,
nodeType: error.nodeType ?? 'UNKNOWN'
}"
/>
<div class="flex gap-4 justify-end">
<FindIssueButton
:errorMessage="error.exceptionMessage"
: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 { 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 { 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 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 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

@@ -30,6 +30,9 @@
</div>
</template>
</ListBox>
<div class="flex justify-end py-3">
<Button label="Open Manager" @click="openManager" size="small" outlined />
</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

@@ -157,9 +157,7 @@ const categories = computed<SettingTreeNode[]>(() =>
].map((node) => ({
...node,
translatedLabel: t(
// @ts-expect-error fixme ts strict error
`settingsCategories.${normalizeI18nKey(node.label)}`,
// @ts-expect-error fixme ts strict error
node.label
)
}))
@@ -177,16 +175,12 @@ onMounted(() => {
})
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
// @ts-expect-error fixme ts strict error
return (
[...(category.children ?? [])]
// @ts-expect-error fixme ts strict error
.sort((a, b) => a.label.localeCompare(b.label))
.map((group) => ({
label: group.label,
settings: flattenTree<SettingParams>(group)
}))
)
return [...(category.children ?? [])]
.sort((a, b) => a.label.localeCompare(b.label))
.map((group) => ({
label: group.label,
settings: flattenTree<SettingParams>(group)
}))
}
const searchQuery = ref<string>('')

View File

@@ -91,8 +91,9 @@
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { merge } from 'lodash'
import Button from 'primevue/button'
import { computed, ref, watch } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -106,21 +107,22 @@ 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()
const { getPackById } = useComfyRegistryStore()
const GRID_STYLE = {
display: 'grid',
@@ -147,9 +149,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,
@@ -175,90 +184,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],
() => {
if (!isUpdateAvailableTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
} else if (!installedPacks.value.length) {
startFetchInstalled()
} else {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
}
},
{ immediate: true }
)
watch(
[isInstalledTab, installedPacks],
() => {
if (!isInstalledTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterInstalledPack(searchResults.value)
} else if (
!installedPacks.value.length &&
!installedPacksReady.value &&
!isLoadingInstalled.value
) {
startFetchInstalled()
} else {
displayPacks.value = installedPacks.value
}
},
{ immediate: true }
)
watch(
[isMissingTab, isWorkflowTab, workflowPacks, installedPacks],
() => {
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
) {
startFetchWorkflowPacks()
if (isMissingTab.value) {
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(() => {
@@ -343,4 +402,24 @@ const handleGridContainerClick = (event: MouseEvent) => {
}
const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1)
whenever(selectedNodePack, async () => {
// Cancel any in-flight requests from previously selected node pack
getPackById.cancel()
if (!selectedNodePack.value?.id) return
// If only a single node pack is selected, fetch full node pack info from registry
if (hasMultipleSelections.value) return
const data = await getPackById.call(selectedNodePack.value.id)
if (data?.id === selectedNodePack.value?.id) {
// If selected node hasn't changed since request, merge registry & Algolia data
selectedNodePacks.value = [merge(selectedNodePack.value, data)]
}
})
onUnmounted(() => {
getPackById.cancel()
})
</script>

View File

@@ -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

@@ -97,10 +97,10 @@ import ContentDivider from '@/components/common/ContentDivider.vue'
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
const { nodePack, isSelected = false } = defineProps<{
nodePack: components['schemas']['Node']
@@ -110,8 +110,8 @@ const { nodePack, isSelected = false } = defineProps<{
const isInstalling = ref(false)
provide(IsInstallingKey, isInstalling)
const { isPackInstalled, isPackEnabled, getInstalledPackVersion } =
useComfyManagerStore()
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
const { isUpdateAvailable } = usePackUpdateStatus(nodePack)
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
const isDisabled = computed(
@@ -120,20 +120,6 @@ const isDisabled = computed(
whenever(isInstalled, () => (isInstalling.value = false))
const isUpdateAvailable = computed(() => {
if (!isInstalled.value) return false
const latestVersion = nodePack.latest_version?.version
if (!latestVersion) return false
const installedVersion = getInstalledPackVersion(nodePack.id ?? '')
// Don't attempt to show update available for nightly GitHub packs
if (installedVersion && !isSemVer(installedVersion)) return false
return compareVersions(latestVersion, installedVersion) > 0
})
// TODO: remove type assertion once comfy_nodes is added to node (pack) info type in backend
const nodesCount = computed(() => (nodePack as any).comfy_nodes?.length)
</script>

View File

@@ -2,7 +2,7 @@
<img
:src="isImageError ? DEFAULT_ICON : imgSrc"
:alt="nodePack.name + ' icon'"
class="object-contain rounded-lg"
class="object-contain rounded-lg max-h-72 max-w-72"
:style="{ width: cssWidth, height: cssHeight }"
@error="isImageError = true"
/>

View File

@@ -17,6 +17,7 @@
:pt="{
header: 'px-0'
}"
@rowDblclick="editKeybinding($event.data)"
>
<Column field="actions" header="">
<template #body="slotProps">

View File

@@ -6,6 +6,7 @@
@update:formValue="updateSettingValue"
>
<template #name-prefix>
<Tag v-if="setting.id === 'Comfy.Locale'" class="pi pi-language" />
<Tag v-if="setting.experimental" :value="$t('g.experimental')" />
<Tag
v-if="setting.deprecated"

View File

@@ -15,15 +15,10 @@ import { computed } from 'vue'
import { KeyComboImpl } from '@/stores/keybindingStore'
const props = withDefaults(
defineProps<{
keyCombo: KeyComboImpl
isModified: boolean
}>(),
{
isModified: false
}
)
const { keyCombo, isModified = false } = defineProps<{
keyCombo: KeyComboImpl
isModified?: boolean
}>()
const keySequences = computed(() => props.keyCombo.getKeySequences())
const keySequences = computed(() => keyCombo.getKeySequences())
</script>

View File

@@ -19,11 +19,15 @@
<script setup lang="ts">
import TabMenu from 'primevue/tabmenu'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
const progressDialogContent = useManagerProgressDialogStore()
const activeTabIndex = ref(0)
const tabs = [{ label: 'Installation Queue' }, { label: 'Failed (0)' }]
const { t } = useI18n()
const tabs = [
{ label: t('manager.installationQueue') },
{ label: t('manager.failed', { count: 0 }) }
]
</script>

View File

@@ -30,7 +30,6 @@ const widgets = computed(() =>
)
)
const DEFAULT_MARGIN = 10
const updateWidgets = () => {
const lgCanvas = canvasStore.canvas
if (!lgCanvas) return
@@ -49,7 +48,7 @@ const updateWidgets = () => {
widgetState.visible = visible
if (visible) {
const margin = widget.options.margin ?? DEFAULT_MARGIN
const margin = widget.margin
widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]
widgetState.size = [
(widget.width ?? node.width) - margin * 2,

View File

@@ -37,6 +37,7 @@
</template>
<script setup lang="ts">
import type { LGraphNode } from '@comfyorg/litegraph'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
@@ -58,16 +59,19 @@ import { useCopy } from '@/composables/useCopy'
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
import { usePaste } from '@/composables/usePaste'
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
import { CORE_SETTINGS } from '@/constants/coreSettings'
import { i18n } from '@/i18n'
import { api } from '@/scripts/api'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useWorkflowService } from '@/services/workflowService'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore } from '@/stores/graphStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -80,6 +84,7 @@ const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore()
const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
@@ -158,6 +163,55 @@ watch(
}
)
// Update the progress of the executing node
watch(
() =>
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
NodeId | null,
number | null
],
([executingNodeId, executingNodeProgress]) => {
for (const node of comfyApp.graph.nodes) {
if (node.id == executingNodeId) {
node.progress = executingNodeProgress ?? undefined
} else {
node.progress = undefined
}
}
}
)
// Update node slot errors
watch(
() => executionStore.lastNodeErrors,
(lastNodeErrors) => {
const removeSlotError = (node: LGraphNode) => {
for (const slot of node.inputs) {
delete slot.hasErrors
}
for (const slot of node.outputs) {
delete slot.hasErrors
}
}
for (const node of comfyApp.graph.nodes) {
removeSlotError(node)
const nodeErrors = lastNodeErrors?.[node.id]
if (!nodeErrors) continue
for (const error of nodeErrors.errors) {
if (error.extra_info && error.extra_info.input_name) {
const inputIndex = node.findInputSlot(error.extra_info.input_name)
if (inputIndex !== -1) {
node.inputs[inputIndex].hasErrors = true
}
}
}
}
comfyApp.canvas.draw(true, true)
}
)
const loadCustomNodesI18n = async () => {
try {
const i18nData = await api.getCustomNodesI18n()
@@ -180,6 +234,7 @@ onMounted(async () => {
useContextMenuTranslation()
useCopy()
usePaste()
useWorkflowAutoSave()
comfyApp.vueAppReady = true
@@ -188,7 +243,20 @@ onMounted(async () => {
// some listeners of litegraph canvas.
ChangeTracker.init(comfyApp)
await loadCustomNodesI18n()
await settingStore.loadSettingValues()
try {
await settingStore.loadSettingValues()
} catch (error) {
if (error instanceof UnauthorizedError) {
console.log(
'Failed loading user settings, user unauthorized, cleaning local Comfy.userId'
)
localStorage.removeItem('Comfy.userId')
localStorage.removeItem('Comfy.userName')
window.location.reload()
} else {
throw error
}
}
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})

View File

@@ -23,7 +23,7 @@
icon="pi pi-expand"
v-tooltip.left="t('graphCanvasMenu.fitView')"
:aria-label="$t('graphCanvasMenu.fitView')"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
@click="async () => await commandStore.execute('Comfy.Canvas.FitView')"
/>
<Button
severity="secondary"
@@ -39,7 +39,7 @@
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
)
"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
@click="async () => await commandStore.execute('Comfy.Canvas.ToggleLock')"
>
<template #icon>
<i-material-symbols:pan-tool-outline
@@ -53,7 +53,10 @@
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
:aria-label="$t('graphCanvasMenu.toggleLinkVisibility')"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
@click="
async () =>
await commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')
"
data-testid="toggle-link-visibility-button"
/>
</ButtonGroup>

View File

@@ -52,8 +52,11 @@ watch(
(canvas: LGraphCanvas | null) => {
if (!canvas) return
canvas.onSelectionChange = useChainCallback(canvas.onSelectionChange, () =>
positionSelectionOverlay(canvas)
canvas.onSelectionChange = useChainCallback(
canvas.onSelectionChange,
// Wait for next frame as sometimes the selected items haven't been
// rendered yet, so the boundingRect is not available on them.
() => requestAnimationFrame(() => positionSelectionOverlay(canvas))
)
},
{ immediate: true }
@@ -88,7 +91,12 @@ watch(
positionSelectionOverlay(canvasStore.canvas as LGraphCanvas)
}, 100)
} else {
visible.value = false
// Selection change update to visible state is delayed by a frame. Here
// we also delay a frame so that the order of events is correct when
// the initial selection and dragging happens at the same time.
requestAnimationFrame(() => {
visible.value = false
})
}
}
)

View File

@@ -12,7 +12,8 @@
severity="secondary"
text
@click="
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
async () =>
await commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
"
data-testid="bypass-button"
v-tooltip.top="{
@@ -29,7 +30,10 @@
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
@click="
async () =>
await commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')
"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Pin.label'),
showDelay: 1000
@@ -39,7 +43,10 @@
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
@click="
async () =>
await commandStore.execute('Comfy.Canvas.DeleteSelectedItems')
"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
@@ -58,7 +65,7 @@
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
@click="() => commandStore.execute(command.id)"
@click="async () => await commandStore.execute(command.id)"
v-tooltip.top="{
value:
st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,

View File

@@ -47,6 +47,7 @@ import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCanvasStore } from '@/stores/graphStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { getItemsColorOption } from '@/utils/litegraphUtil'
@@ -54,6 +55,7 @@ import { getItemsColorOption } from '@/utils/litegraphUtil'
const { t } = useI18n()
const canvasStore = useCanvasStore()
const colorPaletteStore = useColorPaletteStore()
const workflowStore = useWorkflowStore()
const isLightTheme = computed(
() => colorPaletteStore.completedActivePalette.light_theme
)
@@ -108,6 +110,7 @@ const applyColor = (colorOption: ColorOption | null) => {
canvasStore.canvas?.setDirty(true, true)
currentColorOption.value = canvasColorOption
showColorPicker.value = false
workflowStore.activeWorkflow?.changeTracker.checkState()
}
const currentColorOption = ref<CanvasColorOption | null>(null)

View File

@@ -17,7 +17,6 @@
</template>
<script setup lang="ts">
import type { LGraphNode } from '@comfyorg/litegraph'
import { useEventListener } from '@vueuse/core'
import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
@@ -50,7 +49,9 @@ const style = computed<CSSProperties>(() => ({
...positionStyle.value,
...(enableDomClipping.value ? clippingStyle.value : {}),
zIndex: widgetState.zIndex,
pointerEvents: widgetState.readonly ? 'none' : 'auto'
pointerEvents:
widgetState.readonly || widget.computedDisabled ? 'none' : 'auto',
opacity: widget.computedDisabled ? 0.5 : 1
}))
const canvasStore = useCanvasStore()
@@ -63,9 +64,9 @@ const updateDomClipping = () => {
const lgCanvas = canvasStore.canvas
if (!lgCanvas || !widgetElement.value) return
const selectedNode = Object.values(
lgCanvas.selected_nodes ?? {}
)[0] as LGraphNode
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
if (!selectedNode) return
const node = widget.node
const isSelected = selectedNode === node
const renderArea = selectedNode?.renderArea

View File

@@ -7,7 +7,7 @@
<Load3DScene
ref="load3DSceneRef"
:node="node"
:type="type"
:inputSpec="inputSpec"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
@@ -30,6 +30,7 @@
@edgeThresholdChange="listenEdgeThresholdChange"
/>
<Load3DControls
:inputSpec="inputSpec"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
@@ -42,7 +43,6 @@
:hasBackgroundImage="hasBackgroundImage"
:upDirection="upDirection"
:materialMode="materialMode"
:isAnimation="false"
:edgeThreshold="edgeThreshold"
@updateBackgroundImage="handleBackgroundImageUpdate"
@switchCamera="switchCamera"
@@ -54,12 +54,15 @@
@updateUpDirection="handleUpdateUpDirection"
@updateMaterialMode="handleUpdateMaterialMode"
@updateEdgeThreshold="handleUpdateEdgeThreshold"
@uploadTexture="handleUploadTexture"
@exportModel="handleExportModel"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
@@ -72,7 +75,9 @@ import {
} from '@/extensions/core/load3d/interfaces'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComponentWidget } from '@/scripts/domWidget'
import { useToastStore } from '@/stores/toastStore'
const { t } = useI18n()
const { widget } = defineProps<{
widget: ComponentWidget<string[]>
}>()
@@ -153,6 +158,23 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
node.properties['Background Image'] = backgroundImage.value
}
const handleUploadTexture = async (file: File) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert(t('toastMessages.no3dScene'))
return
}
try {
const texturePath = await Load3dUtils.uploadFile(file)
await load3DSceneRef.value.load3d.applyTexture(texturePath)
node.properties['Texture'] = texturePath
} catch (error) {
console.error('Error applying texture:', error)
useToastStore().addAlert(t('toastMessages.failedToApplyTexture'))
}
}
const handleUpdateFOV = (value: number) => {
fov.value = value
@@ -183,6 +205,24 @@ const handleUpdateMaterialMode = (value: MaterialMode) => {
node.properties['Material Mode'] = value
}
const handleExportModel = async (format: string) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert(t('toastMessages.no3dSceneToExport'))
return
}
try {
await load3DSceneRef.value.load3d.exportModel(format)
} catch (error) {
console.error('Error exporting model:', error)
useToastStore().addAlert(
t('toastMessages.failedToExportModel', {
format: format.toUpperCase()
})
)
}
}
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode

View File

@@ -7,7 +7,7 @@
<Load3DAnimationScene
ref="load3DAnimationSceneRef"
:node="node"
:type="type"
:inputSpec="inputSpec"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
@@ -35,6 +35,7 @@
/>
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<Load3DControls
:inputSpec="inputSpec"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
@@ -47,7 +48,6 @@
:hasBackgroundImage="hasBackgroundImage"
:upDirection="upDirection"
:materialMode="materialMode"
:isAnimation="true"
@updateBackgroundImage="handleBackgroundImageUpdate"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"

View File

@@ -1,7 +1,7 @@
<template>
<Load3DScene
:node="node"
:type="type"
:inputSpec="inputSpec"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
@@ -29,14 +29,14 @@ import Load3DScene from '@/components/load3d/Load3DScene.vue'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import {
CameraType,
Load3DAnimationNodeType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const props = defineProps<{
node: any
type: Load3DAnimationNodeType
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
lightIntensity: number

View File

@@ -16,14 +16,14 @@
>
<div class="flex flex-col">
<Button
v-for="(label, category) in categories"
v-for="category in availableCategories"
:key="category"
class="p-button-text w-full flex items-center justify-start"
:class="{ 'bg-gray-600': activeCategory === category }"
@click="selectCategory(category)"
>
<i :class="getCategoryIcon(category)"></i>
<span class="text-white">{{ t(label) }}</span>
<span class="text-white">{{ t(categoryLabels[category]) }}</span>
</Button>
</div>
</div>
@@ -43,13 +43,14 @@
<ModelControls
v-if="activeCategory === 'model'"
:inputSpec="inputSpec"
:upDirection="upDirection"
:materialMode="materialMode"
:isAnimation="isAnimation"
:edgeThreshold="edgeThreshold"
@updateUpDirection="handleUpdateUpDirection"
@updateMaterialMode="handleUpdateMaterialMode"
@updateEdgeThreshold="handleUpdateEdgeThreshold"
@uploadTexture="handleUploadTexture"
ref="modelControlsRef"
/>
@@ -70,6 +71,12 @@
@updateLightIntensity="handleUpdateLightIntensity"
ref="lightControlsRef"
/>
<ExportControls
v-if="activeCategory === 'export'"
@exportModel="handleExportModel"
ref="exportControlsRef"
/>
</div>
<div v-if="showPreviewButton">
<Button class="p-button-rounded p-button-text" @click="togglePreview">
@@ -89,9 +96,10 @@
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import CameraControls from '@/components/load3d/controls/CameraControls.vue'
import ExportControls from '@/components/load3d/controls/ExportControls.vue'
import LightControls from '@/components/load3d/controls/LightControls.vue'
import ModelControls from '@/components/load3d/controls/ModelControls.vue'
import SceneControls from '@/components/load3d/controls/SceneControls.vue'
@@ -101,10 +109,12 @@ import {
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const vTooltip = Tooltip
const props = defineProps<{
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
showPreview: boolean
@@ -117,24 +127,34 @@ const props = defineProps<{
hasBackgroundImage?: boolean
upDirection: UpDirection
materialMode: MaterialMode
isAnimation: boolean
edgeThreshold?: number
}>()
const isMenuOpen = ref(false)
const activeCategory = ref<'scene' | 'model' | 'camera' | 'light'>('scene')
const categories = {
const activeCategory = ref<string>('scene')
const categoryLabels: Record<string, string> = {
scene: 'load3d.scene',
model: 'load3d.model',
camera: 'load3d.camera',
light: 'load3d.light'
light: 'load3d.light',
export: 'load3d.export'
}
const availableCategories = computed(() => {
const baseCategories = ['scene', 'model', 'camera', 'light']
if (!props.inputSpec.isAnimation) {
return [...baseCategories, 'export']
}
return baseCategories
})
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value
}
const selectCategory = (category: 'scene' | 'model' | 'camera' | 'light') => {
const selectCategory = (category: string) => {
activeCategory.value = category
isMenuOpen.value = false
}
@@ -144,7 +164,8 @@ const getCategoryIcon = (category: string) => {
scene: 'pi pi-image',
model: 'pi pi-box',
camera: 'pi pi-camera',
light: 'pi pi-sun'
light: 'pi pi-sun',
export: 'pi pi-download'
}
// @ts-expect-error fixme ts strict error
return `${icons[category]} text-white text-lg`
@@ -161,6 +182,8 @@ const emit = defineEmits<{
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'exportModel', format: string): void
(e: 'uploadTexture', file: File): void
}>()
const backgroundColor = ref(props.backgroundColor)
@@ -209,6 +232,10 @@ const handleUpdateEdgeThreshold = (value: number) => {
emit('updateEdgeThreshold', value)
}
const handleUploadTexture = (file: File) => {
emit('uploadTexture', file)
}
const handleUpdateLightIntensity = (value: number) => {
emit('updateLightIntensity', value)
}
@@ -217,6 +244,10 @@ const handleUpdateFOV = (value: number) => {
emit('updateFOV', value)
}
const handleExportModel = (format: string) => {
emit('exportModel', format)
}
const closeSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

View File

@@ -13,17 +13,16 @@ import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import {
CameraType,
Load3DAnimationNodeType,
Load3DNodeType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useLoad3dService } from '@/services/load3dService'
const props = defineProps<{
node: LGraphNode
type: Load3DNodeType | Load3DAnimationNodeType
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
lightIntensity: number
@@ -60,7 +59,16 @@ const eventConfig = {
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
materialLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.switchingMaterialMode')),
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading()
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
exportLoadingStart: (message: string) => {
loadingOverlayRef.value?.startLoading(message || t('load3d.exportingModel'))
},
exportLoadingEnd: () => {
loadingOverlayRef.value?.endLoading()
},
textureLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.applyingTexture')),
textureLoadingEnd: () => loadingOverlayRef.value?.endLoading()
} as const
watchEffect(() => {
@@ -135,7 +143,7 @@ onMounted(() => {
node.value as LGraphNode,
// @ts-expect-error fixme ts strict error
container.value,
props.type
props.inputSpec
)
handleEvents('add')
})

View File

@@ -15,19 +15,22 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { nextTick, ref } from 'vue'
import { t } from '@/i18n'
const modelLoading = ref(false)
const loadingMessage = ref('')
const startLoading = (message?: string) => {
modelLoading.value = true
const startLoading = async (message?: string) => {
loadingMessage.value = message || t('load3d.loadingModel')
modelLoading.value = true
await nextTick()
}
const endLoading = () => {
const endLoading = async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
modelLoading.value = false
}

View File

@@ -0,0 +1,81 @@
<template>
<div class="flex flex-col">
<div class="relative show-export-formats">
<Button
class="p-button-rounded p-button-text"
@click="toggleExportFormats"
>
<i
class="pi pi-download text-white text-lg"
v-tooltip.right="{
value: t('load3d.exportModel'),
showDelay: 300
}"
></i>
</Button>
<div
v-show="showExportFormats"
class="absolute left-12 top-0 bg-black bg-opacity-50 rounded-lg shadow-lg"
>
<div class="flex flex-col">
<Button
v-for="format in exportFormats"
:key="format.value"
class="p-button-text text-white"
@click="exportModel(format.value)"
>
{{ format.label }}
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const emit = defineEmits<{
(e: 'exportModel', format: string): void
}>()
const showExportFormats = ref(false)
const exportFormats = [
{ label: 'GLB', value: 'glb' },
{ label: 'OBJ', value: 'obj' },
{ label: 'STL', value: 'stl' }
]
const toggleExportFormats = () => {
showExportFormats.value = !showExportFormats.value
}
const exportModel = (format: string) => {
emit('exportModel', format)
showExportFormats.value = false
}
const closeExportFormatsList = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.show-export-formats')) {
showExportFormats.value = false
}
}
onMounted(() => {
document.addEventListener('click', closeExportFormatsList)
})
onUnmounted(() => {
document.removeEventListener('click', closeExportFormatsList)
})
</script>

View File

@@ -58,6 +58,33 @@
</div>
</div>
</div>
<div
v-if="
materialMode === 'original' &&
!props.inputSpec.isAnimation &&
!props.inputSpec.isPreview
"
class="relative show-texture-upload"
>
<Button class="p-button-rounded p-button-text" @click="openTextureUpload">
<i
class="pi pi-image text-white text-lg"
v-tooltip.right="{
value: t('load3d.uploadTexture'),
showDelay: 300
}"
></i>
<input
type="file"
ref="texturePickerRef"
accept="image/*"
@change="uploadTexture"
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
/>
</Button>
</div>
<div v-if="materialMode === 'lineart'" class="relative show-edge-threshold">
<Button
class="p-button-rounded p-button-text"
@@ -100,13 +127,14 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { MaterialMode, UpDirection } from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const vTooltip = Tooltip
const props = defineProps<{
inputSpec: CustomInputSpec
upDirection: UpDirection
materialMode: MaterialMode
isAnimation: boolean
edgeThreshold?: number
}>()
@@ -114,6 +142,7 @@ const emit = defineEmits<{
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'uploadTexture', file: File): void
}>()
const upDirection = ref(props.upDirection || 'original')
@@ -122,6 +151,7 @@ const edgeThreshold = ref(props.edgeThreshold || 85)
const showUpDirection = ref(false)
const showMaterialMode = ref(false)
const showEdgeThreshold = ref(false)
const texturePickerRef = ref<HTMLInputElement | null>(null)
const upDirections: UpDirection[] = [
'original',
@@ -141,7 +171,7 @@ const materialModes = computed(() => {
//'depth' disable for now
]
if (!props.isAnimation) {
if (!props.inputSpec.isAnimation && !props.inputSpec.isPreview) {
modes.push('lineart')
}
@@ -204,7 +234,7 @@ const selectMaterialMode = (mode: MaterialMode) => {
}
const formatMaterialMode = (mode: MaterialMode) => {
return mode.charAt(0).toUpperCase() + mode.slice(1)
return t(`load3d.materialModes.${mode}`)
}
const toggleEdgeThreshold = () => {
@@ -217,6 +247,18 @@ const updateEdgeThreshold = () => {
emit('updateEdgeThreshold', edgeThreshold.value)
}
const openTextureUpload = () => {
texturePickerRef.value?.click()
}
const uploadTexture = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files && input.files[0]) {
emit('uploadTexture', input.files[0])
}
}
const closeSceneSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

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