Compare commits

..

74 Commits

Author SHA1 Message Date
Austin Mroz
9e8a426c00 Update test for previews fix 2025-09-28 00:19:46 -05:00
Austin Mroz
cc7c22a428 Fix preview initialization 2025-09-28 00:09:19 -05:00
Austin Mroz
94f6778285 Move SubgraphNode components 2025-09-27 16:19:03 -05:00
Austin Mroz
e91bbf030b Fix test cases
Turns out the vuedraggable import has side effects that break other
test cases. This is awful and requires further investigation, but for
now, the import is performed at execution time
2025-09-27 15:34:39 -05:00
Austin Mroz
44d57ce041 Move widget configuration out of sidebar 2025-09-27 12:23:45 -05:00
Austin Mroz
7c20f37973 Finish partial migrations from merge 2025-09-26 14:36:31 -05:00
Austin Mroz
e1971a2595 Merge working branch with main 2025-09-26 13:19:40 -05:00
Austin Mroz
992e4a7f8a Display recommended widgets on conversion.
This code will need refactoring later.
2025-09-26 09:37:06 -05:00
Austin Mroz
92f2578357 Prevent doubled promotion, add demotion option 2025-09-26 08:48:27 -05:00
Austin Mroz
aeafee4e12 Update promoted widget outline color 2025-09-26 08:47:08 -05:00
Austin Mroz
040c6e1691 Disable serialization of proxyWidgets
Also sorts the overlay property values
2025-09-25 14:02:53 -05:00
Austin Mroz
f1c2d0ef6a Add promoted border to DOMWidgets 2025-09-25 11:00:19 -05:00
Austin Mroz
0de4351913 Only update domWidgetStore if graph is active 2025-09-25 10:51:04 -05:00
Austin Mroz
3efb76b765 Add logic to compute widget.prompted
NOTE: Currently fails to track state with nested subgraphs
The event for "Leaving a graph" does not indicate the direction
2025-09-25 10:42:32 -05:00
Austin Mroz
a32fc7a667 Add display code to indicate widget is promoted
Actually calculating that this should be displayed will come later
2025-09-24 15:53:30 -05:00
Austin Mroz
de399e2e6c Add right click widget -> promote inside subgraph 2025-09-24 14:49:14 -05:00
Austin Mroz
768ffb2c9f For "Show Recommended" keep existing promotions 2025-09-24 10:59:14 -05:00
Austin Mroz
91a6f9d762 Fix hiding of non-proxied widgets 2025-09-23 13:40:24 -05:00
Austin Mroz
4ba1e5e485 Remove test for reading proxyWidgets 2025-09-22 16:41:46 -05:00
Austin Mroz
31a40e9769 Move from wrapping an interal method to onConfigure 2025-09-22 15:44:48 -05:00
Austin Mroz
dd58b272b8 Conditionally display show recommended
Also use a slightly nicer button as placeholder
2025-09-22 14:45:05 -05:00
Austin Mroz
e67bd12168 Reimplement disconnect/reconnect logic, add test 2025-09-22 13:32:58 -05:00
Austin Mroz
ebe92ed884 Add widget positioning test 2025-09-22 12:44:57 -05:00
Austin Mroz
e902afc828 Update comments 2025-09-22 09:43:49 -05:00
Austin Mroz
3d279716a3 Split handler into individual functions 2025-09-21 16:24:48 -05:00
Austin Mroz
b00855be3e Use computedHeight for preview display 2025-09-19 18:08:11 -05:00
Austin Mroz
915870d47c Fix accidental style breakage 2025-09-19 15:07:40 -05:00
Austin Mroz
0e8679798b Guard node proxying by widgetName so both function 2025-09-19 14:53:46 -05:00
Austin Mroz
f63a76e41a Repurpose existing graph traversal util 2025-09-19 10:52:21 -05:00
Austin Mroz
3e677e3dbe Add show recommended button, preview work
Adds the framework for a system to automate display of a curated list of
recommended widgets to the node.

As part of this, a return to display of "image previews" was made.
This code is causing lots of problems. Much of the logic is dependent
upon the actual node going through the draw loop. As nodes in the
subgraph don't receive redraws, there's lots of issues with managing the
initial display and ensuring that an initial draw occurs.

This commit includes support for updating previews, but is more brittle
than I would like.
2025-09-19 09:51:04 -05:00
Austin Mroz
02bf937741 Clean up widget item styling 2025-09-16 18:11:24 -05:00
Austin Mroz
784ebfe331 Styling cleanup, fix node size on append 2025-09-16 17:16:36 -05:00
Austin Mroz
26a9527d23 Actually test that widget names aren't equal 2025-09-16 14:41:59 -05:00
Austin Mroz
b287bb645b Fix unused exports to satisfy knip 2025-09-16 14:23:50 -05:00
Austin Mroz
ee5903877a More tests with better organization 2025-09-16 14:18:03 -05:00
Austin Mroz
1d7546f9bf Add basic tests 2025-09-16 12:34:47 -05:00
Austin Mroz
dbb9a9acd4 Move proxyWidgets out of extensions dir 2025-09-16 12:25:00 -05:00
Austin Mroz
dcc056754d Remove extension code and setTimeout 2025-09-16 11:13:01 -05:00
Austin Mroz
4a7bc4aadf Fix typo in last_y breaking mouse events
Further prune hot loop, and remove cast
2025-09-16 01:19:47 -05:00
Austin Mroz
3187694a31 Simplify proxy handler code 2025-09-15 17:57:47 -05:00
Austin Mroz
9b6611dd20 Move schema to dedicated file, remove cast 2025-09-15 16:35:25 -05:00
Austin Mroz
ccc8d6e441 serialize to string at proxyWidgets boundry
While node.properties function with anything serializeable, the format
for proxyWidgets is not a valid option for type. After great
consideration, all access to and from this value goes through a JSON
serialization and parsing always includes a zod validation step.

This is sturdier to outside misuse, has even lower risk of custom node
breakage, and means that there's now proper type checking at the
boundries of interaction.

Performance was a major concern against this, but the path is quite
cold. I estimate the value of optimization here to be 3-4 orders of
magnitude less important than anything occuring during the draw loop
(like access to proxyWidget elements)
2025-09-15 15:44:15 -05:00
Austin Mroz
166cdc385c Fix type checking on ProxyWidget implementation 2025-09-13 13:05:27 -05:00
Austin Mroz
7fd4fc556a Swap label order: cleaner and more consistent 2025-09-13 10:36:47 -05:00
Austin Mroz
ed2cc26cff Implement a DisconnectedWidget, refactor to use 2025-09-13 10:33:31 -05:00
Austin Mroz
073175a719 Fix typechecking and linting
Note: the proxywidget code is still marked as ignored. This will require
further careful review in the future.
2025-09-12 13:19:47 -05:00
Austin Mroz
ec06e28af7 Pull proxyWidget code out of SubgraphNode
This was needed from an organizational standpoint. For now, it requires
an ugly setTimeout to prevent proxyWidgets from being clobbered during
initialization, but this will be cleaned up later.

This also allows for the proxy widget code to have type checks ignored.
I fully intend to find a functional solution here, but this provides a
migration path where typechecking can be enabled for the rest of the PR
first

Also cleans up type checking on graph change in scripts/app.ts
2025-09-12 09:42:08 -05:00
Austin Mroz
5d4c4ef63e Restrict show/hide all to search query 2025-09-12 08:06:36 -05:00
Austin Mroz
eb85406d70 linting 2025-09-11 17:36:39 -05:00
Austin Mroz
e820515db4 On widget change, also set back canvas dirty 2025-09-11 17:24:12 -05:00
Austin Mroz
b9dcb813d8 Implement show/hide all. dw state in prop handler 2025-09-11 17:03:48 -05:00
Austin Mroz
1e72ed8a78 Cursor fixes, show/hide all framework 2025-09-11 15:10:48 -05:00
Austin Mroz
4016929070 Merge main into austin/widgets-v2 2025-09-11 13:04:34 -05:00
Austin Mroz
e0eb699a93 Add searchbar 2025-09-11 12:02:34 -05:00
Austin Mroz
68bb29bb17 Add vuedraggable as dependency 2025-09-10 16:11:48 -05:00
Austin Mroz
bb25301e36 Update widget state after visibility toggle 2025-09-10 15:37:31 -05:00
Austin Mroz
51d68654f1 Spacing and filtering cleanup 2025-09-10 14:19:40 -05:00
Austin Mroz
ec09d9b365 Working drag and drop 2025-09-10 12:32:02 -05:00
Austin Mroz
2212151686 Fix removal of DOM Widgets
Needs further review for what the actual purpose of the active state in
the widget store is if it's not being used for determining visibility
2025-09-05 16:43:34 -05:00
Austin Mroz
eab71667b6 Better computedDisabled handling
computedDisabled is inherited, but propogates
2025-09-05 11:21:51 -05:00
Austin Mroz
b424c62a99 wip cleanup, filtering, setdirty 2025-09-05 10:11:51 -05:00
Austin Mroz
4150b1ccb4 Minor formatting fix and log removal 2025-09-04 15:02:50 -05:00
Austin Mroz
985c0beb8e Immediately display DOMWidgets when added 2025-09-04 13:31:49 -05:00
Austin Mroz
9b18125d16 Basic widget toggling 2025-09-04 13:13:18 -05:00
Austin Mroz
1cca658351 Functional listing of widgets using TreeExplorer 2025-09-04 12:05:58 -05:00
Austin Mroz
e4c6514d9d Minimal subgraph widget ui framework
Displays all widgets poorly and allows re ordering
2025-09-01 18:08:06 -07:00
Austin Mroz
e6aeaf4112 Add persistence to proxy widgets 2025-09-01 15:44:39 -07:00
Austin Mroz
11978b6b71 Mostly functional DOMWidgets 2025-09-01 10:12:12 -07:00
Austin Mroz
703dc8fd4c Remove private variables, simplify logic
Turns out, ComboWidget was the only one using private variables.
Everything else is simple
2025-08-30 07:44:36 -07:00
Austin Mroz
ca7698dae5 what even is a reciever 2025-08-29 14:40:41 -07:00
Austin Mroz
b505bd9a80 More private suffering 2025-08-29 10:09:49 -07:00
Austin Mroz
d367f901a0 wip 2025-08-29 10:08:54 -07:00
Austin Mroz
7bd6fa45cf further exp 2025-08-28 11:54:42 -07:00
Austin Mroz
5be68c03fc Wip mirror widget test 2025-08-27 21:15:12 -07:00
306 changed files with 24219 additions and 28713 deletions

2
.gitattributes vendored
View File

@@ -12,5 +12,5 @@
*.yaml text eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true
src/types/comfyRegistryTypes.ts linguist-generated=true
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true

View File

@@ -1,67 +0,0 @@
name: Setup Frontend
description: 'Setup ComfyUI frontend development environment'
inputs:
extra_server_params:
description: 'Additional parameters to pass to ComfyUI server'
required: false
default: ''
runs:
using: 'composite'
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Copy ComfyUI_devtools from frontend repo
shell: bash
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
shell: bash
working-directory: ComfyUI
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
- name: Build & Install ComfyUI_frontend
shell: bash
working-directory: ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Start ComfyUI server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist ${{ inputs.extra_server_params }} &
wait-for-it --service 127.0.0.1:8188 -t 600

View File

@@ -60,7 +60,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -1,4 +1,4 @@
name: Storybook and Chromatic CI
name: 'Chromatic'
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
@@ -8,100 +8,13 @@ on:
branches: [main]
jobs:
# Post starting comment for non-forked PRs
comment-on-pr-start:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting" \
"$(date -u '+%m/%d/%Y, %I:%M:%S %p')"
# Build Storybook for all PRs (free Cloudflare deployment)
storybook-build:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
outputs:
conclusion: ${{ steps.job-status.outputs.conclusion }}
workflow-url: ${{ steps.workflow-url.outputs.url }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Storybook
run: pnpm build-storybook
- name: Set job status
id: job-status
if: always()
run: |
echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
- name: Get workflow URL
id: workflow-url
if: always()
run: |
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4
with:
name: storybook-static
path: storybook-static/
retention-days: 7
# Chromatic deployment only for version-bump-* branches or manual triggers
chromatic-deployment:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'version-bump-'))
outputs:
conclusion: ${{ steps.job-status.outputs.conclusion }}
workflow-url: ${{ steps.workflow-url.outputs.url }}
chromatic-build-url: ${{ steps.chromatic.outputs.buildUrl }}
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
# Only run for PRs from version-bump-* branches or manual triggers
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Chromatic baseline
@@ -116,6 +29,7 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
@@ -140,92 +54,4 @@ jobs:
buildScriptName: build-storybook
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
onlyChanged: true # Only capture changed stories
- name: Set job status
id: job-status
if: always()
run: |
echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
- name: Get workflow URL
id: workflow-url
if: always()
run: |
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
# Deploy and comment for non-forked PRs only
deploy-and-comment:
needs: [storybook-build]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && always()
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: storybook-static
path: storybook-static
- name: Make deployment script executable
run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
- name: Deploy Storybook and comment on PR
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
WORKFLOW_CONCLUSION: ${{ needs.storybook-build.outputs.conclusion }}
WORKFLOW_URL: ${{ needs.storybook-build.outputs.workflow-url }}
run: |
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"completed"
# Update comment with Chromatic URLs for version-bump branches
update-comment-with-chromatic:
needs: [chromatic-deployment, deploy-and-comment]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && startsWith(github.head_ref, 'version-bump-') && needs.chromatic-deployment.outputs.chromatic-build-url != ''
permissions:
pull-requests: write
steps:
- name: Update comment with Chromatic URLs
uses: actions/github-script@v7
with:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
// Find the existing Storybook comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.pull_request.number }}
});
const storybookComment = comments.find(comment =>
comment.body.includes('<!-- STORYBOOK_BUILD_STATUS -->')
);
if (storybookComment && buildUrl && storybookUrl) {
// Append Chromatic info to existing comment
const updatedBody = storybookComment.body.replace(
/---\n(.*)$/s,
`---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: storybookComment.id,
body: updatedBody
});
}

View File

@@ -50,7 +50,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}

View File

@@ -15,7 +15,7 @@ jobs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Download dist artifact
uses: actions/download-artifact@v4
with:

View File

@@ -1,26 +0,0 @@
name: Devtools Python Check
on:
pull_request:
paths:
- 'tools/devtools/**'
push:
branches: [ main ]
paths:
- 'tools/devtools/**'
jobs:
syntax:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Validate Python syntax
run: python3 -m compileall -q tools/devtools

View File

@@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: comfyanonymous/ComfyUI
path: ComfyUI
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
@@ -37,7 +37,7 @@ jobs:
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Checkout custom node repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
@@ -78,7 +78,7 @@ jobs:
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
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
@@ -86,7 +86,7 @@ jobs:
run: pnpm dev:electron &
working-directory: ComfyUI_frontend
- name: Capture base i18n
run: pnpm exec tsx scripts/diff-i18n capture
run: npx tsx scripts/diff-i18n capture
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n
@@ -99,7 +99,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Diff base vs updated i18n
run: pnpm exec tsx scripts/diff-i18n diff
run: npx tsx scripts/diff-i18n diff
working-directory: ComfyUI_frontend
- name: Update i18n in custom node repository
run: |

View File

@@ -13,10 +13,9 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
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

View File

@@ -14,8 +14,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache tool outputs
uses: actions/cache@v4
@@ -34,7 +33,7 @@ jobs:
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
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

View File

@@ -1,15 +0,0 @@
name: Validate JSON
on:
push:
branches:
- main
pull_request:
jobs:
json-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}

View File

@@ -30,7 +30,7 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Get PR Number
id: pr

View File

@@ -0,0 +1,126 @@
name: PR Storybook Comment
on:
workflow_run:
workflows: ['Chromatic']
types: [requested, completed]
jobs:
comment-storybook:
runs-on: ubuntu-latest
if: >-
github.repository == 'Comfy-Org/ComfyUI_frontend'
&& github.event.workflow_run.event == 'pull_request'
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Log when no PR found
if: steps.pr.outputs.result == 'null'
run: |
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
echo "Event: ${{ github.event.workflow_run.event }}"
- name: Get workflow run details
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
id: workflow-run
uses: actions/github-script@v7
with:
script: |
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
return {
conclusion: run.data.conclusion,
html_url: run.data.html_url
};
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Storybook Started
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Build is starting...**
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🎨 Running Chromatic visual tests...
---
⏱️ Please wait while the Storybook build is in progress...
- name: Comment PR - Storybook Complete
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|| '❌'
}} **${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|| 'Build failed!'
}}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 🔗 Links
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
---
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && ' Chromatic was skipped for this PR.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && ' The Chromatic run was cancelled.'
|| '⚠️ Please check the workflow logs for error details.'
}}

View File

@@ -1,90 +0,0 @@
name: PR Storybook Deploy (Forks)
on:
workflow_run:
workflows: ['Storybook and Chromatic CI']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name
permissions:
pull-requests: write
actions: read
steps:
- name: Log workflow trigger info
run: |
echo "Repository: ${{ github.repository }}"
echo "Event: ${{ github.event.workflow_run.event }}"
echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}"
echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}"
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
- name: Get PR Number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
});
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
if (!pr) {
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
return null;
}
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
return pr.number;
- name: Handle Storybook Start
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
- name: Download and Deploy Storybook
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: storybook-static
path: storybook-static
- name: Handle Storybook Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
WORKFLOW_URL: ${{ github.event.workflow_run.html_url }}
run: |
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"

View File

@@ -10,10 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- name: Checkout workflow repo
uses: actions/checkout@v5
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
@@ -22,11 +19,11 @@ jobs:
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests and update snapshots
id: playwright-tests
run: pnpm exec playwright test --update-snapshots
run: npx playwright test --update-snapshots
continue-on-error: true
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4

View File

@@ -15,14 +15,14 @@ jobs:
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
@@ -148,7 +148,7 @@ jobs:
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
run: npx playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
working-directory: ComfyUI_frontend
env:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report
@@ -230,7 +230,7 @@ jobs:
run: |
# Run tests with both HTML and JSON reporters
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --project=${{ matrix.browser }} \
npx playwright test --project=${{ matrix.browser }} \
--reporter=list \
--reporter=html \
--reporter=json
@@ -250,7 +250,7 @@ jobs:
if: ${{ !cancelled() }}
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
@@ -281,10 +281,10 @@ jobs:
- name: Merge into HTML Report
run: |
# Generate HTML report
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
npx playwright merge-reports --reporter=html ./all-blob-reports
# Generate JSON report separately with explicit output path
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
npx playwright merge-reports --reporter=json ./all-blob-reports
working-directory: ComfyUI_frontend
- name: Upload HTML report
@@ -306,7 +306,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Get start time
id: start-time
@@ -333,7 +333,7 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Download all playwright reports
uses: actions/download-artifact@v4

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -51,7 +51,7 @@ jobs:
comfyui-manager-repo-${{ runner.os }}-
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager
@@ -68,7 +68,7 @@ jobs:
- name: Generate Manager API types
run: |
echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..."
pnpm dlx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
- name: Validate generated types
run: |

View File

@@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -50,7 +50,7 @@ jobs:
comfy-api-repo-${{ runner.os }}-
- name: Checkout comfy-api repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: Comfy-Org/comfy-api
path: comfy-api
@@ -68,18 +68,17 @@ jobs:
- name: Generate API types
run: |
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
mkdir -p ./packages/registry-types/src
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./packages/registry-types/src/comfyRegistryTypes.ts
npx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
- name: Validate generated types
run: |
if [ ! -f ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
if [ ! -f ./src/types/comfyRegistryTypes.ts ]; then
echo "Error: Types file was not generated."
exit 1
fi
# Check if file is not empty
if [ ! -s ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
if [ ! -s ./src/types/comfyRegistryTypes.ts ]; then
echo "Error: Generated types file is empty."
exit 1
fi
@@ -87,12 +86,12 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated Comfy Registry API types..."
pnpm lint:fix:no-cache -- ./packages/registry-types/src/comfyRegistryTypes.ts
pnpm lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
- name: Check for changes
id: check-changes
run: |
if [[ -z $(git status --porcelain ./packages/registry-types/src/comfyRegistryTypes.ts) ]]; then
if [[ -z $(git status --porcelain ./src/types/comfyRegistryTypes.ts) ]]; then
echo "No changes to Comfy Registry API types detected."
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
@@ -122,4 +121,4 @@ jobs:
labels: CNR
delete-branch: true
add-paths: |
packages/registry-types/src/comfyRegistryTypes.ts
src/types/comfyRegistryTypes.ts

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
pnpm exec lint-staged
pnpm exec tsx scripts/check-unused-i18n-keys.ts
pnpm exec tsx scripts/check-unused-i18n-keys.ts

View File

@@ -1,2 +1,2 @@
packages/registry-types/src/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts
src/types/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -45,7 +45,7 @@ const config: StorybookConfig = {
compiler: 'vue3',
customCollections: {
comfy: FileSystemIconLoader(
process.cwd() + '/packages/design-system/src/icons'
process.cwd() + '/src/assets/icons/custom'
)
}
}),

View File

@@ -31,9 +31,10 @@
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
## Commit & Pull Request Guidelines
- Commits: Use `[skip ci]` for locale-only updates when appropriate.
- PRs: Include clear description, linked issues (`- Fixes #123`), and screenshots/GIFs for UI changes.
- Commits: Prefer Conventional Commits (e.g., `feat(ui): add sidebar`), `refactor(litegraph): …`. Use `[skip ci]` for locale-only updates when appropriate.
- PRs: Include clear description, linked issues (`Fixes #123`), and screenshots/GIFs for UI changes. Add/adjust tests and i18n strings when applicable.
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
## Security & Configuration Tips
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
- Backend: Dev server expects ComfyUI backend at `localhost:8188` by default; configure via `.env`.

View File

@@ -265,9 +265,9 @@ The project supports three types of icons, all with automatic imports (no manual
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i-lucide:settings />`, `<i-mdi:folder />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation.
For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).
For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md).
## Working with litegraph.js

View File

@@ -29,7 +29,7 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
```bash
pnpm exec playwright install chromium --with-deps
npx playwright install chromium --with-deps
```
### Environment Configuration
@@ -73,7 +73,7 @@ For tests that specifically need to test release functionality, see the example
**Always use UI mode for development:**
```bash
pnpm exec playwright test --ui
npx playwright test --ui
```
UI mode features:
@@ -84,13 +84,13 @@ UI mode features:
- **Console/Network Tabs**: View logs and API calls at each step
- **Attachments Tab**: View all snapshots with expected and actual images
![Playwright UI Mode](https://github.com/user-attachments/assets/9b9cb09f-6da7-4fa0-81df-2effceced755)
![Playwright UI Mode](https://github.com/user-attachments/assets/c158c93f-b39a-44c5-a1a1-e0cc975ee9f2)
For CI or headless testing:
```bash
pnpm exec playwright test # Run all tests
pnpm exec playwright test widget.spec.ts # Run specific test file
npx playwright test # Run all tests
npx playwright test widget.spec.ts # Run specific test file
```
### Local Development Config
@@ -386,7 +386,7 @@ export default defineConfig({
Option 2 - Generate local baselines for comparison:
```bash
pnpm exec playwright test --update-snapshots
npx playwright test --update-snapshots
```
### Creating New Screenshot Baselines

View File

@@ -1,221 +0,0 @@
{
"id": "e74f5af9-b886-4a21-abbf-ed535d12e2fb",
"revision": 0,
"last_node_id": 8,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "LoadAudio",
"pos": [
41.52964782714844,
16.930862426757812
],
"size": [
444,
125
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "AUDIO",
"type": "AUDIO",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadAudio"
},
"widgets_values": [
null,
null,
""
]
},
{
"id": 2,
"type": "LoadVideo",
"pos": [
502.28570556640625,
16.857147216796875
],
"size": [
444,
525
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadVideo"
},
"widgets_values": [
null,
"image"
]
},
{
"id": 3,
"type": "DevToolsLoadAnimatedImageTest",
"pos": [
41.71427917480469,
188.0000457763672
],
"size": [
444,
553
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
},
"widgets_values": [
null,
"image"
]
},
{
"id": 5,
"type": "LoadImage",
"pos": [
958.285888671875,
16.57145118713379
],
"size": [
444,
553
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
null,
"image"
]
},
{
"id": 6,
"type": "LoadImageMask",
"pos": [
503.4285888671875,
588
],
"size": [
444,
563
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImageMask"
},
"widgets_values": [
null,
"alpha",
"image"
]
},
{
"id": 7,
"type": "LoadImageOutput",
"pos": [
965.1429443359375,
612
],
"size": [
444,
553
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImageOutput"
},
"widgets_values": [
null,
false,
"refresh",
"image"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
},
"frontendVersion": "1.28.3"
},
"version": 0.4
}

View File

@@ -1,4 +1,3 @@
import type { KeyCombo } from '@comfyorg/keybinding'
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
@@ -7,6 +6,7 @@ import * as fs from 'fs'
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'

View File

@@ -1,7 +1,7 @@
import type { Keybinding } from '@comfyorg/keybinding'
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -80,12 +80,6 @@ test.describe('Templates', () => {
// Load a template
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
await comfyPage.page
.locator(
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
)
.click()
await comfyPage.templates.loadTemplate('default')
await expect(comfyPage.templates.content).toBeHidden()
@@ -108,72 +102,48 @@ test.describe('Templates', () => {
expect(await comfyPage.templates.content.isVisible()).toBe(true)
})
test('Uses proper locale files for templates', async ({ comfyPage }) => {
// Set locale to French before opening templates
await comfyPage.setSetting('Comfy.Locale', 'fr')
// Load the templates dialog and wait for the French index file request
const requestPromise = comfyPage.page.waitForRequest(
'**/templates/index.fr.json'
)
await comfyPage.executeCommand('Comfy.BrowseTemplates')
const request = await requestPromise
// Verify French index was requested
expect(request.url()).toContain('templates/index.fr.json')
await expect(comfyPage.templates.content).toBeVisible()
})
test('Falls back to English templates when locale file not found', async ({
test('Uses title field as fallback when the key is not found in locales', async ({
comfyPage
}) => {
// Set locale to a language that doesn't have a template file
await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists
// Wait for the German request (expected to 404)
const germanRequestPromise = comfyPage.page.waitForRequest(
'**/templates/index.de.json'
)
// Wait for the fallback English request
const englishRequestPromise = comfyPage.page.waitForRequest(
'**/templates/index.json'
)
// Intercept the German file to simulate a 404
await comfyPage.page.route('**/templates/index.de.json', async (route) => {
// Capture request for the index.json
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
// Add a new template that won't have a translation pre-generated
const response = [
{
moduleName: 'default',
title: 'FALLBACK CATEGORY',
type: 'image',
templates: [
{
name: 'unknown_key_has_no_translation_available',
title: 'FALLBACK TEMPLATE NAME',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'No translations found'
}
]
}
]
await route.fulfill({
status: 404,
headers: { 'Content-Type': 'text/plain' },
body: 'Not Found'
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
})
// Allow the English index to load normally
await comfyPage.page.route('**/templates/index.json', (route) =>
route.continue()
)
// Load the templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Verify German was requested first, then English as fallback
const germanRequest = await germanRequestPromise
const englishRequest = await englishRequestPromise
expect(germanRequest.url()).toContain('templates/index.de.json')
expect(englishRequest.url()).toContain('templates/index.json')
// Verify English titles are shown as fallback
// Expect the title to be used as fallback for template cards
await expect(
comfyPage.templates.content.getByRole('heading', {
name: 'Image Generation'
})
comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME')
).toBeVisible()
// Expect the title to be used as fallback for the template categories
await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible()
})
test('template cards are dynamically sized and responsive', async ({
@@ -181,33 +151,46 @@ test.describe('Templates', () => {
}) => {
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await comfyPage.templates.content.waitFor({ state: 'visible' })
await expect(comfyPage.templates.content).toBeVisible()
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
const nav = comfyPage.page
.locator('header')
.filter({ hasText: 'Templates' })
// Wait for at least one template card to appear
await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({
timeout: 5000
})
const cardCount = await comfyPage.page
.locator('[data-testid^="template-workflow-"]')
.count()
expect(cardCount).toBeGreaterThan(0)
// Take snapshot of the template grid
const templateGrid = comfyPage.templates.content.locator('.grid').first()
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at desktop size
await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png')
const mobileSize = { width: 640, height: 800 }
await comfyPage.page.setViewportSize(mobileSize)
expect(cardCount).toBeGreaterThan(0)
// Check cards at mobile viewport size
await comfyPage.page.setViewportSize({ width: 640, height: 800 })
await expect(templateGrid).toBeVisible()
await expect(nav).not.toBeVisible() // Nav should collapse at mobile size
await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png')
const tabletSize = { width: 1024, height: 800 }
await comfyPage.page.setViewportSize(tabletSize)
expect(cardCount).toBeGreaterThan(0)
// Check cards at tablet size
await comfyPage.page.setViewportSize({ width: 1024, height: 800 })
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at tablet size
await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png')
})
test('hover effects work on template cards', async ({ comfyPage }) => {
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Get a template card
const firstCard = comfyPage.page.locator('.template-card').first()
await expect(firstCard).toBeVisible({ timeout: 5000 })
// Take snapshot before hover
await expect(firstCard).toHaveScreenshot('template-card-before-hover.png')
// Hover over the card
await firstCard.hover()
// Take snapshot after hover to verify hover effect
await expect(firstCard).toHaveScreenshot('template-card-after-hover.png')
})
test('template cards descriptions adjust height dynamically', async ({
@@ -274,42 +257,21 @@ test.describe('Templates', () => {
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Wait for cards to load
// Verify cards are visible with varying content lengths
await expect(
comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
comfyPage.page.getByText('This is a short description.')
).toBeVisible({ timeout: 5000 })
await expect(
comfyPage.page.getByText('This is a medium length description')
).toBeVisible({ timeout: 5000 })
await expect(
comfyPage.page.getByText('This is a much longer description')
).toBeVisible({ timeout: 5000 })
// Verify all three cards with different descriptions are visible
const shortDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
await expect(shortDescCard).toBeVisible()
await expect(mediumDescCard).toBeVisible()
await expect(longDescCard).toBeVisible()
// Verify descriptions are visible and have line-clamp class
// The description is in a p tag with text-muted class
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
await expect(shortDesc).toContainText('short description')
await expect(mediumDesc).toContainText('medium length description')
await expect(longDesc).toContainText('much longer description')
// Verify grid layout maintains consistency
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
// Take snapshot of a grid with specific cards
const templateGrid = comfyPage.templates.content
.locator('.grid:has-text("Short Description")')
.first()
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(
'template-grid-varying-content.png'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -85,7 +85,6 @@ test.describe('Version Mismatch Warnings', () => {
test('should persist dismissed state across sessions', async ({
comfyPage
}) => {
test.setTimeout(30_000)
// Mock system_stats route to indicate that the installed version is always ahead of the required version
await comfyPage.page.route('**/system_stats**', async (route) => {
await route.fulfill({

View File

@@ -1,20 +1,67 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures'
} from '../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
test.describe('Vue Node Collapse', () => {
test.describe('NodeHeader', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
})
test('should allow collapsing node with collapse icon', async ({
comfyPage
}) => {
test('displays node title', async ({ comfyPage }) => {
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
expect(nodes.length).toBeGreaterThanOrEqual(1)
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
const title = await vueNode.getTitle()
expect(title).toBe('KSampler')
// Verify title is visible in the header
const header = await vueNode.getHeader()
await expect(header).toContainText('KSampler')
})
test('allows title renaming', async ({ comfyPage }) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Test renaming with Enter
await vueNode.setTitle('My Custom Sampler')
const newTitle = await vueNode.getTitle()
expect(newTitle).toBe('My Custom Sampler')
// Verify the title is displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('My Custom Sampler')
// Test cancel with Escape
const titleElement = await vueNode.getTitleElement()
await titleElement.dblclick()
await comfyPage.nextFrame()
// Type a different value but cancel
const input = (await vueNode.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill('This Should Be Cancelled')
await input.press('Escape')
await comfyPage.nextFrame()
// Title should remain as the previously saved value
const titleAfterCancel = await vueNode.getTitle()
expect(titleAfterCancel).toBe('My Custom Sampler')
})
test('handles node collapsing', async ({ comfyPage }) => {
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
@@ -43,7 +90,7 @@ test.describe('Vue Node Collapse', () => {
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
})
test('should show collapse/expand icon state', async ({ comfyPage }) => {
test('shows collapse/expand icon state', async ({ comfyPage }) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
@@ -63,9 +110,7 @@ test.describe('Vue Node Collapse', () => {
expect(iconClass).toContain('pi-chevron-down')
})
test('should preserve title when collapsing/expanding', async ({
comfyPage
}) => {
test('preserves title when collapsing/expanding', async ({ comfyPage }) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,33 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should allow creating groups with hotkey', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-create-group.png'
)
})
test('should allow fitting group to contents', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-fit-to-contents.png'
)
})
})

View File

@@ -1,18 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Canvas Pan', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
})
})

View File

@@ -1,33 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Zoom', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should not capture drag while zooming with ctrl+shift+drag', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBoundingBox = await checkpointNode.boundingBox()
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
// the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
{ x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY }
)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
})
})

View File

@@ -1,696 +0,0 @@
import type { Locator, Page } from '@playwright/test'
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
import { fitToViewInstant } from '../../../../helpers/fitToView'
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
const box = await locator.boundingBox()
if (!box) throw new Error('Slot bounding box not available')
return {
x: box.x + box.width / 2,
y: box.y + box.height / 2
}
}
async function getInputLinkDetails(
page: Page,
nodeId: NodeId,
slotIndex: number
) {
return await page.evaluate(
([targetNodeId, targetSlot]) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return null
const node = graph.getNodeById(targetNodeId)
if (!node) return null
const input = node.inputs?.[targetSlot]
if (!input) return null
const linkId = input.link
if (linkId == null) return null
const link = graph.getLink?.(linkId)
if (!link) return null
return {
id: link.id,
originId: link.origin_id,
originSlot:
typeof link.origin_slot === 'string'
? Number.parseInt(link.origin_slot, 10)
: link.origin_slot,
targetId: link.target_id,
targetSlot:
typeof link.target_slot === 'string'
? Number.parseInt(link.target_slot, 10)
: link.target_slot,
parentId: link.parentId ?? null
}
},
[nodeId, slotIndex] as const
)
}
// Test helpers to reduce repetition across cases
function slotLocator(
page: Page,
nodeId: NodeId,
slotIndex: number,
isInput: boolean
) {
const key = getSlotKey(String(nodeId), slotIndex, isInput)
return page.locator(`[data-slot-key="${key}"]`)
}
async function expectVisibleAll(...locators: Locator[]) {
await Promise.all(locators.map((l) => expect(l).toBeVisible()))
}
async function getSlotCenter(
page: Page,
nodeId: NodeId,
slotIndex: number,
isInput: boolean
) {
const locator = slotLocator(page, nodeId, slotIndex, isInput)
await expect(locator).toBeVisible()
return await getCenter(locator)
}
async function connectSlots(
page: Page,
from: { nodeId: NodeId; index: number },
to: { nodeId: NodeId; index: number },
nextFrame: () => Promise<void>
) {
const fromLoc = slotLocator(page, from.nodeId, from.index, false)
const toLoc = slotLocator(page, to.nodeId, to.index, true)
await expectVisibleAll(fromLoc, toLoc)
await fromLoc.dragTo(toLoc)
await nextFrame()
}
test.describe('Vue Node Link Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})
test('should show a link dragging out from a slot when dragging on a slot', async ({
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
await expect(slot).toBeVisible()
const start = await getCenter(slot)
// Arbitrary value
const dragTarget = {
x: start.x + 180,
y: start.y - 140
}
await comfyMouse.move(start)
await comfyMouse.drag(dragTarget)
await comfyPage.nextFrame()
try {
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-dragging-link.png'
)
} finally {
await comfyMouse.drop()
}
})
test('should create a link when dropping on a compatible slot', async ({
comfyPage
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
await connectSlots(
comfyPage.page,
{ nodeId: samplerNode.id, index: 0 },
{ nodeId: vaeNode.id, index: 0 },
() => comfyPage.nextFrame()
)
expect(await samplerOutput.getLinkCount()).toBe(1)
expect(await vaeInput.getLinkCount()).toBe(1)
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
expect(linkDetails).not.toBeNull()
expect(linkDetails).toMatchObject({
originId: samplerNode.id,
originSlot: 0,
targetId: vaeNode.id,
targetSlot: 0
})
})
test('should not create a link when slot types are incompatible', async ({
comfyPage
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
expect(samplerNode && clipNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
const clipInput = await clipNode.getInput(0)
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true)
await expectVisibleAll(outputSlot, inputSlot)
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0)
expect(await clipInput.getLinkCount()).toBe(0)
const graphLinkDetails = await getInputLinkDetails(
comfyPage.page,
clipNode.id,
0
)
expect(graphLinkDetails).toBeNull()
})
test('should not create a link when dropping onto a slot on the same node', async ({
comfyPage
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
const samplerInput = await samplerNode.getInput(3)
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true)
await expectVisibleAll(outputSlot, inputSlot)
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0)
expect(await samplerInput.getLinkCount()).toBe(0)
})
test('should reuse the existing origin when dragging an input link', async ({
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const vaeInputCenter = await getSlotCenter(
comfyPage.page,
vaeNode.id,
0,
true
)
await comfyMouse.move(samplerOutputCenter)
await comfyMouse.drag(vaeInputCenter)
await comfyMouse.drop()
const dragTarget = {
x: vaeInputCenter.x + 160,
y: vaeInputCenter.y - 100
}
await comfyMouse.move(vaeInputCenter)
await comfyMouse.drag(dragTarget)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-input-drag-reuses-origin.png'
)
await comfyMouse.drop()
})
test('ctrl+alt drag from an input starts a fresh link', async ({
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
const samplerOutputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const vaeInputCenter = await getSlotCenter(
comfyPage.page,
vaeNode.id,
0,
true
)
await comfyMouse.move(samplerOutputCenter)
await comfyMouse.drag(vaeInputCenter)
await comfyMouse.drop()
await comfyPage.nextFrame()
const dragTarget = {
x: vaeInputCenter.x + 140,
y: vaeInputCenter.y - 110
}
await comfyMouse.move(vaeInputCenter)
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Alt')
try {
await comfyMouse.drag(dragTarget)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-input-drag-ctrl-alt.png'
)
} finally {
await comfyMouse.drop().catch(() => {})
await comfyPage.page.keyboard.up('Alt').catch(() => {})
await comfyPage.page.keyboard.up('Control').catch(() => {})
}
await comfyPage.nextFrame()
// Tcehnically intended to disconnect existing as well
expect(await vaeInput.getLinkCount()).toBe(0)
expect(await samplerOutput.getLinkCount()).toBe(0)
})
test('dropping an input link back on its slot restores the original connection', async ({
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
const samplerOutputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const vaeInputCenter = await getSlotCenter(
comfyPage.page,
vaeNode.id,
0,
true
)
await comfyMouse.move(samplerOutputCenter)
try {
await comfyMouse.drag(vaeInputCenter)
} finally {
await comfyMouse.drop()
}
await comfyPage.nextFrame()
const originalLink = await getInputLinkDetails(
comfyPage.page,
vaeNode.id,
0
)
expect(originalLink).not.toBeNull()
const dragTarget = {
x: vaeInputCenter.x + 150,
y: vaeInputCenter.y - 100
}
// To prevent needing a screenshot expectation for whether the link's off
const vaeInputLocator = slotLocator(comfyPage.page, vaeNode.id, 0, true)
const inputBox = await vaeInputLocator.boundingBox()
if (!inputBox) throw new Error('Input slot bounding box not available')
const isOutsideX =
dragTarget.x < inputBox.x || dragTarget.x > inputBox.x + inputBox.width
const isOutsideY =
dragTarget.y < inputBox.y || dragTarget.y > inputBox.y + inputBox.height
expect(isOutsideX || isOutsideY).toBe(true)
await comfyMouse.move(vaeInputCenter)
await comfyMouse.drag(dragTarget)
await comfyMouse.move(vaeInputCenter)
await comfyMouse.drop()
await comfyPage.nextFrame()
const restoredLink = await getInputLinkDetails(
comfyPage.page,
vaeNode.id,
0
)
expect(restoredLink).not.toBeNull()
if (!restoredLink || !originalLink) {
throw new Error('Expected both original and restored links to exist')
}
expect(restoredLink).toMatchObject({
originId: originalLink.originId,
originSlot: originalLink.originSlot,
targetId: originalLink.targetId,
targetSlot: originalLink.targetSlot,
parentId: originalLink.parentId
})
expect(await samplerOutput.getLinkCount()).toBe(1)
expect(await vaeInput.getLinkCount()).toBe(1)
})
test('rerouted input drag preview remains anchored to reroute', async ({
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
await connectSlots(
comfyPage.page,
{ nodeId: samplerNode.id, index: 0 },
{ nodeId: vaeNode.id, index: 0 },
() => comfyPage.nextFrame()
)
const outputPosition = await samplerOutput.getPosition()
const inputPosition = await vaeInput.getPosition()
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
// This avoids relying on an exact path hit-test position.
await comfyPage.page.evaluate(
([targetNodeId, targetSlot, clientPoint]) => {
const app = (window as any)['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) throw new Error('Graph not available')
const node = graph.getNodeById(targetNodeId)
if (!node) throw new Error('Target node not found')
const input = node.inputs?.[targetSlot]
if (!input) throw new Error('Target input slot not found')
const linkId = input.link
if (linkId == null) throw new Error('Expected existing link on input')
const link = graph.getLink(linkId)
if (!link) throw new Error('Link not found')
// Convert the client/canvas pixel coordinates to graph space
const pos = app.canvas.ds.convertCanvasToOffset([
clientPoint.x,
clientPoint.y
])
graph.createReroute(pos, link)
},
[vaeNode.id, 0, reroutePoint] as const
)
await comfyPage.nextFrame()
const vaeInputCenter = await getSlotCenter(
comfyPage.page,
vaeNode.id,
0,
true
)
const dragTarget = {
x: vaeInputCenter.x + 160,
y: vaeInputCenter.y - 120
}
let dropped = false
try {
await comfyMouse.move(vaeInputCenter)
await comfyMouse.drag(dragTarget)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-reroute-input-drag.png'
)
await comfyMouse.move(vaeInputCenter)
await comfyMouse.drop()
dropped = true
} finally {
if (!dropped) {
await comfyMouse.drop().catch(() => {})
}
}
await comfyPage.nextFrame()
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
expect(linkDetails).not.toBeNull()
expect(linkDetails?.originId).toBe(samplerNode.id)
expect(linkDetails?.parentId).not.toBeNull()
})
test('rerouted output shift-drag preview remains anchored to reroute', async ({
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
await connectSlots(
comfyPage.page,
{ nodeId: samplerNode.id, index: 0 },
{ nodeId: vaeNode.id, index: 0 },
() => comfyPage.nextFrame()
)
const outputPosition = await samplerOutput.getPosition()
const inputPosition = await vaeInput.getPosition()
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
// This avoids relying on an exact path hit-test position.
await comfyPage.page.evaluate(
([targetNodeId, targetSlot, clientPoint]) => {
const app = (window as any)['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) throw new Error('Graph not available')
const node = graph.getNodeById(targetNodeId)
if (!node) throw new Error('Target node not found')
const input = node.inputs?.[targetSlot]
if (!input) throw new Error('Target input slot not found')
const linkId = input.link
if (linkId == null) throw new Error('Expected existing link on input')
const link = graph.getLink(linkId)
if (!link) throw new Error('Link not found')
// Convert the client/canvas pixel coordinates to graph space
const pos = app.canvas.ds.convertCanvasToOffset([
clientPoint.x,
clientPoint.y
])
graph.createReroute(pos, link)
},
[vaeNode.id, 0, reroutePoint] as const
)
await comfyPage.nextFrame()
const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dragTarget = {
x: outputCenter.x + 150,
y: outputCenter.y - 140
}
let dropPending = false
let shiftHeld = false
try {
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
shiftHeld = true
dropPending = true
await comfyMouse.drag(dragTarget)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-reroute-output-shift-drag.png'
)
await comfyMouse.move(outputCenter)
await comfyMouse.drop()
dropPending = false
} finally {
if (dropPending) await comfyMouse.drop().catch(() => {})
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
await comfyPage.nextFrame()
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
expect(linkDetails).not.toBeNull()
expect(linkDetails?.originId).toBe(samplerNode.id)
expect(linkDetails?.parentId).not.toBeNull()
})
test('dragging input to input drags existing link', async ({
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
// Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1)
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 1 },
() => comfyPage.nextFrame()
)
// Verify initial link exists between CLIP -> KSampler input[1]
const initialLink = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
1
)
expect(initialLink).not.toBeNull()
expect(initialLink).toMatchObject({
originId: clipNode.id,
targetId: samplerNode.id,
targetSlot: 1
})
// Step 2: Drag from KSampler's second input to its third input (index 2)
const input2Center = await getSlotCenter(
comfyPage.page,
samplerNode.id,
1,
true
)
const input3Center = await getSlotCenter(
comfyPage.page,
samplerNode.id,
2,
true
)
await comfyMouse.move(input2Center)
await comfyMouse.drag(input3Center)
await comfyMouse.drop()
await comfyPage.nextFrame()
// Expect old link removed from input[1]
const afterSecondInput = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
1
)
expect(afterSecondInput).toBeNull()
// Expect new link exists at input[2] from CLIP
const afterThirdInput = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
2
)
expect(afterThirdInput).not.toBeNull()
expect(afterThirdInput).toMatchObject({
originId: clipNode.id,
targetId: samplerNode.id,
targetSlot: 2
})
})
test('shift-dragging an output with multiple links should drag all links', async ({
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
const clipOutput = await clipNode.getOutput(0)
// Connect output[0] -> inputs[1] and [2]
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 1 },
() => comfyPage.nextFrame()
)
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 2 },
() => comfyPage.nextFrame()
)
expect(await clipOutput.getLinkCount()).toBe(2)
const outputCenter = await getSlotCenter(
comfyPage.page,
clipNode.id,
0,
false
)
const dragTarget = {
x: outputCenter.x + 40,
y: outputCenter.y - 140
}
let dropPending = false
let shiftHeld = false
try {
await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
shiftHeld = true
await comfyMouse.drag(dragTarget)
dropPending = true
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-shift-output-multi-link.png'
)
} finally {
if (dropPending) await comfyMouse.drop().catch(() => {})
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
})
})

View File

@@ -1,76 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
test.describe('Vue Nodes Renaming', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
})
test('should display node title', async ({ comfyPage }) => {
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
expect(nodes.length).toBeGreaterThanOrEqual(1)
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
const title = await vueNode.getTitle()
expect(title).toBe('KSampler')
// Verify title is visible in the header
const header = await vueNode.getHeader()
await expect(header).toContainText('KSampler')
})
test('should allow title renaming by double clicking on the node header', async ({
comfyPage
}) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Test renaming with Enter
await vueNode.setTitle('My Custom Sampler')
const newTitle = await vueNode.getTitle()
expect(newTitle).toBe('My Custom Sampler')
// Verify the title is displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('My Custom Sampler')
// Test cancel with Escape
const titleElement = await vueNode.getTitleElement()
await titleElement.dblclick()
await comfyPage.nextFrame()
// Type a different value but cancel
const input = (await vueNode.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill('This Should Be Cancelled')
await input.press('Escape')
await comfyPage.nextFrame()
// Title should remain as the previously saved value
const titleAfterCancel = await vueNode.getTitle()
expect(titleAfterCancel).toBe('My Custom Sampler')
})
test('Double click node body does not trigger edit', async ({
comfyPage
}) => {
const loadCheckpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBbox = await loadCheckpointNode.boundingBox()
if (!nodeBbox) throw new Error('Node not found')
await loadCheckpointNode.dblclick()
const editingTitleInput = comfyPage.page.getByTestId('node-title-input')
await expect(editingTitleInput).not.toBeVisible()
})
})

View File

@@ -0,0 +1,221 @@
import type { Locator } from '@playwright/test'
import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../helpers/fitToView'
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
const box = await locator.boundingBox()
if (!box) throw new Error('Slot bounding box not available')
return {
x: box.x + box.width / 2,
y: box.y + box.height / 2
}
}
test.describe('Vue Node Link Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})
test('should show a link dragging out from a slot when dragging on a slot', async ({
comfyPage,
comfyMouse
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const outputSlot = await samplerNode.getOutput(0)
await outputSlot.removeLinks()
await comfyPage.nextFrame()
const slotKey = getSlotKey(String(samplerNode.id), 0, false)
const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`)
await expect(slotLocator).toBeVisible()
const start = await getCenter(slotLocator)
const canvasBox = await comfyPage.canvas.boundingBox()
if (!canvasBox) throw new Error('Canvas bounding box not available')
// Arbitrary value
const dragTarget = {
x: start.x + 180,
y: start.y - 140
}
await comfyMouse.move(start)
await comfyMouse.drag(dragTarget)
await comfyPage.nextFrame()
try {
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-dragging-link.png'
)
} finally {
await comfyMouse.drop()
}
})
test('should create a link when dropping on a compatible slot', async ({
comfyPage
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
expect(vaeNodes.length).toBeGreaterThan(0)
const vaeNode = vaeNodes[0]
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true)
const outputSlot = comfyPage.page.locator(
`[data-slot-key="${outputSlotKey}"]`
)
const inputSlot = comfyPage.page.locator(
`[data-slot-key="${inputSlotKey}"]`
)
await expect(outputSlot).toBeVisible()
await expect(inputSlot).toBeVisible()
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(1)
expect(await vaeInput.getLinkCount()).toBe(1)
const linkDetails = await comfyPage.page.evaluate((sourceId) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return null
const source = graph.getNodeById(sourceId)
if (!source) return null
const linkId = source.outputs[0]?.links?.[0]
if (linkId == null) return null
const link = graph.links[linkId]
if (!link) return null
return {
originId: link.origin_id,
originSlot: link.origin_slot,
targetId: link.target_id,
targetSlot: link.target_slot
}
}, samplerNode.id)
expect(linkDetails).not.toBeNull()
expect(linkDetails).toMatchObject({
originId: samplerNode.id,
originSlot: 0,
targetId: vaeNode.id,
targetSlot: 0
})
})
test('should not create a link when slot types are incompatible', async ({
comfyPage
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
expect(clipNodes.length).toBeGreaterThan(0)
const clipNode = clipNodes[0]
const samplerOutput = await samplerNode.getOutput(0)
const clipInput = await clipNode.getInput(0)
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
const inputSlotKey = getSlotKey(String(clipNode.id), 0, true)
const outputSlot = comfyPage.page.locator(
`[data-slot-key="${outputSlotKey}"]`
)
const inputSlot = comfyPage.page.locator(
`[data-slot-key="${inputSlotKey}"]`
)
await expect(outputSlot).toBeVisible()
await expect(inputSlot).toBeVisible()
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0)
expect(await clipInput.getLinkCount()).toBe(0)
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return 0
const source = graph.getNodeById(sourceId)
if (!source) return 0
return source.outputs[0]?.links?.length ?? 0
}, samplerNode.id)
expect(graphLinkCount).toBe(0)
})
test('should not create a link when dropping onto a slot on the same node', async ({
comfyPage
}) => {
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
expect(samplerNodes.length).toBeGreaterThan(0)
const samplerNode = samplerNodes[0]
const samplerOutput = await samplerNode.getOutput(0)
const samplerInput = await samplerNode.getInput(3)
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true)
const outputSlot = comfyPage.page.locator(
`[data-slot-key="${outputSlotKey}"]`
)
const inputSlot = comfyPage.page.locator(
`[data-slot-key="${inputSlotKey}"]`
)
await expect(outputSlot).toBeVisible()
await expect(inputSlot).toBeVisible()
await outputSlot.dragTo(inputSlot)
await comfyPage.nextFrame()
expect(await samplerOutput.getLinkCount()).toBe(0)
expect(await samplerInput.getLinkCount()).toBe(0)
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
const app = window['app']
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return 0
const source = graph.getNodeById(sourceId)
if (!source) return 0
return source.outputs[0]?.links?.length ?? 0
}, samplerNode.id)
expect(graphLinkCount).toBe(0)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -1,7 +1,7 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
} from '../../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
@@ -15,8 +15,7 @@ test.describe('Vue Node Selection', () => {
const modifiers = [
{ key: 'Control', name: 'ctrl' },
{ key: 'Shift', name: 'shift' },
{ key: 'Meta', name: 'meta' }
{ key: 'Shift', name: 'shift' }
] as const
for (const { key: modifier, name } of modifiers) {

View File

@@ -1,49 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Custom Colors', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('displays color picker button and allows color selection', async ({
comfyPage
}) => {
const loadCheckpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
await loadCheckpointNode.getByText('Load Checkpoint').click()
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container')
.locator('i[data-testid="blue"]')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-color-blue.png'
)
})
test('should load node colors from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-dark-all-colors.png'
)
})
test('should show brightened node colors on light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.loadWorkflow('nodes/every_node_color')
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-light-all-colors.png'
)
})
})

View File

@@ -1,85 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const PIN_HOTKEY = 'p'
const PIN_INDICATOR = '[data-testid="node-pin-indicator"]'
test.describe('Vue Node Pin', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling pin on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const pinIndicator = checkpointNode.locator(PIN_INDICATOR)
await expect(pinIndicator).toBeVisible()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
await expect(pinIndicator).not.toBeVisible()
})
test('should allow toggling pin on multiple selected nodes with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
await comfyPage.page.keyboard.press(PIN_HOTKEY)
const pinIndicator1 = checkpointNode.locator(PIN_INDICATOR)
await expect(pinIndicator1).toBeVisible()
const pinIndicator2 = ksamplerNode.locator(PIN_INDICATOR)
await expect(pinIndicator2).toBeVisible()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
await expect(pinIndicator1).not.toBeVisible()
await expect(pinIndicator2).not.toBeVisible()
})
test('should not allow dragging pinned nodes', async ({ comfyPage }) => {
const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint')
await checkpointNodeHeader.click()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
// Try to drag the node
const headerPos = await checkpointNodeHeader.boundingBox()
if (!headerPos) throw new Error('Failed to get header position')
await comfyPage.dragAndDrop(
{ x: headerPos.x, y: headerPos.y },
{ x: headerPos.x + 256, y: headerPos.y + 256 }
)
// Verify the node is not dragged (same position before and after click-and-drag)
const headerPosAfterDrag = await checkpointNodeHeader.boundingBox()
if (!headerPosAfterDrag)
throw new Error('Failed to get header position after drag')
expect(headerPosAfterDrag).toEqual(headerPos)
// Unpin the node with the hotkey
await checkpointNodeHeader.click()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
// Try to drag the node again
await comfyPage.dragAndDrop(
{ x: headerPos.x, y: headerPos.y },
{ x: headerPos.x + 256, y: headerPos.y + 256 }
)
// Verify the node is dragged
const headerPosAfterDrag2 = await checkpointNodeHeader.boundingBox()
if (!headerPosAfterDrag2)
throw new Error('Failed to get header position after drag')
expect(headerPosAfterDrag2).not.toEqual(headerPos)
})
})

View File

@@ -1,21 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Upload Widgets', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png'
)
})
})

View File

@@ -1,49 +0,0 @@
import {
type ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Multiline String Widget', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
const getFirstClipNode = (comfyPage: ComfyPage) =>
comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode (Prompt)').first()
const getFirstMultilineStringWidget = (comfyPage: ComfyPage) =>
getFirstClipNode(comfyPage).getByRole('textbox', { name: 'text' })
test('should allow entering text', async ({ comfyPage }) => {
const textarea = getFirstMultilineStringWidget(comfyPage)
await textarea.fill('Hello World')
await expect(textarea).toHaveValue('Hello World')
await textarea.fill('Hello World 2')
await expect(textarea).toHaveValue('Hello World 2')
})
test('should support entering multiline content', async ({ comfyPage }) => {
const textarea = getFirstMultilineStringWidget(comfyPage)
const multilineValue = ['Line 1', 'Line 2', 'Line 3'].join('\n')
await textarea.fill(multilineValue)
await expect(textarea).toHaveValue(multilineValue)
})
test('should retain value after focus changes', async ({ comfyPage }) => {
const textarea = getFirstMultilineStringWidget(comfyPage)
await textarea.fill('Keep me around')
// Click another node
const loadCheckpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await loadCheckpointNode.click()
await getFirstClipNode(comfyPage).click()
await expect(textarea).toHaveValue('Keep me around')
})
})

View File

@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
const fileName = fileURLToPath(import.meta.url)
const dirName = dirname(fileName)
const customIconsPath = join(dirName, 'icons')
const customIconsPath = join(dirName, '..', 'src', 'assets', 'icons', 'custom')
// Iconify collection structure
interface IconifyIcon {

View File

@@ -1,134 +0,0 @@
# Keybinding Domain DDD Refactoring
## Overview
This document outlines the refactoring of ComfyUI's keybinding functionality from technical layers to domain-driven design (DDD) following VSCode's proven three-layer architecture.
## Architecture
### Three-Layer Design
Following VSCode's enterprise-grade architecture:
1. **Base Layer** - Foundational utilities (no dependencies)
2. **Platform Layer** - Core abstractions (reusable across frontends)
3. **Workbench Layer** - UI-specific implementation
### Final Structure
```
src/
├── base/keybinding/ # Base Layer
│ └── reservedKeyCombos.ts # Reserved key combinations
├── platform/keybinding/ # Platform Layer
│ ├── constants/
│ │ └── coreKeybindings.ts # Core keybinding definitions
│ └── types/
│ └── keybinding.ts # Schema types and interfaces
└── workbench/keybindings/ # Workbench Layer
├── components/ # UI Components
│ ├── KeybindingPanel.vue # Settings panel
│ ├── KeyComboDisplay.vue # Key display component
│ └── shortcuts/ # Shortcuts panel
│ ├── EssentialsPanel.vue
│ ├── ShortcutsList.vue
│ └── ViewControlsPanel.vue
├── composables/
│ └── useShortcutsTab.ts # UI logic
├── services/
│ └── keybindingService.ts # Service implementation
└── stores/
└── keybindingStore.ts # State management
```
## Migration Summary
### Before (Technical Layers)
- `services/keybindingService.ts`
- `stores/keybindingStore.ts`
- `constants/coreKeybindings.ts`
- `schemas/keyBindingSchema.ts`
- `components/dialog/content/setting/KeybindingPanel.vue`
- `components/bottomPanel/tabs/shortcuts/`
- `composables/bottomPanelTabs/useShortcutsTab.ts`
### After (Domain-Driven)
All keybinding functionality organized by architectural layer and domain responsibility.
## Key Import Paths
```typescript
// Platform types and constants
import type { Keybinding, KeyCombo } from '@/platform/keybinding/types/keybinding'
import { CORE_KEYBINDINGS } from '@/platform/keybinding/constants/coreKeybindings'
// Base utilities
import { RESERVED_BY_TEXT_INPUT } from '@/base/keybinding/reservedKeyCombos'
// Workbench services
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
// UI components
import KeybindingPanel from '@/workbench/keybindings/components/KeybindingPanel.vue'
import { useShortcutsTab } from '@/workbench/keybindings/composables/useShortcutsTab'
```
## Benefits
### 1. Clear Architectural Boundaries
- **Base**: Reusable across any JavaScript environment
- **Platform**: Reusable across any UI framework
- **Workbench**: Vue/PrimeVue specific implementation
### 2. Frontend Flexibility
The base + platform layers enable building alternative frontends (React, Angular, etc.) while reusing core keybinding logic.
### 3. VSCode Alignment
Follows the same proven patterns used by one of the most successful code editors, ensuring scalability and maintainability.
### 4. Domain Cohesion
All keybinding-related functionality is now located together, making it easier to:
- Find related code
- Make changes across the domain
- Test domain functionality
- Understand feature scope
### 5. Dependency Management
Clear dependency flow: `base/``platform/``workbench/`
## Migration Process
1.**Analysis**: Identified all keybinding-related files
2.**Structure Creation**: Built three-layer directory structure
3.**File Migration**: Moved files to appropriate layers
4.**Import Updates**: Updated all import paths using `@` aliases
5.**Testing**: Verified TypeScript compilation and linting
6.**Cleanup**: Removed old files and empty directories
## Quality Assurance
- **TypeScript**: `pnpm typecheck` passes
- **Code Quality**: `pnpm lint --fix` applied
- **Testing**: All test imports updated
- **No Breaking Changes**: All functionality preserved
## Future Considerations
### Potential Enhancements
- Add platform-level abstractions as needed
- Create additional keybinding editor components
- Consider splitting large components into smaller, focused ones
### Extension Points
- New UI components can be added to `workbench/keybindings/components/`
- Platform-level services can be extended in `platform/keybinding/`
- Base utilities can be enhanced in `base/keybinding/`
This refactoring establishes a solid foundation for future keybinding feature development while maintaining backward compatibility and improving code organization.

View File

@@ -110,7 +110,7 @@ pnpm build
For faster iteration during development, use watch mode:
```bash
pnpm exec vite build --watch
npx vite build --watch
```
Note: Watch mode provides faster rebuilds than full builds, but still no hot reload

View File

@@ -18,7 +18,7 @@ export default defineConfig([
'src/scripts/*',
'src/extensions/core/*',
'src/types/vue-shim.d.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
'src/types/comfyRegistryTypes.ts',
'src/types/generatedManagerTypes.ts',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*'
@@ -181,31 +181,5 @@ export default defineConfig([
{ disallowTypeAnnotations: false }
]
}
},
{
files: ['**/*.spec.ts'],
ignores: ['browser_tests/tests/**/*.spec.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message: '.spec.ts files are only allowed under browser_tests/tests/'
}
]
}
},
{
files: ['browser_tests/tests/**/*.test.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'.test.ts files are not allowed in browser_tests/tests/; use .spec.ts instead'
}
]
}
}
])

View File

@@ -1,30 +1,15 @@
import type { KnipConfig } from 'knip'
const config: KnipConfig = {
workspaces: {
'.': {
entry: [
'{build,scripts}/**/*.{js,ts}',
'src/assets/css/style.css',
'src/main.ts',
'src/scripts/ui/menu/index.ts',
'src/types/index.ts'
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
'packages/tailwind-utils': {
project: ['src/**/*.{js,ts}']
},
'packages/design-system': {
entry: ['src/**/*.ts'],
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
},
'packages/registry-types': {
entry: ['src/comfyRegistryTypes.ts'],
project: ['src/**/*.{js,ts}']
}
},
ignoreBinaries: ['python3'],
entry: [
'{build,scripts}/**/*.{js,ts}',
'src/assets/css/style.css',
'src/main.ts',
'src/scripts/ui/menu/index.ts',
'src/types/index.ts'
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'],
ignoreBinaries: ['only-allow', 'openapi-typescript'],
ignoreDependencies: [
// Weird importmap things
'@iconify/json',
@@ -38,7 +23,7 @@ const config: KnipConfig = {
ignore: [
// Auto generated manager types
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
'src/types/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
],

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.28.3",
"version": "1.28.2",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -19,11 +19,11 @@
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:all": "nx run test",
"test:browser": "pnpm exec nx e2e",
"test:browser": "npx nx e2e",
"test:component": "nx run test src/components/",
"test:litegraph": "vitest run --config vitest.litegraph.config.ts",
"test:unit": "nx run test tests-ui/tests",
"preinstall": "pnpm dlx only-allow pnpm",
"preinstall": "npx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
"lint": "eslint src --cache",
@@ -35,14 +35,15 @@
"knip": "knip --cache",
"knip:no-cache": "knip",
"locale": "lobe-i18n locale",
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"collect-i18n": "npx playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"storybook": "nx storybook -p 6006",
"build-storybook": "storybook build",
"devtools:pycheck": "python3 -m compileall -q tools/devtools"
"build-storybook": "storybook build"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@iconify-json/lucide": "^1.2.66",
"@iconify/tailwind": "^1.2.0",
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
"@lobehub/i18n-cli": "^1.25.1",
"@nx/eslint": "21.4.1",
@@ -106,9 +107,6 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "0.4.73-0",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/keybinding": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@iconify/json": "^2.2.380",
"@primeuix/forms": "0.0.2",
@@ -127,7 +125,6 @@
"@tiptap/extension-table-row": "^2.10.4",
"@tiptap/starter-kit": "^2.10.4",
"@vueuse/core": "^11.0.0",
"@vueuse/integrations": "^13.9.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0",
@@ -156,6 +153,7 @@
"vue": "^3.5.13",
"vue-i18n": "^9.14.3",
"vue-router": "^4.4.3",
"vuedraggable": "^4.1.0",
"vuefire": "^3.2.1",
"yjs": "^13.6.27",
"zod": "^3.23.8",

View File

@@ -1,31 +0,0 @@
{
"name": "@comfyorg/design-system",
"version": "1.0.0",
"description": "Shared design system for ComfyUI Frontend",
"type": "module",
"exports": {
"./tailwind-config": {
"import": "./tailwind.config.ts",
"types": "./tailwind.config.ts"
},
"./css/*": "./src/css/*"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"nx": {
"tags": [
"scope:shared",
"type:design"
]
},
"dependencies": {
"@iconify-json/lucide": "^1.1.178",
"@iconify/tailwind": "^1.1.3"
},
"devDependencies": {
"tailwindcss": "^3.4.17",
"typescript": "^5.4.5"
},
"packageManager": "pnpm@10.17.1"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
import lucide from '@iconify-json/lucide/icons.json' with { type: 'json' }
import { addDynamicIconSelectors } from '@iconify/tailwind'
import { iconCollection } from './src/iconCollection'
export default {
content: [],
safelist: [
'icon-[lucide--folder]',
'icon-[lucide--package]',
'icon-[lucide--image]',
'icon-[lucide--video]',
'icon-[lucide--box]',
'icon-[lucide--audio-waveform]',
'icon-[lucide--message-circle]'
],
plugins: [
addDynamicIconSelectors({
iconSets: {
comfy: iconCollection,
lucide
},
prefix: 'icon'
})
]
}

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

View File

@@ -1,29 +0,0 @@
{
"name": "@comfyorg/keybinding",
"version": "1.0.0",
"type": "module",
"description": "Framework-agnostic keybinding system for ComfyUI",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"nx": {
"tags": [
"scope:shared",
"type:lib"
]
},
"dependencies": {
"zod": "^3.22.4"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}

View File

@@ -1,11 +0,0 @@
// Types
export type * from './types/keybinding'
export { zKeybinding } from './types/keybinding'
// Models (Implementation classes)
export { KeyComboImpl } from './models/KeyCombo'
export { KeybindingImpl } from './models/Keybinding'
// Constants
export { CORE_KEYBINDINGS } from './constants/coreKeybindings'
export { RESERVED_BY_TEXT_INPUT } from './constants/reservedKeyCombos'

View File

@@ -1,83 +0,0 @@
import { RESERVED_BY_TEXT_INPUT } from '../constants/reservedKeyCombos'
import type { KeyCombo } from '../types/keybinding'
export class KeyComboImpl implements KeyCombo {
key: string
// ctrl or meta(cmd on mac)
ctrl: boolean
alt: boolean
shift: boolean
constructor(obj: KeyCombo) {
this.key = obj.key
this.ctrl = obj.ctrl ?? false
this.alt = obj.alt ?? false
this.shift = obj.shift ?? false
}
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
})
}
equals(other: unknown): boolean {
return other instanceof KeyComboImpl
? this.key.toUpperCase() === other.key.toUpperCase() &&
this.ctrl === other.ctrl &&
this.alt === other.alt &&
this.shift === other.shift
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
get hasModifier(): boolean {
return this.ctrl || this.alt || this.shift
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
}
get modifierCount(): number {
const modifiers = [this.ctrl, this.alt, this.shift]
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
}
get isShiftOnly(): boolean {
return this.shift && this.modifierCount === 1
}
get isReservedByTextInput(): boolean {
return (
!this.hasModifier ||
this.isShiftOnly ||
RESERVED_BY_TEXT_INPUT.has(this.toString())
)
}
getKeySequences(): string[] {
const sequences: string[] = []
if (this.ctrl) {
sequences.push('Ctrl')
}
if (this.alt) {
sequences.push('Alt')
}
if (this.shift) {
sequences.push('Shift')
}
sequences.push(this.key)
return sequences
}
}

View File

@@ -1,22 +0,0 @@
import type { Keybinding } from '../types/keybinding'
import { KeyComboImpl } from './KeyCombo'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetElementId?: string
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.targetElementId = obj.targetElementId
}
equals(other: unknown): boolean {
return other instanceof KeybindingImpl
? this.commandId === other.commandId &&
this.combo.equals(other.combo) &&
this.targetElementId === other.targetElementId
: false
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,16 +0,0 @@
{
"name": "@comfyorg/registry-types",
"version": "1.0.0",
"description": "Comfy Registry API TypeScript types",
"packageManager": "pnpm@10.17.1",
"type": "module",
"exports": {
".": "./src/comfyRegistryTypes.ts"
},
"nx": {
"tags": [
"scope:shared",
"type:types"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

View File

@@ -1,22 +0,0 @@
{
"name": "@comfyorg/shared-frontend-utils",
"private": true,
"version": "1.0.0",
"description": "Shared frontend utils for ComfyUI Frontend",
"scripts": {
"typecheck": "tsc --noEmit"
},
"keywords": [],
"packageManager": "pnpm@10.17.1",
"type": "module",
"exports": {
"./formatUtil": "./src/formatUtil.ts",
"./networkUtil": "./src/networkUtil.ts"
},
"dependencies": {
"axios": "^1.11.0"
},
"devDependencies": {
"typescript": "^5.9.2"
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

372
pnpm-lock.yaml generated
View File

@@ -17,15 +17,6 @@ importers:
'@comfyorg/comfyui-electron-types':
specifier: 0.4.73-0
version: 0.4.73-0
'@comfyorg/design-system':
specifier: workspace:*
version: link:packages/design-system
'@comfyorg/keybinding':
specifier: workspace:*
version: link:packages/keybinding
'@comfyorg/registry-types':
specifier: workspace:*
version: link:packages/registry-types
'@comfyorg/tailwind-utils':
specifier: workspace:*
version: link:packages/tailwind-utils
@@ -80,9 +71,6 @@ importers:
'@vueuse/core':
specifier: ^11.0.0
version: 11.0.0(vue@3.5.13(typescript@5.9.2))
'@vueuse/integrations':
specifier: ^13.9.0
version: 13.9.0(axios@1.11.0)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.2))
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -167,6 +155,9 @@ importers:
vue-router:
specifier: ^4.4.3
version: 4.4.3(vue@3.5.13(typescript@5.9.2))
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.5.13(typescript@5.9.2))
vuefire:
specifier: ^3.2.1
version: 3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.2))
@@ -183,6 +174,12 @@ importers:
'@eslint/js':
specifier: ^9.35.0
version: 9.35.0
'@iconify-json/lucide':
specifier: ^1.2.66
version: 1.2.66
'@iconify/tailwind':
specifier: ^1.2.0
version: 1.2.0
'@intlify/eslint-plugin-vue-i18n':
specifier: ^4.1.0
version: 4.1.0(eslint@9.35.0(jiti@2.4.2))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.4.2)))(yaml-eslint-parser@1.3.0)
@@ -358,44 +355,6 @@ importers:
specifier: ^3.24.1
version: 3.24.1(zod@3.24.1)
packages/design-system:
dependencies:
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.66
'@iconify/tailwind':
specifier: ^1.1.3
version: 1.2.0
devDependencies:
tailwindcss:
specifier: ^3.4.17
version: 3.4.17
typescript:
specifier: ^5.4.5
version: 5.9.2
packages/keybinding:
dependencies:
zod:
specifier: ^3.22.4
version: 3.24.1
devDependencies:
typescript:
specifier: ^5.4.5
version: 5.9.2
packages/registry-types: {}
packages/shared-frontend-utils:
dependencies:
axios:
specifier: ^1.11.0
version: 1.11.0
devDependencies:
typescript:
specifier: ^5.9.2
version: 5.9.2
packages/tailwind-utils:
dependencies:
clsx:
@@ -2949,73 +2908,18 @@ packages:
'@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
'@vueuse/core@13.9.0':
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
peerDependencies:
vue: ^3.5.0
'@vueuse/integrations@13.9.0':
resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==}
peerDependencies:
async-validator: ^4
axios: ^1
change-case: ^5
drauu: ^0.4
focus-trap: ^7
fuse.js: ^7
idb-keyval: ^6
jwt-decode: ^4
nprogress: ^0.2
qrcode: ^1.5
sortablejs: ^1
universal-cookie: ^7 || ^8
vue: ^3.5.0
peerDependenciesMeta:
async-validator:
optional: true
axios:
optional: true
change-case:
optional: true
drauu:
optional: true
focus-trap:
optional: true
fuse.js:
optional: true
idb-keyval:
optional: true
jwt-decode:
optional: true
nprogress:
optional: true
qrcode:
optional: true
sortablejs:
optional: true
universal-cookie:
optional: true
'@vueuse/metadata@11.0.0':
resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==}
'@vueuse/metadata@12.8.2':
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
'@vueuse/metadata@13.9.0':
resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
'@vueuse/shared@11.0.0':
resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==}
'@vueuse/shared@12.8.2':
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
'@vueuse/shared@13.9.0':
resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
peerDependencies:
vue: ^3.5.0
'@webgpu/types@0.1.51':
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
@@ -3157,16 +3061,10 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -3342,10 +3240,6 @@ packages:
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
@@ -3475,10 +3369,6 @@ packages:
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
@@ -3684,9 +3574,6 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
diff-match-patch@1.0.5:
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
@@ -3694,9 +3581,6 @@ packages:
resolution: {integrity: sha512-7SCDfnQtBObcngVXNPZcnxGxqqPTK4UqeXeKAch+RGH5qpqadWbV9FmN71x9Bb4tTs0TNFb4FT/4Kz4P4Cjqcw==}
engines: {node: '>=6.0.0'}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
doctypes@1.1.0:
resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==}
@@ -4585,10 +4469,6 @@ packages:
resolution: {integrity: sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
@@ -4812,10 +4692,6 @@ packages:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
engines: {node: '>=14'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -5178,9 +5054,6 @@ packages:
resolution: {integrity: sha512-SsI/exkodHsh+ofCV7An2PZWRaJC7eFVl7gtHQlMWFEDmWtb7cELr/GK32Nhe/6dZQhbr81o+Moswx9aXN3RRg==}
engines: {node: '>=18.2.0'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -5272,10 +5145,6 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -5451,10 +5320,6 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pinia@2.2.2:
resolution: {integrity: sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==}
peerDependencies:
@@ -5467,10 +5332,6 @@ packages:
typescript:
optional: true
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@@ -5487,47 +5348,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.1.0:
resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@4.0.2:
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.0:
resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==}
engines: {node: '>=4'}
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.1:
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -5724,9 +5548,6 @@ packages:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@@ -5934,6 +5755,9 @@ packages:
resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==}
engines: {node: '>= 18'}
sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -6048,11 +5872,6 @@ packages:
subscribable-things@2.1.53:
resolution: {integrity: sha512-zWvN9F/eYQWDKszXl4NXkyqPXvMDZDmXfcHiM5C5WQZTTY2OK+2TZeDlA9oio69FEPqPu9T6yeEcAhQ2uRmnaw==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
superjson@2.2.2:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
@@ -6093,11 +5912,6 @@ packages:
peerDependencies:
tailwindcss: '>=3.1.0'
tailwindcss@3.4.17:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
engines: {node: '>=14.0.0'}
hasBin: true
tailwindcss@4.1.12:
resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==}
@@ -6122,13 +5936,6 @@ packages:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
three@0.170.0:
resolution: {integrity: sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==}
@@ -6215,9 +6022,6 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-map@1.0.3:
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
@@ -6535,8 +6339,8 @@ packages:
vue-component-type-helpers@3.0.7:
resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==}
vue-component-type-helpers@3.1.0:
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
vue-component-type-helpers@3.0.8:
resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -6590,6 +6394,11 @@ packages:
typescript:
optional: true
vuedraggable@4.1.0:
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
vuefire@3.2.1:
resolution: {integrity: sha512-APj/iFdEec9kO71Lsiv/7opo9xL0D43l7cjwh84rJ5WMzrmpi9z774zzN+PPhBpD6bXyueLcfg0VlOUhI9/jUA==}
engines: {node: '>=18'}
@@ -9094,7 +8903,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.0
vue-component-type-helpers: 3.0.8
'@swc/helpers@0.5.17':
dependencies:
@@ -9877,28 +9686,10 @@ snapshots:
transitivePeerDependencies:
- typescript
'@vueuse/core@13.9.0(vue@3.5.13(typescript@5.9.2))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.9.0
'@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.2))
vue: 3.5.13(typescript@5.9.2)
'@vueuse/integrations@13.9.0(axios@1.11.0)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.2))':
dependencies:
'@vueuse/core': 13.9.0(vue@3.5.13(typescript@5.9.2))
'@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.2))
vue: 3.5.13(typescript@5.9.2)
optionalDependencies:
axios: 1.11.0
fuse.js: 7.0.0
'@vueuse/metadata@11.0.0': {}
'@vueuse/metadata@12.8.2': {}
'@vueuse/metadata@13.9.0': {}
'@vueuse/shared@11.0.0(vue@3.5.13(typescript@5.9.2))':
dependencies:
vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.2))
@@ -9912,10 +9703,6 @@ snapshots:
transitivePeerDependencies:
- typescript
'@vueuse/shared@13.9.0(vue@3.5.13(typescript@5.9.2))':
dependencies:
vue: 3.5.13(typescript@5.9.2)
'@webgpu/types@0.1.51': {}
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
@@ -10049,15 +9836,11 @@ snapshots:
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@5.0.2: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
@@ -10263,8 +10046,6 @@ snapshots:
pascal-case: 3.1.2
tslib: 2.8.1
camelcase-css@2.0.1: {}
camelcase@8.0.0: {}
caniuse-lite@1.0.30001737: {}
@@ -10378,8 +10159,6 @@ snapshots:
commander@2.20.3: {}
commander@4.1.1: {}
commander@8.3.0: {}
compare-versions@6.1.1: {}
@@ -10562,8 +10341,6 @@ snapshots:
dependencies:
dequal: 2.0.3
didyoumean@1.2.2: {}
diff-match-patch@1.0.5: {}
dirty-json@0.9.2:
@@ -10572,8 +10349,6 @@ snapshots:
unescape-js: 1.1.4
utf8: 3.0.0
dlv@1.1.3: {}
doctypes@1.1.0: {}
dom-accessibility-api@0.5.16: {}
@@ -11523,8 +11298,6 @@ snapshots:
chalk: 4.1.2
pretty-format: 30.0.5
jiti@1.21.7: {}
jiti@2.4.2: {}
jiti@2.5.1: {}
@@ -11746,8 +11519,6 @@ snapshots:
lilconfig@3.1.2: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
lines-and-columns@2.0.3: {}
@@ -12304,12 +12075,6 @@ snapshots:
'@babel/runtime': 7.28.4
tslib: 2.8.1
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.8: {}
nanoid@5.1.5: {}
@@ -12423,8 +12188,6 @@ snapshots:
object-assign@4.1.1: {}
object-hash@3.0.0: {}
object-keys@1.1.1: {}
ohash@2.0.11: {}
@@ -12615,8 +12378,6 @@ snapshots:
pidtree@0.6.0: {}
pify@2.3.0: {}
pinia@2.2.2(typescript@5.9.2)(vue@3.5.13(typescript@5.9.2)):
dependencies:
'@vue/devtools-api': 6.6.3
@@ -12625,8 +12386,6 @@ snapshots:
optionalDependencies:
typescript: 5.9.2
pirates@4.0.7: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
@@ -12647,42 +12406,11 @@ snapshots:
optionalDependencies:
fsevents: 2.3.2
postcss-import@15.1.0(postcss@8.5.1):
dependencies:
postcss: 8.5.1
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.10
postcss-js@4.1.0(postcss@8.5.1):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.1
postcss-load-config@4.0.2(postcss@8.5.1):
dependencies:
lilconfig: 3.1.3
yaml: 2.8.1
optionalDependencies:
postcss: 8.5.1
postcss-nested@6.2.0(postcss@8.5.1):
dependencies:
postcss: 8.5.1
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.0:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.5.1:
dependencies:
nanoid: 3.3.8
@@ -12958,10 +12686,6 @@ snapshots:
react@19.1.1: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
@@ -13224,6 +12948,8 @@ snapshots:
smol-toml@1.4.2: {}
sortablejs@1.14.0: {}
source-map-js@1.2.1: {}
source-map-support@0.5.19:
@@ -13344,16 +13070,6 @@ snapshots:
rxjs-interop: 2.0.0
tslib: 2.8.1
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
glob: 10.4.5
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
ts-interface-checker: 0.1.13
superjson@2.2.2:
dependencies:
copy-anything: 3.0.5
@@ -13391,33 +13107,6 @@ snapshots:
dependencies:
tailwindcss: 4.1.12
tailwindcss@3.4.17:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.3
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.7
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.5.1
postcss-import: 15.1.0(postcss@8.5.1)
postcss-js: 4.1.0(postcss@8.5.1)
postcss-load-config: 4.0.2(postcss@8.5.1)
postcss-nested: 6.2.0(postcss@8.5.1)
postcss-selector-parser: 6.1.2
resolve: 1.22.10
sucrase: 3.35.0
transitivePeerDependencies:
- ts-node
tailwindcss@4.1.12: {}
tapable@2.2.3: {}
@@ -13452,14 +13141,6 @@ snapshots:
glob: 10.4.5
minimatch: 9.0.5
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
three@0.170.0: {}
tiny-invariant@1.3.3: {}
@@ -13525,8 +13206,6 @@ snapshots:
ts-dedent@2.2.0: {}
ts-interface-checker@0.1.13: {}
ts-map@1.0.3: {}
tsconfig-paths@4.2.0:
@@ -13892,7 +13571,7 @@ snapshots:
vue-component-type-helpers@3.0.7: {}
vue-component-type-helpers@3.1.0: {}
vue-component-type-helpers@3.0.8: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:
@@ -13958,6 +13637,11 @@ snapshots:
optionalDependencies:
typescript: 5.9.2
vuedraggable@4.1.0(vue@3.5.13(typescript@5.9.2)):
dependencies:
sortablejs: 1.14.0
vue: 3.5.13(typescript@5.9.2)
vuefire@3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.2)):
dependencies:
vue: 3.5.13(typescript@5.9.2)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,77 +0,0 @@
#!/bin/bash
set -euo pipefail
usage() {
echo "Usage: $0 [--debug]" >&2
}
debug=0
while [ "$#" -gt 0 ]; do
case "$1" in
--debug)
debug=1
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
esac
shift
done
# Validate JSON syntax in tracked files using jq
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed" >&2
exit 127
fi
EXCLUDE_PATTERNS=(
'**/tsconfig*.json'
)
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then
# shellcheck disable=SC2206
EXCLUDE_PATTERNS+=( ${JSON_LINT_EXCLUDES} )
fi
pathspecs=(-- '*.json')
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
if [[ ${pattern:0:1} == ':' ]]; then
pathspecs+=("$pattern")
else
pathspecs+=(":(glob,exclude)${pattern}")
fi
done
mapfile -t json_files < <(git ls-files "${pathspecs[@]}")
if [ "${#json_files[@]}" -eq 0 ]; then
echo 'No JSON files found.'
exit 0
fi
if [ "$debug" -eq 1 ]; then
echo 'JSON files to validate:'
printf ' %s\n' "${json_files[@]}"
fi
failed=0
for file in "${json_files[@]}"; do
if ! jq -e . "$file" >/dev/null; then
echo "Invalid JSON syntax: $file" >&2
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo 'JSON validation failed.' >&2
exit 1
fi
echo 'All JSON files are valid.'

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