Compare commits

..

10 Commits

Author SHA1 Message Date
bymyself
c330529da8 fix: address CodeRabbit review feedback
- Remove unused keysBySource from store public API
- Fix socketless required input validation (widget-only inputs)

Amp-Thread-ID: https://ampcode.com/threads/T-019c0d45-1155-70d9-830a-ed6e50e23ee8
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 21:24:16 -08:00
bymyself
50d339e2ba feat: add proactive validation for missing required connections
- Create useRequiredConnectionValidator composable that validates on graphChanged events
- Detect missing required connections by checking slot.link and widget values
- Use debounced validation (200ms) for performance on large graphs
- Register event listener with proper cleanup on unmount

Completes COM-12907: Missing workflow connection now highlights BEFORE clicking Run

Amp-Thread-ID: https://ampcode.com/threads/T-019c0c91-73e0-7188-9770-c315279fadd8
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 18:15:47 -08:00
bymyself
ed10909f38 feat: add centralized graphErrorStateStore for multi-source error handling
- Create graphErrorStateStore with command-based API (REPLACE_SOURCE, CLEAR_SOURCE, CLEAR_ALL)
- Support multiple error sources: 'frontend' (proactive validation) and 'backend' (execution errors)
- Add useGraphErrorState projection hook that applies errors to graph nodes and propagates up subgraph hierarchy
- Migrate executionStore to use new store instead of direct node.has_errors/slot.hasErrors mutation

Part of COM-12907: Missing workflow connection doesn't highlight

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c0c91-73e0-7188-9770-c315279fadd8
2026-01-29 18:13:12 -08:00
Jin Yi
65ff23c5af [bugfix] Fix manager missing node tab with shared composable (#8409) 2026-01-29 06:23:47 +00:00
Alexander Brown
6ce60a11a4 test: use createTestingPinia instead of createPinia (#8376)
Replace \createPinia\ with \createTestingPinia({ stubActions: false })\
from \@pinia/testing\ across 45 test files for proper test isolation.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8376-test-use-createTestingPinia-instead-of-createPinia-2f66d73d36508137a9f0daffcddc86f7)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 22:21:38 -08:00
Christian Byrne
3b5d124029 fix: use getAuthHeader in createCustomer to support API key auth (#8408)
## Summary

Fixes authentication failure when using API key authentication on
staging server after frontend update to 1.33.10.

<img width="1160" height="709" alt="image"
src="https://github.com/user-attachments/assets/fe56866d-1819-419e-9f53-35a123d764c3"
/>


## Changes

- **What**: Changed `createCustomer()` to use `getAuthHeader()` instead
of `getFirebaseAuthHeader()`, allowing API key users to authenticate
successfully

## Review Focus

- Verify `getAuthHeader()` correctly falls back to API key when no
Firebase token exists
- Backend `/customers` endpoint supports `X-API-KEY` header (per cloud
PR #1766)

Fixes COM-12398

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8408-fix-use-getAuthHeader-in-createCustomer-to-support-API-key-auth-2f76d73d3650819994e3e6d3ed9f3dfa)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 22:12:28 -08:00
Alexander Brown
bd4920febc Chore: Actions updates and cleanup (#8377)
## Summary

...

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8377-WIP-Chore-Actions-updates-and-cleanup-2f66d73d3650818483a8dffa32a6f245)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 21:22:39 -08:00
Jin Yi
bd916096ac fix: add null check in getCanvasCenter to prevent crash on asset insert (#8399)
## Summary
Adds null check in `getCanvasCenter()` to prevent crash when inserting
asset as node before canvas is fully initialized.

## Changes
- **What**: Added optional chaining for `app.canvas?.ds?.visible_area`
with fallback to `[0, 0]`

## Review Focus
- Simple defensive fix - returns origin position if canvas not ready

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8399-fix-add-null-check-in-getCanvasCenter-to-prevent-crash-on-asset-insert-2f76d73d365081e88c08ef40ea9e7b78)
by [Unito](https://www.unito.io)
2026-01-29 13:19:51 +09:00
guill
9be853f6b5 feat: support dev-only nodes (#8359)
## Summary

Support `dev_only` property to node definitions that hides nodes from
search and menus unless dev mode is enabled. Dev-only nodes display a
"DEV" badge when visible.

This functionality is primarily intended to support unit-testing nodes
on Comfy Cloud, but also has other uses.

## Changes

- **What**: Nodes flagged as dev_only in the node schema will only
appear in search and menus if Dev Mode is on.

## Screenshots (if applicable)

With Dev Mode off:
<img width="2189" height="1003" alt="image"
src="https://github.com/user-attachments/assets/a08e1fd7-dca9-4ce1-9964-5f4f3b7b95ac"
/>

With Dev Mode on:
<img width="2201" height="1066" alt="image"
src="https://github.com/user-attachments/assets/7fe6cd1f-f774-4f48-b604-a528e286b584"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8359-feat-support-dev-only-nodes-2f66d73d36508102839ee7cd66a26129)
by [Unito](https://www.unito.io)
2026-01-28 19:41:45 -08:00
Christian Byrne
2103dcc788 fix: increase Vue node resize handle size for better usability (#8391)
## Summary

Increases the resize handle size on Vue nodes to improve usability,
especially when nodes are selected.

## Changes

- **What**: Increased resize handle from 12px to 20px and offset it
slightly outside the node boundary to avoid overlap with selection
outline

## Review Focus

The resize handle was too small and became harder to grab when the node
was selected (the 2px outline rendered outside the box, visually
obscuring the corner). This fix increases the hit area and positions it
to extend beyond the node edge.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8391-fix-increase-Vue-node-resize-handle-size-for-better-usability-2f76d73d36508136b2aac51bc0d53551)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-28 19:15:40 -08:00
105 changed files with 1128 additions and 488 deletions

1
.gitattributes vendored
View File

@@ -11,6 +11,7 @@
*.ts text eol=lf
*.vue text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -104,14 +104,14 @@ runs:
- name: Find existing comment
id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }}

View File

@@ -16,7 +16,7 @@ runs:
# Checkout ComfyUI repo, install the dev_tools node and start server
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
@@ -33,7 +33,7 @@ runs:
fi
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.10'

View File

@@ -12,29 +12,17 @@ runs:
# Install pnpm, Node.js, build frontend
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml'
# Restore tool caches before running any build/lint operations
- name: Restore tool output cache
uses: actions/cache/restore@v4
with:
path: |
./.cache
./tsconfig.tsbuildinfo
key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }}
restore-keys: |
tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-
tool-cache-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile

View File

@@ -11,7 +11,7 @@ runs:
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
- name: Cache Playwright Browsers
uses: actions/cache@v4
uses: actions/cache@v5 # v5.0.2
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'

View File

@@ -13,15 +13,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -36,7 +36,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'

View File

@@ -18,15 +18,15 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -35,7 +35,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager
@@ -86,7 +86,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'

View File

@@ -17,15 +17,15 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -34,7 +34,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Checkout comfy-api repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/comfy-api
path: comfy-api
@@ -87,7 +87,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'

View File

@@ -13,6 +13,6 @@ jobs:
json-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh

View File

@@ -18,23 +18,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Run ESLint with auto-fix
run: pnpm lint:fix
@@ -73,7 +62,7 @@ jobs:
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
@@ -86,7 +75,7 @@ jobs:
- name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.11'

View File

@@ -17,21 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build project
run: pnpm build
@@ -46,7 +35,7 @@ jobs:
echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload size data
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: size-data
path: temp/size

View File

@@ -31,11 +31,11 @@ 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@v6
- name: Get PR Number
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data: prs } = await github.rest.pulls.list({
@@ -68,7 +68,7 @@ jobs:
- name: Download and Deploy Reports
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
@@ -25,7 +25,7 @@ jobs:
# Upload only built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist/
@@ -51,9 +51,9 @@ jobs:
shardTotal: [8]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -72,7 +72,7 @@ jobs:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
- name: Upload blob report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: blob-report-chromium-${{ matrix.shardIndex }}
@@ -98,9 +98,9 @@ jobs:
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -128,7 +128,7 @@ jobs:
pnpm exec playwright merge-reports --reporter=json ./blob-report
- name: Upload Playwright report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-${{ matrix.browser }}
@@ -141,16 +141,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Download blob reports
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
path: ./all-blob-reports
pattern: blob-report-chromium-*
@@ -165,7 +162,7 @@ jobs:
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: playwright-report-chromium
path: ./playwright-report/
@@ -183,7 +180,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get start time
id: start-time
@@ -210,10 +207,10 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download all playwright reports
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: playwright-report-*
path: reports

View File

@@ -31,11 +31,11 @@ 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@v6
- name: Get PR Number
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data: prs } = await github.rest.pulls.list({
@@ -68,7 +68,7 @@ jobs:
- 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
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Post starting comment
env:
@@ -36,21 +36,10 @@ jobs:
workflow-url: ${{ steps.workflow-url.outputs.url }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- 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: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook
run: pnpm build-storybook
@@ -69,7 +58,7 @@ jobs:
- name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: storybook-static
path: storybook-static/
@@ -86,27 +75,16 @@ jobs:
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0 # Required for Chromatic baseline
- 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: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook and run Chromatic
id: chromatic
uses: chromaui/action@latest
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
@@ -136,11 +114,11 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: storybook-static
path: storybook-static
@@ -170,7 +148,7 @@ jobs:
pull-requests: write
steps:
- name: Update comment with Chromatic URLs
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';

View File

@@ -16,21 +16,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Run Vitest tests
run: pnpm test:unit

View File

@@ -0,0 +1,21 @@
name: Validate Action SHA Pins
on:
pull_request:
paths:
- '.github/workflows/**'
- '.github/actions/**'
- '.pinact.yaml'
permissions:
contents: read
jobs:
validate-pins:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: suzuki-shunsuke/pinact-action@3d49c6412901042473ffa78becddab1aea46bbea # v1.3.1
with:
skip_push: 'true'

View File

@@ -17,10 +17,10 @@ jobs:
yaml-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

View File

@@ -18,12 +18,12 @@ jobs:
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment
- name: Setup ComfyUI Frontend

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
@@ -36,7 +36,7 @@ jobs:
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
@@ -113,7 +113,7 @@ jobs:
git commit -m "Update locales"
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
@@ -40,7 +40,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'Update locales for node definitions'

View File

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

View File

@@ -23,18 +23,18 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'pnpm'
@@ -44,7 +44,7 @@ jobs:
pnpm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review
uses: anthropics/claude-code-action@v1.0.6
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with:
label_trigger: 'claude-review'
prompt: |

View File

@@ -33,24 +33,13 @@ jobs:
github.event_name == 'workflow_dispatch'
)
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Download size data
uses: dawidd6/action-download-artifact@v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: size-data
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
@@ -75,7 +64,7 @@ jobs:
fi
- name: Download previous size data
uses: dawidd6/action-download-artifact@v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: ci-size-data.yaml
@@ -89,12 +78,12 @@ jobs:
- name: Read size report
id: size-report
uses: juliangruber/read-file-action@v1
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with:
path: ./size-report.md
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@v3
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}

View File

@@ -38,7 +38,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find Update Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: 'find-update-comment'
with:
issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -46,7 +46,7 @@ jobs:
body-includes: 'Updating Playwright Expectations'
- name: Add Starting Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -56,7 +56,7 @@ jobs:
reactions: eyes
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.pr-info.outputs.branch }}
- name: Setup frontend
@@ -66,7 +66,7 @@ jobs:
# Upload built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist/
@@ -91,11 +91,11 @@ jobs:
shardTotal: [4]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -149,7 +149,7 @@ jobs:
# Upload ONLY the changed files from this shard
- name: Upload changed snapshots
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: steps.changed-snapshots.outputs.has-changes == 'true'
with:
name: snapshots-shard-${{ matrix.shardIndex }}
@@ -157,7 +157,7 @@ jobs:
retention-days: 1
- name: Upload test report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-shard-${{ matrix.shardIndex }}
@@ -170,13 +170,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: snapshots-shard-*
path: ./downloaded-snapshots
@@ -301,7 +301,7 @@ jobs:
echo "✓ Commit and push successful"
- name: Add Done Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true'
with:
comment-id: ${{ needs.setup.outputs.comment-id }}

View File

@@ -20,13 +20,13 @@ jobs:
dist_tag: ${{ steps.dist.outputs.dist_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
@@ -71,7 +71,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2

View File

@@ -77,19 +77,19 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: 'pnpm'

View File

@@ -61,13 +61,13 @@ jobs:
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
path: frontend
- name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/ComfyUI
sparse-checkout: |
@@ -75,12 +75,12 @@ jobs:
path: comfyui
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
@@ -169,7 +169,7 @@ jobs:
steps:
- name: Checkout ComfyUI fork
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
token: ${{ secrets.PR_GH_TOKEN }}

View File

@@ -18,13 +18,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'

View File

@@ -19,12 +19,12 @@ jobs:
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
@@ -55,7 +55,7 @@ jobs:
pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist-files
path: |
@@ -66,16 +66,13 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Create release
id: create_release
uses: >-
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -98,13 +95,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
@@ -119,8 +116,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: >-
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
@@ -147,7 +143,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2

View File

@@ -69,18 +69,18 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@@ -15,12 +15,12 @@ jobs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
@@ -40,7 +40,7 @@ jobs:
pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist-files
path: |
@@ -52,13 +52,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
@@ -73,7 +73,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist

View File

@@ -65,7 +65,7 @@ jobs:
- name: Close stale nightly version bump PRs
if: github.event_name == 'schedule'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |
@@ -118,7 +118,7 @@ jobs:
core.info(`Closed ${closed.length} stale PR(s).`)
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.prepared-inputs.outputs.branch }}
fetch-depth: 0
@@ -142,12 +142,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
@@ -180,7 +180,7 @@ jobs:
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
@@ -51,12 +51,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: 'pnpm'
@@ -79,7 +79,7 @@ jobs:
echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -22,18 +22,18 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-depth: 50
ref: main
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'pnpm'
@@ -49,7 +49,7 @@ jobs:
fi
- name: Run Claude Documentation Review
uses: anthropics/claude-code-action@v1.0.6
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with:
prompt: |
Is all documentation still 100% accurate?
@@ -130,7 +130,7 @@ jobs:
- name: Create or Update Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'docs: weekly documentation accuracy update'

24
.pinact.yaml Normal file
View File

@@ -0,0 +1,24 @@
# pinact configuration
# https://github.com/suzuki-shunsuke/pinact
version: 3
files:
- pattern: .github/workflows/*.yaml
- pattern: .github/actions/**/*.yaml
# Actions that don't need SHA pinning (official GitHub actions are trusted)
ignore_actions:
- name: actions/cache
ref: v5
- name: actions/checkout
ref: v6
- name: actions/setup-node
ref: v6
- name: actions/setup-python
ref: v6
- name: actions/upload-artifact
ref: v6
- name: actions/download-artifact
ref: v7
- name: actions/github-script
ref: v8

View File

@@ -8,3 +8,6 @@ rules:
line-length: disable
document-start: disable
truthy: disable
comments:
min-spaces-from-content: 1

View File

@@ -18,7 +18,8 @@ Basic setup for testing Pinia stores:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useWorkflowStore } from '@/domains/workflow/ui/stores/workflowStore'
@@ -27,8 +28,8 @@ describe('useWorkflowStore', () => {
let store: ReturnType<typeof useWorkflowStore>
beforeEach(() => {
// Create a fresh pinia and activate it for each test
setActivePinia(createPinia())
// Create a fresh testing pinia and activate it for each test
setActivePinia(createTestingPinia({ stubActions: false }))
// Initialize the store
store = useWorkflowStore()

View File

@@ -21,16 +21,17 @@
</div>
</div>
<div class="option-badges">
<Tag
v-if="nodeDef.experimental"
:value="$t('g.experimental')"
severity="primary"
/>
<Tag
v-if="nodeDef.deprecated"
:value="$t('g.deprecated')"
severity="danger"
/>
<Tag
v-if="nodeDef.experimental"
:value="$t('g.experimental')"
severity="primary"
/>
<Tag v-if="nodeDef.dev_only" :value="$t('g.devOnly')" severity="info" />
<Tag
v-if="showNodeFrequency && nodeFrequency > 0"
:value="formatNumberWithSuffix(nodeFrequency, { roundToInt: true })"

View File

@@ -0,0 +1,76 @@
import { watch } from 'vue'
import type { LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { useGraphErrorStateStore } from '@/stores/graphErrorStateStore'
import { parseNodeLocatorId } from '@/types/nodeIdentification'
import {
findSubgraphByUuid,
forEachNode,
forEachSubgraphNode
} from '@/utils/graphTraversalUtil'
export function useGraphErrorState(): void {
const store = useGraphErrorStateStore()
watch(
() => store.version,
() => {
const rootGraph = app.rootGraph
if (!rootGraph) return
forEachNode(rootGraph, (node) => {
node.has_errors = false
if (node.inputs) {
for (const slot of node.inputs) {
slot.hasErrors = false
}
}
})
for (const [nodeId, keys] of store.keysByNode) {
if (keys.size === 0) continue
const parsed = parseNodeLocatorId(nodeId)
if (!parsed) continue
const targetGraph = parsed.subgraphUuid
? findSubgraphByUuid(rootGraph, parsed.subgraphUuid)
: rootGraph
if (!targetGraph) continue
const node = targetGraph.getNodeById(parsed.localNodeId)
if (!node) continue
node.has_errors = true
for (const key of keys) {
const error = store.errorsByKey.get(key)
if (error && error.target.kind === 'slot' && node.inputs) {
const slotName = error.target.slotName
const slot = node.inputs.find((s) => s.name === slotName)
if (slot) {
slot.hasErrors = true
}
}
}
propagateErrorToParents(node)
}
},
{ immediate: true }
)
}
function propagateErrorToParents(node: LGraphNode): void {
const subgraph = node.graph as Subgraph | undefined
if (!subgraph || subgraph.isRootGraph) return
const subgraphId = subgraph.id
if (!subgraphId) return
forEachSubgraphNode(app.rootGraph, subgraphId, (subgraphNode) => {
subgraphNode.has_errors = true
propagateErrorToParents(subgraphNode)
})
}

View File

@@ -0,0 +1,122 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useGraphErrorStateStore } from '@/stores/graphErrorStateStore'
vi.mock('@/scripts/api', () => ({
api: {
addEventListener: vi.fn(),
removeEventListener: vi.fn()
}
}))
vi.mock('@/scripts/app', () => ({
app: {
rootGraph: null
}
}))
vi.mock('@/stores/nodeDefStore', () => ({
useNodeDefStore: vi.fn(() => ({
nodeDefsByName: {}
}))
}))
vi.mock('@/utils/graphTraversalUtil', () => ({
forEachNode: vi.fn()
}))
describe('useRequiredConnectionValidator', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
describe('store integration', () => {
it('adds errors for missing required connections', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:missing:1:model',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'model' },
code: 'MISSING_REQUIRED_INPUT'
}
]
})
expect(store.hasSlotError('1', 'model')).toBe(true)
expect(store.hasErrorsForNode('1')).toBe(true)
})
it('clears errors when connections are made', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:missing:1:model',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'model' }
}
]
})
expect(store.hasSlotError('1', 'model')).toBe(true)
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: []
})
expect(store.hasSlotError('1', 'model')).toBe(false)
})
it('preserves backend errors when frontend errors change', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'backend',
errors: [
{
key: 'backend:node:2',
source: 'backend',
target: { kind: 'node', nodeId: '2' }
}
]
})
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:missing:1:model',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'model' }
}
]
})
expect(store.hasErrorsForNode('1')).toBe(true)
expect(store.hasErrorsForNode('2')).toBe(true)
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: []
})
expect(store.hasErrorsForNode('1')).toBe(false)
expect(store.hasErrorsForNode('2')).toBe(true)
})
})
})

View File

@@ -0,0 +1,74 @@
import { useDebounceFn } from '@vueuse/core'
import { onUnmounted } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useGraphErrorStateStore } from '@/stores/graphErrorStateStore'
import type { GraphError } from '@/stores/graphErrorStateStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import { forEachNode } from '@/utils/graphTraversalUtil'
export function useRequiredConnectionValidator(): void {
const errorStore = useGraphErrorStateStore()
const nodeDefStore = useNodeDefStore()
function validate(): void {
const rootGraph = app.rootGraph
if (!rootGraph) return
const errors: GraphError[] = []
forEachNode(rootGraph, (node: LGraphNode) => {
const nodeDef = nodeDefStore.nodeDefsByName[node.type ?? '']
if (!nodeDef?.input?.required) return
const subgraphId =
node.graph && !node.graph.isRootGraph ? node.graph.id : null
const locatorId = subgraphId
? createNodeLocatorId(subgraphId, node.id)
: String(node.id)
for (const inputName of Object.keys(nodeDef.input.required)) {
const slot = node.inputs?.find((s) => s.name === inputName)
const hasConnection = slot?.link !== null && slot?.link !== undefined
const hasWidgetValue = hasWidgetValueForInput(node, inputName)
if (!hasConnection && !hasWidgetValue) {
errors.push({
key: `frontend:missing:${locatorId}:${inputName}`,
source: 'frontend',
target: { kind: 'slot', nodeId: locatorId, slotName: inputName },
code: 'MISSING_REQUIRED_INPUT'
})
}
}
})
errorStore.execute({ type: 'REPLACE_SOURCE', source: 'frontend', errors })
}
function hasWidgetValueForInput(
node: LGraphNode,
inputName: string
): boolean {
if (!node.widgets) return false
const widget = node.widgets.find((w) => w.name === inputName)
if (!widget) return false
return (
widget.value !== undefined && widget.value !== null && widget.value !== ''
)
}
const debouncedValidate = useDebounceFn(validate, 200)
api.addEventListener('graphChanged', debouncedValidate)
onUnmounted(() => {
api.removeEventListener('graphChanged', debouncedValidate)
})
validate()
}

View File

@@ -10,17 +10,8 @@ import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type {
IBaseWidget,
IWidgetAssetOptions,
TWidgetValue
} from '@/lib/litegraph/src/types/widgets'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import {
assetFilenameSchema,
assetItemSchema
} from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils'
import { isCloud } from '@/platform/distribution/types'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { app } from '@/scripts/app'
import {
@@ -28,10 +19,10 @@ import {
addValueControlWidgets,
isValidWidgetType
} from '@/scripts/widgets'
import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
import { mergeInputSpec } from '@/utils/nodeDefUtil'
import { applyTextReplacements } from '@/utils/searchAndReplace'
import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
const replacePropertyName = 'Run widget replace on values'
export class PrimitiveNode extends LGraphNode {
@@ -237,20 +228,6 @@ export class PrimitiveNode extends LGraphNode {
// Store current size as addWidget resizes the node
const [oldWidth, oldHeight] = this.size
let widget: IBaseWidget
// Cloud: Use asset widget for model-eligible inputs
if (isCloud && type === 'COMBO') {
const isEligible = assetService.isAssetBrowserEligible(
node.comfyClass,
widgetName
)
if (isEligible) {
widget = this.#createAssetWidget(node, widgetName, inputData)
this.#finalizeWidget(widget, oldWidth, oldHeight, recreating)
return
}
}
if (isValidWidgetType(type)) {
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
} else {
@@ -300,84 +277,20 @@ export class PrimitiveNode extends LGraphNode {
}
}
this.#finalizeWidget(widget, oldWidth, oldHeight, recreating)
}
#createAssetWidget(
targetNode: LGraphNode,
widgetName: string,
inputData: InputSpec
): IBaseWidget {
const defaultValue = inputData[1]?.default as string | undefined
const assetBrowserDialog = useAssetBrowserDialog()
const openModal = async (widget: IBaseWidget) => {
await assetBrowserDialog.show({
nodeType: targetNode.comfyClass ?? '',
inputName: widgetName,
currentValue: widget.value as string,
onAssetSelected: (asset) => {
const validatedAsset = assetItemSchema.safeParse(asset)
if (!validatedAsset.success) {
console.error('Invalid asset item:', validatedAsset.error.errors)
return
}
const filename = getAssetFilename(validatedAsset.data)
const validatedFilename = assetFilenameSchema.safeParse(filename)
if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors
)
return
}
const oldValue = widget.value
widget.value = validatedFilename.data
widget.callback?.(
widget.value,
app.canvas,
this,
app.canvas.graph_mouse,
{} as CanvasPointerEvent
)
this.onWidgetChanged?.(
widget.name,
validatedFilename.data,
oldValue,
widget
)
}
})
}
const options: IWidgetAssetOptions = { openModal }
return this.addWidget(
'asset',
'value',
defaultValue ?? '',
() => {},
options
)
}
#finalizeWidget(
widget: IBaseWidget,
oldWidth: number,
oldHeight: number,
recreating: boolean
) {
// When our value changes, update other widgets to reflect our changes
// e.g. so LoadImage shows correct image
widget.callback = useChainCallback(widget.callback, () => {
this.applyToGraph()
})
// Use the biggest dimensions in case the widgets caused the node to grow
this.setSize([
Math.max(this.size[0], oldWidth),
Math.max(this.size[1], oldHeight)
])
if (!recreating) {
// Grow our node more if required
const sz = this.computeSize()
if (this.size[0] < sz[0]) {
this.size[0] = sz[0]

View File

@@ -233,6 +233,14 @@ export class LGraphNode
static description?: string
static filter?: string
static skip_list?: boolean
static nodeData?: {
dev_only?: boolean
deprecated?: boolean
experimental?: boolean
output_node?: boolean
api_node?: boolean
name?: string
}
static resizeHandleSize = 15
static resizeEdgeSize = 5

View File

@@ -121,6 +121,7 @@
"customize": "Customize",
"experimental": "BETA",
"deprecated": "DEPR",
"devOnly": "DEV",
"loadWorkflow": "Load Workflow",
"goToNode": "Go to Node",
"setAsBackground": "Set as Background",

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as VueI18nModule from 'vue-i18n'
@@ -79,7 +80,7 @@ describe('useSubscriptionCredits', () => {
let authStore: ReturnType<typeof useFirebaseAuthStore>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
authStore = useFirebaseAuthStore()
vi.clearAllMocks()
})

View File

@@ -1,5 +1,6 @@
import { until } from '@vueuse/core'
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
@@ -40,7 +41,7 @@ describe('useVersionCompatibilityStore', () => {
let mockSettingStore: { get: ReturnType<typeof vi.fn> }
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
// Clear the mock dismissal storage
mockDismissalStorage.value = {}

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
@@ -66,7 +67,7 @@ describe('useWorkflowStore', () => {
}
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useWorkflowStore()
bookmarkStore = useWorkflowBookmarkStore()
vi.clearAllMocks()

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as I18n from 'vue-i18n'
@@ -108,7 +109,7 @@ describe('useWorkflowPersistence', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'))
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
localStorage.clear()
sessionStorage.clear()
vi.clearAllMocks()

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useTeamWorkspaceStore } from './teamWorkspaceStore'
@@ -111,7 +112,7 @@ const mockMemberWorkspace = {
describe('useTeamWorkspaceStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
vi.stubGlobal('localStorage', mockLocalStorage)
sessionStorage.clear()

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -15,7 +16,7 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({
describe('useMinimapSettings', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
})

View File

@@ -150,7 +150,9 @@
v-if="!isCollapsed && nodeData.resizable !== false"
role="button"
:aria-label="t('g.resizeFromBottomRight')"
:class="cn(baseResizeHandleClasses, 'right-0 bottom-0 cursor-se-resize')"
:class="
cn(baseResizeHandleClasses, '-right-1 -bottom-1 cursor-se-resize')
"
@pointerdown.stop="handleResizePointerDown"
/>
</div>
@@ -344,7 +346,7 @@ function initSizeStyles() {
}
const baseResizeHandleClasses =
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
'absolute h-5 w-5 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
const MIN_NODE_WIDTH = 225

View File

@@ -1,5 +1,6 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { setActivePinia } from 'pinia'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import { describe, expect, it, vi } from 'vitest'
@@ -29,7 +30,7 @@ const makeNodeData = (overrides: Partial<VueNodeData> = {}): VueNodeData => ({
})
const setupMockStores = () => {
const pinia = createPinia()
const pinia = createTestingPinia({ stubActions: false })
setActivePinia(pinia)
const settingStore = useSettingStore()

View File

@@ -1,6 +1,6 @@
/* eslint-disable vue/one-component-per-file */
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
@@ -84,7 +84,7 @@ const mountSlots = (nodeData: VueNodeData, readonly = false) => {
})
return mount(NodeSlots, {
global: {
plugins: [i18n, createPinia()],
plugins: [i18n, createTestingPinia({ stubActions: false })],
stubs: {
InputSlot: InputSlotStub,
OutputSlot: OutputSlotStub

View File

@@ -174,6 +174,7 @@ export const zComfyNodeDef = z.object({
python_module: z.string(),
deprecated: z.boolean().optional(),
experimental: z.boolean().optional(),
dev_only: z.boolean().optional(),
api_node: z.boolean().optional()
})

View File

@@ -257,6 +257,7 @@ export const zComfyNodeDef = z.object({
python_module: z.string(),
deprecated: z.boolean().optional(),
experimental: z.boolean().optional(),
dev_only: z.boolean().optional(),
/**
* Whether the node is an API node. Running API nodes requires login to
* Comfy Org account.

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
@@ -30,7 +31,7 @@ describe('keybindingService - Escape key handling', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
// Mock command store execute
const commandStore = useCommandStore()

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { app } from '@/scripts/app'
@@ -68,7 +69,7 @@ describe('keybindingService - Event Forwarding', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
// Mock command store execute
const commandStore = useCommandStore()

View File

@@ -261,7 +261,7 @@ export const useLitegraphService = () => {
static comfyClass: string
static override title: string
static override category: string
static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
static override nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
_initialMinSize = { width: 1, height: 1 }
@@ -394,7 +394,7 @@ export const useLitegraphService = () => {
static comfyClass: string
static override title: string
static override category: string
static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
static override nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
_initialMinSize = { width: 1, height: 1 }
@@ -496,6 +496,13 @@ export const useLitegraphService = () => {
// because `registerNodeType` will overwrite the assignments.
node.category = nodeDef.category
node.title = nodeDef.display_name || nodeDef.name
// Set skip_list for dev-only nodes based on current DevMode setting
// This ensures nodes registered after initial load respect the current setting
if (nodeDef.dev_only) {
const settingStore = useSettingStore()
node.skip_list = !settingStore.get('Comfy.DevMode')
}
}
/**
@@ -876,7 +883,11 @@ export const useLitegraphService = () => {
function getCanvasCenter(): Point {
const dpi = Math.max(window.devicePixelRatio ?? 1, 1)
const [x, y, w, h] = app.canvas.ds.visible_area
const visibleArea = app.canvas?.ds?.visible_area
if (!visibleArea) {
return [0, 0]
}
const [x, y, w, h] = visibleArea
return [x + w / dpi / 2, y + h / dpi / 2]
}

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
@@ -83,7 +84,7 @@ describe('useComfyRegistryStore', () => {
}
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
mockRegistryService = {
isLoading: ref(false),

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
@@ -11,7 +12,7 @@ const MockComponent = defineComponent({
describe('dialogStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
})
describe('priority system', () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
@@ -30,7 +31,7 @@ describe('domWidgetStore', () => {
let store: ReturnType<typeof useDomWidgetStore>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useDomWidgetStore()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { app } from '@/scripts/app'
@@ -59,7 +60,7 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
mockNodeIdToNodeLocatorId.mockReset()
mockNodeLocatorIdToNodeExecutionId.mockReset()
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useExecutionStore()
})
@@ -137,7 +138,7 @@ describe('useExecutionStore - Node Error Lookups', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useExecutionStore()
})

View File

@@ -29,10 +29,11 @@ import type {
} from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useGraphErrorStateStore } from '@/stores/graphErrorStateStore'
import type { GraphError } from '@/stores/graphErrorStateStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import type { NodeLocatorId } from '@/types/nodeIdentification'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil'
interface QueuedPrompt {
/**
@@ -584,59 +585,47 @@ export const useExecutionStore = defineStore('execution', () => {
}
/**
* Update node and slot error flags when validation errors change.
* Propagates errors up subgraph chains.
* Sync backend validation errors to centralized graph error store.
* The store handles flag updates and subgraph propagation.
*/
watch(lastNodeErrors, () => {
if (!app.rootGraph) return
const errorStore = useGraphErrorStateStore()
// Clear all error flags
forEachNode(app.rootGraph, (node) => {
node.has_errors = false
if (node.inputs) {
for (const slot of node.inputs) {
slot.hasErrors = false
}
}
})
if (!lastNodeErrors.value) {
errorStore.execute({ type: 'CLEAR_SOURCE', source: 'backend' })
return
}
if (!lastNodeErrors.value) return
const errors: GraphError[] = []
// Set error flags on nodes and slots
for (const [executionId, nodeError] of Object.entries(
lastNodeErrors.value
)) {
const node = getNodeByExecutionId(app.rootGraph, executionId)
if (!node) continue
const locatorId = executionIdToNodeLocatorId(executionId)
if (!locatorId) continue
node.has_errors = true
errors.push({
key: `backend:node:${locatorId}`,
source: 'backend',
target: { kind: 'node', nodeId: locatorId },
message: nodeError.errors[0]?.message
})
// Mark input slots with errors
if (node.inputs) {
for (const error of nodeError.errors) {
const slotName = error.extra_info?.input_name
if (!slotName) continue
const slot = node.inputs.find((s) => s.name === slotName)
if (slot) {
slot.hasErrors = true
}
}
}
// Propagate errors to parent subgraph nodes
const parts = executionId.split(':')
for (let i = parts.length - 1; i > 0; i--) {
const parentExecutionId = parts.slice(0, i).join(':')
const parentNode = getNodeByExecutionId(
app.rootGraph,
parentExecutionId
)
if (parentNode) {
parentNode.has_errors = true
for (const error of nodeError.errors) {
const slotName = error.extra_info?.input_name
if (slotName) {
errors.push({
key: `backend:slot:${locatorId}:${slotName}`,
source: 'backend',
target: { kind: 'slot', nodeId: locatorId, slotName },
code: 'VALIDATION_ERROR',
message: error.message
})
}
}
}
errorStore.execute({ type: 'REPLACE_SOURCE', source: 'backend', errors })
})
return {

View File

@@ -1,6 +1,7 @@
import { FirebaseError } from 'firebase/app'
import * as firebaseAuth from 'firebase/auth'
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as vuefire from 'vuefire'
@@ -153,7 +154,7 @@ describe('useFirebaseAuthStore', () => {
})
// Initialize Pinia
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useFirebaseAuthStore()
// Reset and set up getIdToken mock
@@ -175,7 +176,7 @@ describe('useFirebaseAuthStore', () => {
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any)
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useFirebaseAuthStore()
})

View File

@@ -278,7 +278,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
}
const createCustomer = async (): Promise<CreateCustomerResponse> => {
const authHeader = await getFirebaseAuthHeader()
const authHeader = await getAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}

View File

@@ -0,0 +1,257 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useGraphErrorStateStore } from './graphErrorStateStore'
import type { GraphError } from './graphErrorStateStore'
describe('graphErrorStateStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
describe('REPLACE_SOURCE command', () => {
it('adds errors for a source', () => {
const store = useGraphErrorStateStore()
const errors: GraphError[] = [
{
key: 'frontend:slot:123:model',
source: 'frontend',
target: { kind: 'slot', nodeId: '123', slotName: 'model' },
code: 'MISSING_REQUIRED_INPUT'
}
]
store.execute({ type: 'REPLACE_SOURCE', source: 'frontend', errors })
expect(store.hasErrorsForNode('123')).toBe(true)
expect(store.hasSlotError('123', 'model')).toBe(true)
expect(store.version).toBe(1)
})
it('replaces all errors for a source', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:1:a',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'a' }
},
{
key: 'frontend:slot:2:b',
source: 'frontend',
target: { kind: 'slot', nodeId: '2', slotName: 'b' }
}
]
})
expect(store.hasErrorsForNode('1')).toBe(true)
expect(store.hasErrorsForNode('2')).toBe(true)
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:3:c',
source: 'frontend',
target: { kind: 'slot', nodeId: '3', slotName: 'c' }
}
]
})
expect(store.hasErrorsForNode('1')).toBe(false)
expect(store.hasErrorsForNode('2')).toBe(false)
expect(store.hasErrorsForNode('3')).toBe(true)
})
it('preserves errors from other sources', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'backend',
errors: [
{
key: 'backend:node:1',
source: 'backend',
target: { kind: 'node', nodeId: '1' }
}
]
})
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:2:a',
source: 'frontend',
target: { kind: 'slot', nodeId: '2', slotName: 'a' }
}
]
})
expect(store.hasErrorsForNode('1')).toBe(true)
expect(store.hasErrorsForNode('2')).toBe(true)
})
})
describe('CLEAR_SOURCE command', () => {
it('clears errors for a source', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:1:a',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'a' }
}
]
})
store.execute({ type: 'CLEAR_SOURCE', source: 'frontend' })
expect(store.hasErrorsForNode('1')).toBe(false)
})
it('preserves other sources', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'backend',
errors: [
{
key: 'backend:node:1',
source: 'backend',
target: { kind: 'node', nodeId: '1' }
}
]
})
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:2:a',
source: 'frontend',
target: { kind: 'slot', nodeId: '2', slotName: 'a' }
}
]
})
store.execute({ type: 'CLEAR_SOURCE', source: 'frontend' })
expect(store.hasErrorsForNode('1')).toBe(true)
expect(store.hasErrorsForNode('2')).toBe(false)
})
})
describe('CLEAR_ALL command', () => {
it('clears all errors', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'backend',
errors: [
{
key: 'backend:node:1',
source: 'backend',
target: { kind: 'node', nodeId: '1' }
}
]
})
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:2:a',
source: 'frontend',
target: { kind: 'slot', nodeId: '2', slotName: 'a' }
}
]
})
store.execute({ type: 'CLEAR_ALL' })
expect(store.hasErrorsForNode('1')).toBe(false)
expect(store.hasErrorsForNode('2')).toBe(false)
})
})
describe('getErrorsForNode', () => {
it('returns all errors for a node', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:slot:1:a',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'a' }
},
{
key: 'frontend:slot:1:b',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'b' }
}
]
})
const errors = store.getErrorsForNode('1')
expect(errors).toHaveLength(2)
})
it('returns empty array for node without errors', () => {
const store = useGraphErrorStateStore()
expect(store.getErrorsForNode('999')).toEqual([])
})
})
describe('getSlotErrors', () => {
it('returns only slot errors for specific slot', () => {
const store = useGraphErrorStateStore()
store.execute({
type: 'REPLACE_SOURCE',
source: 'frontend',
errors: [
{
key: 'frontend:node:1',
source: 'frontend',
target: { kind: 'node', nodeId: '1' }
},
{
key: 'frontend:slot:1:model',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'model' }
},
{
key: 'frontend:slot:1:clip',
source: 'frontend',
target: { kind: 'slot', nodeId: '1', slotName: 'clip' }
}
]
})
const slotErrors = store.getSlotErrors('1', 'model')
expect(slotErrors).toHaveLength(1)
expect(slotErrors[0].target).toEqual({
kind: 'slot',
nodeId: '1',
slotName: 'model'
})
})
})
})

View File

@@ -0,0 +1,144 @@
import { defineStore } from 'pinia'
import { shallowRef } from 'vue'
import type { NodeLocatorId } from '@/types/nodeIdentification'
type GraphErrorSource = 'frontend' | 'backend'
type GraphErrorTarget =
| { kind: 'node'; nodeId: NodeLocatorId }
| { kind: 'slot'; nodeId: NodeLocatorId; slotName: string }
export interface GraphError {
key: string
source: GraphErrorSource
target: GraphErrorTarget
code?: string
message?: string
}
type GraphErrorCommand =
| { type: 'REPLACE_SOURCE'; source: GraphErrorSource; errors: GraphError[] }
| { type: 'CLEAR_SOURCE'; source: GraphErrorSource }
| { type: 'CLEAR_ALL' }
export const useGraphErrorStateStore = defineStore('graphErrorState', () => {
const errorsByKey = shallowRef(new Map<string, GraphError>())
const keysBySource = shallowRef(new Map<GraphErrorSource, Set<string>>())
const keysByNode = shallowRef(new Map<NodeLocatorId, Set<string>>())
const version = shallowRef(0)
function addErrorInternal(error: GraphError): void {
const newErrorsByKey = new Map(errorsByKey.value)
newErrorsByKey.set(error.key, error)
errorsByKey.value = newErrorsByKey
const newKeysBySource = new Map(keysBySource.value)
if (!newKeysBySource.has(error.source)) {
newKeysBySource.set(error.source, new Set())
}
newKeysBySource.get(error.source)!.add(error.key)
keysBySource.value = newKeysBySource
const nodeId = error.target.nodeId
const newKeysByNode = new Map(keysByNode.value)
if (!newKeysByNode.has(nodeId)) {
newKeysByNode.set(nodeId, new Set())
}
newKeysByNode.get(nodeId)!.add(error.key)
keysByNode.value = newKeysByNode
}
function clearSourceInternal(source: GraphErrorSource): void {
const keys = keysBySource.value.get(source)
if (!keys || keys.size === 0) return
const newErrorsByKey = new Map(errorsByKey.value)
const newKeysByNode = new Map(keysByNode.value)
for (const key of keys) {
const error = newErrorsByKey.get(key)
if (error) {
const nodeId = error.target.nodeId
const nodeKeys = newKeysByNode.get(nodeId)
if (nodeKeys) {
const newNodeKeys = new Set(nodeKeys)
newNodeKeys.delete(key)
if (newNodeKeys.size === 0) {
newKeysByNode.delete(nodeId)
} else {
newKeysByNode.set(nodeId, newNodeKeys)
}
}
newErrorsByKey.delete(key)
}
}
errorsByKey.value = newErrorsByKey
keysByNode.value = newKeysByNode
const newKeysBySource = new Map(keysBySource.value)
newKeysBySource.delete(source)
keysBySource.value = newKeysBySource
}
function execute(command: GraphErrorCommand): void {
switch (command.type) {
case 'REPLACE_SOURCE': {
clearSourceInternal(command.source)
for (const error of command.errors) {
addErrorInternal(error)
}
break
}
case 'CLEAR_SOURCE': {
clearSourceInternal(command.source)
break
}
case 'CLEAR_ALL': {
errorsByKey.value = new Map()
keysBySource.value = new Map()
keysByNode.value = new Map()
break
}
}
version.value++
}
function getErrorsForNode(nodeId: NodeLocatorId): GraphError[] {
const keys = keysByNode.value.get(nodeId)
if (!keys) return []
return [...keys]
.map((k) => errorsByKey.value.get(k))
.filter((e): e is GraphError => e !== undefined)
}
function hasErrorsForNode(nodeId: NodeLocatorId): boolean {
const keys = keysByNode.value.get(nodeId)
return keys !== undefined && keys.size > 0
}
function getSlotErrors(
nodeId: NodeLocatorId,
slotName: string
): GraphError[] {
return getErrorsForNode(nodeId).filter(
(e) => e.target.kind === 'slot' && e.target.slotName === slotName
)
}
function hasSlotError(nodeId: NodeLocatorId, slotName: string): boolean {
return getSlotErrors(nodeId, slotName).length > 0
}
return {
version,
errorsByKey,
keysByNode,
execute,
getErrorsForNode,
hasErrorsForNode,
getSlotErrors,
hasSlotError
}
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
@@ -30,7 +31,7 @@ const createMockOutputs = (
describe('imagePreviewStore getPreviewParam', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
vi.mocked(litegraphUtil.isVideoNode).mockReturnValue(false)
})

View File

@@ -1,11 +1,12 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
describe('useKeybindingStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('should add and retrieve default keybindings', () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { assetService } from '@/platform/assets/services/assetService'
@@ -89,7 +90,7 @@ describe('useModelStore', () => {
let store: ReturnType<typeof useModelStore>
beforeEach(async () => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.resetAllMocks()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
@@ -82,7 +83,7 @@ vi.mock('@/stores/nodeDefStore', async (importOriginal) => {
describe('useModelToNodeStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
})
@@ -330,7 +331,7 @@ describe('useModelToNodeStore', () => {
it('should not register when nodeDefStore is empty', () => {
// Create fresh Pinia for this test to avoid state persistence
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({
nodeDefsByName: {}
@@ -355,7 +356,7 @@ describe('useModelToNodeStore', () => {
it('should return empty Record when nodeDefStore is empty', () => {
// Create fresh Pinia for this test to avoid state persistence
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({
nodeDefsByName: {}

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
@@ -9,7 +10,7 @@ describe('useNodeDefStore', () => {
let store: ReturnType<typeof useNodeDefStore>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useNodeDefStore()
})

View File

@@ -1,9 +1,10 @@
import axios from 'axios'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
import type {
@@ -17,6 +18,7 @@ import type {
ComfyOutputTypesSpec as ComfyOutputSpecV1,
PriceBadge
} from '@/schemas/nodeDefSchema'
import { useSettingStore } from '@/platform/settings/settingStore'
import { NodeSearchService } from '@/services/nodeSearchService'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
@@ -41,6 +43,7 @@ export class ComfyNodeDefImpl
readonly help: string
readonly deprecated: boolean
readonly experimental: boolean
readonly dev_only: boolean
readonly output_node: boolean
readonly api_node: boolean
/**
@@ -133,6 +136,7 @@ export class ComfyNodeDefImpl
this.deprecated = obj.deprecated ?? obj.category === ''
this.experimental =
obj.experimental ?? obj.category.startsWith('_for_testing')
this.dev_only = obj.dev_only ?? false
this.output_node = obj.output_node
this.api_node = !!obj.api_node
this.input = obj.input ?? {}
@@ -174,6 +178,7 @@ export class ComfyNodeDefImpl
get nodeLifeCycleBadgeText(): string {
if (this.deprecated) return '[DEPR]'
if (this.experimental) return '[BETA]'
if (this.dev_only) return '[DEV]'
return ''
}
}
@@ -299,12 +304,27 @@ export interface NodeDefFilter {
}
export const useNodeDefStore = defineStore('nodeDef', () => {
const settingStore = useSettingStore()
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
const showDeprecated = ref(false)
const showExperimental = ref(false)
const showDevOnly = computed(() => settingStore.get('Comfy.DevMode'))
const nodeDefFilters = ref<NodeDefFilter[]>([])
// Update skip_list on all registered node types when dev mode changes
// This ensures LiteGraph's getNodeTypesCategories/getNodeTypesInCategory
// correctly filter dev-only nodes from the right-click context menu
watchEffect(() => {
const devModeEnabled = showDevOnly.value
for (const nodeType of Object.values(LiteGraph.registered_node_types)) {
if (nodeType.nodeData?.dev_only) {
nodeType.skip_list = !devModeEnabled
}
}
})
const nodeDefs = computed(() => {
const subgraphStore = useSubgraphStore()
// Blueprints first for discoverability in the node library sidebar
@@ -422,6 +442,14 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
predicate: (nodeDef) => showExperimental.value || !nodeDef.experimental
})
// Dev-only nodes filter
registerNodeDefFilter({
id: 'core.dev_only',
name: 'Hide Dev-Only Nodes',
description: 'Hides nodes marked as dev-only unless dev mode is enabled',
predicate: (nodeDef) => showDevOnly.value || !nodeDef.dev_only
})
// Subgraph nodes filter
// Filter out litegraph typed subgraphs, saved blueprints are added in separately
registerNodeDefFilter({
@@ -446,6 +474,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
nodeDefsByDisplayName,
showDeprecated,
showExperimental,
showDevOnly,
nodeDefFilters,
nodeDefs,

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
@@ -71,7 +72,7 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => {
let mockFetchApi: ReturnType<typeof vi.fn>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
mockFetchApi = vi.fn()

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
@@ -240,7 +241,7 @@ describe('useQueueStore', () => {
let store: ReturnType<typeof useQueueStore>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useQueueStore()
vi.clearAllMocks()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { ServerConfig } from '@/constants/serverConfig'
@@ -14,7 +15,7 @@ describe('useServerConfigStore', () => {
let store: ReturnType<typeof useServerConfigStore>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useServerConfigStore()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
@@ -47,7 +48,7 @@ vi.mock('@/utils/graphTraversalUtil', () => ({
describe('useSubgraphNavigationStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('should not clear navigation stack when workflow internal state changes', async () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
@@ -46,7 +47,7 @@ const mockCanvas = app.canvas as any
describe('useSubgraphNavigationStore - Viewport Persistence', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
// Reset canvas state
mockCanvas.ds.scale = 1
mockCanvas.ds.offset = [0, 0]

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
@@ -78,7 +79,7 @@ describe('useSubgraphStore', () => {
}
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useSubgraphStore()
vi.clearAllMocks()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
@@ -25,7 +26,7 @@ describe('useSystemStatsStore', () => {
beforeEach(() => {
// Mock API to prevent automatic fetch on store creation
vi.mocked(api.getSystemStats).mockResolvedValue(null as any)
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useSystemStatsStore()
vi.clearAllMocks()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useTemplateRankingStore } from '@/stores/templateRankingStore'
@@ -12,7 +13,7 @@ vi.mock('axios', () => ({
describe('templateRankingStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
@@ -19,7 +20,7 @@ describe('useUserFileStore', () => {
let store: ReturnType<typeof useUserFileStore>
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
store = useUserFileStore()
vi.resetAllMocks()
})

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
@@ -53,7 +54,7 @@ vi.mock('@/utils/envUtil', () => ({
describe('useBottomPanelStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('should initialize with empty panels', () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
@@ -14,7 +15,7 @@ describe('nodeHelpStore', () => {
}
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('should initialize with empty state', () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type NodeSearchBoxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
@@ -34,7 +35,7 @@ function createMockSettingStore(): ReturnType<typeof useSettingStore> {
describe('useSearchBoxStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
vi.restoreAllMocks()
})

View File

@@ -11,7 +11,7 @@ describe('nodeFilterUtil', () => {
): LGraphNode => {
// Create a custom class with the nodeData static property
class MockNode extends LGraphNode {
static nodeData = isOutputNode ? { output_node: true } : {}
static override nodeData = isOutputNode ? { output_node: true } : {}
}
const node = new MockNode('')
@@ -71,11 +71,11 @@ describe('nodeFilterUtil', () => {
})
it('should handle nodes with undefined output_node', () => {
class MockNodeWithOtherData extends LGraphNode {
static nodeData = { someOtherProperty: true }
class MockNodeWithEmptyData extends LGraphNode {
static override nodeData = {}
}
const node = new MockNodeWithOtherData('')
const node = new MockNodeWithEmptyData('')
node.id = 1
const result = filterOutputNodes([node])

View File

@@ -45,6 +45,8 @@ import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDi
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
import GlobalToast from '@/components/toast/GlobalToast.vue'
import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue'
import { useGraphErrorState } from '@/composables/graph/useGraphErrorState'
import { useRequiredConnectionValidator } from '@/composables/graph/useRequiredConnectionValidator'
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
import { useCoreCommands } from '@/composables/useCoreCommands'
import { useErrorHandling } from '@/composables/useErrorHandling'
@@ -85,6 +87,8 @@ import ManagerProgressToast from '@/workbench/extensions/manager/components/Mana
setupAutoQueueHandler()
useProgressFavicon()
useBrowserTabTitle()
useGraphErrorState()
useRequiredConnectionValidator()
const { t } = useI18n()
const toast = useToast()

View File

@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import Button from '@/components/ui/button/Button.vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, ref } from 'vue'
@@ -44,11 +45,11 @@ vi.mock(
)
describe('NodeConflictDialogContent', () => {
let pinia: ReturnType<typeof createPinia>
let pinia: ReturnType<typeof createTestingPinia>
beforeEach(() => {
vi.clearAllMocks()
pinia = createPinia()
pinia = createTestingPinia({ stubActions: false })
setActivePinia(pinia)
// Reset mock data
mockConflictData.value = []

View File

@@ -1,6 +1,6 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import PrimeVue from 'primevue/config'
import Tooltip from 'primevue/tooltip'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -89,7 +89,7 @@ describe('PackVersionBadge', () => {
...props
},
global: {
plugins: [PrimeVue, createPinia(), i18n],
plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n],
directives: {
tooltip: Tooltip
},

View File

@@ -1,6 +1,6 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import Button from '@/components/ui/button/Button.vue'
import PrimeVue from 'primevue/config'
import Listbox from 'primevue/listbox'
@@ -115,7 +115,7 @@ describe('PackVersionSelectorPopover', () => {
...props
},
global: {
plugins: [PrimeVue, createPinia(), i18n],
plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n],
components: {
Listbox,
VerifiedIcon,

View File

@@ -1,6 +1,6 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import PrimeVue from 'primevue/config'
import ToggleSwitch from 'primevue/toggleswitch'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -81,7 +81,7 @@ describe('PackEnableToggle', () => {
...props
},
global: {
plugins: [PrimeVue, createPinia(), i18n]
plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n]
}
})
}

View File

@@ -1,6 +1,6 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
@@ -73,7 +73,7 @@ describe('PackCardFooter', () => {
...props
},
global: {
plugins: [PrimeVue, createPinia(), i18n],
plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n],
provide: {
[IsInstallingKey]: ref(false)
}

View File

@@ -1,6 +1,6 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import PrimeVue from 'primevue/config'
import { describe, expect, it } from 'vitest'
import { nextTick } from 'vue'
@@ -32,7 +32,7 @@ describe('GridSkeleton', () => {
...props
},
global: {
plugins: [PrimeVue, createPinia(), i18n],
plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n],
stubs: {
PackCardSkeleton: true
}

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
@@ -36,7 +37,7 @@ describe('usePacksSelection', () => {
beforeEach(() => {
vi.clearAllMocks()
const pinia = createPinia()
const pinia = createTestingPinia({ stubActions: false })
setActivePinia(pinia)
managerStore = useComfyManagerStore()

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
@@ -48,7 +49,7 @@ describe('usePacksStatus', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
setActivePinia(createTestingPinia({ stubActions: false }))
conflictDetectionStore = useConflictDetectionStore()
})

View File

@@ -1,3 +1,4 @@
import { createSharedComposable } from '@vueuse/core'
import { computed, onUnmounted, ref } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
@@ -9,7 +10,6 @@ import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { components } from '@/types/comfyRegistryTypes'
import { mapAllNodes } from '@/utils/graphTraversalUtil'
import { useNodePacks } from '@/workbench/extensions/manager/composables/nodePack/useNodePacks'
import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type WorkflowPack = {
id:
@@ -22,9 +22,10 @@ const CORE_NODES_PACK_NAME = 'comfy-core'
/**
* Handles parsing node pack metadata from nodes on the graph and fetching the
* associated node packs from the registry
* associated node packs from the registry.
* This is a shared singleton composable - all components use the same instance.
*/
export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
const _useWorkflowPacks = () => {
const nodeDefStore = useNodeDefStore()
const systemStatsStore = useSystemStatsStore()
const { inferPackFromNodeName } = useComfyRegistryStore()
@@ -129,7 +130,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
)
const { startFetch, cleanup, error, isLoading, nodePacks, isReady } =
useNodePacks(workflowPacksIds, options)
useNodePacks(workflowPacksIds)
const isIdInWorkflow = (packId: string) =>
workflowPacksIds.value.includes(packId)
@@ -153,3 +154,5 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
filterWorkflowPack
}
}
export const useWorkflowPacks = createSharedComposable(_useWorkflowPacks)

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