Compare commits

...

114 Commits

Author SHA1 Message Date
christian-byrne
c1f30b96f9 Add key function param in node tree build 2025-01-14 18:19:15 -07:00
Gremlation
c13190cd07 Fix execution_interrupted (#2244) 2025-01-14 11:46:42 -05:00
filtered
00f031e382 [Refactor] Remove old workarounds (#2242) 2025-01-14 10:52:34 -05:00
filtered
04153caaf5 Fix prettier output in CI does not match IDE (#2243) 2025-01-14 10:51:32 -05:00
Chenlei Hu
210bfdeb7d 1.7.10 (#2241) 2025-01-13 20:25:36 -05:00
bymyself
ce0726d85e Restore all open workflows on load (#2238) 2025-01-13 20:24:40 -05:00
bymyself
dd69f9dc30 [Style] Update workflow template cards style (#2239) 2025-01-13 20:21:03 -05:00
Chenlei Hu
3f261f0e53 [Desktop] Add user journey events to the metrics collection list (UI) (#2237)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 17:16:16 -05:00
Chenlei Hu
3b2cc23f65 [Desktop] Mark window style setting as experimental (#2236) 2025-01-12 16:14:51 -05:00
bymyself
c50a86b258 [CI] Fix vite config condition (#2235) 2025-01-12 16:09:46 -05:00
Chenlei Hu
1a8c2bba42 1.7.9 (#2234) 2025-01-12 13:43:05 -05:00
Benjamin Lu
fc09951b3e [Style] Visual improvements to WorkflowTabs (#2232)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-12 13:41:55 -05:00
Terry Jia
76d5f39607 [3d] use threejs native viewHelper (#2230) 2025-01-12 13:23:23 -05:00
bymyself
9d3bc0f173 Add optional report feature to error dialog (#2229)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 13:23:02 -05:00
bymyself
d9b350e159 Add bookmark option in workflow tab context menu (#2231)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-12 13:22:22 -05:00
Chenlei Hu
44610674ee [Desktop] Fix server start view layout (#2226) 2025-01-10 22:14:01 -05:00
Chenlei Hu
9bfce5b8d0 Disable i18n on PRs from forked repo (#2225) 2025-01-10 19:23:20 -05:00
Chenlei Hu
8986fa356a 1.7.8 (#2223) 2025-01-10 19:06:03 -05:00
Chenlei Hu
0c4fd4af1c [Style] Set topbar height to 2.5rem (40px) (#2224) 2025-01-10 19:05:44 -05:00
Chenlei Hu
30cd46ce1f [Desktop] Allow dragging window on empty titlebar (#2222) 2025-01-10 13:41:26 -05:00
filtered
3122c33310 Allow Ctrl + C to send interrupt in terminal (#2221) 2025-01-10 13:30:11 -05:00
Chenlei Hu
91d8d04dc6 [Style] Reduce opacity on workflow tabs scrollbar (#2220) 2025-01-10 10:59:58 -05:00
Chenlei Hu
8f5aa1ff08 [Desktop] Native window in graph view (#2216) 2025-01-09 23:00:28 -05:00
Chenlei Hu
e076783b89 [Desktop] Native window virtual menu bar (#2215) 2025-01-09 20:21:03 -05:00
Chenlei Hu
04c23001fc [Desktop] Set window action buttons style (#2214) 2025-01-09 19:35:22 -05:00
Chenlei Hu
cb265fb0bf 1.7.7 (#2213) 2025-01-09 19:03:43 -05:00
Chenlei Hu
e9211fe377 [Desktop] Add window style setting (#2212)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-09 17:33:57 -05:00
Chenlei Hu
ffc7febeac [BrowserTest] Disable flaky test (#2211) 2025-01-09 14:46:09 -05:00
Chenlei Hu
b15e626607 [Style] Replace px with rem in sidebar width (#2210) 2025-01-09 14:25:34 -05:00
Chenlei Hu
906b5e35a3 [Style] Set fixed top menu bar height (#2209) 2025-01-09 14:14:14 -05:00
Chenlei Hu
e8cd9c7642 [BrowserTest] Replace ComfyPage.reload with ComfyPage.setup (#2208) 2025-01-09 12:54:05 -05:00
Chenlei Hu
93e184e379 Explicitly add zod-to-json-schema as dev dependency (#2207) 2025-01-09 11:41:40 -05:00
Chenlei Hu
1d02cd3c47 [Docs] Extract comfy workflow zod schema as json schema (#2206) 2025-01-09 11:30:47 -05:00
Chenlei Hu
b86e3f71cb 1.7.6 (#2202) 2025-01-08 11:24:59 -05:00
bymyself
1ece2462bd Use more conventional import/export icons in color palette settings (#2199)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-08 10:58:21 -05:00
bymyself
73ecacfa2d Fix searchbox dismissed when closing filter panel (#2196) 2025-01-08 10:57:55 -05:00
bymyself
67e6df7c72 Fix search result category text on light theme (#2198) 2025-01-08 10:57:10 -05:00
Jarvis
7e8510028d fix: Make sure use the correct graph parameter (#2200) 2025-01-08 10:56:27 -05:00
bymyself
dd4dd8b68a [Doc] Add anchors to sections in README (#2197) 2025-01-08 10:55:59 -05:00
bymyself
7111022617 Fix node text highlight color on light theme (#2195) 2025-01-08 09:16:57 -05:00
Chenlei Hu
daee073045 [Style] Fix second row workflow tabs overflow (#2194) 2025-01-07 16:28:49 -05:00
Chenlei Hu
a1a834a76d Support 2nd row location for workflow tabs (#2193)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-07 16:05:37 -05:00
Chenlei Hu
c437d32691 Truncate sidebar title first to avoid tool buttons wrap (#2192) 2025-01-07 15:14:09 -05:00
Chenlei Hu
0130d41be5 Try fix workflow tabs vertical scrollbar (#2191) 2025-01-07 13:39:14 -05:00
Chenlei Hu
527561d148 1.7.5 (#2189) 2025-01-07 10:47:34 -05:00
Chenlei Hu
1c4481c342 Horizontal scroll workflow tabs on overflow (#2188) 2025-01-07 10:45:58 -05:00
Chenlei Hu
07000a23d4 Wrap each init task with error handling (#2187) 2025-01-07 10:40:32 -05:00
Chenlei Hu
ea6c9e7ca5 Replace unset unknown keybinding error with warn (#2186) 2025-01-07 10:33:23 -05:00
Chenlei Hu
90698fced6 [BugFix] Migrate deprecated Comfy.Keybinding.UnsetBindings values (#2185) 2025-01-07 10:23:25 -05:00
iola1999
9716aea10d fix: Fix infinite recursion in mask upload retry logic (#2181) 2025-01-07 16:19:13 +01:00
Chenlei Hu
077ded2cce [Refactor] Simplify keybindingStore with _.groupBy (#2180) 2025-01-06 19:46:20 -05:00
Chenlei Hu
ffb20b8789 1.7.4 (#2179) 2025-01-06 19:24:40 -05:00
Chenlei Hu
549a2fdc92 Remove main_repo_release.py (#2147) 2025-01-06 17:01:41 -05:00
bymyself
e123295423 Fix saved keybinding persistence (#2176) 2025-01-06 15:54:35 -05:00
Chenlei Hu
613b44610a 1.7.3 (#2175) 2025-01-06 11:04:54 -05:00
Chenlei Hu
e82d795ff9 Trigger node def locale update manually (#2170) 2025-01-06 10:54:59 -05:00
bymyself
ba31f8fa68 Add Close Workflow to commands (#2171)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-01-06 10:50:44 -05:00
Alexander Piskun
f2eb4e1519 (fix): added missing slash to "loadWorkflow" templates endpoint (#2174) 2025-01-06 10:49:07 -05:00
Chenlei Hu
975c2248c5 Rename Keybinding.targetSelector to targetElementId (#2169)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-05 16:03:38 -05:00
bymyself
477f4b275d Fix node bookmark color customization (#2168) 2025-01-05 13:05:09 -05:00
bymyself
179b8c22a9 [BrowserTest] Refactor test fixtures (#2165) 2025-01-05 13:03:07 -05:00
bymyself
6525389273 Update Settings schema (#2167) 2025-01-05 12:56:29 -05:00
Chenlei Hu
59fc5ac77e Update litegraph 0.8.60 (#2164) 2025-01-04 20:46:38 -05:00
bymyself
ed844e04b8 Fix text wrapping in dropdown menu (#2162) 2025-01-04 20:30:58 -05:00
bymyself
972af1977d Improve menu accessibility (#2163) 2025-01-04 20:30:27 -05:00
Chenlei Hu
5ac0b7b181 1.7.2 (#2161) 2025-01-04 19:42:15 -05:00
Chenlei Hu
31ea39e44c Prevent reference sharing on settingStore content (#2160) 2025-01-04 19:31:32 -05:00
Chenlei Hu
e65653c107 Add support for custom light color palette (#2156)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-04 18:53:47 -05:00
Chenlei Hu
e46706777c [BrowserTest] Disable flaky queue sidebar tests (#2158) 2025-01-04 16:33:36 -05:00
Chenlei Hu
1a817a48cb 1.7.1 (#2152) 2025-01-03 20:19:11 -05:00
Chenlei Hu
871967349f 1.7.0 (#2151)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-03 20:14:51 -05:00
filtered
11258f4a95 Revert markdown note node, reland as new node (#2148) 2025-01-03 16:05:23 -05:00
bymyself
c3e05c2a10 Add alt text to images in gallery and queue (#2136) 2025-01-03 13:49:06 -05:00
Chenlei Hu
ea1e776dcc Update litegraph 0.8.59 (#2140) 2025-01-03 11:15:16 -05:00
Dr.Lt.Data
f0c273f845 elaborate korean translation (#2137) 2025-01-03 11:14:56 -05:00
Chenlei Hu
ea489851ed [BugFix] Fix open workflow after insert (#2138) 2025-01-03 11:09:28 -05:00
Chenlei Hu
5717c33a0b [BrowserTest] Add test on paste with link (#2130)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 20:38:36 -05:00
Chenlei Hu
c31919c418 Remove invertMenuScrolling extension (#2131)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 20:19:00 -05:00
bymyself
2e7de4701e Reduce debounce delay in settings search (#2126) 2025-01-02 19:51:31 -05:00
Chenlei Hu
f53723da0f 1.6.15 (#2129) 2025-01-02 17:54:31 -05:00
bymyself
5c7cbe968e Fix gallery changing image every time a task finishes (#2125) 2025-01-02 17:51:28 -05:00
Chenlei Hu
cb607ee070 Update litegraph 0.8.58 (#2128) 2025-01-02 17:47:32 -05:00
Terry Jia
f4b5677901 [3d] support for fov and mask (#2116) 2025-01-02 15:45:37 -05:00
Chenlei Hu
9c1eacf0af Fix duplication of persisted workflow (#2124)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-02 12:16:39 -05:00
Chenlei Hu
bb988edf9f 1.6.14 (#2120) 2025-01-01 19:29:47 -05:00
Chenlei Hu
39e9f421f4 Fix double trigger of loadColorPalette effect (#2118)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-01 19:27:22 -05:00
Chenlei Hu
3d2b9a8d9d Call api.storeSetting only when setting value changes (#2119) 2025-01-01 17:34:58 -05:00
pythongosssss
c77a5cab5b Add support for nested dynamic prompts (#2117) 2025-01-01 15:29:17 -05:00
Chenlei Hu
e525a3ad98 1.6.13 (#2115) 2024-12-31 19:05:26 -05:00
bymyself
f507142e6a Fix error when adding custom setting with no category (#2112)
Co-authored-by: huchenlei <huchenlei@proton.me>
2024-12-31 19:04:58 -05:00
Chenlei Hu
3189e310a8 Add default confirm dialog (#2114) 2024-12-31 18:18:48 -05:00
Chenlei Hu
174a9a114a Expose dialogService to extensionManager (#2113) 2024-12-31 17:01:37 -05:00
Chenlei Hu
0b3c0cc0c9 Fix i18n-custom-nodes action (#1991)
Co-authored-by: snomiao <snomiao@gmail.com>
2024-12-31 16:03:33 -05:00
bymyself
74b02237db Add virtual scroll to Queue Tab for improved performance (#2108) 2024-12-31 12:35:29 -05:00
bymyself
6dc5c6958f Compress test assets (#2109) 2024-12-31 12:33:29 -05:00
bymyself
70bece2d48 Update history task item schema (#2110) 2024-12-31 12:33:11 -05:00
bymyself
7eea2e5aa0 Increase width of settings dialog on mobile (#2111) 2024-12-31 12:30:40 -05:00
Chenlei Hu
eb94337c43 1.6.12 (#2107) 2024-12-30 21:30:07 -05:00
Chenlei Hu
98b96d2b1a Fix install view stepper layout shift (#2106) 2024-12-30 19:19:40 -05:00
Chenlei Hu
1b10e16b2d Adjust styles on various views (#2105) 2024-12-30 19:09:55 -05:00
Chenlei Hu
d8f074fea0 [Lint] Sort imports (#2104) 2024-12-30 17:26:37 -05:00
Chenlei Hu
e88817ea36 Lint/format vite.config.mts (#2103) 2024-12-30 16:48:57 -05:00
Chenlei Hu
4ae77e22d4 Add eslint to presubmit hook (#2102) 2024-12-30 16:47:12 -05:00
Chenlei Hu
9f23f81879 [Refactor] Move goToNode to litegraphService (#2101) 2024-12-30 16:38:10 -05:00
Chenlei Hu
3bf0dfd218 [Refactor] Move addNodeOnGraph to litegraphService (#2100) 2024-12-30 16:09:44 -05:00
Yuki Shindo
be7e2c6505 Add category-based filtering in settings search (#2086)
Co-authored-by: huchenlei <huchenlei@proton.me>
2024-12-30 11:23:02 -05:00
Chenlei Hu
6dce285210 Fix litegraph grab cursor style (#2097) 2024-12-30 10:50:53 -05:00
Zoltán Dócs
5218024395 Show workflow templates from custom nodes (#2032) 2024-12-29 22:26:03 -05:00
Chenlei Hu
92a5a8057f 1.6.11 (#2096) 2024-12-29 21:04:37 -05:00
Chenlei Hu
b19f7ca228 Add BaseViewTemplate (#2094) 2024-12-29 18:07:22 -05:00
Chenlei Hu
94f964cf17 [Cleanup] Remove legacy slow jest test utils (#2093) 2024-12-29 17:05:05 -05:00
Chenlei Hu
a7fd9bf0d9 Remove life on default error handler toast (#2091) 2024-12-29 16:42:09 -05:00
Chenlei Hu
642094ead1 Remove legacy slow jest tests (#2092) 2024-12-29 16:25:06 -05:00
Chenlei Hu
95a4623c37 Re-enable keybinding jest test (#2090) 2024-12-29 14:55:18 -05:00
344 changed files with 6250 additions and 5492 deletions

View File

@@ -20,4 +20,4 @@ jobs:
run: npm ci
- name: Run Prettier check
run: npx prettier --check './**/*.{js,ts,tsx,vue}'
run: npm run format:check

View File

@@ -107,21 +107,56 @@ jobs:
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
echo "Fork failed - repository might already be forked"
# Exit 0 to prevent the workflow from failing
exit 0
}
# Enable workflows on the forked repository
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
-F can_approve_pull_request_reviews=true \
-F default_workflow_permissions="write" \
-F enabled=true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Commit and push changes
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
- name: Commit changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
# Create and switch to new branch
git checkout -b update-locales
# Stage and commit changes
git add -A
git commit -m "Update locales"
git remote add org_fork git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git
git push org_fork update-locales
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@v2
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
# github public key to confirm it's github server
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
- name: Push changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Force push to create the branch
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}

47
.github/workflows/i18n-node-defs.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Update Node Definitions Locales
on:
workflow_dispatch:
inputs:
trigger_type:
description: 'Type of trigger (manual or automatic)'
required: false
type: string
default: 'manual'
jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"
title: "Update locales for node definitions"
body: |
Automated PR to update locales for node definitions
This PR was created automatically by the frontend update workflow.
branch: update-locales-node-defs-{{ github.event.inputs.trigger_type }}-{{ github.run_id }}
base: main
labels: dependencies

View File

@@ -5,6 +5,8 @@ on:
jobs:
update-locales:
# Don't run on fork PRs
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
@@ -17,13 +19,12 @@ jobs:
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
# Pipe output so that it doesn't error out on stdout.clearLine
run: npm run locale 2>&1 | cat
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -25,6 +25,8 @@ jobs:
id: current_version
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: |
npm ci
npm run build

View File

@@ -19,8 +19,7 @@ jobs:
- name: Run Jest tests
run: |
npm run test:generate
npm run test:generate:examples
npm run test:jest:fast -- --verbose
npm run test:jest -- --verbose
working-directory: ComfyUI_frontend
playwright-tests-chromium:

5
.gitignore vendored
View File

@@ -41,4 +41,7 @@ browser_tests/*/*-win32.png
dist.zip
/temp/
/temp/
# Generated JSON Schemas
/schemas/

View File

@@ -3,5 +3,16 @@
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80
}
"printWidth": 80,
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
"options": {
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
}
]
}

117
README.md
View File

@@ -58,17 +58,17 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
### Major features
<details>
<details id='feature-native-translation'>
<summary>v1.5: Native translation (i18n)</summary>
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
</details>
<details>
<details id='feature-mask-editor'>
<summary>v1.4: New mask editor</summary>
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
@@ -76,7 +76,7 @@ Stable releases are published bi-weekly in the ComfyUI main repository.
![image](https://github.com/user-attachments/assets/f0ea6ee5-00ee-4e5d-a09c-6938e86a1f17)
</details>
<details>
<details id='feature-integrated-server-terminal'>
<summary>v1.3.22: Integrated server terminal</summary>
Press Ctrl + ` to toggle integrated terminal.
@@ -84,7 +84,7 @@ Press Ctrl + ` to toggle integrated terminal.
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details>
<details id='feature-keybinding-customization'>
<summary>v1.3.7: Keybinding customization</summary>
## Basic UI
@@ -101,7 +101,7 @@ https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details>
<details id='feature-node-library-sidebar'>
<summary>v1.2.4: Node library sidebar tab</summary>
#### Drag & Drop
@@ -111,13 +111,13 @@ https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
</details>
<details>
<details id='feature-queue-sidebar'>
<summary>v1.2.0: Queue/History sidebar tab</summary>
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
</details>
<details>
<details id='feature-node-search'>
<summary>v1.1.0: Node search box</summary>
#### Fuzzy search & Node preview
@@ -129,26 +129,26 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
### QoL changes
<details>
<details id='feature-nested-group'>
<summary>v1.3.32: **Litegraph** Nested group</summary>
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
</details>
<details>
<details id='feature-group-selection'>
<summary>v1.3.24: **Litegraph** Group selection</summary>
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
</details>
<details>
<details id='feature-toggle-link-visibility'>
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
</details>
<details>
<details id='feature-auto-widget-conversion'>
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
Dropping a link of correct type on node widget will automatically convert the widget to input.
@@ -157,7 +157,7 @@ Dropping a link of correct type on node widget will automatically convert the wi
</details>
<details>
<details id='feature-pan-mode'>
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
@@ -167,42 +167,42 @@ or by holding the space key.
</details>
<details>
<details id='feature-shift-drag-link-creation'>
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
</details>
<details>
<details id='feature-optional-input-donuts'>
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
![GYEIRidb0AYGO-v](https://github.com/user-attachments/assets/e6cde0b6-654b-4afd-a117-133657a410b1)
</details>
<details>
<details id='feature-group-title-edit'>
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
</details>
<details>
<details id='feature-group-selection-shortcut'>
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
</details>
<details>
<details id='feature-node-title-edit'>
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
</details>
<details>
<details id='feature-drag-multi-link'>
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
@@ -211,7 +211,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
</details>
<details>
<details id='feature-auto-connect-link'>
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
#### Before
@@ -221,7 +221,7 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
</details>
<details>
<details id='feature-hide-text-overflow'>
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
@@ -229,6 +229,55 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
### Developer APIs
<details>
<summary>v1.6.13: prompt/confirm/alert replacements for ComfyUI desktop</summary>
Several browser-only APIs are not available in ComfyUI desktop's electron environment.
- `window.prompt`
- `window.confirm`
- `window.alert`
Please use the following APIs as replacements.
```js
// window.prompt
window['app'].extensionManager.dialog
.prompt({
title: 'Test Prompt',
message: 'Test Prompt Message'
})
.then((value: string) => {
// Do something with the value user entered
})
```
![image](https://github.com/user-attachments/assets/c73f74d0-9bb4-4555-8d56-83f1be4a1d7e)
```js
// window.confirm
window['app'].extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
// Do something with the value user entered
})
```
![image](https://github.com/user-attachments/assets/8dec7a42-7443-4245-85be-ceefb1116e96)
```js
// window.alert
window['app'].extensionManager.toast
.addAlert("Test Alert")
```
![image](https://github.com/user-attachments/assets/9b18bdca-76ef-4432-95de-5cd2369684f2)
</details>
<details>
<summary>v1.3.34: Register about panel badges</summary>
@@ -249,7 +298,7 @@ app.registerExtension({
</details>
<details>
<details id='extension-api-bottom-panel-tabs'>
<summary>v1.3.22: Register bottom panel tabs</summary>
```js
@@ -272,7 +321,7 @@ app.registerExtension({
</details>
<details>
<details id='extension-api-settings'>
<summary>v1.3.22: New settings API</summary>
Legacy settings API.
@@ -318,7 +367,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
</details>
<details>
<details id='extension-api-commands-keybindings'>
<summary>v1.3.7: Register commands and keybindings</summary>
Extensions can call the following API to register commands and keybindings. Do
@@ -347,7 +396,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
</details>
<details>
<details id='extension-api-topbar-menu'>
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
Extensions can call the following API to register custom topbar menu items.
@@ -376,7 +425,7 @@ app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
![image](https://github.com/user-attachments/assets/ae7b082f-7ce9-4549-a446-4563567102fe)
</details>
<details>
<details id='extension-api-toast'>
<summary>v1.2.27: Extension API to add toast message</summary>i
Extensions can call the following API to add toast messages.
@@ -394,7 +443,7 @@ Documentation of all supported options can be found here: <https://primevue.org/
![image](https://github.com/user-attachments/assets/de02cd7e-cd81-43d1-a0b0-bccef92ff487)
</details>
<details>
<details id='extension-api-sidebar-tab'>
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
Extensions now can call the following API to register a sidebar tab.
@@ -471,8 +520,7 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
- `git clone https://github.com/comfyanonymous/ComfyUI_examples.git` to `tests-ui/ComfyUI_examples` or the EXAMPLE_REPO_PATH location specified in .env
- `npm i` to install all dependencies
- `npm run test:generate` to fetch `tests-ui/data/object_info.json`
- `npm run test:generate:examples` to extract the example workflows
- `npm run test` to execute all unit tests.
- `npm run test:jest` to execute all unit tests.
### Component Test
@@ -578,10 +626,3 @@ You can switch languages by opening the ComfyUI Settings and selecting from the
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
- Option 2: Copy everything under `dist/` to `ComfyUI/web/` in your ComfyUI checkout manually.
## Publish release to ComfyUI main repo
Run following command to publish a release to ComfyUI main repo. The script will create a new branch and do a commit to `web/` folder by checkout `dist.zip`
from GitHub release.
- `python scripts/main_repo_release.py <path_to_comfyui_main_repo> <version>`

View File

@@ -1,6 +1,7 @@
import type { Response } from '@playwright/test'
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
import { expect, mergeTests } from '@playwright/test'
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
import { comfyPageFixture } from './fixtures/ComfyPage'
import { webSocketFixture } from './fixtures/ws.ts'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 152 B

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Browser tab title', () => {

View File

@@ -1,7 +1,7 @@
import {
ComfyPage,
comfyPageFixture as test,
comfyExpect as expect
comfyExpect as expect,
comfyPageFixture as test
} from './fixtures/ComfyPage'
async function beforeChange(comfyPage: ComfyPage) {

View File

@@ -1,7 +1,9 @@
import { expect } from '@playwright/test'
import type { Palette } from '../src/types/colorPaletteTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes = {
const customColorPalettes: Record<string, Palette> = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -127,17 +129,39 @@ const customColorPalettes = {
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
}
}
},
// A custom light theme with fg color red
light_red: {
id: 'light_red',
name: 'Light Red',
light_theme: true,
colors: {
node_slot: {},
litegraph_base: {},
comfy_base: {
'fg-color': '#ff0000'
}
}
}
}
test.describe('Color Palette', () => {
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
// doesn't update the store immediately.
await comfyPage.setup()
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-light-red.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
@@ -149,6 +173,12 @@ test.describe('Color Palette', () => {
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.getToastErrorCount()).toBe(0)
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
// Legacy `custom_` prefix is still supported
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Keybindings', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Copy Paste', () => {
@@ -10,6 +11,14 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.page.keyboard.press('Control+Shift+V')
await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png')
})
test('Can copy and paste text', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await textBox.click()

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,4 +1,6 @@
import { expect } from '@playwright/test'
import { Keybinding } from '../src/types/keyBindingTypes'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Load workflow warning', () => {
@@ -42,6 +44,18 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
})
})
test.describe('Missing models warning', () => {
@@ -102,4 +116,52 @@ test.describe('Settings', () => {
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
})
})
test('Should persist keybinding setting', async ({ comfyPage }) => {
// Open the settings dialog
await comfyPage.page.keyboard.press('Control+,')
await comfyPage.page.waitForSelector('.settings-container')
// Open the keybinding tab
await comfyPage.page.getByLabel('Keybinding').click()
await comfyPage.page.waitForSelector(
'[placeholder="Search Keybindings..."]'
)
// Focus the 'New Blank Workflow' row
const newBlankWorkflowRow = comfyPage.page.locator('tr', {
has: comfyPage.page.getByRole('cell', { name: 'New Blank Workflow' })
})
await newBlankWorkflowRow.click()
// Click edit button
const editKeybindingButton = newBlankWorkflowRow.locator('.pi-pencil')
await editKeybindingButton.click()
// Set new keybinding
const input = comfyPage.page.getByPlaceholder('Press keys for new binding')
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
'**/api/settings/Comfy.Keybinding.NewBindings'
)
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('Comfy.NewBlankWorkflow')
.getByLabel('Save')
await saveButton.click()
const request = await requestPromise
const expectedSetting: Keybinding = {
commandId: 'Comfy.NewBlankWorkflow',
combo: {
key: 'n',
ctrl: false,
alt: true,
shift: false
}
}
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Topbar commands', () => {
@@ -157,4 +158,42 @@ test.describe('Topbar commands', () => {
expect(await badge.textContent()).toContain('Test Badge')
})
})
test.describe('Dialog', () => {
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].extensionManager.dialog
.prompt({
title: 'Test Prompt',
message: 'Test Prompt Message'
})
.then((value: string) => {
window['value'] = value
})
})
await comfyPage.fillPromptDialog('Hello, world!')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(
'Hello, world!'
)
})
test('Should allow showing a confirmation dialog', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
window['app'].extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
window['value'] = value
})
})
await comfyPage.confirmDialog.click('confirm')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
})
})
})

View File

@@ -1,28 +1,37 @@
import type { Page, Locator, APIRequestContext } from '@playwright/test'
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import { ComfyActionbar } from '../helpers/actionbar'
import dotenv from 'dotenv'
dotenv.config()
import * as fs from 'fs'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import type { NodeId } from '../../src/types/comfyWorkflow'
import type { KeyCombo } from '../../src/types/keyBindingTypes'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { SettingDialog } from './components/SettingDialog'
import {
NodeLibrarySidebarTab,
QueueSidebarTab,
WorkflowsSidebarTab
} from './components/SidebarTab'
import { Topbar } from './components/Topbar'
import { NodeReference } from './utils/litegraphUtils'
import type { Position, Size } from './types'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { SettingDialog } from './components/SettingDialog'
import { NodeReference } from './utils/litegraphUtils'
import TaskHistory from './utils/taskHistory'
dotenv.config()
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
class ComfyMenu {
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _workflowsTab: WorkflowsSidebarTab | null = null
private _queueTab: QueueSidebarTab | null = null
private _topbar: Topbar | null = null
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
public readonly saveButton: Locator
@@ -36,15 +45,23 @@ class ComfyMenu {
}
get nodeLibraryTab() {
return new NodeLibrarySidebarTab(this.page)
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab
}
get workflowsTab() {
return new WorkflowsSidebarTab(this.page)
this._workflowsTab ??= new WorkflowsSidebarTab(this.page)
return this._workflowsTab
}
get queueTab() {
this._queueTab ??= new QueueSidebarTab(this.page)
return this._queueTab
}
get topbar() {
return new Topbar(this.page)
this._topbar ??= new Topbar(this.page)
return this._topbar
}
async toggleTheme() {
@@ -83,11 +100,13 @@ class ConfirmDialog {
public readonly delete: Locator
public readonly overwrite: Locator
public readonly reject: Locator
public readonly confirm: Locator
constructor(public readonly page: Page) {
this.delete = page.locator('button.p-button[aria-label="Delete"]')
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
this.confirm = page.locator('button.p-button[aria-label="Confirm"]')
}
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
@@ -98,6 +117,8 @@ class ConfirmDialog {
}
export class ComfyPage {
private _history: TaskHistory | null = null
public readonly url: string
// All canvas position operations are based on default view of canvas.
public readonly canvas: Locator
@@ -231,6 +252,11 @@ export class ComfyPage {
}
}
setupHistory(): TaskHistory {
this._history ??= new TaskHistory(this)
return this._history
}
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
await this.goto()
if (clearStorage) {
@@ -343,11 +369,6 @@ export class ComfyPage {
}, settingId)
}
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
await this.page.reload({ timeout: 15000 })
await this.setup({ clearStorage })
}
async goto() {
await this.page.goto(this.url)
}
@@ -518,6 +539,13 @@ export class ComfyPage {
return this.page.locator('.p-dialog-content input[type="text"]')
}
async fillPromptDialog(value: string) {
await this.promptDialogInput.fill(value)
await this.page.keyboard.press('Enter')
await this.promptDialogInput.waitFor({ state: 'hidden' })
await this.nextFrame()
}
async disconnectEdge() {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
@@ -785,9 +813,7 @@ export class ComfyPage {
await this.canvas.press('Control+a')
const node = await this.getFirstNodeRef()
await node!.clickContextMenuOption('Convert to Group Node')
await this.promptDialogInput.fill(groupNodeName)
await this.page.keyboard.press('Enter')
await this.promptDialogInput.waitFor({ state: 'hidden' })
await this.fillPromptDialog(groupNodeName)
await this.nextFrame()
}
@@ -799,6 +825,11 @@ export class ComfyPage {
async getNodeRefById(id: NodeId) {
return new NodeReference(id, this)
}
async getNodes() {
return await this.page.evaluate(() => {
return window['app'].graph.nodes
})
}
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
return Promise.all(
(

View File

@@ -1,5 +1,5 @@
import { Page } from 'playwright'
import { test as base } from '@playwright/test'
import { Page } from 'playwright'
export class UserSelectPage {
constructor(

View File

@@ -3,6 +3,13 @@ import { Locator, Page } from '@playwright/test'
export class ComfyNodeSearchFilterSelectionPanel {
constructor(public readonly page: Page) {}
get header() {
return this.page
.getByRole('dialog')
.locator('div')
.filter({ hasText: 'Add node filter condition' })
}
async selectFilterType(filterType: string) {
await this.page
.locator(

View File

@@ -22,6 +22,12 @@ class SidebarTab {
}
await this.tabButton.click()
}
async close() {
if (!this.tabButton.isVisible()) {
return
}
await this.tabButton.click()
}
}
export class NodeLibrarySidebarTab extends SidebarTab {
@@ -146,4 +152,118 @@ export class WorkflowsSidebarTab extends SidebarTab {
await this.page.keyboard.press('Enter')
await this.page.waitForTimeout(300)
}
async insertWorkflow(locator: Locator) {
await locator.click({ button: 'right' })
await this.page
.locator('.p-contextmenu-item-content', { hasText: 'Insert' })
.click()
}
}
export class QueueSidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
super(page, 'queue')
}
get root() {
return this.page.locator('.sidebar-content-container', { hasText: 'Queue' })
}
get tasks() {
return this.root.locator('[data-virtual-grid-item]')
}
get visibleTasks() {
return this.tasks.locator('visible=true')
}
get clearButton() {
return this.root.locator('.clear-all-button')
}
get collapseTasksButton() {
return this.getToggleExpandButton(false)
}
get expandTasksButton() {
return this.getToggleExpandButton(true)
}
get noResultsPlaceholder() {
return this.root.locator('.no-results-placeholder')
}
private getToggleExpandButton(isExpanded: boolean) {
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
}
async open() {
await super.open()
return this.root.waitFor({ state: 'visible' })
}
async close() {
await super.close()
await this.root.waitFor({ state: 'hidden' })
}
async expandTasks() {
await this.expandTasksButton.click()
await this.collapseTasksButton.waitFor({ state: 'visible' })
}
async collapseTasks() {
await this.collapseTasksButton.click()
await this.expandTasksButton.waitFor({ state: 'visible' })
}
async waitForTasks() {
return Promise.all([
this.tasks.first().waitFor({ state: 'visible' }),
this.tasks.last().waitFor({ state: 'visible' })
])
}
async scrollTasks(direction: 'up' | 'down') {
const scrollToEl =
direction === 'up' ? this.tasks.last() : this.tasks.first()
await scrollToEl.scrollIntoViewIfNeeded()
await this.waitForTasks()
}
async clearTasks() {
await this.clearButton.click()
const confirmButton = this.page.getByLabel('Delete')
await confirmButton.click()
await this.noResultsPlaceholder.waitFor({ state: 'visible' })
}
/** Set the width of the tab (out of 100). Must call before opening the tab */
async setTabWidth(width: number) {
if (width < 0 || width > 100) {
throw new Error('Width must be between 0 and 100')
}
return this.page.evaluate((width) => {
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
}, width)
}
getTaskPreviewButton(taskIndex: number) {
return this.tasks.nth(taskIndex).getByRole('button')
}
async openTaskPreview(taskIndex: number) {
const previewButton = this.getTaskPreviewButton(taskIndex)
await previewButton.hover()
await previewButton.click()
return this.getGalleryImage(taskIndex).waitFor({ state: 'visible' })
}
getGalleryImage(galleryItemIndex: number) {
// Aria labels of Galleria items are 1-based indices
const galleryLabel = `${galleryItemIndex + 1}`
return this.page.getByLabel(galleryLabel).locator('.galleria-image')
}
}

View File

@@ -9,6 +9,12 @@ export class Topbar {
.allInnerTexts()
}
async getActiveTabName(): Promise<string> {
return this.page
.locator('.workflow-tabs .p-togglebutton-checked')
.innerText()
}
async openSubmenuMobile() {
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
}

View File

@@ -1,6 +1,7 @@
import { ManageGroupNode } from '../../helpers/manageGroupNode'
import type { NodeId } from '../../../src/types/comfyWorkflow'
import type { Page } from '@playwright/test'
import type { NodeId } from '../../../src/types/comfyWorkflow'
import { ManageGroupNode } from '../../helpers/manageGroupNode'
import type { ComfyPage } from '../ComfyPage'
import type { Position, Size } from '../types'
@@ -234,9 +235,7 @@ export class NodeReference {
}
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
await this.clickContextMenuOption('Convert to Group Node')
await this.comfyPage.promptDialogInput.fill(groupNodeName)
await this.comfyPage.page.keyboard.press('Enter')
await this.comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await this.comfyPage.fillPromptDialog(groupNodeName)
await this.comfyPage.nextFrame()
const nodes = await this.comfyPage.getNodeRefsByType(
`workflow>${groupNodeName}`

View File

@@ -0,0 +1,161 @@
import fs from 'fs'
import _ from 'lodash'
import path from 'path'
import type { Request, Route } from 'playwright'
import { v4 as uuidv4 } from 'uuid'
import type {
HistoryTaskItem,
TaskItem,
TaskOutput
} from '../../../src/types/apiTypes'
import type { ComfyPage } from '../ComfyPage'
/** keyof TaskOutput[string] */
type OutputFileType = 'images' | 'audio' | 'animated'
const DEFAULT_IMAGE = 'example.webp'
const getFilenameParam = (request: Request) => {
const url = new URL(request.url())
return url.searchParams.get('filename') || DEFAULT_IMAGE
}
const getContentType = (filename: string, fileType: OutputFileType) => {
const subtype = path.extname(filename).slice(1)
switch (fileType) {
case 'images':
return `image/${subtype}`
case 'audio':
return `audio/${subtype}`
case 'animated':
return `video/${subtype}`
}
}
const setQueueIndex = (task: TaskItem) => {
task.prompt[0] = TaskHistory.queueIndex++
}
const setPromptId = (task: TaskItem) => {
task.prompt[1] = uuidv4()
}
export default class TaskHistory {
static queueIndex = 0
static readonly defaultTask: Readonly<HistoryTaskItem> = {
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
outputs: {},
status: {
status_str: 'success',
completed: true,
messages: []
},
taskType: 'History'
}
private tasks: HistoryTaskItem[] = []
private outputContentTypes: Map<string, string> = new Map()
constructor(readonly comfyPage: ComfyPage) {}
private loadAsset: (filename: string) => Buffer = _.memoize(
(filename: string) => {
const filePath = this.comfyPage.assetPath(filename)
return fs.readFileSync(filePath)
}
)
private async handleGetHistory(route: Route) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(this.tasks)
})
}
private async handleGetView(route: Route) {
const fileName = getFilenameParam(route.request())
if (!this.outputContentTypes.has(fileName)) route.continue()
const asset = this.loadAsset(fileName)
return route.fulfill({
status: 200,
contentType: this.outputContentTypes.get(fileName),
body: asset,
headers: {
'Cache-Control': 'public, max-age=31536000',
'Content-Length': asset.byteLength.toString()
}
})
}
async setupRoutes() {
return this.comfyPage.page.route(
/.*\/api\/(view|history)(\?.*)?$/,
async (route) => {
const request = route.request()
const method = request.method()
const isViewReq = request.url().includes('view') && method === 'GET'
if (isViewReq) return this.handleGetView(route)
const isHistoryPath = request.url().includes('history')
const isGetHistoryReq = isHistoryPath && method === 'GET'
if (isGetHistoryReq) return this.handleGetHistory(route)
const isClearReq =
method === 'POST' &&
isHistoryPath &&
request.postDataJSON()?.clear === true
if (isClearReq) return this.clearTasks()
return route.continue()
}
)
}
private createOutputs(
filenames: string[],
filetype: OutputFileType
): TaskOutput {
return filenames.reduce((outputs, filename, i) => {
const nodeId = `${i + 1}`
outputs[nodeId] = {
[filetype]: [{ filename, subfolder: '', type: 'output' }]
}
const contentType = getContentType(filename, filetype)
this.outputContentTypes.set(filename, contentType)
return outputs
}, {})
}
private addTask(task: HistoryTaskItem) {
setPromptId(task)
setQueueIndex(task)
this.tasks.push(task)
}
clearTasks() {
this.tasks = []
}
withTask(
outputFilenames: string[],
outputFiletype: OutputFileType = 'images',
overrides: Partial<HistoryTaskItem> = {}
): this {
this.addTask({
...TaskHistory.defaultTask,
outputs: this.createOutputs(outputFilenames, outputFiletype),
...overrides
})
return this
}
/** Repeats the last task in the task history a specified number of times. */
repeat(n: number): this {
for (let i = 0; i < n; i++)
this.addTask(structuredClone(this.tasks.at(-1)) as HistoryTaskItem)
return this
}
}

View File

@@ -1,7 +1,8 @@
import { FullConfig } from '@playwright/test'
import { backupPath } from './utils/backupUtils'
import dotenv from 'dotenv'
import { backupPath } from './utils/backupUtils'
dotenv.config()
export default function globalSetup(config: FullConfig) {

View File

@@ -1,7 +1,8 @@
import { FullConfig } from '@playwright/test'
import { restorePath } from './utils/backupUtils'
import dotenv from 'dotenv'
import { restorePath } from './utils/backupUtils'
dotenv.config()
export default function globalTeardown(config: FullConfig) {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Graph Canvas Menu', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
import type { NodeReference } from './fixtures/utils/litegraphUtils'

View File

@@ -1,4 +1,5 @@
import type { Page, Locator } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
import type { AutoQueueMode } from '../../src/stores/queueStore'
export class ComfyActionbar {

View File

@@ -1,4 +1,5 @@
import { Locator, Page } from '@playwright/test'
export class ManageGroupNode {
footer: Locator
header: Locator

View File

@@ -1,4 +1,5 @@
import { Locator, Page } from '@playwright/test'
export class ComfyTemplates {
readonly content: Locator

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Item Interaction', () => {
@@ -452,6 +453,9 @@ test.describe('Canvas Interaction', () => {
expect(await getCursorStyle()).toBe('default')
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')
// Move mouse should not alter cursor style.
await comfyPage.page.mouse.move(10, 20)
expect(await getCursorStyle()).toBe('grabbing')
await comfyPage.page.mouse.up()
expect(await getCursorStyle()).toBe('default')
@@ -465,6 +469,43 @@ test.describe('Canvas Interaction', () => {
expect(await getCursorStyle()).toBe('default')
})
// https://github.com/Comfy-Org/litegraph.js/pull/424
test('Properly resets dragging state after pan mode sequence', async ({
comfyPage
}) => {
const getCursorStyle = async () => {
return await comfyPage.page.evaluate(() => {
return (
document.getElementById('graph-canvas')!.style.cursor || 'default'
)
})
}
// Initial state check
await comfyPage.page.mouse.move(10, 10)
expect(await getCursorStyle()).toBe('default')
// Click and hold
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')
// Press space while holding click
await comfyPage.page.keyboard.down('Space')
expect(await getCursorStyle()).toBe('grabbing')
// Release click while space is still down
await comfyPage.page.mouse.up()
expect(await getCursorStyle()).toBe('grab')
// Release space
await comfyPage.page.keyboard.up('Space')
expect(await getCursorStyle()).toBe('default')
// Move mouse - cursor should remain default
await comfyPage.page.mouse.move(20, 20)
expect(await getCursorStyle()).toBe('default')
})
test('Can pan when dragging a link', async ({ comfyPage }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
@@ -552,7 +593,7 @@ test.describe('Load workflow', () => {
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.reload({ clearStorage: false })
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
})
@@ -569,11 +610,72 @@ test.describe('Load workflow', () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
await comfyPage.reload({ clearStorage: false })
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
})
test.describe('Restore all open workflows on reload', () => {
let workflowA: string
let workflowB: string
const generateUniqueFilename = (extension = '') =>
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.menu.topbar.saveWorkflow(workflowB)
// Wait for localStorage to persist the workflow paths before reloading
await comfyPage.page.waitForFunction(
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
)
await comfyPage.setup({ clearStorage: false })
})
test('Restores topbar workflow tabs after reload', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
const tabs = await comfyPage.menu.topbar.getTabNames()
const activeWorkflowName = await comfyPage.menu.topbar.getActiveTabName()
expect(tabs).toEqual(expect.arrayContaining([workflowA, workflowB]))
expect(tabs.indexOf(workflowA)).toBeLessThan(tabs.indexOf(workflowB))
expect(activeWorkflowName).toEqual(workflowB)
})
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
await comfyPage.menu.workflowsTab.open()
const openWorkflows =
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
const activeWorkflowName =
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
const workflowPathA = `${workflowA}.json`
const workflowPathB = `${workflowB}.json`
expect(openWorkflows).toEqual(
expect.arrayContaining([workflowPathA, workflowPathB])
)
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
openWorkflows.indexOf(workflowPathB)
)
expect(activeWorkflowName).toEqual(workflowPathB)
})
})
})
test.describe('Load duplicate workflow', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Keybindings', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
function listenForEvent(): Promise<Event> {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Load Workflow in Media', () => {

View File

@@ -1,5 +1,9 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture } from './fixtures/ComfyPage'
import { webSocketFixture } from './fixtures/ws'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -17,7 +21,7 @@ test.describe('Menu', () => {
expect(await comfyPage.menu.getThemeId()).toBe('light')
// Theme id should persist after reload.
await comfyPage.reload()
await comfyPage.setup()
expect(await comfyPage.menu.getThemeId()).toBe('light')
await comfyPage.menu.toggleTheme()
@@ -288,6 +292,47 @@ test.describe('Menu', () => {
}
})
})
test('Can customize bookmark color after interacting with color options', async ({
comfyPage
}) => {
// Open customization dialog
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
// Click a color option multiple times
const customColorOption = comfyPage.page.locator(
'.p-togglebutton-content > .pi-palette'
)
await customColorOption.click()
await customColorOption.click()
// Use the color picker
await comfyPage.page
.getByLabel('Customize Folder')
.getByRole('textbox')
.click()
await comfyPage.page.locator('.p-colorpicker-color-background').click()
// Finalize the customization
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByLabel('Confirm').click()
await comfyPage.nextFrame()
// Verify the color selection is saved
const setting = await comfyPage.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
await expect(setting).toHaveProperty(['foo/', 'color'])
await expect(setting['foo/'].color).not.toBeNull()
await expect(setting['foo/'].color).not.toBeUndefined()
await expect(setting['foo/'].color).not.toBe('')
})
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
@@ -395,6 +440,56 @@ test.describe('Menu', () => {
)
})
test('Can duplicate workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
expect.arrayContaining(['workflow1.json'])
)
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json'
])
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json'
])
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json',
'*workflow1 (Copy) (3).json'
])
})
test('Can open workflow after insert', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'single_ksampler.json'
})
await comfyPage.setup()
const tab = comfyPage.menu.workflowsTab
await tab.open()
await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = (await comfyPage.getNodes()).length
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
await comfyPage.nextFrame()
expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1)
await tab.getPersistedItem('workflow1.json').click()
await comfyPage.nextFrame()
expect((await comfyPage.getNodes()).length).toEqual(1)
})
test('Can rename nested workflow from opened workflow item', async ({
comfyPage
}) => {
@@ -474,7 +569,7 @@ test.describe('Menu', () => {
})
await comfyPage.setSetting('Comfy.Locale', 'zh')
await comfyPage.reload()
await comfyPage.setup()
const downloadedContentZh = await comfyPage.getExportedWorkflow({
api: false
@@ -570,6 +665,15 @@ test.describe('Menu', () => {
).toEqual(['*Unsaved Workflow.json'])
})
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.executeCommand('Workspace.CloseWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
@@ -703,3 +807,209 @@ test.describe('Menu', () => {
})
})
})
test.describe.skip('Queue sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test('can display tasks', async ({ comfyPage }) => {
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
})
test('can display tasks after closing then opening', async ({
comfyPage
}) => {
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.close()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
})
test.describe('Virtual scroll', () => {
const layouts = [
{ description: 'Five columns layout', width: 95, rows: 3, cols: 5 },
{ description: 'Three columns layout', width: 55, rows: 3, cols: 3 },
{ description: 'Two columns layout', width: 40, rows: 3, cols: 2 }
]
test.beforeEach(async ({ comfyPage }) => {
await comfyPage
.setupHistory()
.withTask(['example.webp'])
.repeat(50)
.setupRoutes()
})
layouts.forEach(({ description, width, rows, cols }) => {
const preRenderedRows = 1
const preRenderedTasks = preRenderedRows * cols * 2
const visibleTasks = rows * cols
const expectRenderLimit = visibleTasks + preRenderedTasks
test.describe(description, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.menu.queueTab.setTabWidth(width)
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
})
test('should not render items outside of view', async ({
comfyPage
}) => {
const renderedCount =
await comfyPage.menu.queueTab.visibleTasks.count()
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
})
test('should teardown items after scrolling away', async ({
comfyPage
}) => {
await comfyPage.menu.queueTab.scrollTasks('down')
const renderedCount =
await comfyPage.menu.queueTab.visibleTasks.count()
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
})
test('should re-render items after scrolling away then back', async ({
comfyPage
}) => {
await comfyPage.menu.queueTab.scrollTasks('down')
await comfyPage.menu.queueTab.scrollTasks('up')
const renderedCount =
await comfyPage.menu.queueTab.visibleTasks.count()
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
})
})
})
})
test.describe('Expand tasks', () => {
test.beforeEach(async ({ comfyPage }) => {
// 2-item batch and 3-item batch -> 3 additional items when expanded
await comfyPage
.setupHistory()
.withTask(['example.webp', 'example.webp', 'example.webp'])
.withTask(['example.webp', 'example.webp'])
.setupRoutes()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
})
test('can expand tasks with multiple outputs', async ({ comfyPage }) => {
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
await comfyPage.menu.queueTab.expandTasks()
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
initialCount + 3
)
})
test('can collapse flat tasks', async ({ comfyPage }) => {
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
await comfyPage.menu.queueTab.expandTasks()
await comfyPage.menu.queueTab.collapseTasks()
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
initialCount
)
})
})
test.describe('Clear tasks', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage
.setupHistory()
.withTask(['example.webp'])
.repeat(6)
.setupRoutes()
await comfyPage.menu.queueTab.open()
})
test('can clear all tasks', async ({ comfyPage }) => {
await comfyPage.menu.queueTab.clearTasks()
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0)
expect(
await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible()
).toBe(true)
})
test('can load new tasks after clearing all', async ({ comfyPage }) => {
await comfyPage.menu.queueTab.clearTasks()
await comfyPage.menu.queueTab.close()
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
})
})
test.describe('Gallery', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage
.setupHistory()
.withTask(['example.webp'])
.repeat(1)
.setupRoutes()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
})
test('displays gallery image after opening task preview', async ({
comfyPage
}) => {
await comfyPage.menu.queueTab.openTaskPreview(0)
expect(comfyPage.menu.queueTab.getGalleryImage(0)).toBeVisible()
})
test('should maintain active gallery item when new tasks are added', async ({
comfyPage,
ws
}) => {
const initialIndex = 0
await comfyPage.menu.queueTab.openTaskPreview(initialIndex)
// Add a new task while the gallery is still open
comfyPage.setupHistory().withTask(['example.webp'])
await ws.trigger({
type: 'status',
data: {
status: { exec_info: { queue_remaining: 0 } }
}
})
await comfyPage.menu.queueTab.waitForTasks()
// The index of all tasks increments when a new task is prepended
const expectIndex = initialIndex + 1
expect(comfyPage.menu.queueTab.getGalleryImage(expectIndex)).toBeVisible()
})
test.describe('Gallery navigation', () => {
const paths: {
description: string
path: ('Right' | 'Left')[]
expectIndex: number
}[] = [
{ description: 'Right', path: ['Right'], expectIndex: 1 },
{ description: 'Left', path: ['Right', 'Left'], expectIndex: 0 },
{ description: 'Right wrap', path: ['Right', 'Right'], expectIndex: 0 },
{ description: 'Left wrap', path: ['Left'], expectIndex: 1 }
]
paths.forEach(({ description, path, expectIndex }) => {
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
await comfyPage.menu.queueTab.openTaskPreview(0)
for (const direction of path)
await comfyPage.page.keyboard.press(`Arrow${direction}`)
expect(
comfyPage.menu.queueTab.getGalleryImage(expectIndex)
).toBeVisible()
})
})
})
})
})

View File

@@ -1,7 +1,8 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
import type { ComfyApp } from '../src/scripts/app'
import { NodeBadgeMode } from '../src/types/nodeSource'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Node Badge', () => {
test('Can add badge', async ({ comfyPage }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
// If an input is optional by node definition, it should be shown as

View File

@@ -1,6 +1,6 @@
import {
comfyPageFixture as test,
comfyExpect as expect
comfyExpect as expect,
comfyPageFixture as test
} from './fixtures/ComfyPage'
test.describe('Node search box', () => {
@@ -132,6 +132,48 @@ test.describe('Node search box', () => {
await expectFilterChips(comfyPage, ['MODEL'])
})
// Flaky test.
// Sample test failure:
// https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/12696912248/job/35391990861?pr=2210
/*
1) [chromium-2x] nodeSearchBox.spec.ts:135:5 Node search box Filtering Outer click dismisses filter panel but keeps search box visible
Error: expect(locator).not.toBeVisible()
Locator: getByRole('dialog').locator('div').filter({ hasText: 'Add node filter condition' })
Expected: not visible
Received: visible
Call log:
- expect.not.toBeVisible with timeout 5000ms
- waiting for getByRole('dialog').locator('div').filter({ hasText: 'Add node filter condition' })
143 |
144 | // Verify the filter selection panel is hidden
> 145 | expect(panel.header).not.toBeVisible()
| ^
146 |
147 | // Verify the node search dialog is still visible
148 | expect(comfyPage.searchBox.input).toBeVisible()
at /home/runner/work/ComfyUI_frontend/ComfyUI_frontend/ComfyUI_frontend/browser_tests/nodeSearchBox.spec.ts:145:32
*/
test.skip('Outer click dismisses filter panel but keeps search box visible', async ({
comfyPage
}) => {
await comfyPage.searchBox.filterButton.click()
const panel = comfyPage.searchBox.filterSelectionPanel
await panel.header.waitFor({ state: 'visible' })
const panelBounds = await panel.header.boundingBox()
await comfyPage.page.mouse.click(panelBounds!.x - 10, panelBounds!.y - 10)
// Verify the filter selection panel is hidden
expect(panel.header).not.toBeVisible()
// Verify the node search dialog is still visible
expect(comfyPage.searchBox.input).toBeVisible()
})
test('Can add multiple filters', async ({ comfyPage }) => {
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.addFilter('CLIP', 'Output Type')

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
import type { NodeReference } from './fixtures/utils/litegraphUtils'

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Properties Panel', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Reroute Node', () => {

View File

@@ -1,6 +1,7 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
import { NodeBadgeMode } from '../src/types/nodeSource'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Canvas Right Click Menu', () => {
// See https://github.com/comfyanonymous/ComfyUI/issues/3883

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Templates', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Combo text widget', () => {

View File

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import { userSelectPageFixture as test } from './fixtures/UserSelectPage'
/**

View File

@@ -1,5 +1,5 @@
import path from 'path'
import fs from 'fs-extra'
import path from 'path'
type PathParts = readonly [string, ...string[]]

View File

@@ -1,8 +1,8 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default [
{

2
global.d.ts vendored
View File

@@ -1 +1,3 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string

View File

@@ -1,12 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest'
import baseConfig from './jest.config.base'
const jestConfig: JestConfigWithTsJest = {
...baseConfig,
displayName: 'fast',
setupFiles: ['./tests-ui/tests/fast/globalSetup.ts'],
setupFilesAfterEnv: undefined,
testMatch: ['**/tests-ui/tests/fast/**/*.test.ts']
}
export default jestConfig

View File

@@ -1,11 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest'
import baseConfig from './jest.config.base'
const jestConfig: JestConfigWithTsJest = {
...baseConfig,
displayName: 'slow (DOM)',
testTimeout: 10000,
testMatch: ['**/tests-ui/tests/slow/**/*.test.ts']
}
export default jestConfig

View File

@@ -21,8 +21,7 @@ const jestConfig: JestConfigWithTsJest = {
},
clearMocks: true,
resetModules: true,
setupFiles: ['./tests-ui/tests/globalSetup.ts'],
setupFilesAfterEnv: ['./tests-ui/tests/afterSetup.ts']
setupFiles: ['./tests-ui/tests/globalSetup.ts']
}
export default jestConfig

View File

@@ -1,14 +1,17 @@
export default {
'./**/*.js': (stagedFiles) => formatFiles(stagedFiles),
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
...formatFiles(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
...formatAndEslint(stagedFiles),
'vue-tsc --noEmit',
'tsc --noEmit',
'tsc-strict'
]
}
function formatFiles(fileNames) {
return [`prettier --write ${fileNames.join(' ')}`]
function formatAndEslint(fileNames) {
return [
`prettier --write ${fileNames.join(' ')} --plugin @trivago/prettier-plugin-sort-imports`,
`eslint --fix ${fileNames.join(' ')}`
]
}

288
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.6.10",
"version": "1.7.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.6.10",
"version": "1.7.10",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.3",
"@comfyorg/litegraph": "^0.8.50",
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -48,6 +49,7 @@
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.44.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
@@ -86,7 +88,8 @@
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0"
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
}
},
"../litegraph.js": {
@@ -197,12 +200,14 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.24.7",
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
@@ -249,15 +254,17 @@
}
},
"node_modules/@babel/generator": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz",
"integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
"integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.24.7",
"@babel/parser": "^7.26.3",
"@babel/types": "^7.26.3",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
@@ -590,42 +597,13 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.0"
"@babel/types": "^7.26.3"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1895,33 +1873,32 @@
}
},
"node_modules/@babel/template": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.24.7",
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7"
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz",
"integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==",
"version": "7.26.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
"integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.24.7",
"@babel/helper-environment-visitor": "^7.24.7",
"@babel/helper-function-name": "^7.24.7",
"@babel/helper-hoist-variables": "^7.24.7",
"@babel/helper-split-export-declaration": "^7.24.7",
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7",
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.3",
"@babel/parser": "^7.26.3",
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.3",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -1940,9 +1917,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@@ -1959,15 +1936,15 @@
"dev": true
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.3.tgz",
"integrity": "sha512-hSM3mchpsYN0e7oZ7XLWjEvFDvE1rgzaB9YkCeqIiZYZgLL78T79ssM0n5ra17Zv7Mqwl6ErZblXvbQE/36RPw==",
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.7.tgz",
"integrity": "sha512-APC3C4VZOo9W6h0xiAGxnsU9iNp3T8rN9w/5KmOCI0GUoKtKg5U2OaicTmnMwcDSQe5Jxflmej53GyJ1nH9oRw==",
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.8.50",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.50.tgz",
"integrity": "sha512-sBwiXySHHjNmHQ4erf53cYsArXAAdFBAPY7/g+1Sp6aWA4krrLekD3yHmetgbfKq/pUy/MbZ8JgHTd68rx0mhg==",
"version": "0.8.60",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.60.tgz",
"integrity": "sha512-LkZalBcka1xVxkL7JnkF/1EzyvspLyrSthzyN9ZumWJw7kAaZkO9omraXv2t/UiFsqwMr5M/AV5UY915Vq8cxQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
@@ -4486,6 +4463,96 @@
"string-argv": "~0.3.1"
}
},
"node_modules/@sentry-internal/browser-utils": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz",
"integrity": "sha512-pLtu0Fa1Ou0v3M1OEO1MB1EONJVmXEGtoTwFRCO1RPQI2ulmkG6BikINClFG5IBpoYKZ33WkEXuM6U5xh+pdZg==",
"dependencies": {
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.48.0.tgz",
"integrity": "sha512-6PwcJNHVPg0EfZxmN+XxVOClfQpv7MBAweV8t9i5l7VFr8sM/7wPNSeU/cG7iK19Ug9ZEkBpzMOe3G4GXJ5bpw==",
"dependencies": {
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.48.0.tgz",
"integrity": "sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==",
"dependencies": {
"@sentry-internal/browser-utils": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.48.0.tgz",
"integrity": "sha512-LdivLfBXXB9us1aAc6XaL7/L2Ob4vi3C/fEOXElehg3qHjX6q6pewiv5wBvVXGX1NfZTRvu+X11k6TZoxKsezw==",
"dependencies": {
"@sentry-internal/replay": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/browser": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.48.0.tgz",
"integrity": "sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==",
"dependencies": {
"@sentry-internal/browser-utils": "8.48.0",
"@sentry-internal/feedback": "8.48.0",
"@sentry-internal/replay": "8.48.0",
"@sentry-internal/replay-canvas": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/core": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.48.0.tgz",
"integrity": "sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/vue": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-8.48.0.tgz",
"integrity": "sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==",
"dependencies": {
"@sentry/browser": "8.48.0",
"@sentry/core": "8.48.0"
},
"engines": {
"node": ">=14.18"
},
"peerDependencies": {
"pinia": "2.x",
"vue": "2.x || 3.x"
},
"peerDependenciesMeta": {
"pinia": {
"optional": true
}
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -4917,6 +4984,41 @@
"node": ">= 10"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.0.tgz",
"integrity": "sha512-yEIJ7xMKYQwyNRjxSdi4Gs37iszikAjxfky+3hu9bn24u8eHLJNDMAoOTyowp8p6EpSl8IQMdkfBx+WnJTttsw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/generator": "^7.26.2",
"@babel/parser": "^7.26.2",
"@babel/traverse": "^7.25.9",
"@babel/types": "^7.26.0",
"javascript-natural-sort": "^0.7.1",
"lodash": "^4.17.21"
},
"engines": {
"node": ">18.12"
},
"peerDependencies": {
"@vue/compiler-sfc": "3.x",
"prettier": "2.x - 3.x",
"prettier-plugin-svelte": "3.x",
"svelte": "4.x"
},
"peerDependenciesMeta": {
"@vue/compiler-sfc": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
},
"svelte": {
"optional": true
}
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@@ -6937,9 +7039,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001640",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz",
"integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==",
"version": "1.0.30001690",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
"dev": true,
"funding": [
{
@@ -6954,7 +7056,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
@@ -10353,6 +10456,13 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"dev": true,
"license": "MIT"
},
"node_modules/jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@@ -12106,15 +12216,16 @@
}
},
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=4"
"node": ">=6"
}
},
"node_modules/json-buffer": {
@@ -19421,21 +19532,22 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.23.5",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz",
"integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==",
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.23.3"
"zod": "^3.24.1"
}
},
"node_modules/zod-validation-error": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.6.10",
"version": "1.7.10",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -17,11 +17,9 @@
"update-litegraph": "node scripts/update-litegraph.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
"test": "jest --config jest.config.base.ts",
"test:jest:fast": "jest --config jest.config.fast.ts",
"test:jest:slow": "jest --config jest.config.slow.ts",
"test:generate:examples": "npx tsx tests-ui/extractExamples",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:jest": "jest --config jest.config.ts",
"test:generate": "npx tsx tests-ui/setup",
"test:browser": "npx playwright test",
"test:component": "vitest run src/components/",
@@ -30,7 +28,8 @@
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts"
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts"
},
"devDependencies": {
"@babel/core": "^7.24.7",
@@ -40,6 +39,7 @@
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.44.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
@@ -78,13 +78,15 @@
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0"
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.3",
"@comfyorg/litegraph": "^0.8.50",
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",

View File

@@ -8,7 +8,7 @@ const config: PlaywrightTestConfig = {
},
reporter: 'list',
timeout: 10000,
testMatch: /collect-i18n\.ts/
testMatch: /collect-i18n-.*\.ts/
}
export default config

View File

@@ -1,15 +1,13 @@
import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import type { ComfyCommandImpl } from '../src/stores/commandStore'
import type { FormItem, SettingParams } from '../src/types/settingTypes'
import type { ComfyApi } from '../src/scripts/api'
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
const commandsPath = './src/locales/en/commands.json'
const settingsPath = './src/locales/en/settings.json'
@@ -21,7 +19,7 @@ const extractMenuCommandLocaleStrings = (): Set<string> => {
return labels
}
test('collect-i18n', async ({ comfyPage }) => {
test('collect-i18n-general', async ({ comfyPage }) => {
const commands = (
await comfyPage.page.evaluate(() => {
const workspace = window['app'].extensionManager
@@ -128,105 +126,6 @@ test('collect-i18n', async ({ comfyPage }) => {
])
)
// Node Definitions
const nodeDefs: ComfyNodeDefImpl[] = Object.values(
await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
).map((def) => new ComfyNodeDefImpl(def))
console.log(`Collected ${nodeDefs.length} node definitions`)
const allDataTypesLocale = Object.fromEntries(
nodeDefs
.flatMap((nodeDef) => {
const inputDataTypes = Object.values(nodeDef.inputs.all).map(
(inputSpec) => inputSpec.type
)
const outputDataTypes = nodeDef.outputs.all.map((output) => output.type)
const allDataTypes = [...inputDataTypes, ...outputDataTypes].flatMap(
(type: string) => type.split(',')
)
return allDataTypes.map((dataType) => [
normalizeI18nKey(dataType),
dataType
])
})
.sort((a, b) => a[0].localeCompare(b[0]))
)
function extractInputs(nodeDef: ComfyNodeDefImpl) {
const inputs = Object.fromEntries(
nodeDef.inputs.all.flatMap((input) => {
const name = input.name
const tooltip = input.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
normalizeI18nKey(input.name),
{
name,
tooltip
}
]
]
})
)
return Object.keys(inputs).length > 0 ? inputs : undefined
}
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
const outputs = Object.fromEntries(
nodeDef.outputs.all.flatMap((output, i) => {
// Ignore data types if they are already translated in allDataTypesLocale.
const name = output.name in allDataTypesLocale ? undefined : output.name
const tooltip = output.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
i.toString(),
{
name,
tooltip
}
]
]
})
)
return Object.keys(outputs).length > 0 ? outputs : undefined
}
const allNodeDefsLocale = Object.fromEntries(
nodeDefs
.sort((a, b) => a.name.localeCompare(b.name))
.map((nodeDef) => [
normalizeI18nKey(nodeDef.name),
{
display_name: nodeDef.display_name ?? nodeDef.name,
description: nodeDef.description || undefined,
inputs: extractInputs(nodeDef),
outputs: extractOutputs(nodeDef)
}
])
)
const allNodeCategoriesLocale = Object.fromEntries(
nodeDefs.flatMap((nodeDef) =>
nodeDef.category
.split('/')
.map((category) => [normalizeI18nKey(category), category])
)
)
fs.writeFileSync(
localePath,
JSON.stringify(
@@ -240,16 +139,13 @@ test('collect-i18n', async ({ comfyPage }) => {
...allSettingCategoriesLocale
},
serverConfigItems: allServerConfigsLocale,
serverConfigCategories: allServerConfigCategoriesLocale,
dataTypes: allDataTypesLocale,
nodeCategories: allNodeCategoriesLocale
serverConfigCategories: allServerConfigCategoriesLocale
},
null,
2
)
)
fs.writeFileSync(nodeDefsPath, JSON.stringify(allNodeDefsLocale, null, 2))
fs.writeFileSync(commandsPath, JSON.stringify(allCommandsLocale, null, 2))
fs.writeFileSync(settingsPath, JSON.stringify(allSettingsLocale, null, 2))
})

View File

@@ -0,0 +1,125 @@
import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import type { ComfyApi } from '../src/scripts/api'
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
test('collect-i18n-node-defs', async ({ comfyPage }) => {
const nodeDefs: ComfyNodeDefImpl[] = Object.values(
await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
).map((def) => new ComfyNodeDefImpl(def))
console.log(`Collected ${nodeDefs.length} node definitions`)
const allDataTypesLocale = Object.fromEntries(
nodeDefs
.flatMap((nodeDef) => {
const inputDataTypes = Object.values(nodeDef.inputs.all).map(
(inputSpec) => inputSpec.type
)
const outputDataTypes = nodeDef.outputs.all.map((output) => output.type)
const allDataTypes = [...inputDataTypes, ...outputDataTypes].flatMap(
(type: string) => type.split(',')
)
return allDataTypes.map((dataType) => [
normalizeI18nKey(dataType),
dataType
])
})
.sort((a, b) => a[0].localeCompare(b[0]))
)
function extractInputs(nodeDef: ComfyNodeDefImpl) {
const inputs = Object.fromEntries(
nodeDef.inputs.all.flatMap((input) => {
const name = input.name
const tooltip = input.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
normalizeI18nKey(input.name),
{
name,
tooltip
}
]
]
})
)
return Object.keys(inputs).length > 0 ? inputs : undefined
}
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
const outputs = Object.fromEntries(
nodeDef.outputs.all.flatMap((output, i) => {
// Ignore data types if they are already translated in allDataTypesLocale.
const name = output.name in allDataTypesLocale ? undefined : output.name
const tooltip = output.tooltip
if (name === undefined && tooltip === undefined) {
return []
}
return [
[
i.toString(),
{
name,
tooltip
}
]
]
})
)
return Object.keys(outputs).length > 0 ? outputs : undefined
}
const allNodeDefsLocale = Object.fromEntries(
nodeDefs
.sort((a, b) => a.name.localeCompare(b.name))
.map((nodeDef) => [
normalizeI18nKey(nodeDef.name),
{
display_name: nodeDef.display_name ?? nodeDef.name,
description: nodeDef.description || undefined,
inputs: extractInputs(nodeDef),
outputs: extractOutputs(nodeDef)
}
])
)
const allNodeCategoriesLocale = Object.fromEntries(
nodeDefs.flatMap((nodeDef) =>
nodeDef.category
.split('/')
.map((category) => [normalizeI18nKey(category), category])
)
)
const locale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
fs.writeFileSync(
localePath,
JSON.stringify(
{
...locale,
dataTypes: allDataTypesLocale,
nodeCategories: allNodeCategoriesLocale
},
null,
2
)
)
fs.writeFileSync(nodeDefsPath, JSON.stringify(allNodeDefsLocale, null, 2))
})

View File

@@ -1,8 +1,10 @@
import { copy } from 'fs-extra'
import { config } from 'dotenv'
import { copy } from 'fs-extra'
config()
const sourceDir = './dist'
// eslint-disable-next-line no-undef
const targetDir = process.env.DEPLOY_COMFYUI_DIR
copy(sourceDir, targetDir)

View File

@@ -1,12 +1,12 @@
import {
readFileSync,
writeFileSync,
readdirSync,
mkdirSync,
existsSync,
rmSync
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync
} from 'fs'
import { join, dirname } from 'path'
import { dirname, join } from 'path'
// Ensure directories exist
function ensureDir(dir: string) {

View File

@@ -0,0 +1,35 @@
import fs from 'fs'
import path from 'path'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { zComfyWorkflow, zComfyWorkflow1 } from '../src/types/comfyWorkflow'
// Convert both workflow schemas to JSON Schema
const workflow04Schema = zodToJsonSchema(zComfyWorkflow, {
name: 'ComfyWorkflow0_4',
$refStrategy: 'none'
})
const workflow1Schema = zodToJsonSchema(zComfyWorkflow1, {
name: 'ComfyWorkflow1_0',
$refStrategy: 'none'
})
// Create output directory if it doesn't exist
const outputDir = './schemas'
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
// Write schemas to files
fs.writeFileSync(
path.join(outputDir, 'workflow-0_4.json'),
JSON.stringify(workflow04Schema, null, 2)
)
fs.writeFileSync(
path.join(outputDir, 'workflow-1_0.json'),
JSON.stringify(workflow1Schema, null, 2)
)
console.log('JSON Schemas generated successfully!')

View File

@@ -1,71 +0,0 @@
import os
import sys
import requests
import zipfile
import shutil
import git
import tempfile
def download_and_extract(version, temp_dir):
url = f"https://github.com/Comfy-Org/ComfyUI_frontend/releases/download/v{version}/dist.zip"
response = requests.get(url)
if response.status_code == 200:
zip_path = os.path.join(temp_dir, "dist.zip")
with open(zip_path, "wb") as file:
file.write(response.content)
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
# Clean up the zip file after extraction
os.remove(zip_path)
else:
raise Exception(
f"Failed to download release asset. Status code: {response.status_code}"
)
def update_repo(repo_path, version, temp_dir):
repo = git.Repo(repo_path)
# Stash any changes
repo.git.stash()
# Create and checkout new branch
new_branch = f"release-{version}"
repo.git.checkout("-b", new_branch, "-t", "origin/master")
# Remove all files under web/ directory
web_dir = os.path.join(repo_path, "web")
if os.path.exists(web_dir):
shutil.rmtree(web_dir)
# Move content from temp_dir to web/
shutil.move(temp_dir, web_dir)
# Add changes and commit
repo.git.add(all=True)
commit_message = f"Update web content to release v{version}"
repo.git.commit("-m", commit_message)
def main(repo_path: str, version: str):
with tempfile.TemporaryDirectory() as temp_dir:
try:
download_and_extract(version, temp_dir)
update_repo(repo_path, version, temp_dir)
print(f"Successfully updated repo to release v{version}")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python script.py <repo_path> <version>")
sys.exit(1)
repo_path = sys.argv[1]
version = sys.argv[2]
main(repo_path, version)

View File

@@ -9,13 +9,15 @@
</template>
<script setup lang="ts">
import config from '@/config'
import { computed, onMounted } from 'vue'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useEventListener } from '@vueuse/core'
import BlockUI from 'primevue/blockui'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import { useEventListener } from '@vueuse/core'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron, showNativeMenu } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()

View File

@@ -10,6 +10,7 @@
--bg-color: #fff;
--comfy-menu-bg: #353535;
--comfy-menu-secondary-bg: #292929;
--comfy-topbar-height: 2.5rem;
--comfy-input-bg: #222;
--input-text: #ddd;
--descrip-text: #999;
@@ -763,3 +764,17 @@ audio.comfy-audio.empty-audio-widget {
.p-tree-node-content {
padding: var(--comfy-tree-explorer-item-padding) !important;
}
/* [Desktop] Electron window specific styles */
.app-drag {
app-region: drag;
}
.no-drag {
app-region: no-drag;
}
.window-actions-spacer {
width: calc(100vw - env(titlebar-area-width, 100vw));
}
/* End of [Desktop] Electron window specific styles */

View File

@@ -27,6 +27,7 @@
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#AAA",
"NODE_TEXT_HIGHLIGHT_COLOR": "#FFF",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#333",
"NODE_DEFAULT_BGCOLOR": "#353535",

View File

@@ -1,6 +1,7 @@
{
"id": "light",
"name": "Light",
"light_theme": true,
"colors": {
"node_slot": {
"CLIP": "#FFA726",
@@ -13,7 +14,12 @@
"MASK": "#9CCC65",
"MODEL": "#7E57C2",
"STYLE_MODEL": "#D4E157",
"VAE": "#FF7043"
"VAE": "#FF7043",
"NOISE": "#B0B0B0",
"GUIDER": "#66FFFF",
"SAMPLER": "#ECB4B4",
"SIGMAS": "#CDFFCD",
"TAESD": "#DCC274"
},
"litegraph_base": {
"BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==",
@@ -22,6 +28,7 @@
"NODE_SELECTED_TITLE_COLOR": "#000",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#444",
"NODE_TEXT_HIGHLIGHT_COLOR": "#1e293b",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#F7F7F7",
"NODE_DEFAULT_BGCOLOR": "#F5F5F5",

View File

@@ -5,11 +5,12 @@
</template>
<script setup lang="ts">
import { useTitle } from '@vueuse/core'
import { computed } from 'vue'
import { useExecutionStore } from '@/stores/executionStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useTitle } from '@vueuse/core'
import { computed } from 'vue'
const DEFAULT_TITLE = 'ComfyUI'
const TITLE_SUFFIX = ' - ComfyUI'

View File

@@ -46,13 +46,14 @@
</template>
<script setup lang="ts">
import { useSettingStore } from '@/stores/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')

View File

@@ -1,24 +1,31 @@
<template>
<Button
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger"
class="comfy-menu-hamburger no-drag"
:style="positionCSS"
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
@click="exitFocusMode"
@contextmenu="showNativeMenu"
/>
>
<Button
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"
@contextmenu="showNativeMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { computed, CSSProperties, watchEffect } from 'vue'
import { CSSProperties, computed, watchEffect } from 'vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { showNativeMenu } from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()
@@ -50,8 +57,6 @@ const positionCSS = computed<CSSProperties>(() =>
<style scoped>
.comfy-menu-hamburger {
pointer-events: auto;
position: fixed;
z-index: 9999;
@apply pointer-events-auto fixed z-[9999] flex flex-row;
}
</style>

View File

@@ -3,6 +3,7 @@
class="batch-count"
:class="props.class"
v-tooltip.bottom="$t('menu.batchCount')"
:aria-label="$t('menu.batchCount')"
>
<InputNumber
class="w-14"
@@ -30,12 +31,13 @@
</template>
<script lang="ts" setup>
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { storeToRefs } from 'pinia'
import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
interface Props {
class?: string
}

View File

@@ -13,10 +13,6 @@
</template>
<script lang="ts" setup>
import { computed, inject, nextTick, onMounted, Ref, ref, watch } from 'vue'
import Panel from 'primevue/panel'
import ComfyQueueButton from './ComfyQueueButton.vue'
import { useSettingStore } from '@/stores/settingStore'
import {
useDraggable,
useElementBounding,
@@ -26,6 +22,12 @@ import {
watchDebounced
} from '@vueuse/core'
import { clamp } from 'lodash'
import Panel from 'primevue/panel'
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import ComfyQueueButton from './ComfyQueueButton.vue'
const settingsStore = useSettingStore()

View File

@@ -39,6 +39,7 @@
:severity="executingPrompt ? 'danger' : 'secondary'"
:disabled="!executingPrompt"
text
:aria-label="$t('menu.interrupt')"
@click="() => commandStore.execute('Comfy.Interrupt')"
>
</Button>
@@ -48,6 +49,7 @@
:severity="hasPendingTasks ? 'danger' : 'secondary'"
:disabled="!hasPendingTasks"
text
:aria-label="$t('sideToolbar.queueTab.clearPendingTasks')"
@click="() => commandStore.execute('Comfy.ClearPendingTasks')"
/>
</ButtonGroup>
@@ -55,20 +57,22 @@
</template>
<script setup lang="ts">
import SplitButton from 'primevue/splitbutton'
import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import BatchCountEdit from './BatchCountEdit.vue'
import ButtonGroup from 'primevue/buttongroup'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import {
useQueuePendingTaskCountStore,
useQueueSettingsStore
} from '@/stores/queueStore'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import BatchCountEdit from './BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())

View File

@@ -41,9 +41,10 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Tabs from 'primevue/tabs'
import TabList from 'primevue/tablist'
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'

View File

@@ -7,7 +7,8 @@
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue'
import { Ref, ref } from 'vue'
import { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
const emit = defineEmits<{

View File

@@ -3,10 +3,12 @@
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, Ref } from 'vue'
import { IDisposable } from '@xterm/xterm'
import { Ref, onMounted, onUnmounted } from 'vue'
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
import { electronAPI } from '@/utils/envUtil'
import { IDisposable } from '@xterm/xterm'
import BaseTerminal from './BaseTerminal.vue'
const terminalCreated = (

View File

@@ -10,15 +10,17 @@
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, Ref, ref } from 'vue'
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore'
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import BaseTerminal from './BaseTerminal.vue'
import ProgressSpinner from 'primevue/progressspinner'
import { Ref, onMounted, onUnmounted, ref } from 'vue'
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore'
import { LogEntry, LogsWsMessage, TerminalSize } from '@/types/apiTypes'
import BaseTerminal from './BaseTerminal.vue'
const errorMessage = ref('')
const loading = ref(true)

View File

@@ -0,0 +1,40 @@
<template>
<div :class="['flex flex-wrap', $attrs.class]">
<div
v-for="checkbox in checkboxes"
:key="checkbox.value"
class="flex items-center gap-2"
>
<Checkbox
v-model="internalSelection"
:inputId="checkbox.value"
:value="checkbox.value"
/>
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
</div>
</div>
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import { computed } from 'vue'
interface CheckboxItem {
label: string
value: string
}
const props = defineProps<{
checkboxes: CheckboxItem[]
modelValue: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const internalSelection = computed({
get: () => props.modelValue,
set: (value: string[]) => emit('update:modelValue', value)
})
</script>

View File

@@ -12,12 +12,14 @@
:data-test="src"
class="comfy-image-blur"
:style="{ 'background-image': `url(${src})` }"
:alt="alt"
/>
<img
:src="src"
@error="handleImageError"
class="comfy-image-main"
:class="[...classArray]"
:alt="alt"
/>
</span>
<div v-if="imageBroken" class="broken-image-placeholder">
@@ -34,9 +36,11 @@ const props = withDefaults(
src: string
class?: string | string[] | object
contain: boolean
alt?: string
}>(),
{
contain: false
contain: false,
alt: 'Image content'
}
)

View File

@@ -3,7 +3,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { onMounted, ref, watch } from 'vue'
const props = defineProps<{
renderFunction: () => HTMLElement

View File

@@ -26,6 +26,7 @@
:options="colorOptions"
optionLabel="name"
dataKey="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
@@ -70,15 +71,16 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import Dialog from 'primevue/dialog'
import SelectButton from 'primevue/selectbutton'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ColorPicker from 'primevue/colorpicker'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import Dialog from 'primevue/dialog'
import Divider from 'primevue/divider'
import SelectButton from 'primevue/selectbutton'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
const { t } = useI18n()
const props = defineProps<{

View File

@@ -75,13 +75,14 @@
</template>
<script setup lang="ts">
import { useDownload } from '@/hooks/downloadHooks'
import Button from 'primevue/button'
import ProgressBar from 'primevue/progressbar'
import { ref, computed } from 'vue'
import { formatSize } from '@/utils/formatUtil'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDownload } from '@/hooks/downloadHooks'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string

View File

@@ -15,9 +15,10 @@
</template>
<script setup lang="ts">
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
import { onBeforeUnmount } from 'vue'
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
const props = defineProps<{
extension: VueExtension | CustomExtension
}>()

View File

@@ -24,9 +24,10 @@
</template>
<script setup lang="ts">
import { useDownload } from '@/hooks/downloadHooks'
import Button from 'primevue/button'
import { computed } from 'vue'
import { useDownload } from '@/hooks/downloadHooks'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{

View File

@@ -42,8 +42,8 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Button from 'primevue/button'
import { ref } from 'vue'
const props = defineProps<{
modelValue: string

View File

@@ -25,16 +25,17 @@
</template>
<script setup lang="ts">
import { FormItem } from '@/types/settingTypes'
import { markRaw, type Component } from 'vue'
import InputText from 'primevue/inputtext'
import InputNumber from 'primevue/inputnumber'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
import ToggleSwitch from 'primevue/toggleswitch'
import { type Component, markRaw } from 'vue'
import CustomFormValue from '@/components/common/CustomFormValue.vue'
import InputSlider from '@/components/common/InputSlider.vue'
import FormImageUpload from '@/components/common/FormImageUpload.vue'
import FormColorPicker from '@/components/common/FormColorPicker.vue'
import FormImageUpload from '@/components/common/FormImageUpload.vue'
import InputSlider from '@/components/common/InputSlider.vue'
import { FormItem } from '@/types/settingTypes'
const formValue = defineModel<any>('formValue')
const props = defineProps<{

View File

@@ -24,9 +24,9 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import InputNumber from 'primevue/inputnumber'
import Slider from 'primevue/slider'
import { ref, watch } from 'vue'
const props = defineProps<{
modelValue: number

View File

@@ -19,8 +19,8 @@
</template>
<script setup lang="ts">
import Card from 'primevue/card'
import Button from 'primevue/button'
import Card from 'primevue/card'
const props = defineProps<{
class?: string

View File

@@ -42,15 +42,16 @@
</template>
<script setup lang="ts" generic="TFilter extends SearchFilter">
import type { SearchFilter } from './SearchFilterChip.vue'
import { debounce } from 'lodash'
import Button from 'primevue/button'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import Button from 'primevue/button'
import SearchFilterChip from './SearchFilterChip.vue'
import { toRefs } from 'vue'
import type { SearchFilter } from './SearchFilterChip.vue'
import SearchFilterChip from './SearchFilterChip.vue'
const props = withDefaults(
defineProps<{
class?: string

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