Compare commits

..

1 Commits

Author SHA1 Message Date
snomiao
8eac858bd6 [bugfix] Fix Chinese locale detection to properly handle traditional Chinese
- Update getDefaultLocale() to distinguish between zh-CN (simplified) and zh-TW/zh-HK/zh-Hant (traditional)
- Fixes issue where browsers set to traditional Chinese were incorrectly defaulting to simplified Chinese translations
- Preserves existing behavior for all other languages

Fixes #5151

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 04:48:14 +00:00
291 changed files with 24788 additions and 31584 deletions

View File

@@ -128,25 +128,7 @@ echo "Last stable release: $LAST_STABLE"
### Step 4: Analyze Dependency Updates
1. **Use pnpm's built-in dependency analysis:**
```bash
# Get outdated dependencies with pnpm
pnpm outdated --format table > outdated-deps-${NEW_VERSION}.txt
# Check for license compliance
pnpm licenses ls --json > licenses-${NEW_VERSION}.json
# Analyze why specific dependencies exist
echo "Dependency analysis:" > dep-analysis-${NEW_VERSION}.md
MAJOR_DEPS=("vue" "vite" "@vitejs/plugin-vue" "typescript" "pinia")
for dep in "${MAJOR_DEPS[@]}"; do
echo -e "\n## $dep\n\`\`\`" >> dep-analysis-${NEW_VERSION}.md
pnpm why "$dep" >> dep-analysis-${NEW_VERSION}.md || echo "Not found" >> dep-analysis-${NEW_VERSION}.md
echo "\`\`\`" >> dep-analysis-${NEW_VERSION}.md
done
```
2. **Check for significant dependency updates:**
1. **Check significant dependency updates:**
```bash
# Extract all dependency changes for major version bumps
OTHER_DEP_CHANGES=""
@@ -218,48 +200,22 @@ echo "Last stable release: $LAST_STABLE"
PR data: [contents of prs-${NEW_VERSION}.json]
```
3. **Generate GTM notification using this EXACT Slack-compatible format:**
3. **Generate GTM notification:**
```bash
# Only create file if GTM-worthy features exist:
if [ "$GTM_FEATURES_FOUND" = "true" ]; then
cat > gtm-summary-${NEW_VERSION}.md << 'EOF'
*GTM Summary: ComfyUI Frontend v${NEW_VERSION}*
_Disclaimer: the below is AI-generated_
1. *[Feature Title]* (#[PR_NUMBER])
* *Author:* @[username]
* *Demo:* [Media Link or "No demo available"]
* *Why users should care:* [One compelling sentence]
* *Key Features:*
* [Feature detail 1]
* [Feature detail 2]
2. *[Feature Title]* (#[PR_NUMBER])
* *Author:* @[username]
* *Demo:* [Media Link]
* *Why users should care:* [One compelling sentence]
* *Key Features:*
* [Feature detail 1]
* [Feature detail 2]
EOF
# Save to gtm-summary-${NEW_VERSION}.md based on analysis
# If GTM-worthy features exist, include them with testing instructions
# If not, note that this is a maintenance/bug fix release
# Check if notification is needed
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
echo "✅ No GTM notification needed for this release"
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
else
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
echo "📤 Share this file in #gtm channel to notify the team"
else
echo "✅ No GTM notification needed for this release"
echo "📄 No gtm-summary file created - no marketing-worthy features"
fi
```
**CRITICAL Formatting Requirements:**
- Use single asterisk (*) for emphasis, NOT double (**)
- Use underscore (_) for italics
- Use 4 spaces for indentation (not tabs)
- Convert author names to @username format (e.g., "John Smith" → "@john")
- No section headers (#), no code language specifications
- Always include "Disclaimer: the below is AI-generated"
- Keep content minimal - no testing instructions, additional sections, etc.
### Step 6: Version Preview
**Version Preview:**
@@ -272,42 +228,37 @@ echo "Last stable release: $LAST_STABLE"
### Step 7: Security and Dependency Audit
1. Run pnpm security audit:
1. Run security audit:
```bash
pnpm audit --audit-level moderate
pnpm licenses ls --summary
npm audit --audit-level moderate
```
2. Check for known vulnerabilities in dependencies
3. Run comprehensive dependency health check:
```bash
pnpm doctor
```
4. Scan for hardcoded secrets or credentials:
3. Scan for hardcoded secrets or credentials:
```bash
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
```
5. Verify no sensitive data in recent commits
6. **SECURITY REVIEW**: Address any critical findings before proceeding?
4. Verify no sensitive data in recent commits
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
### Step 8: Pre-Release Testing
1. Run complete test suite:
```bash
pnpm test:unit
pnpm test:component
npm run test:unit
npm run test:component
```
2. Run type checking:
```bash
pnpm typecheck
npm run typecheck
```
3. Run linting (may have issues with missing packages):
```bash
pnpm lint || echo "Lint issues - verify if critical"
npm run lint || echo "Lint issues - verify if critical"
```
4. Test build process:
```bash
pnpm build
pnpm build:types
npm run build
npm run build:types
```
5. **QUALITY GATE**: All tests and builds passing?
@@ -537,7 +488,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```bash
# Check npm availability
for i in {1..10}; do
if pnpm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then
if npm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then
echo "✅ npm package available"
break
fi

View File

@@ -80,7 +80,7 @@ For each commit:
- **CONFIRMATION REQUIRED**: Conflicts resolved correctly?
3. After successful cherry-pick:
- Show the changes: `git show HEAD`
- Run validation: `pnpm typecheck && pnpm lint`
- Run validation: `npm run typecheck && npm run lint`
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
### Step 6: Create PR to Core Branch
@@ -197,7 +197,7 @@ For each commit:
5. Track progress:
- GitHub release draft/publication
- PyPI upload
- pnpm types publication
- npm types publication
### Step 12: Post-Release Verification
@@ -211,7 +211,7 @@ For each commit:
```
3. Verify npm package:
```bash
pnpm view @comfyorg/comfyui-frontend-types@1.23.5
npm view @comfyorg/comfyui-frontend-types@1.23.5
```
4. Generate release summary with:
- Version released

View File

@@ -1,158 +0,0 @@
# Setup Repository
Bootstrap the ComfyUI Frontend monorepo with all necessary dependencies and verification checks.
## Overview
This command will:
1. Install pnpm package manager (if not present)
2. Install all project dependencies
3. Verify the project builds successfully
4. Run unit tests to ensure functionality
5. Start development server to verify frontend boots correctly
## Prerequisites Check
First, let's verify the environment:
```bash
# Check Node.js version (should be >= 24)
node --version
# Check if we're in a git repository
git status
```
## Step 1: Install pnpm
```bash
# Check if pnpm is already installed
pnpm --version 2>/dev/null || {
echo "Installing pnpm..."
npm install -g pnpm
}
# Verify pnpm installation
pnpm --version
```
## Step 2: Install Dependencies
```bash
# Install all dependencies using pnpm
echo "Installing project dependencies..."
pnpm install
# Verify node_modules exists and has packages
ls -la node_modules | head -5
```
## Step 3: Verify Build
```bash
# Run TypeScript type checking
echo "Running TypeScript checks..."
pnpm typecheck
# Build the project
echo "Building project..."
pnpm build
# Verify dist folder was created
ls -la dist/
```
## Step 4: Run Unit Tests
```bash
# Run unit tests
echo "Running unit tests..."
pnpm test:unit
# If tests fail, show the output and stop
if [ $? -ne 0 ]; then
echo "❌ Unit tests failed. Please fix failing tests before continuing."
exit 1
fi
echo "✅ Unit tests passed successfully"
```
## Step 5: Verify Development Server
```bash
# Start development server in background
echo "Starting development server..."
pnpm dev &
SERVER_PID=$!
# Wait for server to start (check for port 5173 or similar)
echo "Waiting for server to start..."
sleep 10
# Check if server is running
if curl -s http://localhost:5173 > /dev/null 2>&1; then
echo "✅ Development server started successfully at http://localhost:5173"
# Kill the background server
kill $SERVER_PID
wait $SERVER_PID 2>/dev/null
else
echo "❌ Development server failed to start or is not accessible"
kill $SERVER_PID 2>/dev/null
wait $SERVER_PID 2>/dev/null
exit 1
fi
```
## Step 6: Final Verification
```bash
# Run linting to ensure code quality
echo "Running linter..."
pnpm lint
# Show project status
echo ""
echo "🎉 Repository setup complete!"
echo ""
echo "Available commands:"
echo " pnpm dev - Start development server"
echo " pnpm build - Build for production"
echo " pnpm test:unit - Run unit tests"
echo " pnpm test:component - Run component tests"
echo " pnpm typecheck - Run TypeScript checks"
echo " pnpm lint - Run ESLint"
echo " pnpm format - Format code with Prettier"
echo ""
echo "Next steps:"
echo "1. Run 'pnpm dev' to start developing"
echo "2. Open http://localhost:5173 in your browser"
echo "3. Check README.md for additional setup instructions"
```
## Troubleshooting
If any step fails:
1. **pnpm installation fails**: Try using `curl -fsSL https://get.pnpm.io/install.sh | sh -`
2. **Dependencies fail to install**: Try clearing cache with `pnpm store prune` and retry
3. **Build fails**: Check for TypeScript errors and fix them first
4. **Tests fail**: Review test output and fix failing tests
5. **Dev server fails**: Check if port 5173 is already in use
## Manual Verification Steps
After running the setup, manually verify:
1. **Dependencies installed**: `ls node_modules | wc -l` should show many packages
2. **Build artifacts**: `ls dist/` should show built files
3. **Server accessible**: Open http://localhost:5173 in browser
4. **Hot reload works**: Edit a file and see changes reflect
## Environment Requirements
- Node.js >= 24
- Git repository
- Internet connection for package downloads
- Available ports (typically 5173 for dev server)

View File

@@ -5,7 +5,7 @@ Follow these steps systematically to verify our changes:
1. **Server Setup**
- Check if the dev server is running on port 5173 using browser navigation or port checking
- If not running, start it with `pnpm dev` from the root directory
- If not running, start it with `npm run dev` from the root directory
- If the server fails to start, provide detailed troubleshooting steps by reading package.json and README.md for accurate instructions
- Wait for the server to be fully ready before proceeding

6
.gitattributes vendored
View File

@@ -2,13 +2,9 @@
* text=auto
# Force TS to LF to make the unixy scripts not break on Windows
*.cjs text eol=lf
*.js text eol=lf
*.json text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.js text eol=lf
# Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -3,48 +3,54 @@ name: 'Chromatic'
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
on:
workflow_dispatch: # Allow manual triggering
push:
branches: [main]
pull_request:
branches: [main]
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
# Only run for PRs from version-bump-* branches or manual triggers
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
permissions:
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
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'
cache: 'npm'
- name: Get current time
id: current-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Cache tool outputs
uses: actions/cache@v4
- name: Comment PR - Build Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
🔄 **Building Storybook and running visual tests...**
⏳ Build started at: ${{ steps.current-time.outputs.time }} UTC
---
*This comment will be updated when the build completes*
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Build Storybook and run Chromatic
id: chromatic
@@ -55,3 +61,36 @@ jobs:
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Build Complete
if: github.event_name == 'pull_request' && always()
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 📊 Build Summary
- **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }}
- **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }}
- **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }}
- **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }}
### 🔗 Links
${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }}
${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }}
---
${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}

View File

@@ -53,20 +53,14 @@ jobs:
with:
fetch-depth: 0
- 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 for analysis tools
run: |
pnpm install -g typescript @vue/compiler-sfc
npm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review
uses: anthropics/claude-code-action@main

View File

@@ -16,26 +16,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
dist
tsconfig.tsbuildinfo
key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
dev-release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -46,9 +29,9 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
pnpm build
pnpm zipdist
npm ci
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -42,14 +42,9 @@ jobs:
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- uses: actions/setup-python@v4
with:
python-version: '3.10'
@@ -68,8 +63,8 @@ jobs:
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- name: Build & Install ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
npm ci
npm run build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
working-directory: ComfyUI_frontend
@@ -84,18 +79,18 @@ jobs:
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Capture base i18n
run: npx tsx scripts/diff-i18n capture
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n
run: npm run collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -13,22 +13,22 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
run: npm run collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -1,52 +1,37 @@
name: Update Locales
on:
# Manual dispatch for urgent translation updates
workflow_dispatch:
# Only trigger on PRs to main/master - additional branch filtering in job condition
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened]
branches: [ main, master, dev* ]
paths-ignore:
- '.github/**'
- '.husky/**'
- '.vscode/**'
- 'browser_tests/**'
- 'tests-ui/**'
jobs:
update-locales:
# Branch detection: Only run for manual dispatch or version-bump-* branches from main repo
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
# Don't run on fork PRs
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
ComfyUI_frontend/.cache
ComfyUI_frontend/.cache
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
i18n-tools-cache-${{ runner.os }}-
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -16,43 +16,23 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- 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: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
.eslintcache
tsconfig.tsbuildinfo
.prettierCache
.knip-cache
key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }}
restore-keys: |
lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
lint-format-cache-${{ runner.os }}-
ci-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Run ESLint with auto-fix
run: pnpm lint:fix
run: npm run lint:fix
- name: Run Prettier with auto-format
run: pnpm format
run: npm run format
- name: Check for changes
id: verify-changed-files
@@ -74,13 +54,12 @@ jobs:
- name: Final validation
run: |
pnpm lint
pnpm format:check
pnpm knip
npm run lint
npm run format:check
npm run knip
- 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
with:
script: |
@@ -93,7 +72,6 @@ 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
with:
script: |
@@ -101,5 +79,5 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\npnpm prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\npnpm lint:fix\npnpm format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\nnpm run prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\nnpm run lint:fix\nnpm run format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
})

View File

@@ -1,280 +0,0 @@
name: PR Playwright Deploy and Comment
on:
workflow_run:
workflows: ["Tests CI"]
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-reports:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed'
permissions:
actions: read
strategy:
fail-fast: false
matrix:
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Get PR info
id: pr-info
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return { number: null, sanitized_branch: null };
}
const pr = pullRequests[0];
const branchName = context.payload.workflow_run.head_branch;
const sanitizedBranch = branchName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/^-|-$/g, '');
return {
number: pr.number,
sanitized_branch: sanitizedBranch
};
- name: Set project name
if: fromJSON(steps.pr-info.outputs.result).number != null
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ fromJSON(steps.pr-info.outputs.result).sanitized_branch }}" >> $GITHUB_OUTPUT
- name: Download playwright report
if: fromJSON(steps.pr-info.outputs.result).number != null
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: playwright-report-${{ matrix.browser }}
path: playwright-report
- name: Install Wrangler
if: fromJSON(steps.pr-info.outputs.result).number != null
run: npm install -g wrangler
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
if: fromJSON(steps.pr-info.outputs.result).number != null
id: cloudflare-deploy
continue-on-error: true
run: |
# Retry logic for wrangler deploy (3 attempts)
RETRY_COUNT=0
MAX_RETRIES=3
SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
if npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ $SUCCESS = false ]; then
echo "All deployment attempts failed"
exit 1
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
comment-tests-starting:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'requested'
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body for start
if: steps.pr.outputs.result != 'null'
id: comment-body-start
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
echo "<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Tests are starting...** " >> comment.md
echo "" >> comment.md
echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 🚀 Running Tests" >> comment.md
echo "- 🧪 **chromium**: Running tests..." >> comment.md
echo "- 🧪 **chromium-0.5x**: Running tests..." >> comment.md
echo "- 🧪 **chromium-2x**: Running tests..." >> comment.md
echo "- 🧪 **mobile-chrome**: Running tests..." >> comment.md
echo "" >> comment.md
echo "---" >> comment.md
echo "⏱️ Please wait while tests are running across all browsers..." >> comment.md
- name: Comment PR - Tests Started
if: steps.pr.outputs.result != 'null'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
comment-tests-completed:
runs-on: ubuntu-latest
needs: deploy-reports
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed' && always()
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Download all deployment info
if: steps.pr.outputs.result != 'null'
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body for completion
if: steps.pr.outputs.result != 'null'
id: comment-body-completed
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
# Validate URLs before using them in comments
sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
if [ "$sanitized_url" = "INVALID_URL" ]; then
echo "Invalid deployment URL detected: $url"
url="#" # Use safe fallback
fi
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
if: steps.pr.outputs.result != 'null'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md

View File

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

View File

@@ -19,25 +19,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
tsconfig.tsbuildinfo
key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -57,9 +41,9 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
pnpm build
pnpm zipdist
npm ci
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
@@ -129,31 +113,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
registry-url: https://registry.npmjs.org
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
tsconfig.tsbuildinfo
dist
key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
types-tools-cache-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
- run: pnpm build:types
- run: npm ci
- run: npm run build:types
- name: Publish package
run: pnpm publish --access public
run: npm publish --access public
working-directory: dist
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,14 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -7,11 +7,15 @@ on:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
@@ -33,40 +37,51 @@ jobs:
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Cache tool outputs
uses: actions/cache@v4
- name: Comment PR - Tests Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
path: |
ComfyUI_frontend/.cache
ComfyUI_frontend/tsconfig.tsbuildinfo
key: playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ hashFiles('ComfyUI_frontend/src/**/*.{ts,vue,js}', 'ComfyUI_frontend/*.config.*') }}
restore-keys: |
playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
playwright-setup-cache-${{ runner.os }}-
playwright-tools-cache-${{ runner.os }}-
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- PLAYWRIGHT_TEST_STATUS -->
---
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
---
*This comment will be updated when tests complete*
- name: Build ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
npm ci
npm run build
working-directory: ComfyUI_frontend
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Generate sanitized branch name
id: branch-info
run: |
# Get branch name and sanitize it for Cloudflare branch names
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
@@ -79,6 +94,8 @@ jobs:
needs: setup
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
contents: read
strategy:
fail-fast: false
@@ -97,16 +114,35 @@ jobs:
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Set project name
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>${{ matrix.browser }}</bold>: Running tests...
- name: Install requirements
run: |
@@ -122,19 +158,13 @@ jobs:
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ matrix.browser }}
restore-keys: |
playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Wrangler
run: npm install -g wrangler
- name: Run Playwright tests (${{ matrix.browser }})
id: playwright
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
@@ -147,3 +177,178 @@ jobs:
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
id: cloudflare-deploy
if: always()
run: |
# Retry logic for wrangler deploy (3 attempts)
RETRY_COUNT=0
MAX_RETRIES=3
SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
if npx wrangler pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ $SUCCESS = false ]; then
echo "All deployment attempts failed"
exit 1
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Save deployment info for summary
if: always()
run: |
mkdir -p deployment-info
# Use step conclusion to determine test result
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
EXIT_CODE="0"
else
EXIT_CODE="1"
fi
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
- name: Upload deployment info
if: always()
uses: actions/upload-artifact@v4
with:
name: deployment-info-${{ matrix.browser }}
path: deployment-info/
retention-days: 1
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Complete
if: always() && github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
comment-summary:
needs: playwright-tests
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Download all deployment info
uses: actions/download-artifact@v4
with:
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body
id: comment-body
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
- name: Check test results and fail if needed
run: |
# Check if all tests passed and fail the job if not
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "false" ]; then
echo "❌ Tests failed in one or more browsers. Failing the CI job."
exit 1
else
echo "✅ All tests passed across all browsers!"
fi

View File

@@ -14,33 +14,19 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
electron-types-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Update electron types
run: pnpm install @comfyorg/comfyui-electron-types@latest
run: npm install @comfyorg/comfyui-electron-types@latest
- name: Get new version
id: get-version
run: |
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./pnpm-lock.yaml')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)")
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package-lock.json')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request

View File

@@ -19,36 +19,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-manager-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache ComfyUI-Manager repository
uses: actions/cache@v4
with:
path: ComfyUI-Manager
key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfyui-manager-repo-${{ runner.os }}-
run: npm ci
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v4
@@ -86,7 +64,7 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated ComfyUI-Manager API types..."
pnpm lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
npm run lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
- name: Check for changes
id: check-changes
@@ -121,4 +99,4 @@ jobs:
labels: Manager
delete-branch: true
add-paths: |
src/types/generatedManagerTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -18,36 +18,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-registry-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache comfy-api repository
uses: actions/cache@v4
with:
path: comfy-api
key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfy-api-repo-${{ runner.os }}-
run: npm ci
- name: Checkout comfy-api repository
uses: actions/checkout@v4
@@ -86,7 +64,7 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated Comfy Registry API types..."
pnpm lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
npm run lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
- name: Check for changes
id: check-changes

View File

@@ -26,21 +26,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
cache: 'npm'
- name: Bump version
id: bump-version
run: |
pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
npm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT

View File

@@ -13,34 +13,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- 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: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
coverage
.vitest-cache
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
restore-keys: |
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
vitest-cache-${{ runner.os }}-
test-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Run Vitest tests
run: |
pnpm test:component
pnpm test:unit
npm run test:component
npm run test:unit

12
.gitignore vendored
View File

@@ -10,6 +10,7 @@ lerna-debug.log*
# Package manager lockfiles (allow users to use different package managers)
bun.lock
bun.lockb
pnpm-lock.yaml
yarn.lock
# Cache files
@@ -22,7 +23,7 @@ dist-ssr
*.local
# Claude configuration
.claude/*.local.json
CLAUDE.local.md
.claude/settings.json
# Editor directories and files
.vscode/*
@@ -77,12 +78,3 @@ vite.config.mts.timestamp-*.mjs
*storybook.log
storybook-static
.nx/cache
.nx/workspace-data
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*

1
.nvmrc
View File

@@ -1 +0,0 @@
24

View File

@@ -2,9 +2,9 @@
## Quick Commands
- `pnpm storybook`: Start Storybook development server
- `pnpm build-storybook`: Build static Storybook
- `pnpm test:component`: Run component tests (includes Storybook components)
- `npm run storybook`: Start Storybook development server
- `npm run build-storybook`: Build static Storybook
- `npm run test:component`: Run component tests (includes Storybook components)
## Development Workflow for Storybook
@@ -19,8 +19,8 @@
- Ensure proper theming and styling
3. **Code Quality**:
- Run `pnpm typecheck` to verify TypeScript
- Run `pnpm lint` to check for linting issues
- Run `npm run typecheck` to verify TypeScript
- Run `npm run lint` to check for linting issues
- Follow existing story patterns and conventions
## Story Creation Guidelines
@@ -138,13 +138,13 @@ The Storybook preview is configured with:
```bash
# Check TypeScript issues
pnpm typecheck
npm run typecheck
# Lint Storybook files
pnpm lint .storybook/
npm run lint .storybook/
# Build to check for production issues
pnpm build-storybook
npm run build-storybook
```
## File Organization

View File

@@ -40,10 +40,10 @@ Storybook is a frontend workshop for building UI components and pages in isolati
```bash
# Start Storybook development server
pnpm storybook
npm run storybook
# Build static Storybook for deployment
pnpm build-storybook
npm run build-storybook
```
### Creating Stories

View File

@@ -4,26 +4,17 @@
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Light theme default - with explicit color to override media queries */
body:not(.dark-theme) {
background-color: #fff !important;
color: #000 !important;
}
/* Override browser dark mode preference for light theme */
@media (prefers-color-scheme: dark) {
body:not(.dark-theme) {
color: #000 !important;
--fg-color: #000 !important;
--bg-color: #fff !important;
}
/* Light theme default */
body {
background-color: #ffffff;
color: #1a1a1a;
}
/* Dark theme styles */
body.dark-theme,
.dark-theme body {
background-color: #202020;
color: #fff;
background-color: #0a0a0a;
color: #e5e5e5;
}
/* Ensure Storybook canvas follows theme */
@@ -33,33 +24,11 @@
.dark-theme .sb-show-main,
.dark-theme .docs-story {
background-color: #202020 !important;
background-color: #0a0a0a !important;
}
/* CSS Variables for theme consistency */
body:not(.dark-theme) {
--fg-color: #000;
--bg-color: #fff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
}
body.dark-theme {
--fg-color: #fff;
--bg-color: #202020;
--content-bg: #4e4e4e;
--content-fg: #fff;
--content-hover-bg: #222;
--content-hover-fg: #fff;
}
/* Override Storybook's problematic & selector styles */
/* Reset only the specific properties that Storybook injects */
#storybook-root li+li,
#storybook-docs li+li {
margin: inherit;
padding: inherit;
/* Fix for Storybook controls panel in dark mode */
.dark-theme .docblock-argstable-body {
color: #e5e5e5;
}
</style>

View File

@@ -8,19 +8,19 @@
- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.js`, `.prettierrc`.
## Build, Test, and Development Commands
- `pnpm dev`: Start Vite dev server.
- `pnpm dev:electron`: Dev server with Electron API mocks.
- `pnpm build`: Type-check then production build to `dist/`.
- `pnpm preview`: Preview the production build locally.
- `pnpm test:unit`: Run Vitest unit tests (`tests-ui/`).
- `pnpm test:component`: Run component tests (`src/components/`).
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`).
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint). `pnpm format` / `format:check`: Prettier.
- `pnpm typecheck`: Vue TSC type checking.
- `npm run dev`: Start Vite dev server.
- `npm run dev:electron`: Dev server with Electron API mocks.
- `npm run build`: Type-check then production build to `dist/`.
- `npm run preview`: Preview the production build locally.
- `npm run test:unit`: Run Vitest unit tests (`tests-ui/`).
- `npm run test:component`: Run component tests (`src/components/`).
- `npm run test:browser`: Run Playwright E2E tests (`browser_tests/`).
- `npm run lint` / `npm run lint:fix`: Lint (ESLint). `npm run format` / `format:check`: Prettier.
- `npm run typecheck`: Vue TSC type checking.
## Coding Style & Naming Conventions
- Language: TypeScript, Vue SFCs (`.vue`). Indent 2 spaces; single quotes; no semicolons; width 80 (see `.prettierrc`).
- Imports: sorted/grouped by plugin; run `pnpm format` before committing.
- Imports: sorted/grouped by plugin; run `npm run format` before committing.
- ESLint: Vue + TS rules; no floating promises; unused imports disallowed; i18n raw text restrictions in templates.
- Naming: Vue components in PascalCase (e.g., `MenuHamburger.vue`); composables `useXyz.ts`; Pinia stores `*Store.ts`.
@@ -33,7 +33,7 @@
## Commit & Pull Request Guidelines
- Commits: Prefer Conventional Commits (e.g., `feat(ui): add sidebar`), `refactor(litegraph): …`. Use `[skip ci]` for locale-only updates when appropriate.
- PRs: Include clear description, linked issues (`Fixes #123`), and screenshots/GIFs for UI changes. Add/adjust tests and i18n strings when applicable.
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
- Quality gates: `npm run lint`, `npm run typecheck`, and relevant tests must pass. Keep PRs focused and small.
## Security & Configuration Tips
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.

View File

@@ -1,52 +1,22 @@
# ComfyUI Frontend Project Guidelines
## Repository Setup
For first-time setup, use the Claude command:
```
/setup_repo
```
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
## Quick Commands
- `pnpm`: See all available commands
- `pnpm dev`: Start development server (port 5173, via nx)
- `pnpm typecheck`: Type checking
- `pnpm build`: Build for production (via nx)
- `pnpm lint`: Linting (via nx)
- `pnpm format`: Prettier formatting
- `pnpm test:component`: Run component tests with browser environment
- `pnpm test:unit`: Run all unit tests
- `pnpm test:browser`: Run E2E tests via Playwright
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
- `pnpm storybook`: Start Storybook development server (port 6006)
- `pnpm knip`: Detect unused code and dependencies
## Monorepo Architecture
The project now uses **Nx** for build orchestration and task management:
- **Task Orchestration**: Commands like `dev`, `build`, `lint`, and `test:browser` run via Nx
- **Caching**: Nx provides intelligent caching for faster rebuilds
- **Configuration**: Managed through `nx.json` with plugins for ESLint, Storybook, Vite, and Playwright
- **Dependencies**: Nx handles dependency graph analysis and parallel execution
Key Nx features:
- Build target caching and incremental builds
- Parallel task execution across the monorepo
- Plugin-based architecture for different tools
- `npm run`: See all available commands
- `npm run typecheck`: Type checking
- `npm run lint`: Linting
- `npm run format`: Prettier formatting
- `npm run test:component`: Run component tests with browser environment
- `npm run test:unit`: Run all unit tests
- `npm run test:unit -- tests-ui/tests/example.test.ts`: Run single test file
## Development Workflow
1. **First-time setup**: Run `/setup_repo` Claude command
2. Make code changes
3. Run tests (see subdirectory CLAUDE.md files)
4. Run typecheck, lint, format
5. Check README updates
6. Consider docs.comfy.org updates
1. Make code changes
2. Run tests (see subdirectory CLAUDE.md files)
3. Run typecheck, lint, format
4. Check README updates
5. Consider docs.comfy.org updates
## Git Conventions
@@ -82,44 +52,6 @@ When referencing Comfy-Org repos:
2. Use GitHub API for branches/PRs/metadata
3. Curl GitHub website if needed
## Settings and Feature Flags Quick Reference
### Settings Usage
```typescript
const settingStore = useSettingStore()
const value = settingStore.get('Comfy.SomeSetting') // Get setting
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
```
### Dynamic Defaults
```typescript
{
id: 'Comfy.Example.Setting',
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
}
```
### Version-Based Defaults
```typescript
{
id: 'Comfy.Example.Feature',
defaultValue: 'legacy',
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
}
```
### Feature Flags
```typescript
if (api.serverSupportsFeature('feature_name')) { // Check capability
// Use enhanced feature
}
const value = api.getServerFeature('config_name', defaultValue) // Get config
```
**Documentation:**
- Settings system: `docs/SETTINGS.md`
- Feature flags system: `docs/FEATURE_FLAGS.md`
## Common Pitfalls
- NEVER use `any` type - use proper TypeScript types

View File

@@ -14,4 +14,4 @@
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Mask Editor extension
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs

View File

@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (v18 or later to build; v24 for vite dev server) and pnpm
- Node.js (v16 or later; v20/v22 strongly recommended) and npm
- Git for version control
- A running ComfyUI backend instance
@@ -39,7 +39,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
2. Install dependencies:
```bash
pnpm install
npm install
```
3. Configure environment (optional):
@@ -57,13 +57,13 @@ python main.py --port 8188
### Git pre-commit hooks
Run `pnpm prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
### Dev Server
- Start local ComfyUI backend at `localhost:8188`
- Run `pnpm dev` to start the dev server
- Run `pnpm dev:electron` to start the dev server with electron API mocked
- Run `npm run dev` to start the dev server
- Run `npm run dev:electron` to start the dev server with electron API mocked
#### Access dev server on touch devices
@@ -155,7 +155,7 @@ For ComfyUI_frontend development, you can ask coding assistants to use Playwrigh
##### Setup for Claude Code
After installing dependencies with `pnpm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
After installing dependencies with `npm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
@@ -210,14 +210,14 @@ Here's how Claude Code can use the Playwright MCP server to inspect the interfac
### Unit Tests
- `pnpm i` to install all dependencies
- `pnpm test:unit` to execute all unit tests
- `npm i` to install all dependencies
- `npm run test:unit` to execute all unit tests
### Component Tests
Component tests verify Vue components in `src/components/`.
- `pnpm test:component` to execute all component tests
- `npm run test:component` to execute all component tests
### Playwright Tests
@@ -228,12 +228,12 @@ Playwright tests verify the whole app. See [browser_tests/README.md](browser_tes
Before submitting a PR, ensure all tests pass:
```bash
pnpm test:unit
pnpm test:component
pnpm test:browser
pnpm typecheck
pnpm lint
pnpm format
npm run test:unit
npm run test:component
npm run test:browser
npm run typecheck
npm run lint
npm run format
```
## Code Style Guidelines
@@ -265,7 +265,7 @@ The project supports three types of icons, all with automatic imports (no manual
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i-lucide:settings />`, `<i-mdi:folder />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation.
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/`.
For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md).

View File

@@ -124,7 +124,6 @@ export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
public readonly canvas: Locator
public readonly selectionToolbox: Locator
public readonly widgetTextBox: Locator
// Buttons
@@ -159,7 +158,6 @@ export class ComfyPage {
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
this.selectionToolbox = page.locator('.selection-toolbox')
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })

View File

@@ -65,7 +65,6 @@ export class Topbar {
}
async openTopbarMenu() {
await this.page.waitForTimeout(1000)
await this.page.locator('.comfyui-logo-wrapper').click()
const menu = this.page.locator('.comfy-command-menu')
await menu.waitFor({ state: 'visible' })

View File

@@ -1,57 +0,0 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function verifyCustomIconSvg(iconElement: Locator) {
const svgVariable = await iconElement.evaluate((element) => {
const styles = getComputedStyle(element)
return styles.getPropertyValue('--svg')
})
expect(svgVariable).toBeTruthy()
const dataUrlMatch = svgVariable.match(
/url\("data:image\/svg\+xml,([^"]+)"\)/
)
expect(dataUrlMatch).toBeTruthy()
const encodedSvg = dataUrlMatch![1]
const decodedSvg = decodeURIComponent(encodedSvg)
// Check for SVG header to confirm it's a valid SVG
expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'")
}
test.describe('Custom Icons', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => {
// Find the icon in the sidebar
const icon = comfyPage.page.locator(
'.icon-\\[comfy--ai-model\\].side-bar-button-icon'
)
await expect(icon).toBeVisible()
// Verify the custom SVG content
await verifyCustomIconSvg(icon)
})
test('Browse Templates menu item uses custom template icon', async ({
comfyPage
}) => {
// Open the topbar menu
await comfyPage.menu.topbar.openTopbarMenu()
const menuItem = comfyPage.menu.topbar.getMenuItem('Browse Templates')
// Find the icon as a previous sibling of the menu item label
const templateIcon = menuItem
.locator('..')
.locator('.icon-\\[comfy--template\\]')
await expect(templateIcon).toBeVisible()
// Verify the custom SVG content
await verifyCustomIconSvg(templateIcon)
})
})

View File

@@ -59,6 +59,18 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
})
})
test.describe('Missing models warning', () => {
@@ -291,16 +303,37 @@ test.describe('Settings', () => {
})
})
test.describe('Support', () => {
test('Should open external zendesk link', async ({ comfyPage }) => {
test.describe('Feedback dialog', () => {
test('Should open from topmenu help command', async ({ comfyPage }) => {
// Open feedback dialog from top menu
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const newPage = await pagePromise
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
await newPage.close()
// Verify feedback dialog content is visible
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
await expect(feedbackHeader).toBeVisible()
})
test('Should close when close button clicked', async ({ comfyPage }) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
// Close feedback dialog
await comfyPage.page
.getByLabel('', { exact: true })
.getByLabel('Close')
.click()
await feedbackHeader.waitFor({ state: 'hidden' })
// Verify dialog is closed
await expect(feedbackHeader).not.toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -7,11 +7,13 @@ test.describe('Graph Canvas Menu', () => {
// Set link render mode to spline to make sure it's not affected by other tests'
// side effects.
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
// Enable canvas menu for all tests
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
test('Can toggle link visibility', async ({ comfyPage }) => {
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
// so no cleanup is needed.
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
@@ -34,45 +36,4 @@ test.describe('Graph Canvas Menu', () => {
hiddenLinkRenderMode
)
})
test('Focus mode button is clickable and has correct test id', async ({
comfyPage
}) => {
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
await expect(focusButton).toBeVisible()
await expect(focusButton).toBeEnabled()
// Test that the button can be clicked without error
await focusButton.click()
await comfyPage.nextFrame()
})
test('Zoom controls popup opens and closes', async ({ comfyPage }) => {
// Find the zoom button by its percentage text content
const zoomButton = comfyPage.page.locator('button').filter({
hasText: '%'
})
await expect(zoomButton).toBeVisible()
// Click to open zoom controls
await zoomButton.click()
await comfyPage.nextFrame()
// Zoom controls modal should be visible
const zoomModal = comfyPage.page
.locator('div')
.filter({
hasText: 'Zoom To Fit'
})
.first()
await expect(zoomModal).toBeVisible()
// Click backdrop to close
const backdrop = comfyPage.page.locator('.fixed.inset-0').first()
await backdrop.click()
await comfyPage.nextFrame()
// Modal should be hidden
await expect(zoomModal).not.toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -780,18 +780,9 @@ test.describe('Viewport settings', () => {
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await toggleButton.click()
// close zoom menu
await zoomControlsButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -15,10 +15,8 @@ test.describe('Load Workflow in Media', () => {
'workflow.mp4',
'workflow.mov',
'workflow.m4v',
'workflow.svg'
// TODO: Re-enable after fixing test asset to use core nodes only
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
'workflow.svg',
'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -35,44 +35,34 @@ test.describe('Minimap', () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(toggleButton).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
await expect(minimapContainer).toBeVisible()
})
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).not.toBeVisible()
await expect(toggleButton).toContainText('Show Minimap')
await expect(toggleButton).not.toHaveClass(/minimap-active/)
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toContainText('Hide Minimap')
await expect(toggleButton).toHaveClass(/minimap-active/)
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {

View File

@@ -41,12 +41,15 @@ test.describe('Node Help', () => {
// Select the node with panning to ensure toolbox is visible
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Wait for selection toolbox to appear
await expect(comfyPage.selectionToolbox).toBeVisible()
// Wait for selection overlay container and toolbox to appear
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
// Click the help button in the selection toolbox
const helpButton = comfyPage.selectionToolbox.locator(
'button:has(.pi-question-circle)'
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await expect(helpButton).toBeVisible()
await helpButton.click()

View File

@@ -48,8 +48,7 @@ test.describe('Remote COMBO Widget', () => {
const waitForWidgetUpdate = async (comfyPage: ComfyPage) => {
// Force re-render to trigger first access of widget's options
await comfyPage.page.mouse.click(400, 300)
// Wait for the widget to actually update instead of fixed timeout
await comfyPage.page.waitForTimeout(300)
await comfyPage.page.waitForTimeout(256)
}
test.beforeEach(async ({ comfyPage }) => {
@@ -211,15 +210,9 @@ test.describe('Remote COMBO Widget', () => {
await waitForWidgetUpdate(comfyPage)
const initialOptions = await getWidgetOptions(comfyPage, nodeName)
// Wait for the refresh (TTL) to expire with extra buffer for processing
// TTL is 300ms, wait 600ms to ensure it has expired
await comfyPage.page.waitForTimeout(600)
// Click on the canvas to trigger widget refresh
await comfyPage.page.mouse.click(400, 300)
// Wait a bit for the refresh to complete
await comfyPage.page.waitForTimeout(100)
// Wait for the refresh (TTL) to expire
await comfyPage.page.waitForTimeout(512)
await comfyPage.page.mouse.click(100, 100)
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
expect(refreshedOptions).not.toEqual(initialOptions)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -14,17 +14,20 @@ test.describe('Selection Toolbox', () => {
test('shows selection toolbox', async ({ comfyPage }) => {
// By default, selection toolbox should be enabled
await expect(comfyPage.selectionToolbox).not.toBeVisible()
expect(
await comfyPage.page.locator('.selection-overlay-container').isVisible()
).toBe(false)
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection toolbox should be visible with multiple nodes selected
await expect(comfyPage.selectionToolbox).toBeVisible()
// Border is now drawn on canvas, check via screenshot
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-multiple-nodes-border.png'
)
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
})
test('shows at correct position when node is pasted', async ({
@@ -36,16 +39,18 @@ test.describe('Selection Toolbox', () => {
await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV()
const toolboxContainer = comfyPage.selectionToolbox
await expect(toolboxContainer).toBeVisible()
const overlayContainer = comfyPage.page.locator(
'.selection-overlay-container'
)
await expect(overlayContainer).toBeVisible()
// Verify toolbox is positioned (canvas-based positioning has different coordinates)
const boundingBox = await toolboxContainer.boundingBox()
// Verify the absolute position
const boundingBox = await overlayContainer.boundingBox()
expect(boundingBox).not.toBeNull()
// Canvas-based positioning can vary, just verify toolbox appears in reasonable bounds
expect(boundingBox!.x).toBeGreaterThan(-200) // Not too far off-screen left
expect(boundingBox!.x).toBeLessThan(1000) // Not too far off-screen right
expect(boundingBox!.y).toBeGreaterThan(-100) // Not too far off-screen top
// 10px offset for the pasted node
expect(Math.round(boundingBox!.x)).toBeCloseTo(90, -1) // Allow ~10px tolerance
// 30px offset of node title height
expect(Math.round(boundingBox!.y)).toBeCloseTo(60, -1)
})
test('hide when select and drag happen at the same time', async ({
@@ -60,35 +65,38 @@ test.describe('Selection Toolbox', () => {
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
await comfyPage.nextFrame()
await expect(comfyPage.selectionToolbox).not.toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container')
).not.toBeVisible()
})
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])
// Selection toolbox should be visible but without border
await expect(comfyPage.selectionToolbox).toBeVisible()
// Border is now drawn on canvas, check via screenshot
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-single-node-no-border.png'
)
// Selection overlay should be visible but without border
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection border should show with multiple selections (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-multiple-selections-border.png'
)
// Selection overlay should show border with multiple selections
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
// Deselect to single node
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
// Border should be hidden again (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-single-selection-no-border.png'
)
// Border should be hidden again
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
})
test('displays bypass button in toolbox when nodes are selected', async ({

View File

@@ -193,7 +193,6 @@ test.describe('Workflows sidebar', () => {
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.page.waitForTimeout(200)
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
])

View File

@@ -256,7 +256,6 @@ test.describe('Animated image widget', () => {
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.page.waitForTimeout(200)
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -1,100 +0,0 @@
import { existsSync, readFileSync, readdirSync } from 'fs'
import { join } from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const fileName = fileURLToPath(import.meta.url)
const dirName = dirname(fileName)
const customIconsPath = join(dirName, '..', 'src', 'assets', 'icons', 'custom')
// Iconify collection structure
interface IconifyIcon {
body: string
width?: number
height?: number
}
interface IconifyCollection {
prefix: string
icons: Record<string, IconifyIcon>
width?: number
height?: number
}
// Create an Iconify collection for custom icons
export const iconCollection: IconifyCollection = {
prefix: 'comfy',
icons: {},
width: 16,
height: 16
}
/**
* Validates that an SVG file contains valid SVG content
*/
function validateSvgContent(content: string, filename: string): void {
if (!content.trim()) {
throw new Error(`Empty SVG file: ${filename}`)
}
if (!content.includes('<svg')) {
throw new Error(`Invalid SVG file (missing <svg> tag): ${filename}`)
}
// Basic XML structure validation
const openTags = (content.match(/<svg[^>]*>/g) || []).length
const closeTags = (content.match(/<\/svg>/g) || []).length
if (openTags !== closeTags) {
throw new Error(`Malformed SVG file (mismatched svg tags): ${filename}`)
}
}
/**
* Loads custom SVG icons from the icons directory
*/
function loadCustomIcons(): void {
if (!existsSync(customIconsPath)) {
console.warn(`Custom icons directory not found: ${customIconsPath}`)
return
}
try {
const files = readdirSync(customIconsPath)
const svgFiles = files.filter((file) => file.endsWith('.svg'))
if (svgFiles.length === 0) {
console.warn('No SVG files found in custom icons directory')
return
}
svgFiles.forEach((file) => {
const name = file.replace('.svg', '')
const filePath = join(customIconsPath, file)
try {
const content = readFileSync(filePath, 'utf-8')
validateSvgContent(content, file)
iconCollection.icons[name] = {
body: content
}
} catch (error) {
console.error(
`Failed to load custom icon ${file}:`,
error instanceof Error ? error.message : error
)
// Continue loading other icons instead of failing the entire build
}
})
} catch (error) {
console.error(
'Failed to read custom icons directory:',
error instanceof Error ? error.message : error
)
// Don't throw here - allow build to continue without custom icons
}
}
// Load icons when this module is imported
loadCustomIcons()

View File

@@ -1,293 +0,0 @@
# Settings System
## Overview
ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration.
### Settings Architecture
- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts`
- Registered at app startup, loaded/saved via `useSettingStore` (Pinia)
- Persisted per user via backend `/settings` endpoint
- If a value hasn't been set by the user, the store returns the computed default
```typescript
// From src/stores/settingStore.ts:105-122
function getDefaultValue<K extends keyof Settings>(
key: K
): Settings[K] | undefined {
const param = getSettingById(key)
if (param === undefined) return
const versionedDefault = getVersionedDefaultValue(key, param)
if (versionedDefault) {
return versionedDefault
}
return typeof param.defaultValue === 'function'
? param.defaultValue()
: param.defaultValue
}
```
### Settings Registration Process
Settings are registered after server values are loaded:
```typescript
// From src/components/graph/GraphCanvas.vue:311-315
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})
await newUserService().initializeIfNewUser(settingStore)
```
## Dynamic and Environment-Based Defaults
### Computed Defaults
You can compute defaults dynamically using function defaults that access runtime context:
```typescript
// From src/constants/coreSettings.ts:94-101
{
id: 'Comfy.Sidebar.Size',
// Default to small if the window is less than 1536px(2xl) wide
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
}
```
```typescript
// From src/constants/coreSettings.ts:306
{
id: 'Comfy.Locale',
defaultValue: () => navigator.language.split('-')[0] || 'en'
}
```
### Version-Based Defaults
You can vary defaults by installed frontend version using `defaultsByInstallVersion`:
```typescript
// From src/stores/settingStore.ts:129-150
function getVersionedDefaultValue<K extends keyof Settings, TValue = Settings[K]>(
key: K,
param: SettingParams<TValue> | undefined
): TValue | null {
const defaultsByInstallVersion = param?.defaultsByInstallVersion
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
const installedVersion = get('Comfy.InstalledVersion')
if (installedVersion) {
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
(a, b) => compareVersions(b, a)
)
for (const version of sortedVersions) {
if (!isSemVer(version)) continue
if (compareVersions(installedVersion, version) >= 0) {
const versionedDefault = defaultsByInstallVersion[version]
return typeof versionedDefault === 'function'
? versionedDefault()
: versionedDefault
}
}
}
}
return null
}
```
Example versioned defaults from codebase:
```typescript
// From src/constants/coreSettings.ts:38-40
{
id: 'Comfy.Graph.LinkReleaseAction',
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
defaultsByInstallVersion: {
'1.24.1': LinkReleaseTriggerAction.SEARCH_BOX
}
}
// Another versioned default example
{
id: 'Comfy.Graph.LinkReleaseAction.Shift',
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,
defaultsByInstallVersion: {
'1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU
}
}
```
### Real Examples from Codebase
Here are actual settings showing different patterns:
```typescript
// Number setting with validation
{
id: 'LiteGraph.Node.TooltipDelay',
name: 'Tooltip Delay',
type: 'number',
attrs: {
min: 100,
max: 3000,
step: 50
},
defaultValue: 500,
versionAdded: '1.9.0'
}
// Hidden system setting for tracking
{
id: 'Comfy.InstalledVersion',
name: 'The frontend version that was running when the user first installed ComfyUI',
type: 'hidden',
defaultValue: null,
versionAdded: '1.24.0'
}
// Slider with complex tooltip
{
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
name: 'Low quality rendering zoom threshold',
tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in.',
type: 'slider',
attrs: {
min: 0.1,
max: 1.0,
step: 0.05
},
defaultValue: 0.5
}
```
### New User Version Capture
The initial installed version is captured for new users to ensure versioned defaults remain stable:
```typescript
// From src/services/newUserService.ts:49-53
await settingStore.set(
'Comfy.InstalledVersion',
__COMFYUI_FRONTEND_VERSION__
)
```
## Practical Patterns for Environment-Based Defaults
### Dynamic Default Patterns
```typescript
// Device-based default
{
id: 'Comfy.Example.MobileDefault',
type: 'boolean',
defaultValue: () => /Mobile/i.test(navigator.userAgent)
}
// Environment-based default
{
id: 'Comfy.Example.DevMode',
type: 'boolean',
defaultValue: () => import.meta.env.DEV
}
// Window size based
{
id: 'Comfy.Example.CompactUI',
type: 'boolean',
defaultValue: () => window.innerWidth < 1024
}
```
### Version-Based Rollout Pattern
```typescript
{
id: 'Comfy.Example.NewFeature',
type: 'combo',
options: ['legacy', 'enhanced'],
defaultValue: 'legacy',
defaultsByInstallVersion: {
'1.25.0': 'enhanced'
}
}
```
## Settings Persistence and Access
### API Interaction
Values are stored per user via the backend. The store writes through API and falls back to defaults when not set:
```typescript
// From src/stores/settingStore.ts:73-75
onChange(settingsById.value[key], newValue, oldValue)
settingValues.value[key] = newValue
await api.storeSetting(key, newValue)
```
### Usage in Components
```typescript
const settingStore = useSettingStore()
// Get setting value (returns computed default if not set by user)
const value = settingStore.get('Comfy.SomeSetting')
// Update setting value
await settingStore.set('Comfy.SomeSetting', newValue)
```
## Advanced Settings Features
### Migration and Backward Compatibility
Settings support migration from deprecated values:
```typescript
// From src/stores/settingStore.ts:68-69, 172-175
const newValue = tryMigrateDeprecatedValue(
settingsById.value[key],
clonedValue
)
// Migration happens during addSetting for existing values:
if (settingValues.value[setting.id] !== undefined) {
settingValues.value[setting.id] = tryMigrateDeprecatedValue(
setting,
settingValues.value[setting.id]
)
}
```
### onChange Callbacks
Settings can define onChange callbacks that receive the setting definition, new value, and old value:
```typescript
// From src/stores/settingStore.ts:73, 177
onChange(settingsById.value[key], newValue, oldValue) // During set()
onChange(setting, get(setting.id), undefined) // During addSetting()
```
### Settings UI and Categories
Settings are automatically grouped for UI based on their `category` or derived from `id`:
```typescript
{
id: 'Comfy.Sidebar.Size',
category: ['Appearance', 'Sidebar', 'Size'],
// UI will group this under Appearance > Sidebar > Size
}
```
## Related Documentation
- Feature flag system: `docs/FEATURE_FLAGS.md`
- Settings schema for backend: `src/schemas/apiSchema.ts` (zSettings)
- Server configuration (separate from user settings): `src/constants/serverConfig.ts`
## Summary
- **Settings**: User preferences with dynamic/versioned defaults, persisted per user
- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env)
- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`

View File

@@ -1,82 +0,0 @@
# Settings and Feature Flags Sequence Diagram
This diagram shows the flow of settings initialization, default resolution, persistence, and feature flags exchange.
This diagram accurately reflects the actual implementation in the ComfyUI frontend codebase.
```mermaid
sequenceDiagram
participant User as User
participant Vue as Vue Component
participant Store as SettingStore (Pinia)
participant API as ComfyApi (WebSocket/REST)
participant Backend as Backend
participant NewUserSvc as NewUserService
Note over Vue,Store: App startup (GraphCanvas.vue)
Vue->>Store: loadSettingValues()
Store->>API: getSettings()
API->>Backend: GET /settings
Backend-->>API: settings map (per-user)
API-->>Store: settings map
Store-->>Vue: loaded
Vue->>Store: register CORE_SETTINGS (addSetting for each)
loop For each setting registration
Store->>Store: tryMigrateDeprecatedValue(existing value)
Store->>Store: onChange(setting, currentValue, undefined)
end
Note over Vue,NewUserSvc: New user detection
Vue->>NewUserSvc: initializeIfNewUser(settingStore)
NewUserSvc->>NewUserSvc: checkIsNewUser(settingStore)
alt New user detected
NewUserSvc->>Store: set("Comfy.InstalledVersion", __COMFYUI_FRONTEND_VERSION__)
Store->>Store: tryMigrateDeprecatedValue(newValue)
Store->>Store: onChange(setting, newValue, oldValue)
Store->>API: storeSetting(key, newValue)
API->>Backend: POST /settings/{id}
else Existing user
Note over NewUserSvc: Skip setting installed version
end
Note over Vue,Store: Component reads a setting
Vue->>Store: get(key)
Store->>Store: exists(key)?
alt User value exists
Store-->>Vue: return stored user value
else Not set by user
Store->>Store: getVersionedDefaultValue(key)
alt Versioned default matched (defaultsByInstallVersion)
Store-->>Vue: return versioned default
else No version match
Store->>Store: evaluate defaultValue (function or constant)
Note over Store: defaultValue can use window size,<br/>locale, env, etc.
Store-->>Vue: return computed default
end
end
Note over User,Store: User updates a setting
User->>Vue: changes setting in UI
Vue->>Store: set(key, newValue)
Store->>Store: tryMigrateDeprecatedValue(newValue)
Store->>Store: check if newValue === oldValue (early return if same)
Store->>Store: onChange(setting, newValue, oldValue)
Store->>Store: update settingValues[key]
Store->>API: storeSetting(key, newValue)
API->>Backend: POST /settings/{id}
Backend-->>API: 200 OK
API-->>Store: ack
Note over API,Backend: Feature Flags WebSocket Exchange
API->>Backend: WS connect
API->>Backend: send { type: "feature_flags", data: clientFeatureFlags.json }
Backend-->>API: WS send { type: "feature_flags", data: server flags }
API->>API: store serverFeatureFlags = data
Note over Vue,API: Feature flag consumption in UI/logic
Vue->>API: serverSupportsFeature(name)
API-->>Vue: boolean (true only if flag === true)
Vue->>API: getServerFeature(name, default)
API-->>Vue: value or default
```

View File

@@ -1,50 +0,0 @@
# 2. Restructure ComfyUI_frontend as a monorepo
Date: 2025-08-25
## Status
Proposed
<!-- [Proposed | Accepted | Rejected | Deprecated | Superseded by [ADR-NNNN](NNNN-title.md)] -->
## Context
[Most of the context is in here](https://github.com/Comfy-Org/ComfyUI_frontend/issues/4661)
TL;DR: As we're merging more subprojects like litegraph, devtools, and soon a fork of PrimeVue,
a monorepo structure will help a lot with code sharing and organization.
For more information on Monorepos, check out [monorepo.tools](https://monorepo.tools/)
## Decision
- Swap out NPM for PNPM
- Add a workspace for the PrimeVue fork
- Move the frontend code into its own app workspace
- Longer term: Extract and reorganize common infrastructure to take advantage of the new monorepo tooling
### Tools proposed
[PNPM](https://pnpm.io/) and [PNPM workspaces](https://pnpm.io/workspaces)
For monorepo management, I'd probably go with [Nx](https://nx.dev/), but I could be conviced otherwise.
There's a [whole list here](https://monorepo.tools/#tools-review) if you're interested.
## Consequences
### Positive
- Adding new projects with shared dependencies becomes really easy
- Makes the process of forking and customizing projects more structured, if not strictly easier
- It *could* speed up the build and development process (not guaranteed)
- It would let us cleanly organize and release packages like `comfyui-frontend-types`
### Negative
- Monorepos take some getting used to
- Reviews and code contribution management has to account for the different projects' situations and constraints
<!-- ## Notes
Optional section for additional information, references, or clarifications. -->

View File

@@ -11,7 +11,6 @@ An Architecture Decision Record captures an important architectural decision mad
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Proposed | 2025-08-25 |
## Creating a New ADR

View File

@@ -105,7 +105,7 @@ The alternative would have been breaking all existing extensions or staying with
Build the frontend for full functionality:
```bash
pnpm build
npm run build
```
For faster iteration during development, use watch mode:

View File

@@ -17,10 +17,9 @@ export default [
'src/scripts/*',
'src/extensions/core/*',
'src/types/vue-shim.d.ts',
// Generated files that don't need linting
'src/types/comfyRegistryTypes.ts',
'src/types/generatedManagerTypes.ts',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*'
'src/types/generatedManagerTypes.ts'
]
},
{

View File

@@ -12,7 +12,6 @@ const config: KnipConfig = {
'playwright.config.ts',
'playwright.i18n.config.ts',
'vitest.config.ts',
'vitest.litegraph.config.ts',
'scripts/**/*.{js,ts}'
],
project: [
@@ -33,8 +32,6 @@ const config: KnipConfig = {
'coverage/**',
// i18n config
'.i18nrc.cjs',
// Vitest litegraph config
'vitest.litegraph.config.ts',
// Test setup files
'browser_tests/globalSetup.ts',
'browser_tests/globalTeardown.ts',
@@ -77,7 +74,7 @@ const config: KnipConfig = {
// Workspace configuration for monorepo-like structure
workspaces: {
'.': {
entry: ['src/main.ts', 'playwright.i18n.config.ts']
entry: ['src/main.ts']
}
}
}

40
nx.json
View File

@@ -1,40 +0,0 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"plugins": [
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
}
},
{
"plugin": "@nx/storybook/plugin",
"options": {
"serveStorybookTargetName": "storybook",
"buildStorybookTargetName": "build-storybook",
"testStorybookTargetName": "test-storybook",
"staticStorybookTargetName": "static-storybook"
}
},
{
"plugin": "@nx/vite/plugin",
"options": {
"buildTargetName": "build",
"testTargetName": "test",
"serveTargetName": "serve",
"devTargetName": "dev",
"previewTargetName": "preview",
"serveStaticTargetName": "serve-static",
"typecheckTargetName": "typecheck",
"buildDepsTargetName": "build-deps",
"watchDepsTargetName": "watch-deps"
}
},
{
"plugin": "@nx/playwright/plugin",
"options": {
"targetName": "e2e"
}
}
]
}

19437
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,28 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.26.9",
"version": "1.26.6",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
"description": "Official front-end implementation of ComfyUI",
"license": "GPL-3.0-only",
"scripts": {
"dev": "nx serve",
"dev:electron": "nx serve --config vite.electron.config.mts",
"build": "pnpm typecheck && nx build",
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
"dev": "vite",
"dev:electron": "vite --config vite.electron.config.mts",
"build": "npm run typecheck && vite build",
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:browser": "npx nx e2e",
"test:unit": "nx run test tests-ui/tests",
"test:component": "nx run test src/components/",
"test:litegraph": "vitest run --config vitest.litegraph.config.ts",
"preinstall": "npx only-allow pnpm",
"test:browser": "npx playwright test",
"test:unit": "vitest run tests-ui/tests",
"test:component": "vitest run src/components/",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
"preview": "vite preview",
"lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix",
"lint:no-cache": "eslint src",
@@ -32,41 +30,31 @@
"knip": "knip --cache",
"knip:no-cache": "knip",
"locale": "lobe-i18n locale",
"collect-i18n": "npx playwright test --config=playwright.i18n.config.ts",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"storybook": "nx storybook -p 6006",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.6",
"@executeautomation/playwright-mcp-server": "^1.0.5",
"@iconify/json": "^2.2.245",
"@iconify/tailwind": "^1.2.0",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@lobehub/i18n-cli": "^1.25.1",
"@nx/eslint": "21.4.1",
"@nx/playwright": "21.4.1",
"@nx/storybook": "21.4.1",
"@nx/vite": "21.4.1",
"@nx/web": "21.4.1",
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
"@storybook/addon-docs": "^9.1.1",
"@storybook/vue3": "^9.1.1",
"@storybook/vue3-vite": "^9.1.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vitest/ui": "^3.0.0",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.19",
"chalk": "^5.3.0",
"commander": "^14.0.0",
"eslint": "^9.12.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
@@ -78,17 +66,10 @@
"happy-dom": "^15.11.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"ink": "^6.2.2",
"jiti": "2.4.2",
"jsdom": "^26.1.0",
"knip": "^5.62.0",
"lint-staged": "^15.2.7",
"lucide-vue-next": "^0.540.0",
"nx": "21.4.1",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"react": "^19.1.1",
"react-reconciler": "^0.32.0",
"storybook": "^9.1.1",
"tailwindcss": "^3.4.4",
"tsx": "^4.15.6",
@@ -96,28 +77,22 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.22.0",
"unplugin-vue-components": "^0.28.0",
"uuid": "^11.1.0",
"vite": "^5.4.19",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^3.2.4",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
"zod-to-json-schema": "^3.24.1",
"lucide-vue-next": "^0.540.0"
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@primeuix/forms": "0.0.2",
"@primeuix/styled": "0.3.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "^4.2.5",
"@primevue/forms": "^4.2.5",
"@primevue/icons": "4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/core": "^10.5.0",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
@@ -137,10 +112,8 @@
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"fast-glob": "^3.3.3",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"glob": "^11.0.3",
"jsondiffpatch": "^0.6.0",
"loglevel": "^1.9.2",
"marked": "^15.0.11",

View File

@@ -1,31 +1,39 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './browser_tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
reporter: 'html',
// /* // Toggle for [LOCAL] testing.
/* Retry on CI only - increased for better flaky test handling */
retries: process.env.CI ? 3 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/*/ // [LOCAL]
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
},
//*/
/* Path to global setup file. Exported function runs once before all the tests */
globalSetup: './browser_tests/globalSetup.ts',
/* Path to global teardown file. Exported function runs once after all the tests */
globalTeardown: './browser_tests/globalTeardown.ts',
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
@@ -82,8 +90,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'pnpm dev',
// url: 'http://127.0.0.1:5173',
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
})

View File

@@ -1,6 +1,6 @@
import { defineConfig } from '@playwright/test'
import { PlaywrightTestConfig } from '@playwright/test'
export default defineConfig({
const config: PlaywrightTestConfig = {
testDir: './scripts',
use: {
baseURL: 'http://localhost:5173',
@@ -9,4 +9,6 @@ export default defineConfig({
reporter: 'list',
timeout: 60000,
testMatch: /collect-i18n-.*\.ts/
})
}
export default config

14531
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
packages:
- apps/**
- packages/**
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs
- vue-demi
onlyBuiltDependencies:
- '@playwright/browser-chromium'
- '@playwright/browser-firefox'
- '@playwright/browser-webkit'
- esbuild
- nx
- oxc-resolver

View File

@@ -15,14 +15,12 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
@@ -49,9 +47,5 @@ onMounted(() => {
if (isElectron()) {
document.addEventListener('contextmenu', showContextMenu)
}
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()
})
</script>

View File

@@ -247,29 +247,9 @@ Icons are automatically imported using `unplugin-icons` - no manual imports need
### Configuration
The icon system has two layers:
1. **Build-time Processing** (`build/customIconCollection.ts`):
- Scans `src/assets/icons/custom/` for SVG files
- Validates SVG content and structure
- Creates Iconify collection for Tailwind CSS
- Provides error handling for malformed files
2. **Vite Runtime** (`vite.config.mts`):
- Enables direct SVG import as Vue components
- Supports dynamic icon loading
The icon system is configured in `vite.config.mts`:
```typescript
// Build script creates Iconify collection
export const iconCollection: IconifyCollection = {
prefix: 'comfy',
icons: {
'workflow': { body: '<svg>...</svg>' },
'node': { body: '<svg>...</svg>' }
}
}
// Vite configuration for component-based usage
Icons({
compiler: 'vue3',
customCollections: {
@@ -291,9 +271,8 @@ Icons are fully typed. If TypeScript doesn't recognize a new custom icon:
### Icon Not Showing
1. **Check filename**: Must be kebab-case without special characters
2. **Restart dev server**: Required after adding new icons
3. **Verify SVG**: Ensure it's valid SVG syntax (build script validates automatically)
3. **Verify SVG**: Ensure it's valid SVG syntax
4. **Check console**: Look for Vue component resolution errors
5. **Build script errors**: Check console during build - malformed SVGs are logged but don't break builds
### Icon Wrong Color
- Replace hardcoded colors with `currentColor`

View File

@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><g stroke="currentColor" stroke-linecap="round" stroke-width="1.3" clip-path="url(#a)"><path d="m4.998 13.909.557-2.225a1.112 1.112 0 0 0-1.08-1.382H2.32c-.51 0-.955.347-1.079.842L.684 13.37a1.112 1.112 0 0 0 1.079 1.382h2.156c.51 0 .956-.347 1.08-.842ZM6.11 7.234l.557-2.224a1.112 1.112 0 0 0-1.08-1.383H3.433c-.51 0-.956.348-1.08.843l-.556 2.225a1.112 1.112 0 0 0 1.08 1.382h2.156c.51 0 .955-.347 1.079-.843ZM11.673 13.909l.556-2.225a1.112 1.112 0 0 0-1.08-1.382H8.994c-.51 0-.955.347-1.079.842l-.556 2.225a1.112 1.112 0 0 0 1.08 1.382h2.156c.51 0 .955-.347 1.079-.842ZM13.141 5.816l-.784 1.83a.334.334 0 0 1-.614 0l-.785-1.83a.333.333 0 0 0-.175-.176l-1.831-.784a.334.334 0 0 1 0-.614l1.831-.785a.333.333 0 0 0 .175-.175l.785-1.831a.334.334 0 0 1 .614 0l.784 1.831a.334.334 0 0 0 .176.175l1.83.785c.27.116.27.498 0 .614l-1.83.784a.334.334 0 0 0-.176.176Z"/></g><defs><clipPath id="a"><path fill="currentColor" d="M0 0h16v16H0z"/></clipPath></defs></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.91396 12.7428L5.41396 10.7428C5.57175 10.1116 5.09439 9.50024 4.44382 9.50024H2.50538C2.04651 9.50024 1.64652 9.81253 1.53523 10.2577L1.03523 12.2577C0.877446 12.8888 1.3548 13.5002 2.00538 13.5002H3.94382C4.40269 13.5002 4.80267 13.1879 4.91396 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M5.91396 6.74277L6.41396 4.74277C6.57175 4.11163 6.09439 3.50024 5.44382 3.50024H3.50538C3.04651 3.50024 2.64652 3.81253 2.53523 4.2577L2.03523 6.2577C1.87745 6.88885 2.3548 7.50024 3.00538 7.50024H4.94382C5.40269 7.50024 5.80267 7.18794 5.91396 6.74277Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M10.914 12.7428L11.414 10.7428C11.5718 10.1116 11.0944 9.50024 10.4438 9.50024H8.50538C8.04651 9.50024 7.64652 9.81253 7.53523 10.2577L7.03523 12.2577C6.87745 12.8888 7.3548 13.5002 8.00538 13.5002H9.94382C10.4027 13.5002 10.8027 13.1879 10.914 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M12.2342 5.46739L11.5287 7.11354C11.4248 7.35597 11.0811 7.35597 10.9772 7.11354L10.2717 5.46739C10.2414 5.39659 10.185 5.34017 10.1141 5.30983L8.468 4.60433C8.22557 4.50044 8.22557 4.15675 8.468 4.05285L10.1141 3.34736C10.185 3.31701 10.2414 3.26059 10.2717 3.18979L10.9772 1.54364C11.0811 1.30121 11.4248 1.30121 11.5287 1.54364L12.2342 3.18979C12.2645 3.26059 12.3209 3.31701 12.3918 3.34736L14.0379 4.05285C14.2803 4.15675 14.2803 4.50044 14.0379 4.60433L12.3918 5.30983C12.3209 5.34017 12.2645 5.39659 12.2342 5.46739Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path stroke="currentColor" stroke-linecap="round" stroke-width="1.3" d="m11.2667 10.45-.0842.3156c-.143.5363-.6286.9094-1.18362.9094H4.6945c-.80486 0-1.39102-.7629-1.18364-1.5406l1.30667-4.90002c.143-.53625.62865-.90937 1.18364-.90937h5.46393c.7243 0 1.2518.68663 1.0652 1.38652l-.0301.11275M15.35 8.00001h-4.9m-6.73748 0H.65002"/></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 10L10.598 10.2577C10.4812 10.6954 10.0848 11 9.63172 11H5.30161C4.64458 11 4.16608 10.3772 4.33538 9.74234L5.40204 5.74234C5.51878 5.30458 5.91523 5 6.36828 5H10.8286C11.4199 5 11.8505 5.56051 11.6982 6.13185L11.6736 6.22389M14 8H10M4.5 8H2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><g stroke="currentColor" stroke-linecap="round" stroke-width="1.3" clip-path="url(#a)"><path d="m14.6685 5.7416.6425-2.57c.2028-.811-.4106-1.5967-1.2466-1.5967H2.5782a1.285 1.285 0 0 0-1.2467.9733l-.6425 2.57c-.2027.8111.4107 1.5968 1.2467 1.5968h11.4861a1.285 1.285 0 0 0 1.2467-.9734Zm0 7.7102.6425-2.5701c.2028-.811-.4106-1.5967-1.2466-1.5967h-5.061a1.285 1.285 0 0 0-1.2467.9734l-.6425 2.5701c-.2028.811.4106 1.5966 1.2466 1.5966h5.061a1.285 1.285 0 0 0 1.2467-.9733Zm-10.2802 0 .6425-2.5701c.2027-.811-.4107-1.5967-1.2467-1.5967H2.5782a1.285 1.285 0 0 0-1.2467.9734L.689 12.8285c-.2027.811.4107 1.5966 1.2467 1.5966h1.206a1.285 1.285 0 0 0 1.2466-.9733Z"/></g><defs><clipPath id="a"><path fill="currentColor" d="M0 0h16v16H0z"/></clipPath></defs></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.1894 6.24254L13.6894 4.24254C13.8471 3.61139 13.3698 3 12.7192 3H3.78077C3.3219 3 2.92192 3.3123 2.81062 3.75746L2.31062 5.75746C2.15284 6.38861 2.63019 7 3.28077 7H12.2192C12.6781 7 13.0781 6.6877 13.1894 6.24254Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M13.1894 12.2425L13.6894 10.2425C13.8471 9.61139 13.3698 9 12.7192 9H8.78077C8.3219 9 7.92192 9.3123 7.81062 9.75746L7.31062 11.7575C7.15284 12.3886 7.6302 13 8.28077 13H12.2192C12.6781 13 13.0781 12.6877 13.1894 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M5.18936 12.2425L5.68936 10.2425C5.84714 9.61139 5.36978 9 4.71921 9H3.78077C3.3219 9 2.92192 9.3123 2.81062 9.75746L2.31062 11.7575C2.15284 12.3886 2.6302 13 3.28077 13H4.21921C4.67808 13 5.07806 12.6877 5.18936 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 970 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path stroke="currentColor" stroke-linecap="round" stroke-width="1.3" d="M9.18613 3.09999H6.81377M9.18613 12.9H7.55288c-3.08678 0-5.35171-2.99581-4.60305-6.08843l.3054-1.26158M14.7486 2.1721l-.5931 2.45c-.132.54533-.6065.92789-1.1508.92789h-2.2993c-.77173 0-1.33797-.74895-1.1508-1.5221l.5931-2.45c.132-.54533.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.74896 1.1508 1.52211Zm-8.3033 0-.59309 2.45c-.13201.54533-.60646.92789-1.15076.92789H2.4021c-.7717 0-1.33793-.74895-1.15077-1.5221l.59309-2.45c.13201-.54533.60647-.9279 1.15077-.9279h2.29935c.77169 0 1.33792.74896 1.15076 1.52211Zm8.3033 9.8-.5931 2.45c-.132.5453-.6065.9279-1.1508.9279h-2.2993c-.77173 0-1.33797-.749-1.1508-1.5221l.5931-2.45c.132-.5453.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.7489 1.1508 1.5221Z"/></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.99999 4H6.99999M8.99999 12H7.6231C5.02081 12 3.11138 9.55445 3.74252 7.02986L3.99999 6M13.6894 3.24254L13.1894 5.24254C13.0781 5.6877 12.6781 6 12.2192 6H10.2808C9.63019 6 9.15284 5.38861 9.31062 4.75746L9.81062 2.75746C9.92192 2.3123 10.3219 2 10.7808 2H12.7192C13.3698 2 13.8471 2.61139 13.6894 3.24254ZM6.68936 3.24254L6.18936 5.24254C6.07806 5.6877 5.67808 6 5.21921 6H3.28077C2.63019 6 2.15284 5.38861 2.31062 4.75746L2.81062 2.75746C2.92191 2.3123 3.3219 2 3.78077 2H5.71921C6.36978 2 6.84714 2.61139 6.68936 3.24254ZM13.6894 11.2425L13.1894 13.2425C13.0781 13.6877 12.6781 14 12.2192 14H10.2808C9.63019 14 9.15284 13.3886 9.31062 12.7575L9.81062 10.7575C9.92192 10.3123 10.3219 10 10.7808 10H12.7192C13.3698 10 13.8471 10.6114 13.6894 11.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 910 B

View File

@@ -107,7 +107,6 @@ const rename = async (
}
}
const isRoot = props.item.key === 'root'
const menuItems = computed<MenuItem[]>(() => {
return [
{
@@ -121,27 +120,7 @@ const menuItems = computed<MenuItem[]>(() => {
command: async () => {
await workflowService.duplicateWorkflow(workflowStore.activeWorkflow!)
},
visible: isRoot
},
{
separator: true,
visible: isRoot
},
{
label: t('menuLabels.Save'),
icon: 'pi pi-save',
command: async () => {
await useCommandStore().execute('Comfy.SaveWorkflow')
},
visible: isRoot
},
{
label: t('menuLabels.Save As'),
icon: 'pi pi-save',
command: async () => {
await useCommandStore().execute('Comfy.SaveWorkflowAs')
},
visible: isRoot
visible: props.item.key === 'root'
},
{
separator: true
@@ -155,7 +134,7 @@ const menuItems = computed<MenuItem[]>(() => {
},
{
separator: true,
visible: isRoot
visible: props.item.key === 'root'
},
{
label: t('breadcrumbsMenu.deleteWorkflow'),
@@ -163,7 +142,7 @@ const menuItems = computed<MenuItem[]>(() => {
command: async () => {
await workflowService.deleteWorkflow(workflowStore.activeWorkflow!)
},
visible: isRoot
visible: props.item.key === 'root'
}
]
})

View File

@@ -16,14 +16,6 @@ const meta: Meta<typeof IconButton> = {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
onClick: { action: 'clicked' }
}
}

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<Button unstyled :class="buttonStyle" @click="onClick">
<slot></slot>
</Button>
</template>
@@ -11,7 +11,6 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonTypeClasses,
getIconButtonSizeClasses
} from '@/types/buttonTypes'
@@ -23,8 +22,6 @@ interface IconButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'secondary',
border = false,
disabled = false,
class: className,
onClick
} = defineProps<IconButtonProps>()
@@ -32,9 +29,7 @@ const {
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} p-0`
const sizeClasses = getIconButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
const typeClasses = getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -28,14 +28,6 @@ const meta: Meta<typeof IconTextButton> = {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
iconPosition: {
control: { type: 'select' },
options: ['left', 'right']

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<Button unstyled :class="buttonStyle" @click="onClick">
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
@@ -13,7 +13,6 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
@@ -27,8 +26,6 @@ interface IconTextButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
iconPosition = 'left',
label,
@@ -38,9 +35,7 @@ const {
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
const typeClasses = getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -24,22 +24,22 @@ export const Basic: Story = {
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="transparent"
type="secondary"
label="Settings"
@click="() => { close() }"
>
<template #icon>
<Download :size="16" />
<Download />
</template>
</IconTextButton>
<IconTextButton
type="transparent"
type="primary"
label="Profile"
@click="() => { close() }"
>
<template #icon>
<ScrollText :size="16" />
<ScrollText />
</template>
</IconTextButton>
</template>

View File

@@ -16,14 +16,6 @@ const meta: Meta<typeof TextButton> = {
options: ['sm', 'md'],
defaultValue: 'md'
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent'],

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
<span>{{ label }}</span>
</Button>
</template>
@@ -11,7 +11,6 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
@@ -24,8 +23,6 @@ interface TextButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
label,
onClick
@@ -34,9 +31,7 @@ const {
const buttonStyle = computed(() => {
const baseClasses = getBaseButtonClasses()
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
const typeClasses = getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -1,131 +0,0 @@
<template>
<div
class="inline-flex items-center justify-center"
:style="{ width: size + 'px', height: size + 'px' }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 14 14"
fill="none"
class="animate-spin"
:style="{ animationDuration: duration }"
>
<g clip-path="url(#clip0_776_9582)">
<!-- Top dot -->
<path
class="dot-animation"
style="animation-delay: 0s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
:fill="color"
/>
<!-- Left dot -->
<path
class="dot-animation"
style="animation-delay: 0.25s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
:fill="color"
/>
<!-- Right dot -->
<path
class="dot-animation"
style="animation-delay: 0.5s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
:fill="color"
/>
<!-- Bottom dot -->
<path
class="dot-animation"
style="animation-delay: 0.75s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
:fill="color"
/>
<!-- Top-left dot -->
<path
class="dot-animation"
style="animation-delay: 0.125s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
:fill="color"
/>
<!-- Bottom-right dot -->
<path
class="dot-animation"
style="animation-delay: 0.625s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
:fill="color"
/>
<!-- Bottom-left dot -->
<path
class="dot-animation"
style="animation-delay: 0.875s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
:fill="color"
/>
<!-- Top-right dot -->
<path
class="dot-animation"
style="animation-delay: 0.375s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
:fill="color"
/>
</g>
<defs>
<clipPath id="clip0_776_9582">
<rect width="14" height="14" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const { size = 24, duration = '2s' } = defineProps<{
size?: number
duration?: string
}>()
const colorPaletteStore = useColorPaletteStore()
const color = computed(() =>
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
)
</script>
<style scoped>
.dot-animation {
animation: dot-pulse 1s ease-in-out infinite;
}
@keyframes dot-pulse {
0%,
80%,
100% {
opacity: 0.3;
}
40% {
opacity: 1;
}
}
</style>

View File

@@ -29,7 +29,7 @@
/>
<template v-if="item.footerComponent" #footer>
<component :is="item.footerComponent" v-bind="item.footerProps" />
<component :is="item.footerComponent" />
</template>
</Dialog>
</template>

View File

@@ -21,9 +21,16 @@
@click="showReport"
/>
<Button
v-show="!reportOpen"
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
<Button
v-if="authStore.currentUser"
v-show="!reportOpen"
text
:label="$t('issueReport.contactSupportTitle')"
@click="showContactSupport"
/>
</div>
@@ -34,6 +41,16 @@
</ScrollPanel>
<Divider />
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
:error-type="error.reportType ?? 'unknownError'"
:extra-fields="[stackTraceField]"
:tags="{
exceptionMessage: error.exceptionMessage,
nodeType: error.nodeType ?? 'UNKNOWN'
}"
/>
<div class="flex gap-4 justify-end">
<FindIssueButton
:error-message="error.exceptionMessage"
@@ -64,12 +81,18 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { ReportField } from '@/types/issueReportTypes'
import {
type ErrorReportData,
generateErrorReport
} from '@/utils/errorReportUtil'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const authStore = useFirebaseAuthStore()
const { error } = defineProps<{
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
/**
@@ -91,6 +114,10 @@ const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const sendReportOpen = ref(false)
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const systemStatsStore = useSystemStatsStore()
@@ -99,6 +126,15 @@ const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => error.traceback
}
})
const showContactSupport = async () => {
await useCommandStore().execute('Comfy.ContactSupport')
}

View File

@@ -0,0 +1,33 @@
<template>
<div class="p-2 h-full" aria-labelledby="issue-report-title">
<Panel
:pt="{
root: 'border-none',
content: 'p-0'
}"
>
<template #header>
<header class="flex flex-col items-center w-full">
<h2 id="issue-report-title" class="text-4xl">
{{ title }}
</h2>
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
</header>
</template>
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
</Panel>
</div>
</template>
<script setup lang="ts">
import Panel from 'primevue/panel'
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
defineProps<{
title: string
subtitle?: string
panelProps: IssueReportPanelProps
}>()
</script>

View File

@@ -31,20 +31,12 @@
</div>
</template>
</ListBox>
<div v-if="showManagerButtons" class="flex justify-end py-3">
<div v-if="isManagerInstalled" class="flex justify-end py-3">
<PackInstallButton
v-if="showInstallAllButton"
size="md"
:disabled="
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
"
:is-loading="isLoading"
:disabled="isLoading || !!error || missingNodePacks.length === 0"
:node-packs="missingNodePacks"
:label="
isLoading
? $t('manager.gettingInfo')
: $t('manager.installAllMissingNodes')
"
variant="black"
:label="$t('manager.installAllMissingNodes')"
/>
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
@@ -54,39 +46,34 @@
import Button from 'primevue/button'
import ListBox from 'primevue/listbox'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useToastStore } from '@/stores/toastStore'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
import PackInstallButton from './manager/button/PackInstallButton.vue'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
const aboutPanelStore = useAboutPanelStore()
// Get missing node packs from workflow with loading and error states
const { missingNodePacks, isLoading, error, missingCoreNodes } =
useMissingNodes()
const comfyManagerStore = useComfyManagerStore()
// Check if any of the missing packs are currently being installed
const isInstalling = computed(() => {
if (!missingNodePacks.value?.length) return false
return missingNodePacks.value.some((pack) =>
comfyManagerStore.isPackInstalling(pack.id)
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
// This allows us to conditionally show the Manager button only when the extension is available
// TODO: Remove this check when Manager functionality is fully migrated into core
const isManagerInstalled = computed(() => {
return aboutPanelStore.badges.some(
(badge) =>
badge.label.includes('ComfyUI-Manager') ||
badge.url.includes('ComfyUI-Manager')
)
})
@@ -111,47 +98,10 @@ const uniqueNodes = computed(() => {
})
})
const managerStateStore = useManagerStateStore()
// Show manager buttons unless manager is disabled
const showManagerButtons = computed(() => {
return managerStateStore.managerUIState !== ManagerUIState.DISABLED
})
// Only show Install All button for NEW_UI (new manager with v4 support)
const showInstallAllButton = computed(() => {
return managerStateStore.managerUIState === ManagerUIState.NEW_UI
})
const openManager = async () => {
const state = managerStateStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
useDialogService().showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
} catch {
// If legacy command doesn't exist, show toast
const { t } = useI18n()
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
break
case ManagerUIState.NEW_UI:
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
break
}
const openManager = () => {
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
}
</script>

View File

@@ -30,20 +30,11 @@ const defaultMockTaskLogs = [
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
taskLogs: [...defaultMockTaskLogs],
succeededTasksLogs: [...defaultMockTaskLogs],
failedTasksLogs: [...defaultMockTaskLogs],
managerQueue: { historyCount: 2 },
isLoading: false
taskLogs: [...defaultMockTaskLogs]
})),
useManagerProgressDialogStore: vi.fn(() => ({
isExpanded: true,
activeTabIndex: 0,
getActiveTabIndex: vi.fn(() => 0),
setActiveTabIndex: vi.fn(),
toggle: vi.fn(),
collapse: mockCollapse,
expand: vi.fn()
collapse: mockCollapse
}))
}))

View File

@@ -18,16 +18,16 @@
'max-h-0': !isExpanded
}"
>
<div v-for="(log, index) in focusedLogs" :key="index">
<div v-for="(panel, index) in taskPanels" :key="index">
<Panel
:expanded="collapsedPanels[index] === true"
:expanded="collapsedPanels[index] || false"
toggleable
class="shadow-elevation-1 rounded-lg mt-2 dark-theme:bg-black dark-theme:border-black"
>
<template #header>
<div class="flex items-center justify-between w-full py-2">
<div class="flex flex-col text-sm font-medium leading-normal">
<span>{{ log.taskName }}</span>
<span>{{ panel.taskName }}</span>
<span class="text-muted">
{{
isInProgress(index)
@@ -52,24 +52,24 @@
</template>
<div
:ref="
index === focusedLogs.length - 1
index === taskPanels.length - 1
? (el) => (lastPanelRef = el as HTMLElement)
: undefined
"
class="overflow-y-auto h-64 rounded-lg bg-black"
:class="{
'h-64': index !== focusedLogs.length - 1,
'flex-grow': index === focusedLogs.length - 1
'h-64': index !== taskPanels.length - 1,
'flex-grow': index === taskPanels.length - 1
}"
@scroll="handleScroll"
>
<div class="h-full">
<div
v-for="(logLine, logIndex) in log.logs"
v-for="(log, logIndex) in panel.logs"
:key="logIndex"
class="text-neutral-400 dark-theme:text-muted"
>
<pre class="whitespace-pre-wrap break-words">{{ logLine }}</pre>
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
</div>
</div>
</div>
@@ -90,31 +90,14 @@ import {
useManagerProgressDialogStore
} from '@/stores/comfyManagerStore'
const comfyManagerStore = useComfyManagerStore()
const { taskLogs } = useComfyManagerStore()
const progressDialogContent = useManagerProgressDialogStore()
const managerStore = useComfyManagerStore()
const isInProgress = (index: number) => {
const log = focusedLogs.value[index]
if (!log) return false
const isInProgress = (index: number) =>
index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0
// Check if this task is in the running or pending queue
const taskQueue = comfyManagerStore.taskQueue
if (!taskQueue) return false
const allQueueTasks = [
...(taskQueue.running_queue || []),
...(taskQueue.pending_queue || [])
]
return allQueueTasks.some((task) => task.ui_id === log.taskId)
}
const focusedLogs = computed(() => {
if (progressDialogContent.getActiveTabIndex() === 0) {
return comfyManagerStore.succeededTasksLogs
}
return comfyManagerStore.failedTasksLogs
})
const taskPanels = computed(() => taskLogs)
const isExpanded = computed(() => progressDialogContent.isExpanded)
const isCollapsed = computed(() => !isExpanded.value)
@@ -132,7 +115,7 @@ const { y: scrollY } = useScroll(sectionsContainerRef, {
const lastPanelRef = ref<HTMLElement | null>(null)
const isUserScrolling = ref(false)
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs)
const isAtBottom = (el: HTMLElement | null) => {
if (!el) return false

View File

@@ -41,6 +41,7 @@
>
<template #header>
<CurrentUserMessage v-if="tabValue === 'Comfy'" />
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
</template>
<SettingsPanel :setting-groups="sortedGroups(category)" />
@@ -75,6 +76,7 @@ import { flattenTree } from '@/utils/treeUtil'
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import PanelTemplate from './setting/PanelTemplate.vue'
import SettingsPanel from './setting/SettingsPanel.vue'
@@ -118,7 +120,7 @@ const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
}
const handleSearch = (query: string) => {
handleSearchBase(query.trim())
handleSearchBase(query)
activeCategory.value = query ? null : defaultCategory.value
}

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