Compare commits

..

22 Commits

Author SHA1 Message Date
AustinMroz
799e2d5569 Version bump 1.27.10 (#5969)
Patch version increment to 1.27.10

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5969-Version-bump-1-27-10-2866d73d365081b78ae2dbbb26f1d113)
by [Unito](https://www.unito.io)
2025-10-07 19:37:22 -05:00
Comfy Org PR Bot
3ab97100d2 [backport 1.27] OpenAIVideoSora2 Frontend pricing (#5967)
Backport of #5958 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5967-backport-1-27-OpenAIVideoSora2-Frontend-pricing-2866d73d365081b8a146d579824a005d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Marwan Ahmed <155799754+marawan206@users.noreply.github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-10-07 19:32:10 -05:00
filtered
58773f51f7 [Release] v1.27.9 (#5948)
## 🐛 Bug Fixes

- Removes broken installer terminal button (#5946)

**Full Changelog**:
https://github.com/Comfy-Org/ComfyUI_frontend/compare/v1.27.8...v1.27.9
2025-10-07 08:40:27 +11:00
filtered
feb3c078fa [desktop] Removes broken installer terminal button (#5946)
## Summary

Removes the terminal toggle button from the desktop installer that
causes broken UX when clicked during desktop install.

The button itself is merely a means to show the version of logs stored
in memory, using the in-app interface, as opposed to reading the files
from disk. The `Show Logs` button (working) takes the user directly into
the log directory.

## Changes

- **What**: Removes "Show Terminal" button from ServerStartView during
install flow
- **Breaking**: Removes show terminal button functionality

## Review Focus

The button is removed and is to be re-added at a later date (if Design
agree).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5946-desktop-Removes-broken-installer-terminal-button-2846d73d365081ac88d6c541a5d9c592)
by [Unito](https://www.unito.io)
2025-10-06 14:34:18 -07:00
AustinMroz
71a2eaa902 Version bump 1.27.8 (#5944)
Patch version increment to 1.27.8

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5944-Version-bump-1-27-8-2846d73d365081eaa942e5a2306fea86)
by [Unito](https://www.unito.io)
2025-10-06 15:30:34 -05:00
filtered
a310c3cd8e Fix images missing during desktop install (#5941)
## Summary

There's missing desktop GPU picker images caused by refactoring of
desktop front-end installation code. Was previously not an issue because
the paths were automatically adjusted by Vue, vite, or some other
tooling. Now that these are set programmatically (component props), this
"helpful" path adjustment is no longer occurring.

## Changes

- **What**: Takes away leading `/` from image paths.

## Screenshots (if applicable)

Current:
<img width="480" height="307" alt="image"
src="https://github.com/user-attachments/assets/20c0dd29-26d1-41b8-a59d-d3b0e69d998a"
/>

Proposed:
<img width="480" height="299" alt="image"
src="https://github.com/user-attachments/assets/aee87d1f-2060-4ec7-a8fc-43ebe2d650c2"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5941-Fix-images-missing-during-desktop-install-2846d73d365081c7b318c0ff31f70f2f)
by [Unito](https://www.unito.io)
2025-10-06 15:22:22 -05:00
AustinMroz
1a6ef57643 1.27.7 (#5862)
## What's Changed
### 🚀 Features
- add pricing for new api nodes (#5725)
- Rework desktop install / startup UX (#5292)

### 🐛 Bug Fixes
- Make experiment asset api setting hidden (#5861)
- Only add the listeners for DomWidget components once (#5849)
- fix invalid JSON (contains merge conflict markers) in 1.27 RC branch's
locale json (#5839)
- fix: Status indicator and close button appearing together  (#5741)
- Fix cyclic prototype errors with subgraphNodes (#5650)
- fix: don't immediately close missing nodes dialog if manager is
disabled (#5649)
- Fix SaveAs (#5644)

### 🔧 Maintenance  
- Add JSON validation CI workflow (#5838)
- Cherry-pick manager flag refactor to core/1.27 (#5646)
- Cherry-pick desktop dialogs framework to core/1.27 (#5634)

**Full Changelog**:
https://github.com/Comfy-Org/ComfyUI_frontend/compare/v1.27.5...v1.27.7

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5862-1-27-7-27e6d73d3650819aa712e682f6b88077)
by [Unito](https://www.unito.io)
2025-09-30 12:40:03 -05:00
AustinMroz
d3dc330d56 [backport 1.27] [chore] make experiment asset api setting hidden (#5861)
Backport of #5851 to to `core/1.27`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5861-backport-1-27-chore-make-experiment-asset-api-setting-hidden-27e6d73d3650819ea7c0fc0d48a393ee)
by [Unito](https://www.unito.io)

Co-authored-by: Arjan Singh <1598641+arjansingh@users.noreply.github.com>
2025-09-30 08:11:06 -07:00
Comfy Org PR Bot
cdff3f90ce [backport 1.27] fix: Only add the listeners for DomWidget components once (#5849)
Backport of #5846 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5849-backport-1-27-fix-Only-add-the-listeners-for-DomWidget-components-once-27d6d73d365081438673f970b0508032)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2025-09-29 21:20:15 -07:00
Comfy Org PR Bot
470663e25d [backport 1.27] add pricing for WanImageToImageApi node (#5830)
Backport of #5829 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5830-backport-1-27-add-pricing-for-WanImageToImageApi-node-27c6d73d36508104abbbfe78153b0d2a)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2025-09-28 17:02:59 -07:00
Comfy Org PR Bot
884a9886e0 [backport 1.27] [ci] add JSON validation CI workflow (#5838)
Backport of #5837 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5838-backport-1-27-ci-add-JSON-validation-CI-workflow-27c6d73d36508113a05fcd1a5bfc6512)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-09-28 16:20:38 -07:00
Christian Byrne
0ee3138485 fix invalid JSON (contains merge conflict markers) in 1.27 RC branch's locale json (#5839)
## Summary

Merge markers were inadvertantly merged in
https://github.com/comfy-org/ComfyuI_frontend/pull/5801, which [I
thought was good to
merge](https://github.com/Comfy-Org/ComfyUI_frontend/pull/5801#issuecomment-3340372578)
but missed this due to difficulty reviewing the thousands of lines of
translations that were in the backlog due to month+ long downtime on
translation CI.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5839-fix-invalid-JSON-contains-merge-conflict-markers-in-1-27-RC-branch-s-locale-json-27c6d73d365081439838ff4055714cf5)
by [Unito](https://www.unito.io)
2025-09-28 16:06:36 -07:00
AustinMroz
d6f0cae35c 1.27.6 (#5801)
Patch version increment to 1.27.6

Forced bump to regenerate locales

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5801-1-27-27a6d73d365081b988b9fc1e2564d193)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-27 10:36:02 -05:00
Christian Byrne
a91948352d fix(collect-i18n-node-defs): backport ComfyNodeDefImpl browser context fix to core/1.27 (#5796)
## Summary
Backports the ComfyNodeDefImpl browser context fix from #5775 to the
core/1.27 branch.

## Changes
- Move ComfyNodeDefImpl instantiation to browser context to avoid
Node.js compatibility issues
- Add workers: 1 to playwright i18n config for consistent execution 
- Fix floating promise by awaiting page.route() call
- Add allowDefaultProject config for playwright and script files to
resolve ESLint parsing

## Original Issue
The `collect-i18n-node-defs.ts` script was failing due to Vue components
being imported into Node.js context, causing Babel compilation errors
with TypeScript 'declare' fields.

## Solution
Uses dynamic imports to defer module loading until runtime in the
browser context, avoiding Babel compilation of problematic
TypeScript/Vue files.

Cherry-picked from 3a9365af1


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5796-fix-collect-i18n-node-defs-backport-ComfyNodeDefImpl-browser-context-fix-to-core-1-27-27a6d73d365081639625f25ecf9553fd)
by [Unito](https://www.unito.io)

Co-authored-by: github-actions <github-actions@github.com>
2025-09-26 07:40:09 -05:00
filtered
df3060b8e2 [Backport 1.27] Rework desktop install / startup UX (#5292)
## Summary

Backporting #5292 to core/1.27 branch to fix desktop installation and
startup UX issues.

## What's Changed

This is a manual backport of commit
b0f81b2245 which reworks the desktop
install and startup user experience.

### Merge Conflicts Resolved

The automatic backport failed with conflicts in:
- `src/components/install/GpuPicker.vue` - Resolved by keeping the
changes made in the PR
- `src/components/install/MirrorsConfiguration.vue` - Resolved by
keeping the file deletion from the PR
- `src/components/install/mirror/MirrorItem.vue` - Resolved by
combination merge (all changes)
- `src/views/ServerStartView.vue` - Resolved by combination merge (all
changes)

## Original PR

- PR: #5292 
- Commit: b0f81b2245
- Title: Rework desktop install / startup UX

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5756-Backport-1-27-Rework-desktop-install-startup-UX-5292-2786d73d365081668f4dc16694c51185)
by [Unito](https://www.unito.io)
2025-09-24 17:12:35 -05:00
AustinMroz
26d777deee [backport 1.27] fix: Status indicator and close button appearing together (#5738) (#5741)
Backport of #5738 to `core/1.27`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5741-backport-1-27-fix-Status-indicator-and-close-button-appearing-together-5738-2776d73d36508181a7fec09edd816ec5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2025-09-23 16:28:38 -07:00
Comfy Org PR Bot
77ca6d54a0 [backport 1.27] add pricing for new api nodes (#5725)
Backport of #5724 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5725-backport-1-27-add-pricing-for-new-api-nodes-2766d73d3650819eac1de10bc967deb2)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2025-09-22 11:44:14 -07:00
Christian Byrne
12f23dac4f [backport] Fix cyclic prototype errors with subgraphNodes (#5650)
Cherry-pick of PR #5637: Fix cyclic prototype errors with subgraphNodes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5650-Hotfix-Fix-cyclic-prototype-errors-with-subgraphNodes-2736d73d365081238853d5b445d08e7f)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-09-18 18:59:11 -07:00
Comfy Org PR Bot
dac73dc7ed [backport 1.27] fix: don't immediately close missing nodes dialog if manager is disabled (#5649)
Backport of #5647 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5649-backport-1-27-fix-don-t-immediately-close-missing-nodes-dialog-if-manager-is-disabled-2736d73d3650813681a7d1983f8ccf96)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-09-18 18:14:18 -07:00
Christian Byrne
a99df5d3dd [backport] Cherry-pick manager flag refactor to core/1.27 (#5646)
Cherry-pick of PR #5635: refactor: Change manager flag from
--disable-manager to --enable-manager

## Summary
- Cherry-picked commit a41b8a6d4f
- Updates frontend to align with backend changes in ComfyUI core PR
#7555
- Changed manager startup argument from `--disable-manager` (opt-out) to
`--enable-manager` (opt-in)
- Manager is now disabled by default unless explicitly enabled

## Original Changes
- Modified `useManagerState.ts` to check for `--enable-manager` flag
presence
- Inverted logic: manager is disabled when the flag is NOT present
- Updated all related tests to reflect the new opt-in behavior
- Fixed edge case where `systemStats` is null

## Testing
-  TypeScript type checking passed
-  ESLint linting passed
-  Prettier formatting passed
-  Knip found no issues
-  Cherry-pick applied cleanly with no conflicts

## Related
- Original PR: #5635
- Backend PR: https://github.com/comfyanonymous/ComfyUI/pull/7555

This hotfix ensures the frontend manager flag logic matches the backend
changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5646-backport-Cherry-pick-manager-flag-refactor-to-core-1-27-2736d73d365081d38d8fddd6e451e156)
by [Unito](https://www.unito.io)

Co-authored-by: Jin Yi <jin12cc@gmail.com>
2025-09-18 17:20:27 -07:00
Comfy Org PR Bot
909866fd1d [backport 1.27] Fix SaveAs (#5644)
Backport of #5643 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5644-backport-1-27-Fix-SaveAs-2726d73d3650815dbaefe7090ee2ffde)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-09-18 17:07:16 -07:00
filtered
c39a03f5fd [Hotfix] Cherry-pick desktop dialogs framework to core/1.27 (#5634)
## Summary
Cherry-picked PR #5605 (Add desktop dialogs framework) to core/1.27
branch for hotfix release.

## Cherry-picked commits
- 09e7d1040: Add desktop dialogs framework (#5605)

## Changes
- Data-driven dialog structure in `desktopDialogs.ts`
- Dynamic dialog view component with i18n support
- Button action types: openUrl, close, cancel
- Button severity levels for styling (primary, secondary, danger, warn)
- Fallback invalid dialog for error handling
- i18n collection script updated for dialog strings

## Testing
- Typecheck passed ✓
- Cherry-pick applied cleanly without conflicts

## Impact
This adds the desktop dialog framework feature to the stable 1.27
branch, allowing desktop applications to display standardized dialogs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5634-Hotfix-Cherry-pick-desktop-dialogs-framework-to-core-1-27-2726d73d3650811188e7f7e58766d52f)
by [Unito](https://www.unito.io)
2025-09-17 23:26:40 -07:00
525 changed files with 13040 additions and 13346 deletions

View File

@@ -67,9 +67,9 @@ This is critical for better file inspection:
Use git locally for much faster analysis:
1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
### Step 1.5: Create Analysis Cache

2
.gitattributes vendored
View File

@@ -13,4 +13,4 @@
# Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true
src/types/generatedManagerTypes.ts linguist-generated=true

View File

@@ -4,25 +4,10 @@ on:
pull_request_target:
types: [closed, labeled]
branches: [main]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to backport'
required: true
type: string
force_rerun:
description: 'Force rerun even if backports exist'
required: false
type: boolean
default: false
jobs:
backport:
if: >
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
github.event_name == 'workflow_dispatch'
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
runs-on: ubuntu-latest
permissions:
contents: write
@@ -30,35 +15,6 @@ jobs:
issues: write
steps:
- name: Validate inputs for manual triggers
if: github.event_name == 'workflow_dispatch'
run: |
# Validate PR number format
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number format. Must be a positive integer."
exit 1
fi
# Validate PR exists and is merged
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
exit 1
fi
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
if [ "$MERGED" != "true" ]; then
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
exit 1
fi
# Validate PR has needs-backport label
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -73,7 +29,7 @@ jobs:
id: check-existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Check for existing backport PRs for this PR number
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
@@ -83,13 +39,6 @@ jobs:
exit 0
fi
# For manual triggers with force_rerun, proceed anyway
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "::warning::Force rerun requested - existing backports will be updated"
exit 0
fi
echo "Found existing backport PRs:"
echo "$EXISTING_BACKPORTS"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -101,17 +50,8 @@ jobs:
run: |
# Extract version labels (e.g., "1.24", "1.22")
VERSIONS=""
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# For manual triggers, get labels from the PR
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
# For automatic triggers, extract from PR event
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi
for label in $LABELS; do
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
for label in $(echo "$LABELS" | jq -r '.[].name'); do
# Match version labels like "1.24" (major.minor only)
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Validate the branch exists before adding to list
@@ -135,20 +75,12 @@ jobs:
if: steps.check-existing.outputs.skip != 'true'
id: backport
env:
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
run: |
FAILED=""
SUCCESS=""
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_TITLE="${{ github.event.pull_request.title }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}"
@@ -201,18 +133,10 @@ jobs:
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
else
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r version branch <<< "${backport}"
@@ -241,16 +165,9 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
PR_NUMBER="${{ inputs.pr_number }}"
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r version reason conflicts <<< "${failure}"

15
.github/workflows/json-validate.yaml vendored Normal file
View File

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

4
.gitignore vendored
View File

@@ -78,8 +78,8 @@ vite.config.mts.timestamp-*.mjs
*storybook.log
storybook-static
# MCP Servers
.playwright-mcp/*
.nx/cache
.nx/workspace-data

View File

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

View File

@@ -15,32 +15,21 @@ const config: StorybookConfig = {
async viteFinal(config) {
// Use dynamic import to avoid CJS deprecation warning
const { mergeConfig } = await import('vite')
const { default: tailwindcss } = await import('@tailwindcss/vite')
// Filter out any plugins that might generate import maps
if (config.plugins) {
config.plugins = config.plugins
// Type guard: ensure we have valid plugin objects with names
.filter(
(plugin): plugin is NonNullable<typeof plugin> & { name: string } => {
return (
plugin !== null &&
plugin !== undefined &&
typeof plugin === 'object' &&
'name' in plugin &&
typeof plugin.name === 'string'
)
}
)
// Business logic: filter out import-map plugins
.filter((plugin) => !plugin.name.includes('import-map'))
config.plugins = config.plugins.filter((plugin: any) => {
if (plugin && plugin.name && plugin.name.includes('import-map')) {
return false
}
return true
})
}
return mergeConfig(config, {
// Replace plugins entirely to avoid inheritance issues
plugins: [
// Only include plugins we explicitly need for Storybook
tailwindcss(),
Icons({
compiler: 'vue3',
customCollections: {

View File

@@ -1,7 +1,7 @@
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import { setup } from '@storybook/vue3'
import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
import type { Preview } from '@storybook/vue3-vite'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -9,9 +9,11 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import '@/assets/css/style.css'
import { i18n } from '@/i18n'
import '@/lib/litegraph/public/css/litegraph.css'
import '../src/assets/css/style.css'
import { i18n } from '../src/i18n'
import '../src/lib/litegraph/public/css/litegraph.css'
import { useWidgetStore } from '../src/stores/widgetStore'
import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
@@ -23,11 +25,13 @@ const ComfyUIPreset = definePreset(Aura, {
// Setup Vue app for Storybook
setup((app) => {
app.directive('tooltip', Tooltip)
// Create Pinia instance
const pinia = createPinia()
app.use(pinia)
// Initialize stores
useColorPaletteStore(pinia)
useWidgetStore(pinia)
app.use(i18n)
app.use(PrimeVue, {
theme: {
@@ -46,8 +50,8 @@ setup((app) => {
app.use(ToastService)
})
// Theme and dialog decorator
export const withTheme = (Story: StoryFn, context: StoryContext) => {
// Dark theme decorator
export const withTheme = (Story: any, context: any) => {
const theme = context.globals.theme || 'light'
// Apply theme class to document root
@@ -59,7 +63,7 @@ export const withTheme = (Story: StoryFn, context: StoryContext) => {
document.body.classList.remove('dark-theme')
}
return Story(context.args, context)
return Story()
}
const preview: Preview = {

View File

@@ -1 +0,0 @@
{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4}

View File

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

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test'
import { Locator, Page } from '@playwright/test'
export class ComfyNodeSearchFilterSelectionPanel {
constructor(public readonly page: Page) {}

View File

@@ -1,6 +1,6 @@
import type { Page } from '@playwright/test'
import { Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage'
import { ComfyPage } from '../ComfyPage'
export class SettingDialog {
constructor(

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test'
import { Locator, Page } from '@playwright/test'
class SidebarTab {
constructor(

View File

@@ -1,5 +1,4 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { Locator, Page, expect } from '@playwright/test'
export class Topbar {
private readonly menuLocator: Locator

View File

@@ -12,10 +12,9 @@ export const webSocketFixture = base.extend<{
// so we can look it up to trigger messages
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
window.WebSocket = class extends window.WebSocket {
constructor(
...rest: ConstructorParameters<typeof window.WebSocket>
) {
super(...rest)
constructor() {
// @ts-expect-error
super(...arguments)
store[this.url] = this
}
}

View File

@@ -1,4 +1,4 @@
import type { FullConfig } from '@playwright/test'
import { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { backupPath } from './utils/backupUtils'

View File

@@ -1,4 +1,4 @@
import type { FullConfig } from '@playwright/test'
import { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { restorePath } from './utils/backupUtils'

View File

@@ -1,104 +0,0 @@
import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces'
import type { ComfyPage } from '../fixtures/ComfyPage'
interface FitToViewOptions {
selectionOnly?: boolean
zoom?: number
padding?: number
}
/**
* Instantly fits the canvas view to graph content without waiting for UI animation.
*
* Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented.
*/
export async function fitToViewInstant(
comfyPage: ComfyPage,
options: FitToViewOptions = {}
) {
const { selectionOnly = false, zoom = 0.75, padding = 10 } = options
const rectangles = await comfyPage.page.evaluate<
ReadOnlyRect[] | null,
{ selectionOnly: boolean }
>(
({ selectionOnly }) => {
const app = window['app']
if (!app?.canvas) return null
const canvas = app.canvas
const items = (() => {
if (selectionOnly && canvas.selectedItems?.size) {
return Array.from(canvas.selectedItems)
}
try {
return Array.from(canvas.positionableItems ?? [])
} catch {
return []
}
})()
if (!items.length) return null
const rects: ReadOnlyRect[] = []
for (const item of items) {
const rect = item?.boundingRect
if (!rect) continue
const x = Number(rect[0])
const y = Number(rect[1])
const width = Number(rect[2])
const height = Number(rect[3])
rects.push([x, y, width, height] as const)
}
return rects.length ? rects : null
},
{ selectionOnly }
)
if (!rectangles || rectangles.length === 0) return
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (const [x, y, width, height] of rectangles) {
minX = Math.min(minX, Number(x))
minY = Math.min(minY, Number(y))
maxX = Math.max(maxX, Number(x) + Number(width))
maxY = Math.max(maxY, Number(y) + Number(height))
}
const hasFiniteBounds =
Number.isFinite(minX) &&
Number.isFinite(minY) &&
Number.isFinite(maxX) &&
Number.isFinite(maxY)
if (!hasFiniteBounds) return
const bounds: ReadOnlyRect = [
minX - padding,
minY - padding,
maxX - minX + 2 * padding,
maxY - minY + 2 * padding
]
await comfyPage.page.evaluate(
({ bounds, zoom }) => {
const app = window['app']
if (!app?.canvas) return
const canvas = app.canvas
canvas.ds.fitToBounds(bounds, { zoom })
canvas.setDirty(true, true)
},
{ bounds, zoom }
)
await comfyPage.nextFrame()
}

View File

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

View File

@@ -1,7 +1,7 @@
import type { Locator, Page } from '@playwright/test'
import { Locator, Page } from '@playwright/test'
import path from 'path'
import type {
import {
TemplateInfo,
WorkflowTemplates
} from '../../src/platform/workflow/templates/types/template'

View File

@@ -29,9 +29,9 @@ test.describe('Actionbar', () => {
// Intercept the prompt queue endpoint
let promptNumber = 0
await comfyPage.page.route('**/api/prompt', async (route, req) => {
comfyPage.page.route('**/api/prompt', async (route, req) => {
await new Promise((r) => setTimeout(r, 100))
await route.fulfill({
route.fulfill({
status: 200,
body: JSON.stringify({
prompt_id: promptNumber,

View File

@@ -1,5 +1,5 @@
import type { ComfyPage } from '../fixtures/ComfyPage'
import {
ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'

View File

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

View File

@@ -1,5 +1,4 @@
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import { Locator, expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import type { SettingParams } from '../../src/platform/settings/types'
import { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Topbar commands', () => {

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Group Node', () => {

View File

@@ -1,13 +1,12 @@
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import { Locator, expect } from '@playwright/test'
import { Position } from '@vueuse/core'
import {
type ComfyPage,
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Remote COMBO Widget', () => {
const mockOptions = ['d', 'c', 'b', 'a']

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -160,9 +160,7 @@ test.describe.skip('Queue sidebar', () => {
comfyPage
}) => {
await comfyPage.nextFrame()
await expect(
comfyPage.menu.queueTab.getGalleryImage(firstImage)
).toBeVisible()
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
})
test('maintains active gallery item when new tasks are added', async ({
@@ -176,9 +174,7 @@ test.describe.skip('Queue sidebar', () => {
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
await newTask.waitFor({ state: 'visible' })
// The active gallery item should still be the initial image
await expect(
comfyPage.menu.queueTab.getGalleryImage(firstImage)
).toBeVisible()
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
})
test.describe('Gallery navigation', () => {
@@ -200,9 +196,7 @@ test.describe.skip('Queue sidebar', () => {
delay: 256
})
await comfyPage.nextFrame()
await expect(
comfyPage.menu.queueTab.getGalleryImage(end)
).toBeVisible()
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
})
})
})

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import type { SystemStats } from '../../src/schemas/apiSchema'
import { SystemStats } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Version Mismatch Warnings', () => {

View File

@@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
test.describe('NodeHeader', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,5 +1,5 @@
import path from 'path'
import type { Plugin } from 'vite'
import { Plugin } from 'vite'
interface ShimResult {
code: string

View File

@@ -1,7 +1,7 @@
import glob from 'fast-glob'
import fs from 'fs-extra'
import { dirname, join } from 'node:path'
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
interface ImportMapSource {
name: string

View File

@@ -33,7 +33,16 @@ export default defineConfig([
},
parserOptions: {
parser: tseslint.parser,
projectService: true,
projectService: {
allowDefaultProject: [
'vite.config.mts',
'vite.electron.config.mts',
'vite.types.config.mts',
'playwright.config.ts',
'playwright.i18n.config.ts',
'scripts/collect-i18n-node-defs.ts'
]
},
tsConfigRootDir: import.meta.dirname,
ecmaVersion: 2020,
sourceType: 'module',
@@ -75,8 +84,6 @@ export default defineConfig([
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off',
// Enforce dark-theme: instead of dark: prefix
@@ -153,14 +160,5 @@ export default defineConfig([
}
]
}
},
{
files: ['tests-ui/**/*'],
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false }
]
}
}
])

View File

@@ -22,7 +22,7 @@ const config: KnipConfig = {
],
ignore: [
// Auto generated manager types
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'src/types/generatedManagerTypes.ts',
'src/types/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts',

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.28.0",
"version": "1.27.10",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -7,6 +7,7 @@ export default defineConfig({
headless: true
},
reporter: 'list',
workers: 1,
timeout: 60000,
testMatch: /collect-i18n-.*\.ts/
})

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

77
scripts/cicd/check-json.sh Executable file
View File

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

View File

@@ -1,9 +1,9 @@
import * as fs from 'fs'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import type { ComfyNodeDef } from '../src/schemas/nodeDefSchema'
import type { ComfyApi } from '../src/scripts/api'
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
@@ -11,23 +11,28 @@ const nodeDefsPath = './src/locales/en/nodeDefs.json'
test('collect-i18n-node-defs', async ({ comfyPage }) => {
// Mock view route
comfyPage.page.route('**/view**', async (route) => {
await comfyPage.page.route('**/view**', async (route) => {
await route.fulfill({
body: JSON.stringify({})
})
})
const nodeDefs: ComfyNodeDefImpl[] = (
Object.values(
await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
) as ComfyNodeDef[]
// Note: Don't mock the object_info API endpoint - let it hit the actual backend
const nodeDefs: ComfyNodeDefImpl[] = await comfyPage.page.evaluate(
async () => {
const api = window['app'].api
const rawNodeDefs = await api.getNodeDefs()
const { ComfyNodeDefImpl } = await import('../src/stores/nodeDefStore')
return (
Object.values(rawNodeDefs)
// Ignore DevTools nodes (used for internal testing)
.filter((def: ComfyNodeDef) => !def.name.startsWith('DevTools'))
.map((def: ComfyNodeDef) => new ComfyNodeDefImpl(def))
)
}
)
// Ignore DevTools nodes (used for internal testing)
.filter((def) => !def.name.startsWith('DevTools'))
.map((def) => new ComfyNodeDefImpl(def))
console.log(`Collected ${nodeDefs.length} node definitions`)

View File

@@ -21,8 +21,7 @@
<script setup lang="ts">
import Button from 'primevue/button'
import type { CSSProperties } from 'vue'
import { computed, watchEffect } from 'vue'
import { CSSProperties, computed, watchEffect } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'

View File

@@ -22,8 +22,7 @@ import {
} from '@vueuse/core'
import { clamp } from 'es-toolkit/compat'
import Panel from 'primevue/panel'
import type { Ref } from 'vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'

View File

@@ -1,5 +1,8 @@
<template>
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
<div
ref="rootEl"
class="relative overflow-hidden h-full w-full bg-neutral-900"
>
<div class="p-terminal rounded-none h-full w-full p-2">
<div ref="terminalEl" class="h-full terminal-host" />
</div>
@@ -26,8 +29,7 @@
import { useElementHover, useEventListener } from '@vueuse/core'
import type { IDisposable } from '@xterm/xterm'
import Button from 'primevue/button'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
@@ -98,12 +100,13 @@ onUnmounted(() => {
</script>
<style scoped>
@reference '../../../../assets/css/style.css';
:deep(.p-terminal) .xterm {
overflow-x: auto;
@apply overflow-hidden;
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
@apply bg-neutral-900 overflow-hidden;
}
</style>

View File

@@ -3,9 +3,8 @@
</template>
<script setup lang="ts">
import type { IDisposable } from '@xterm/xterm'
import type { Ref } from 'vue'
import { onMounted, onUnmounted } from 'vue'
import { IDisposable } from '@xterm/xterm'
import { Ref, onMounted, onUnmounted } from 'vue'
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { electronAPI } from '@/utils/envUtil'

View File

@@ -15,11 +15,10 @@
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import ProgressSpinner from 'primevue/progressspinner'
import type { Ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { Ref, onMounted, onUnmounted, ref } from 'vue'
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import type { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
import { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore'

View File

@@ -47,8 +47,7 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import type { MenuState } from 'primevue/menu'
import Menu from 'primevue/menu'
import Menu, { MenuState } from 'primevue/menu'
import type { MenuItem } from 'primevue/menuitem'
import Tag from 'primevue/tag'
import { computed, nextTick, ref } from 'vue'

View File

@@ -17,7 +17,7 @@
<script setup lang="ts">
import { onBeforeUnmount } from 'vue'
import type { CustomExtension, VueExtension } from '@/types/extensionTypes'
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
const props = defineProps<{
extension: VueExtension | CustomExtension

View File

@@ -44,7 +44,7 @@ import FormRadioGroup from '@/components/common/FormRadioGroup.vue'
import InputKnob from '@/components/common/InputKnob.vue'
import InputSlider from '@/components/common/InputSlider.vue'
import UrlInput from '@/components/common/UrlInput.vue'
import type { FormItem } from '@/platform/settings/types'
import { FormItem } from '@/platform/settings/types'
const formValue = defineModel<any>('formValue')
const props = defineProps<{

View File

@@ -10,7 +10,7 @@
class="absolute inset-0"
/>
<img
v-if="cachedSrc"
v-show="isImageLoaded"
ref="imageRef"
:src="cachedSrc"
:alt="alt"
@@ -77,8 +77,8 @@ const shouldLoad = computed(() => isIntersecting.value)
watch(
shouldLoad,
async (shouldLoadVal) => {
if (shouldLoadVal && src && !cachedSrc.value && !hasError.value) {
async (shouldLoad) => {
if (shouldLoad && src && !cachedSrc.value && !hasError.value) {
try {
const cachedMedia = await getCachedMedia(src)
if (cachedMedia.error) {
@@ -93,7 +93,7 @@ watch(
console.warn('Failed to load cached media:', error)
cachedSrc.value = src
}
} else if (!shouldLoadVal) {
} else if (!shouldLoad) {
if (cachedSrc.value?.startsWith('blob:')) {
releaseUrl(src)
}

View File

@@ -32,7 +32,7 @@
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
import type { PrimeVueSeverity } from '@/types/primeVueTypes'
import { PrimeVueSeverity } from '@/types/primeVueTypes'
const {
disabled,

View File

@@ -0,0 +1,71 @@
<template>
<div :class="wrapperClass">
<div class="grid grid-rows-2 gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img
src="/assets/images/comfy-brand-mark.svg"
:alt="t('g.logoAlt')"
class="w-60"
/>
</div>
<!-- Bottom container: Progress and text -->
<div class="flex flex-col items-center justify-center gap-4">
<ProgressBar
v-if="!hideProgress"
:mode="progressMode"
:value="progressPercentage ?? 0"
:show-value="false"
class="w-90 h-2 mt-8"
:pt="{ value: { class: 'bg-brand-yellow' } }"
/>
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
{{ title }}
</h1>
<p v-if="statusText" class="text-lg text-neutral-400">
{{ statusText }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import ProgressBar from 'primevue/progressbar'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
/** Props for the StartupDisplay component */
interface StartupDisplayProps {
/** Progress: 0-100 for determinate, undefined for indeterminate */
progressPercentage?: number
/** Main title text */
title?: string
/** Status text shown below the title */
statusText?: string
/** Hide the progress bar */
hideProgress?: boolean
/** Use full screen wrapper (default: true) */
fullScreen?: boolean
}
const {
progressPercentage,
title,
statusText,
hideProgress = false,
fullScreen = true
} = defineProps<StartupDisplayProps>()
const progressMode = computed(() =>
progressPercentage === undefined ? 'indeterminate' : 'determinate'
)
const wrapperClass = computed(() =>
fullScreen
? 'flex items-center justify-center min-h-screen'
: 'flex items-center justify-center'
)
</script>

View File

@@ -9,8 +9,10 @@ import { createI18n } from 'vue-i18n'
import EditableText from '@/components/common/EditableText.vue'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { InjectKeyHandleEditLabelFunction } from '@/types/treeExplorerTypes'
import {
InjectKeyHandleEditLabelFunction,
RenderedTreeExplorerNode
} from '@/types/treeExplorerTypes'
// Create a mock i18n instance
const i18n = createI18n({

View File

@@ -59,13 +59,14 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useManagerState } from '@/composables/useManagerState'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useDialogStore } from '@/stores/dialogStore'
import type { MissingNodeType } from '@/types/comfy'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import PackInstallButton from './manager/button/PackInstallButton.vue'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]

View File

@@ -1,5 +1,4 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { VueWrapper, mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import Button from 'primevue/button'
import PrimeVue from 'primevue/config'
@@ -29,7 +28,7 @@ const defaultMockTaskLogs = [
{ taskName: 'Task 2', logs: ['Log 3', 'Log 4'] }
]
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
taskLogs: [...defaultMockTaskLogs],
succeededTasksLogs: [...defaultMockTaskLogs],

View File

@@ -88,7 +88,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import {
useComfyManagerStore,
useManagerProgressDialogStore
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
} from '@/stores/comfyManagerStore'
const comfyManagerStore = useComfyManagerStore()
const progressDialogContent = useManagerProgressDialogStore()

View File

@@ -43,11 +43,11 @@
<script setup lang="ts">
import Message from 'primevue/message'
import { compare } from 'semver'
import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { compareVersions } from '@/utils/formatUtil'
const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]>
@@ -68,7 +68,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first)
return compare(b, a) // Reversed for descending order
return compareVersions(b, a) // Reversed for descending order
})
})

View File

@@ -148,7 +148,7 @@ import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isInChina } from '@/utils/networkUtil'
import ApiKeyForm from './signin/ApiKeyForm.vue'

View File

@@ -17,8 +17,7 @@
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { Form, FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import { ref } from 'vue'

View File

@@ -143,24 +143,24 @@ import IconButton from '@/components/button/IconButton.vue'
import ContentDivider from '@/components/common/ContentDivider.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import ManagerNavSidebar from '@/components/dialog/content/manager/ManagerNavSidebar.vue'
import InfoPanel from '@/components/dialog/content/manager/infoPanel/InfoPanel.vue'
import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue'
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useManagerStatePersistence } from '@/composables/manager/useManagerStatePersistence'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes'
import ManagerNavSidebar from '@/workbench/extensions/manager/components/manager/ManagerNavSidebar.vue'
import InfoPanel from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue'
import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue'
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/workbench/extensions/manager/components/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/workbench/extensions/manager/components/manager/skeleton/GridSkeleton.vue'
import { useManagerStatePersistence } from '@/workbench/extensions/manager/composables/useManagerStatePersistence'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
const { initialTab } = defineProps<{
initialTab?: ManagerTab

View File

@@ -0,0 +1,82 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Tag from 'primevue/tag'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import ManagerHeader from './ManagerHeader.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})
describe('ManagerHeader', () => {
const createWrapper = () => {
return mount(ManagerHeader, {
global: {
plugins: [createPinia(), PrimeVue, i18n],
directives: {
tooltip: Tooltip
},
components: {
Tag
}
}
})
}
it('renders the component title', () => {
const wrapper = createWrapper()
expect(wrapper.find('h2').text()).toBe(
enMessages.manager.discoverCommunityContent
)
})
it('displays the legacy manager UI tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
})
it('applies info severity to the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('p-tag-info')
})
it('displays info icon in the tag', () => {
const wrapper = createWrapper()
const icon = wrapper.find('.pi-info-circle')
expect(icon.exists()).toBe(true)
})
it('has cursor-help class on the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('cursor-help')
})
it('has proper structure with flex container', () => {
const wrapper = createWrapper()
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
expect(flexContainer.exists()).toBe(true)
const tag = flexContainer.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
})
})

View File

@@ -0,0 +1,25 @@
<template>
<div class="w-full">
<div class="flex items-center">
<h2 class="text-lg font-normal text-left">
{{ $t('manager.discoverCommunityContent') }}
</h2>
<div class="flex justify-end ml-auto pr-4 pl-2">
<Tag
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
severity="info"
icon="pi pi-info-circle"
:value="$t('manager.legacyManagerUI')"
class="cursor-help ml-2"
:pt="{
root: { class: 'text-xs' }
}"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Tag from 'primevue/tag'
</script>

View File

@@ -32,7 +32,7 @@ import Listbox from 'primevue/listbox'
import ScrollPanel from 'primevue/scrollpanel'
import ContentDivider from '@/components/common/ContentDivider.vue'
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import type { TabItem } from '@/types/comfyManagerTypes'
defineProps<{
tabs: TabItem[]

View File

@@ -169,7 +169,7 @@ import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import type {
import {
ConflictDetail,
ConflictDetectionResult
} from '@/types/conflictDetectionTypes'

View File

@@ -19,7 +19,7 @@
import Message from 'primevue/message'
import { computed, inject } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { components } from '@/types/comfyRegistryTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
type PackVersionStatus = components['schemas']['NodeVersionStatus']

View File

@@ -1,5 +1,4 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { VueWrapper, mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Tooltip from 'primevue/tooltip'
@@ -35,7 +34,7 @@ const mockInstalledPacks = {
const mockIsPackEnabled = vi.fn(() => true)
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
installedPacks: mockInstalledPacks,
isPackInstalled: (id: string) =>

View File

@@ -43,13 +43,13 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import { valid as validSemver } from 'semver'
import { computed, ref, watch } from 'vue'
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import PackVersionSelectorPopover from '@/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { isSemVer } from '@/utils/formatUtil'
const TRUNCATED_HASH_LENGTH = 7
@@ -81,9 +81,7 @@ const installedVersion = computed(() => {
'nightly'
// If Git hash, truncate to 7 characters
return validSemver(version)
? version
: version.slice(0, TRUNCATED_HASH_LENGTH)
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
})
const toggleVersionSelector = (event: Event) => {

View File

@@ -1,5 +1,4 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { VueWrapper, mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import Button from 'primevue/button'
import PrimeVue from 'primevue/config'
@@ -64,7 +63,7 @@ vi.mock('@/services/comfyRegistryService', () => ({
}))
// Mock the manager store
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
installPack: {
call: mockInstallPack,

View File

@@ -84,7 +84,6 @@ import { whenever } from '@vueuse/core'
import Button from 'primevue/button'
import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner'
import { valid as validSemver } from 'semver'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -93,10 +92,11 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import { isSemVer } from '@/utils/formatUtil'
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
type ManagerDatabaseSource =
@@ -142,7 +142,7 @@ onMounted(() => {
getInitialSelectedVersion() ?? SelectedVersionValues.LATEST
selectedVersion.value =
// Use NIGHTLY when version is a Git hash
validSemver(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
})
const getInitialSelectedVersion = () => {

View File

@@ -1,5 +1,4 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { VueWrapper, mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import ToggleSwitch from 'primevue/toggleswitch'
@@ -8,7 +7,7 @@ import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import PackEnableToggle from './PackEnableToggle.vue'
@@ -33,7 +32,7 @@ const mockNodePack = {
const mockIsPackEnabled = vi.fn()
const mockEnablePack = { call: vi.fn().mockResolvedValue(undefined) }
const mockDisablePack = vi.fn().mockResolvedValue(undefined)
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
isPackEnabled: mockIsPackEnabled,
enablePack: mockEnablePack,

View File

@@ -36,10 +36,10 @@ import { useI18n } from 'vue-i18n'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
const TOGGLE_DEBOUNCE_MS = 256

View File

@@ -30,12 +30,14 @@ import DotSpinner from '@/components/common/DotSpinner.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import type { ButtonSize } from '@/types/buttonTypes'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import {
type ConflictDetail,
ConflictDetectionResult
} from '@/types/conflictDetectionTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']

View File

@@ -16,10 +16,10 @@
<script setup lang="ts">
import IconTextButton from '@/components/button/IconTextButton.vue'
import type { ButtonSize } from '@/types/buttonTypes'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']

View File

@@ -22,8 +22,8 @@ import { ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
type NodePack = components['schemas']['Node']

View File

@@ -64,20 +64,20 @@ import { useScroll, whenever } from '@vueuse/core'
import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { components } from '@/types/comfyRegistryTypes'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes'
interface InfoItem {
key: string

View File

@@ -45,14 +45,14 @@
import { computed, inject, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
const { nodePacks, hasConflict } = defineProps<{
nodePacks: components['schemas']['Node'][]

View File

@@ -57,19 +57,19 @@
import { useAsyncState } from '@vueuse/core'
import { computed, onUnmounted, provide, toRef } from 'vue'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/workbench/extensions/manager/components/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/workbench/extensions/manager/components/manager/packIcon/PackIconStacked.vue'
const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]

View File

@@ -45,12 +45,12 @@ import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { computed, inject, ref, watchEffect } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
import WarningTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/WarningTabPanel.vue'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import DescriptionTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/NodesTabPanel.vue'
import WarningTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue'
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
nodePack: components['schemas']['Node']

View File

@@ -22,7 +22,7 @@
</template>
<script setup lang="ts">
import MarkdownText from '@/workbench/extensions/manager/components/manager/infoPanel/MarkdownText.vue'
import MarkdownText from '@/components/dialog/content/manager/infoPanel/MarkdownText.vue'
export interface TextSection {
title: string

View File

@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import type { components } from '@/types/comfyRegistryTypes'
import { components } from '@/types/comfyRegistryTypes'
import DescriptionTabPanel from './DescriptionTabPanel.vue'

View File

@@ -26,11 +26,11 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { components } from '@/types/comfyRegistryTypes'
import { isValidUrl } from '@/utils/formatUtil'
import InfoTextSection, {
type TextSection
} from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTextSection.vue'
} from '@/components/dialog/content/manager/infoPanel/InfoTextSection.vue'
import { components } from '@/types/comfyRegistryTypes'
import { isValidUrl } from '@/utils/formatUtil'
const { t } = useI18n()

View File

@@ -34,7 +34,7 @@ import { computed, ref, shallowRef, useId } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import NodePreview from '@/components/node/NodePreview.vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components, operations } from '@/types/comfyRegistryTypes'
import { components, operations } from '@/types/comfyRegistryTypes'
import { registryToFrontendV2NodeDef } from '@/utils/mapperUtil'
type ListComfyNodesResponse =

View File

@@ -29,8 +29,8 @@ import { computed } from 'vue'
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
import { t } from '@/i18n'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { components } from '@/types/comfyRegistryTypes'
import { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { getConflictMessage } from '@/utils/conflictMessageUtil'
const { nodePack, conflictResult } = defineProps<{

View File

@@ -37,7 +37,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { components } from '@/types/comfyRegistryTypes'
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'

View File

@@ -75,17 +75,17 @@ import Card from 'primevue/card'
import { computed, provide } from 'vue'
import { useI18n } from 'vue-i18n'
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
import PackBanner from '@/components/dialog/content/manager/packBanner/PackBanner.vue'
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackBanner from '@/workbench/extensions/manager/components/manager/packBanner/PackBanner.vue'
import PackCardFooter from '@/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import {
IsInstallingKey,
type MergedNodePack,
type RegistryPack,
isMergedNodePack
} from '@/workbench/extensions/manager/types/comfyManagerTypes'
} from '@/types/comfyManagerTypes'
const { nodePack, isSelected = false } = defineProps<{
nodePack: MergedNodePack | RegistryPack

View File

@@ -25,13 +25,13 @@
import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']

View File

@@ -37,7 +37,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { components } from '@/types/comfyRegistryTypes'
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'

View File

@@ -18,8 +18,8 @@
</template>
<script setup lang="ts">
import type { components } from '@/types/comfyRegistryTypes'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
import { components } from '@/types/comfyRegistryTypes'
const {
nodePacks,

View File

@@ -62,26 +62,27 @@
<script setup lang="ts">
import { stubTrue } from 'es-toolkit/compat'
import type { AutoCompleteOptionSelectEvent } from 'primevue/autocomplete'
import AutoComplete from 'primevue/autocomplete'
import AutoComplete, {
AutoCompleteOptionSelectEvent
} from 'primevue/autocomplete'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUpdateButton from '@/components/dialog/content/manager/button/PackUpdateButton.vue'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
import type { components } from '@/types/comfyRegistryTypes'
import {
type SearchOption,
SortableAlgoliaField
} from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import type {
QuerySuggestion,
SearchMode,
SortableField
} from '@/types/searchServiceTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUpdateButton from '@/workbench/extensions/manager/components/manager/button/PackUpdateButton.vue'
import SearchFilterDropdown from '@/workbench/extensions/manager/components/manager/registrySearchBar/SearchFilterDropdown.vue'
import {
type SearchOption,
SortableAlgoliaField
} from '@/workbench/extensions/manager/types/comfyManagerTypes'
const { searchResults, sortOptions } = defineProps<{
searchResults?: components['schemas']['Node'][]

View File

@@ -22,7 +22,7 @@
// eslint-disable-next-line no-restricted-imports -- TODO: Migrate to Select component
import Dropdown from 'primevue/dropdown'
import type { SearchOption } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import type { SearchOption } from '@/types/comfyManagerTypes'
defineProps<{
options: SearchOption<T>[]

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