Compare commits
1 Commits
bl-bump-ta
...
issue-5151
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eac858bd6 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
29
.github/workflows/chromatic.yaml
vendored
@@ -3,15 +3,14 @@ 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
|
||||
@@ -21,16 +20,11 @@ jobs:
|
||||
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
|
||||
@@ -38,7 +32,6 @@ jobs:
|
||||
|
||||
- name: Comment PR - Build Started
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -56,21 +49,8 @@ jobs:
|
||||
---
|
||||
*This comment will be updated when the build completes*
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
storybook-static
|
||||
tsconfig.tsbuildinfo
|
||||
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
|
||||
restore-keys: |
|
||||
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
storybook-cache-${{ runner.os }}-
|
||||
storybook-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
- name: Build Storybook and run Chromatic
|
||||
id: chromatic
|
||||
@@ -88,7 +68,6 @@ jobs:
|
||||
|
||||
- name: Comment PR - Build Complete
|
||||
if: github.event_name == 'pull_request' && always()
|
||||
continue-on-error: true
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
8
.github/workflows/claude-pr-review.yml
vendored
@@ -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
|
||||
|
||||
23
.github/workflows/dev-release.yaml
vendored
@@ -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:
|
||||
|
||||
15
.github/workflows/i18n-custom-nodes.yaml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/i18n-node-defs.yaml
vendored
@@ -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
|
||||
|
||||
34
.github/workflows/i18n.yaml
vendored
@@ -1,45 +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 }}-
|
||||
- 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-general.ts
|
||||
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
|
||||
|
||||
38
.github/workflows/lint-and-format.yaml
vendored
@@ -19,40 +19,20 @@ jobs:
|
||||
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.'
|
||||
})
|
||||
45
.github/workflows/release.yaml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/test-browser-exp.yaml
vendored
@@ -10,7 +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
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
49
.github/workflows/test-ui.yaml
vendored
@@ -37,16 +37,9 @@ 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
|
||||
@@ -54,7 +47,6 @@ jobs:
|
||||
|
||||
- name: Comment PR - Tests Started
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -66,28 +58,16 @@ jobs:
|
||||
|
||||
---
|
||||
|
||||
<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-left: 4px;" />
|
||||
<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: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
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 }}-
|
||||
|
||||
- name: Build ComfyUI_frontend
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
npm ci
|
||||
npm run build
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Generate cache key
|
||||
@@ -134,15 +114,9 @@ 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
|
||||
@@ -160,7 +134,6 @@ jobs:
|
||||
|
||||
- name: Comment PR - Browser Test Started
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -168,7 +141,7 @@ jobs:
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: append
|
||||
body: |
|
||||
<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-left: 4px;" />
|
||||
<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
|
||||
@@ -185,21 +158,12 @@ 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: pnpm install -g wrangler
|
||||
run: npm install -g wrangler
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
id: playwright
|
||||
@@ -216,7 +180,6 @@ jobs:
|
||||
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
|
||||
id: cloudflare-deploy
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Retry logic for wrangler deploy (3 attempts)
|
||||
RETRY_COUNT=0
|
||||
@@ -275,7 +238,6 @@ jobs:
|
||||
|
||||
- name: Comment PR - Browser Test Complete
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -361,7 +323,6 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Comment PR - Tests Complete
|
||||
continue-on-error: true
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
20
.github/workflows/update-electron-types.yaml
vendored
@@ -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
|
||||
|
||||
28
.github/workflows/update-manager-types.yaml
vendored
@@ -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
|
||||
|
||||
28
.github/workflows/update-registry-types.yaml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/version-bump.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
25
.github/workflows/vitest.yaml
vendored
@@ -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
|
||||
|
||||
2
.gitignore
vendored
@@ -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,6 +23,7 @@ dist-ssr
|
||||
*.local
|
||||
# Claude configuration
|
||||
.claude/*.local.json
|
||||
.claude/settings.json
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
22
AGENTS.md
@@ -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.
|
||||
|
||||
14
CLAUDE.md
@@ -2,13 +2,13 @@
|
||||
|
||||
## Quick Commands
|
||||
|
||||
- `pnpm`: See all available commands
|
||||
- `pnpm typecheck`: Type checking
|
||||
- `pnpm lint`: Linting
|
||||
- `pnpm format`: Prettier formatting
|
||||
- `pnpm test:component`: Run component tests with browser environment
|
||||
- `pnpm test:unit`: Run all unit tests
|
||||
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
|
||||
- `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
|
||||
|
||||
|
||||
@@ -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 (v16 or later; v24 strongly recommended) 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).
|
||||
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 82 KiB |
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 98 KiB |
@@ -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()
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 98 KiB |
@@ -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 }) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 99 KiB |
@@ -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 ({
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB |
@@ -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'
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 170 KiB |
@@ -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()
|
||||
@@ -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. -->
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
19437
package-lock.json
generated
Normal file
26
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.7",
|
||||
"version": "1.26.6",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -10,7 +10,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:electron": "vite --config vite.electron.config.mts",
|
||||
"build": "pnpm typecheck && vite build",
|
||||
"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",
|
||||
@@ -21,7 +21,6 @@
|
||||
"test:browser": "npx playwright test",
|
||||
"test:unit": "vitest run tests-ui/tests",
|
||||
"test:component": "vitest run src/components/",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --cache",
|
||||
@@ -40,13 +39,11 @@
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@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.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",
|
||||
@@ -58,7 +55,6 @@
|
||||
"@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",
|
||||
@@ -70,22 +66,17 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"ink": "^4.2.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"lucide-vue-next": "^0.540.0",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
"react": "^18.3.1",
|
||||
"react-reconciler": "^0.29.2",
|
||||
"storybook": "^9.1.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"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",
|
||||
@@ -93,20 +84,15 @@
|
||||
"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",
|
||||
@@ -126,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",
|
||||
|
||||
@@ -90,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,
|
||||
// },
|
||||
})
|
||||
|
||||
12519
pnpm-lock.yaml
generated
@@ -1,15 +0,0 @@
|
||||
packages:
|
||||
- apps/**
|
||||
- packages/**
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
- vue-demi
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@playwright/browser-chromium'
|
||||
- '@playwright/browser-firefox'
|
||||
- '@playwright/browser-webkit'
|
||||
- esbuild
|
||||
- oxc-resolver
|
||||
@@ -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`
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
26
src/components/dialog/content/setting/FirstTimeUIMessage.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<Message
|
||||
v-if="show"
|
||||
class="first-time-ui-message"
|
||||
severity="info"
|
||||
:closable="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
{{ $t('g.firstTimeUIMessage') }}
|
||||
</Message>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const show = computed(() => !settingStore.exists('Comfy.UseNewMenu'))
|
||||
const handleClose = async () => {
|
||||
// Explicitly write the current value to the store.
|
||||
const currentValue = settingStore.get('Comfy.UseNewMenu')
|
||||
await settingStore.set('Comfy.UseNewMenu', currentValue)
|
||||
}
|
||||
</script>
|
||||
@@ -2,11 +2,13 @@
|
||||
<!-- Load splitter overlay only after comfyApp is ready. -->
|
||||
<!-- If load immediately, the top-level splitter stateKey won't be correctly
|
||||
synced with the stateStorage (localStorage). -->
|
||||
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
|
||||
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
|
||||
<LiteGraphCanvasSplitterOverlay
|
||||
v-if="comfyAppReady && betaMenuEnabled && !workspaceStore.focusMode"
|
||||
>
|
||||
<template #side-bar-panel>
|
||||
<SideToolbar />
|
||||
</template>
|
||||
<template v-if="!workspaceStore.focusMode" #bottom-panel>
|
||||
<template #bottom-panel>
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
@@ -32,20 +34,22 @@
|
||||
/>
|
||||
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
<NodeSearchboxPopover ref="nodeSearchboxPopoverRef" />
|
||||
<NodeSearchboxPopover />
|
||||
|
||||
<!-- Initialize components after comfyApp is ready. useAbsolutePosition requires
|
||||
canvasStore.canvas to be initialized. -->
|
||||
<template v-if="comfyAppReady">
|
||||
<TitleEditor />
|
||||
<SelectionToolbox v-if="selectionToolboxEnabled" />
|
||||
<SelectionOverlay v-if="selectionToolboxEnabled">
|
||||
<SelectionToolbox />
|
||||
</SelectionOverlay>
|
||||
<DomWidgets />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
@@ -53,6 +57,7 @@ import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import MiniMap from '@/components/graph/MiniMap.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
@@ -86,16 +91,12 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: []
|
||||
}>()
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const nodeSearchboxPopoverRef = shallowRef<InstanceType<
|
||||
typeof NodeSearchboxPopover
|
||||
> | null>(null)
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -319,7 +320,6 @@ onMounted(async () => {
|
||||
canvasStore.canvas = comfyApp.canvas
|
||||
canvasStore.canvas.render_canvas_border = false
|
||||
workspaceStore.spinner = false
|
||||
useSearchBoxStore().setPopoverRef(nodeSearchboxPopoverRef.value)
|
||||
|
||||
window.app = comfyApp
|
||||
window.graph = comfyApp.graph
|
||||
|
||||
@@ -1,278 +1,125 @@
|
||||
<template>
|
||||
<div>
|
||||
<ZoomControlsModal :visible="isModalVisible" />
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
v-if="hasActivePopup"
|
||||
class="fixed inset-0 z-[1200]"
|
||||
@click="hideModal"
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
class="p-buttongroup-vertical p-1 absolute bottom-4 right-2 md:right-4"
|
||||
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
<ButtonGroup
|
||||
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
|
||||
severity="secondary"
|
||||
icon="pi pi-plus"
|
||||
:aria-label="$t('graphCanvasMenu.zoomIn')"
|
||||
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
|
||||
@mouseup="stopRepeat"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
|
||||
severity="secondary"
|
||||
icon="pi pi-minus"
|
||||
:aria-label="$t('graphCanvasMenu.zoomOut')"
|
||||
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
|
||||
@mouseup="stopRepeat"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.left="t('graphCanvasMenu.fitView')"
|
||||
severity="secondary"
|
||||
icon="pi pi-expand"
|
||||
:aria-label="$t('graphCanvasMenu.fitView')"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.left="
|
||||
t(
|
||||
'graphCanvasMenu.' +
|
||||
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
|
||||
) + ' (Space)'
|
||||
"
|
||||
severity="secondary"
|
||||
:aria-label="
|
||||
t(
|
||||
'graphCanvasMenu.' +
|
||||
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
|
||||
)
|
||||
"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.top="selectTooltip"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
severity="secondary"
|
||||
:aria-label="selectTooltip"
|
||||
:pressed="isCanvasReadOnly"
|
||||
icon="i-material-symbols:pan-tool-outline"
|
||||
:class="selectButtonClass"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:mouse-pointer-2 />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-tooltip.top="handTooltip"
|
||||
severity="secondary"
|
||||
:aria-label="handTooltip"
|
||||
:pressed="isCanvasUnlocked"
|
||||
:class="handButtonClass"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:hand />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<!-- vertical line with bg E1DED5 -->
|
||||
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
|
||||
|
||||
<Button
|
||||
v-tooltip.top="fitViewTooltip"
|
||||
severity="secondary"
|
||||
icon="pi pi-expand"
|
||||
:aria-label="fitViewTooltip"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
class="hover:dark-theme:!bg-[#444444] hover:!bg-[#E7E6E6]"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:focus />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
ref="zoomButton"
|
||||
v-tooltip.top="t('zoomControls.label')"
|
||||
severity="secondary"
|
||||
:label="t('zoomControls.label')"
|
||||
:class="zoomButtonClass"
|
||||
:aria-label="t('zoomControls.label')"
|
||||
data-testid="zoom-controls-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
@click="toggleModal"
|
||||
>
|
||||
<span class="inline-flex text-xs">
|
||||
<span>{{ canvasStore.appScalePercentage }}%</span>
|
||||
<i-lucide:chevron-down />
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
|
||||
|
||||
<Button
|
||||
ref="focusButton"
|
||||
v-tooltip.top="focusModeTooltip"
|
||||
severity="secondary"
|
||||
:aria-label="focusModeTooltip"
|
||||
data-testid="focus-mode-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
:class="focusButtonClass"
|
||||
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:lightbulb />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: linkVisibilityTooltip,
|
||||
pt: {
|
||||
root: {
|
||||
style: 'z-index: 2; transform: translateY(-20px);'
|
||||
}
|
||||
}
|
||||
}"
|
||||
severity="secondary"
|
||||
:class="linkVisibleClass"
|
||||
:aria-label="linkVisibilityAriaLabel"
|
||||
data-testid="toggle-link-visibility-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:route-off />
|
||||
</template>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<template #icon>
|
||||
<i-material-symbols:pan-tool-outline
|
||||
v-if="canvasStore.canvas?.read_only"
|
||||
/>
|
||||
<i-simple-line-icons:cursor v-else />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
|
||||
severity="secondary"
|
||||
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
:aria-label="$t('graphCanvasMenu.toggleLinkVisibility')"
|
||||
data-testid="toggle-link-visibility-button"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.left="minimapTooltip"
|
||||
severity="secondary"
|
||||
:icon="'pi pi-map'"
|
||||
:aria-label="$t('graphCanvasMenu.toggleMinimap')"
|
||||
:class="{ 'minimap-active': minimapVisible }"
|
||||
data-testid="toggle-minimap-button"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useZoomControls } from '@/composables/useZoomControls'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import ZoomControlsModal from './modals/ZoomControlsModal.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { formatKeySequence } = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const settingStore = useSettingStore()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const minimap = useMinimap()
|
||||
|
||||
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
|
||||
useZoomControls()
|
||||
|
||||
const stringifiedMinimapStyles = computed(() => {
|
||||
const buttonGroupKeys = ['backgroundColor', 'borderRadius', '']
|
||||
const buttonKeys = ['backgroundColor', 'borderRadius']
|
||||
const additionalButtonStyles = {
|
||||
border: 'none',
|
||||
width: '35px',
|
||||
height: '35px',
|
||||
'margin-right': '2px',
|
||||
'margin-left': '2px'
|
||||
}
|
||||
|
||||
const containerStyles = minimap.containerStyles.value
|
||||
|
||||
const buttonStyles = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(containerStyles).filter(([key]) =>
|
||||
buttonKeys.includes(key)
|
||||
)
|
||||
),
|
||||
...additionalButtonStyles
|
||||
}
|
||||
const buttonGroupStyles = Object.entries(containerStyles)
|
||||
.filter(([key]) => buttonGroupKeys.includes(key))
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
|
||||
|
||||
return { buttonStyles, buttonGroupStyles }
|
||||
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
const minimapTooltip = computed(() => {
|
||||
const baseText = t('graphCanvasMenu.toggleMinimap')
|
||||
const keybinding = keybindingStore.getKeybindingByCommandId(
|
||||
'Comfy.Canvas.ToggleMinimap'
|
||||
)
|
||||
return keybinding ? `${baseText} (${keybinding.combo.toString()})` : baseText
|
||||
})
|
||||
|
||||
// Computed properties for reactive states
|
||||
const isCanvasReadOnly = computed(() => canvasStore.canvas?.read_only ?? false)
|
||||
const isCanvasUnlocked = computed(() => !isCanvasReadOnly.value)
|
||||
const linkHidden = computed(
|
||||
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
||||
)
|
||||
|
||||
// Computed properties for command text
|
||||
const unlockCommandText = computed(() =>
|
||||
formatKeySequence(
|
||||
commandStore.getCommand('Comfy.Canvas.Unlock')
|
||||
).toUpperCase()
|
||||
)
|
||||
const lockCommandText = computed(() =>
|
||||
formatKeySequence(commandStore.getCommand('Comfy.Canvas.Lock')).toUpperCase()
|
||||
)
|
||||
const fitViewCommandText = computed(() =>
|
||||
formatKeySequence(
|
||||
commandStore.getCommand('Comfy.Canvas.FitView')
|
||||
).toUpperCase()
|
||||
)
|
||||
const focusCommandText = computed(() =>
|
||||
formatKeySequence(
|
||||
commandStore.getCommand('Workspace.ToggleFocusMode')
|
||||
).toUpperCase()
|
||||
)
|
||||
|
||||
// Computed properties for button classes and states
|
||||
const selectButtonClass = computed(() =>
|
||||
isCanvasUnlocked.value
|
||||
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
|
||||
: ''
|
||||
)
|
||||
|
||||
const handButtonClass = computed(() =>
|
||||
isCanvasReadOnly.value
|
||||
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
|
||||
: ''
|
||||
)
|
||||
|
||||
const zoomButtonClass = computed(() => [
|
||||
'!w-16',
|
||||
isModalVisible.value
|
||||
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
|
||||
: '',
|
||||
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]'
|
||||
])
|
||||
|
||||
const focusButtonClass = computed(() => ({
|
||||
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]': true,
|
||||
'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]':
|
||||
workspaceStore.focusMode
|
||||
}))
|
||||
|
||||
// Computed properties for tooltip and aria-label texts
|
||||
const selectTooltip = computed(
|
||||
() => `${t('graphCanvasMenu.select')} (${unlockCommandText.value})`
|
||||
)
|
||||
const handTooltip = computed(
|
||||
() => `${t('graphCanvasMenu.hand')} (${lockCommandText.value})`
|
||||
)
|
||||
const fitViewTooltip = computed(
|
||||
() => `${t('graphCanvasMenu.fitView')} (${fitViewCommandText.value})`
|
||||
)
|
||||
const focusModeTooltip = computed(
|
||||
() => `${t('graphCanvasMenu.focusMode')} (${focusCommandText.value})`
|
||||
)
|
||||
const linkVisibilityTooltip = computed(() =>
|
||||
linkHidden.value
|
||||
? t('graphCanvasMenu.showLinks')
|
||||
: t('graphCanvasMenu.hideLinks')
|
||||
)
|
||||
const linkVisibilityAriaLabel = computed(() =>
|
||||
linkHidden.value
|
||||
? t('graphCanvasMenu.showLinks')
|
||||
: t('graphCanvasMenu.hideLinks')
|
||||
)
|
||||
const linkVisibleClass = computed(() => [
|
||||
linkHidden.value
|
||||
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
|
||||
: '',
|
||||
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]'
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
canvasStore.initScaleSync()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
canvasStore.cleanupScaleSync()
|
||||
})
|
||||
let interval: number | null = null
|
||||
const repeat = async (command: string) => {
|
||||
if (interval) return
|
||||
const cmd = () => commandStore.execute(command)
|
||||
await cmd()
|
||||
interval = window.setInterval(cmd, 100)
|
||||
}
|
||||
const stopRepeat = () => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
interval = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-buttongroup-vertical {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
z-index: 1200;
|
||||
flex-direction: column;
|
||||
border-radius: var(--p-button-border-radius);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
@@ -282,4 +129,15 @@ onBeforeUnmount(() => {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.p-button.minimap-active {
|
||||
background-color: var(--p-button-primary-background);
|
||||
border-color: var(--p-button-primary-border-color);
|
||||
color: var(--p-button-primary-color);
|
||||
}
|
||||
|
||||
.p-button.minimap-active:hover {
|
||||
background-color: var(--p-button-primary-hover-background);
|
||||
border-color: var(--p-button-primary-hover-border-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
ref="minimapRef"
|
||||
class="minimap-main-container flex absolute bottom-[66px] right-2 md:right-11 z-[1000]"
|
||||
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
>
|
||||
<MiniMapPanel
|
||||
v-if="showOptionsPanel"
|
||||
@@ -31,25 +31,6 @@
|
||||
<i-lucide:settings-2 />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
class="absolute z-10 right-0"
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
data-testid="close-minmap-button"
|
||||
@click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:x />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<hr
|
||||
class="absolute top-5 bg-[#E1DED5] dark-theme:bg-[#262729] h-[1px] border-0"
|
||||
:style="{
|
||||
width: containerStyles.width
|
||||
}"
|
||||
/>
|
||||
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
@@ -77,12 +58,9 @@ import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
import MiniMapPanel from './MiniMapPanel.vue'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const minimapRef = ref<HTMLDivElement>()
|
||||
|
||||
const {
|
||||
|
||||
106
src/components/graph/SelectionOverlay.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<!-- This component is used to bound the selected items on the canvas. -->
|
||||
<template>
|
||||
<div
|
||||
v-show="visible"
|
||||
class="selection-overlay-container pointer-events-none z-40"
|
||||
:class="{
|
||||
'show-border': showBorder
|
||||
}"
|
||||
:style="style"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { provide, readonly, ref, watch } from 'vue'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const { style, updatePosition } = useAbsolutePosition()
|
||||
const { getSelectableItems } = useSelectedLiteGraphItems()
|
||||
|
||||
const visible = ref(false)
|
||||
const showBorder = ref(false)
|
||||
// Increment counter to notify child components of position/visibility change
|
||||
// This does not include viewport changes.
|
||||
const overlayUpdateCount = ref(0)
|
||||
provide(SelectionOverlayInjectionKey, {
|
||||
visible: readonly(visible),
|
||||
updateCount: readonly(overlayUpdateCount)
|
||||
})
|
||||
|
||||
const positionSelectionOverlay = () => {
|
||||
const selectableItems = getSelectableItems()
|
||||
showBorder.value = selectableItems.size > 1
|
||||
|
||||
if (!selectableItems.size) {
|
||||
visible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
const bounds = createBounds(selectableItems)
|
||||
if (bounds) {
|
||||
updatePosition({
|
||||
pos: [bounds[0], bounds[1]],
|
||||
size: [bounds[2], bounds[3]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
whenever(
|
||||
() => canvasStore.getCanvas().state.selectionChanged,
|
||||
() => {
|
||||
requestAnimationFrame(() => {
|
||||
positionSelectionOverlay()
|
||||
overlayUpdateCount.value++
|
||||
canvasStore.getCanvas().state.selectionChanged = false
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
canvasStore.getCanvas().ds.onChanged = positionSelectionOverlay
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas?.state?.draggingItems,
|
||||
(draggingItems) => {
|
||||
// Litegraph draggingItems state can end early before the bounding boxes of
|
||||
// the selected items are updated. Delay to make sure we put the overlay in
|
||||
// the correct position.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2656
|
||||
if (draggingItems === false) {
|
||||
requestAnimationFrame(() => {
|
||||
visible.value = true
|
||||
positionSelectionOverlay()
|
||||
overlayUpdateCount.value++
|
||||
})
|
||||
} else {
|
||||
// Selection change update to visible state is delayed by a frame. Here
|
||||
// we also delay a frame so that the order of events is correct when
|
||||
// the initial selection and dragging happens at the same time.
|
||||
requestAnimationFrame(() => {
|
||||
visible.value = false
|
||||
overlayUpdateCount.value++
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selection-overlay-container > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.show-border {
|
||||
@apply border-dashed rounded-md border-2 border-[var(--border-color)];
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +1,34 @@
|
||||
<template>
|
||||
<div
|
||||
ref="toolboxRef"
|
||||
style="transform: translate(var(--tb-x), var(--tb-y))"
|
||||
class="fixed left-0 top-0 z-40"
|
||||
<Panel
|
||||
class="selection-toolbox absolute left-1/2 rounded-lg"
|
||||
:class="{ 'animate-slide-up': shouldAnimate }"
|
||||
:pt="{
|
||||
header: 'hidden',
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<Transition name="slide-up">
|
||||
<Panel
|
||||
v-if="visible"
|
||||
class="rounded-lg selection-toolbox"
|
||||
:pt="{
|
||||
header: 'hidden',
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<ExecuteButton />
|
||||
<ColorPickerButton />
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<DeleteButton />
|
||||
<RefreshSelectionButton />
|
||||
<ExtensionCommandButton
|
||||
v-for="command in extensionToolboxCommands"
|
||||
:key="command.id"
|
||||
:command="command"
|
||||
/>
|
||||
<HelpButton />
|
||||
</Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
<ExecuteButton />
|
||||
<ColorPickerButton />
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<DeleteButton />
|
||||
<RefreshSelectionButton />
|
||||
<ExtensionCommandButton
|
||||
v-for="command in extensionToolboxCommands"
|
||||
:key="command.id"
|
||||
:command="command"
|
||||
/>
|
||||
<HelpButton />
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
@@ -49,19 +41,23 @@ import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewer
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
|
||||
import { useRetriggerableAnimation } from '@/composables/element/useRetriggerableAnimation'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const extensionService = useExtensionService()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
|
||||
const toolboxRef = ref<HTMLElement | undefined>()
|
||||
const { visible } = useSelectionToolboxPosition(toolboxRef)
|
||||
const selectionOverlayState = inject(SelectionOverlayInjectionKey)
|
||||
const { shouldAnimate } = useRetriggerableAnimation(
|
||||
selectionOverlayState?.updateCount,
|
||||
{ animateOnMount: true }
|
||||
)
|
||||
|
||||
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
const commandIds = new Set<string>(
|
||||
@@ -83,29 +79,21 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
<style scoped>
|
||||
.selection-toolbox {
|
||||
transform: translateX(-50%) translateY(-120%);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Slide up animation using CSS animation */
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
from {
|
||||
transform: translateX(-50%) translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) translateY(-125%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
to {
|
||||
transform: translateX(-50%) translateY(-120%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-up-enter-active {
|
||||
animation: slideUp 125ms ease-out;
|
||||
}
|
||||
|
||||
.slide-up-leave-active {
|
||||
animation: slideUp 25ms ease-out reverse;
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="w-[250px] absolute flex justify-center right-2 md:right-11 z-[1300] bottom-[66px] !bg-inherit !border-0"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark-theme:bg-[#2b2b2b] border border-gray-200 dark-theme:border-gray-700 rounded-lg shadow-lg p-4 w-4/5"
|
||||
:style="filteredMinimapStyles"
|
||||
@click.stop
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
|
||||
},
|
||||
label: {
|
||||
class: 'flex flex-col items-start w-full'
|
||||
}
|
||||
}"
|
||||
@mousedown="startRepeat('Comfy.Canvas.ZoomIn')"
|
||||
@mouseup="stopRepeat"
|
||||
@mouseleave="stopRepeat"
|
||||
>
|
||||
<template #default>
|
||||
<span class="text-sm font-medium block">{{
|
||||
$t('graphCanvasMenu.zoomIn')
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-500 block">{{
|
||||
zoomInCommandText
|
||||
}}</span>
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
|
||||
},
|
||||
label: {
|
||||
class: 'flex flex-col items-start w-full'
|
||||
}
|
||||
}"
|
||||
@mousedown="startRepeat('Comfy.Canvas.ZoomOut')"
|
||||
@mouseup="stopRepeat"
|
||||
@mouseleave="stopRepeat"
|
||||
>
|
||||
<template #default>
|
||||
<span class="text-sm font-medium block">{{
|
||||
$t('graphCanvasMenu.zoomOut')
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-500 block">{{
|
||||
zoomOutCommandText
|
||||
}}</span>
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
|
||||
},
|
||||
label: {
|
||||
class: 'flex flex-col items-start w-full'
|
||||
}
|
||||
}"
|
||||
@click="executeCommand('Comfy.Canvas.FitView')"
|
||||
>
|
||||
<template #default>
|
||||
<span class="text-sm font-medium block">{{
|
||||
$t('zoomControls.zoomToFit')
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-500 block">{{
|
||||
zoomToFitCommandText
|
||||
}}</span>
|
||||
</template>
|
||||
</Button>
|
||||
<hr class="border-[#E1DED5] mb-1 dark-theme:border-[#2E3037]" />
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
data-testid="toggle-minimap-button"
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
|
||||
},
|
||||
label: {
|
||||
class: 'flex flex-col items-start w-full'
|
||||
}
|
||||
}"
|
||||
@click="executeCommand('Comfy.Canvas.ToggleMinimap')"
|
||||
>
|
||||
<template #default>
|
||||
<span class="text-sm font-medium block">{{
|
||||
minimapToggleText
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-500 block">{{
|
||||
showMinimapCommandText
|
||||
}}</span>
|
||||
</template>
|
||||
</Button>
|
||||
<hr class="border-[#E1DED5] mt-1 dark-theme:border-[#2E3037]" />
|
||||
<div
|
||||
ref="zoomInputContainer"
|
||||
class="flex items-center px-2 bg-[#E7E6E6] focus-within:bg-[#F3F3F3] dark-theme:bg-[#8282821A] rounded p-2 zoomInputContainer"
|
||||
>
|
||||
<InputNumber
|
||||
ref="zoomInput"
|
||||
:default-value="canvasStore.appScalePercentage"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:show-buttons="false"
|
||||
:use-grouping="false"
|
||||
:unstyled="true"
|
||||
input-class="flex-1 bg-transparent border-none outline-none text-sm shadow-none my-0 "
|
||||
fluid
|
||||
@input="applyZoom"
|
||||
@keyup.enter="applyZoom"
|
||||
/>
|
||||
<span class="text-sm text-gray-500 -ml-4">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button, InputNumber, InputNumberInputEvent } from 'primevue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const minimap = useMinimap()
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { formatKeySequence } = useCommandStore()
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const interval = ref<number | null>(null)
|
||||
|
||||
// Computed properties for reactive states
|
||||
const minimapToggleText = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.Visible')
|
||||
? t('zoomControls.hideMinimap')
|
||||
: t('zoomControls.showMinimap')
|
||||
)
|
||||
|
||||
const applyZoom = (val: InputNumberInputEvent) => {
|
||||
const inputValue = val.value as number
|
||||
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
|
||||
return
|
||||
}
|
||||
canvasStore.setAppZoomFromPercentage(inputValue)
|
||||
}
|
||||
|
||||
const executeCommand = (command: string) => {
|
||||
void commandStore.execute(command)
|
||||
}
|
||||
|
||||
const startRepeat = (command: string) => {
|
||||
if (interval.value) return
|
||||
const cmd = () => commandStore.execute(command)
|
||||
void cmd()
|
||||
interval.value = window.setInterval(cmd, 100)
|
||||
}
|
||||
|
||||
const stopRepeat = () => {
|
||||
if (interval.value) {
|
||||
clearInterval(interval.value)
|
||||
interval.value = null
|
||||
}
|
||||
}
|
||||
const filteredMinimapStyles = computed(() => {
|
||||
return {
|
||||
...minimap.containerStyles.value,
|
||||
height: undefined,
|
||||
width: undefined
|
||||
}
|
||||
})
|
||||
const zoomInCommandText = computed(() =>
|
||||
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))
|
||||
)
|
||||
const zoomOutCommandText = computed(() =>
|
||||
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))
|
||||
)
|
||||
const zoomToFitCommandText = computed(() =>
|
||||
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
|
||||
)
|
||||
const showMinimapCommandText = computed(() =>
|
||||
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ToggleMinimap'))
|
||||
)
|
||||
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
|
||||
const zoomInputContainer = ref<HTMLDivElement | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await nextTick()
|
||||
const input = zoomInputContainer.value?.querySelector(
|
||||
'input'
|
||||
) as HTMLInputElement
|
||||
input?.focus()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style>
|
||||
.zoomInputContainer:focus-within {
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
}
|
||||
|
||||
.dark-theme .zoomInputContainer:focus-within {
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
}
|
||||
</style>
|
||||
@@ -136,6 +136,8 @@ const closePopup = async () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
// Learn more handled by anchor href
|
||||
|
||||
// const handleCTA = async () => {
|
||||
// window.open('https://docs.comfy.org/installation/update_comfyui', '_blank')
|
||||
// await closePopup()
|
||||
@@ -159,10 +161,8 @@ defineExpose({
|
||||
<style scoped>
|
||||
/* Popup container - positioning handled by parent */
|
||||
.whats-new-popup-container {
|
||||
--whats-new-popup-bottom: 1rem;
|
||||
|
||||
position: absolute;
|
||||
bottom: var(--whats-new-popup-bottom);
|
||||
bottom: 1rem;
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -171,8 +171,8 @@ defineExpose({
|
||||
.help-center-arrow {
|
||||
position: absolute;
|
||||
bottom: calc(
|
||||
var(--sidebar-width) * 2 + var(--sidebar-width) / 2
|
||||
); /* Position to center of help center icon (2 icons below + half icon height for center) */
|
||||
var(--sidebar-width, 4rem) + 0.25rem
|
||||
); /* Position toward center of help center icon */
|
||||
transform: none;
|
||||
z-index: 999;
|
||||
pointer-events: none;
|
||||
@@ -185,10 +185,7 @@ defineExpose({
|
||||
|
||||
.whats-new-popup-container.sidebar-left.small-sidebar .help-center-arrow {
|
||||
left: -14px; /* Overlap with popup outline */
|
||||
bottom: calc(
|
||||
var(--sidebar-width) * 2 + var(--sidebar-icon-size) / 2 -
|
||||
var(--whats-new-popup-bottom)
|
||||
); /* Position to center of help center icon (2 icons below + half icon height for center - whats new popup bottom position ) */
|
||||
bottom: calc(2.5rem + 0.25rem); /* Adjust for small sidebar */
|
||||
}
|
||||
|
||||
/* Sidebar positioning classes applied by parent */
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed } from 'vue'
|
||||
import { computed, defineModel } from 'vue'
|
||||
|
||||
const { placeHolder, hasBorder = false } = defineProps<{
|
||||
placeHolder?: string
|
||||
|
||||
@@ -61,10 +61,9 @@ let listenerController: AbortController | null = null
|
||||
let disconnectOnReset = false
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const searchBoxStore = useSearchBoxStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
|
||||
const { visible, newSearchBoxEnabled } = storeToRefs(searchBoxStore)
|
||||
const { visible } = storeToRefs(useSearchBoxStore())
|
||||
const dismissable = ref(true)
|
||||
const getNewNodeLocation = (): Point => {
|
||||
return triggerEvent
|
||||
@@ -108,9 +107,12 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
window.requestAnimationFrame(closeDialog)
|
||||
}
|
||||
|
||||
const showSearchBox = (e: CanvasPointerEvent | null) => {
|
||||
const newSearchBoxEnabled = computed(
|
||||
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
const showSearchBox = (e: CanvasPointerEvent) => {
|
||||
if (newSearchBoxEnabled.value) {
|
||||
if (e?.pointerType === 'touch') {
|
||||
if (e.pointerType === 'touch') {
|
||||
setTimeout(() => {
|
||||
showNewSearchBox(e)
|
||||
}, 128)
|
||||
@@ -126,7 +128,7 @@ const getFirstLink = () =>
|
||||
canvasStore.getCanvas().linkConnector.renderLinks.at(0)
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const showNewSearchBox = (e: CanvasPointerEvent | null) => {
|
||||
const showNewSearchBox = (e: CanvasPointerEvent) => {
|
||||
const firstLink = getFirstLink()
|
||||
if (firstLink) {
|
||||
const filter =
|
||||
@@ -302,7 +304,6 @@ watch(visible, () => {
|
||||
})
|
||||
|
||||
useEventListener(document, 'litegraph:canvas', canvasEventHandler)
|
||||
defineExpose({ showSearchBox })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -76,22 +76,6 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Global CSS variables for sidebar
|
||||
* These variables need to be global (not scoped) because they are used by
|
||||
* teleported components like WhatsNewPopup that render outside the sidebar
|
||||
* but need to reference sidebar dimensions for proper positioning.
|
||||
*/
|
||||
:root {
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.small-sidebar) {
|
||||
--sidebar-width: 2.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.side-tool-bar-container {
|
||||
display: flex;
|
||||
@@ -104,6 +88,9 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
color: var(--fg-color);
|
||||
box-shadow: var(--bar-shadow);
|
||||
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
|
||||
@@ -109,7 +109,7 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
}
|
||||
|
||||
.side-bar-button-label {
|
||||
@apply text-[10px] text-center;
|
||||
@apply text-[10px] text-center whitespace-nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
:tooltip="tooltipText"
|
||||
:tooltip="
|
||||
$t('shortcuts.keyboardShortcuts') +
|
||||
' (' +
|
||||
formatKeySequence(command.keybinding!.combo.getKeySequences()) +
|
||||
')'
|
||||
"
|
||||
:selected="isShortcutsPanelVisible"
|
||||
@click="toggleShortcutsPanel"
|
||||
>
|
||||
@@ -12,28 +17,28 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
const commandStore = useCommandStore()
|
||||
const command = commandStore.getCommand('Workspace.ToggleBottomPanel.Shortcuts')
|
||||
const { formatKeySequence } = commandStore
|
||||
const command = useCommandStore().getCommand(
|
||||
'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
)
|
||||
|
||||
const isShortcutsPanelVisible = computed(
|
||||
() => bottomPanelStore.activePanel === 'shortcuts'
|
||||
)
|
||||
|
||||
const tooltipText = computed(
|
||||
() => `${t('shortcuts.keyboardShortcuts')} (${formatKeySequence(command)})`
|
||||
)
|
||||
|
||||
const toggleShortcutsPanel = () => {
|
||||
bottomPanelStore.togglePanel('shortcuts')
|
||||
}
|
||||
|
||||
const formatKeySequence = (sequences: string[]): string => {
|
||||
return sequences
|
||||
.map((seq) => seq.replace(/Control/g, 'Ctrl').replace(/Shift/g, 'Shift'))
|
||||
.join(' + ')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
icon="icon-[comfy--template]"
|
||||
:icon="TemplateIcon"
|
||||
:tooltip="$t('sideToolbar.templates')"
|
||||
:label="$t('sideToolbar.labels.templates')"
|
||||
:is-small="isSmall"
|
||||
@@ -10,13 +10,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
// Import the custom template icon
|
||||
const TemplateIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/template'))
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
|
||||
@@ -94,16 +94,18 @@ describe('HoverDissolveThumbnail', () => {
|
||||
|
||||
// Check base image
|
||||
const baseImageClass = lazyImages[0].props('imageClass')
|
||||
const baseClassList = Array.isArray(baseImageClass)
|
||||
? baseImageClass
|
||||
: baseImageClass.split(' ')
|
||||
expect(baseClassList).toContain('size-full')
|
||||
const baseClassString = Array.isArray(baseImageClass)
|
||||
? baseImageClass.join(' ')
|
||||
: baseImageClass
|
||||
expect(baseClassString).toContain('absolute')
|
||||
expect(baseClassString).toContain('inset-0')
|
||||
|
||||
// Check overlay image
|
||||
const overlayImageClass = lazyImages[1].props('imageClass')
|
||||
const overlayClassList = Array.isArray(overlayImageClass)
|
||||
? overlayImageClass
|
||||
: overlayImageClass.split(' ')
|
||||
expect(overlayClassList).toContain('size-full')
|
||||
const overlayClassString = Array.isArray(overlayImageClass)
|
||||
? overlayImageClass.join(' ')
|
||||
: overlayImageClass
|
||||
expect(overlayClassString).toContain('absolute')
|
||||
expect(overlayClassString).toContain('inset-0')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
<template>
|
||||
<BaseThumbnail :is-hovered="isHovered">
|
||||
<div class="relative w-full h-full">
|
||||
<div class="absolute inset-0">
|
||||
<LazyImage
|
||||
:src="baseImageSrc"
|
||||
:alt="alt"
|
||||
:image-class="baseImageClass"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute inset-0 z-10">
|
||||
<LazyImage
|
||||
:src="overlayImageSrc"
|
||||
:alt="alt"
|
||||
:image-class="overlayImageClass"
|
||||
/>
|
||||
</div>
|
||||
<LazyImage :src="baseImageSrc" :alt="alt" :image-class="baseImageClass" />
|
||||
<LazyImage
|
||||
:src="overlayImageSrc"
|
||||
:alt="alt"
|
||||
:image-class="overlayImageClass"
|
||||
/>
|
||||
</div>
|
||||
</BaseThumbnail>
|
||||
</template>
|
||||
@@ -40,15 +32,14 @@ const isVideoType =
|
||||
false
|
||||
|
||||
const baseImageClass = computed(() => {
|
||||
const sizeClasses = isVideoType
|
||||
? 'size-full object-cover'
|
||||
: 'size-full object-contain'
|
||||
return sizeClasses
|
||||
return `absolute inset-0 ${isVideoType ? 'w-full h-full object-cover' : 'max-w-full max-h-64 object-contain'}`
|
||||
})
|
||||
|
||||
const overlayImageClass = computed(() => {
|
||||
const baseClasses = 'size-full transition-opacity duration-300'
|
||||
const sizeClasses = isVideoType ? 'object-cover' : 'object-contain'
|
||||
const baseClasses = 'absolute inset-0 transition-opacity duration-300'
|
||||
const sizeClasses = isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
const opacityClasses = isHovered ? 'opacity-100' : 'opacity-0'
|
||||
return `${baseClasses} ${sizeClasses} ${opacityClasses}`
|
||||
})
|
||||
|
||||
@@ -187,7 +187,7 @@ const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
key: 'browse-templates',
|
||||
label: t('menuLabels.Browse Templates'),
|
||||
icon: 'icon-[comfy--template]',
|
||||
icon: 'pi pi-folder-open',
|
||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('g.moreWorkflows'), showDelay: 300 }"
|
||||
class="rounded-none"
|
||||
icon="pi pi-ellipsis-h"
|
||||
text
|
||||
severity="secondary"
|
||||
:aria-label="$t('g.moreWorkflows')"
|
||||
@click="menu?.toggle($event)"
|
||||
/>
|
||||
<Menu
|
||||
ref="menu"
|
||||
:model="menuItems"
|
||||
:popup="true"
|
||||
class="max-h-[40vh] overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Menu from 'primevue/menu'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
|
||||
const props = defineProps<{
|
||||
workflows: ComfyWorkflow[]
|
||||
activeWorkflow: ComfyWorkflow | null
|
||||
}>()
|
||||
|
||||
const menu = ref<InstanceType<typeof Menu> | null>(null)
|
||||
const workflowService = useWorkflowService()
|
||||
|
||||
const menuItems = computed(() =>
|
||||
props.workflows.map((workflow: ComfyWorkflow) => ({
|
||||
label: workflow.filename,
|
||||
icon:
|
||||
props.activeWorkflow?.key === workflow.key ? 'pi pi-check' : undefined,
|
||||
command: () => {
|
||||
void workflowService.openWorkflow(workflow)
|
||||
}
|
||||
}))
|
||||
)
|
||||
</script>
|
||||
@@ -16,7 +16,7 @@
|
||||
ref="scrollPanelRef"
|
||||
class="overflow-hidden no-drag"
|
||||
:pt:content="{
|
||||
class: 'p-0 w-full flex',
|
||||
class: 'p-0 w-full',
|
||||
onwheel: handleWheel
|
||||
}"
|
||||
pt:bar-x="h-1"
|
||||
@@ -48,11 +48,6 @@
|
||||
:disabled="!rightArrowEnabled"
|
||||
@mousedown="whileMouseDown($event, () => scroll(1))"
|
||||
/>
|
||||
<WorkflowOverflowMenu
|
||||
v-if="showOverflowArrows"
|
||||
:workflows="workflowStore.openWorkflows"
|
||||
:active-workflow="workflowStore.activeWorkflow"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
|
||||
class="new-blank-workflow-button flex-shrink-0 no-drag rounded-none"
|
||||
@@ -90,8 +85,6 @@ import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
|
||||
import WorkflowOverflowMenu from './WorkflowOverflowMenu.vue'
|
||||
|
||||
interface WorkflowOption {
|
||||
value: string
|
||||
workflow: ComfyWorkflow
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
/**
|
||||
* Manages the position of the selection toolbox independently.
|
||||
* Uses CSS custom properties for performant transform updates.
|
||||
*/
|
||||
export function useSelectionToolboxPosition(
|
||||
toolboxRef: Ref<HTMLElement | undefined>
|
||||
) {
|
||||
const canvasStore = useCanvasStore()
|
||||
const lgCanvas = canvasStore.getCanvas()
|
||||
const { getSelectableItems } = useSelectedLiteGraphItems()
|
||||
|
||||
// World position of selection center
|
||||
const worldPosition = ref({ x: 0, y: 0 })
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
/**
|
||||
* Update position based on selection
|
||||
*/
|
||||
const updateSelectionBounds = () => {
|
||||
const selectableItems = getSelectableItems()
|
||||
|
||||
if (!selectableItems.size) {
|
||||
visible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
const bounds = createBounds(selectableItems)
|
||||
|
||||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
const [xBase, y, width] = bounds
|
||||
|
||||
worldPosition.value = {
|
||||
x: xBase + width / 2,
|
||||
y: y
|
||||
}
|
||||
|
||||
updateTransform()
|
||||
}
|
||||
|
||||
const updateTransform = () => {
|
||||
if (!visible.value) return
|
||||
|
||||
const { scale, offset } = lgCanvas.ds
|
||||
const canvasRect = lgCanvas.canvas.getBoundingClientRect()
|
||||
|
||||
const screenX =
|
||||
(worldPosition.value.x + offset[0]) * scale + canvasRect.left
|
||||
const screenY = (worldPosition.value.y + offset[1]) * scale + canvasRect.top
|
||||
|
||||
// Update CSS custom properties directly for best performance
|
||||
if (toolboxRef.value) {
|
||||
toolboxRef.value.style.setProperty('--tb-x', `${screenX}px`)
|
||||
toolboxRef.value.style.setProperty('--tb-y', `${screenY}px`)
|
||||
}
|
||||
}
|
||||
|
||||
// Sync with canvas transform
|
||||
const { startSync, stopSync } = useCanvasTransformSync(updateTransform, {
|
||||
autoStart: false
|
||||
})
|
||||
|
||||
// Watch for selection changes
|
||||
watch(
|
||||
() => canvasStore.getCanvas().state.selectionChanged,
|
||||
(changed) => {
|
||||
if (changed) {
|
||||
updateSelectionBounds()
|
||||
canvasStore.getCanvas().state.selectionChanged = false
|
||||
|
||||
// Start transform sync if we have selection
|
||||
if (visible.value) {
|
||||
startSync()
|
||||
} else {
|
||||
stopSync()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Watch for dragging state
|
||||
watch(
|
||||
() => canvasStore.canvas?.state?.draggingItems,
|
||||
(dragging) => {
|
||||
if (dragging) {
|
||||
// Hide during node dragging
|
||||
visible.value = false
|
||||
} else {
|
||||
// Update after dragging ends
|
||||
requestAnimationFrame(() => {
|
||||
updateSelectionBounds()
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
visible
|
||||
}
|
||||
}
|
||||
80
src/composables/element/useRetriggerableAnimation.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import type { Ref, WatchSource } from 'vue'
|
||||
|
||||
/**
|
||||
* A composable that manages retriggerable CSS animations.
|
||||
* Provides a boolean ref that can be toggled to restart CSS animations.
|
||||
*
|
||||
* @param trigger - Optional reactive source that triggers the animation when it changes
|
||||
* @param options - Configuration options
|
||||
* @returns An object containing the animation state ref
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <template>
|
||||
* <div :class="{ 'animate-slide-up': shouldAnimate }">
|
||||
* Content
|
||||
* </div>
|
||||
* </template>
|
||||
*
|
||||
* <script setup>
|
||||
* const { shouldAnimate } = useRetriggerableAnimation(someReactiveTrigger)
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useRetriggerableAnimation<T = any>(
|
||||
trigger?: WatchSource<T> | Ref<T>,
|
||||
options: {
|
||||
animateOnMount?: boolean
|
||||
animationDelay?: number
|
||||
} = {}
|
||||
) {
|
||||
const { animateOnMount = true, animationDelay = 0 } = options
|
||||
|
||||
const shouldAnimate = ref(false)
|
||||
|
||||
/**
|
||||
* Retriggers the animation by removing and re-adding the animation class
|
||||
*/
|
||||
const retriggerAnimation = () => {
|
||||
// Remove animation class
|
||||
shouldAnimate.value = false
|
||||
// Force browser reflow to ensure the class removal is processed
|
||||
void document.body.offsetHeight
|
||||
// Re-add animation class in the next frame
|
||||
requestAnimationFrame(() => {
|
||||
if (animationDelay > 0) {
|
||||
setTimeout(() => {
|
||||
shouldAnimate.value = true
|
||||
}, animationDelay)
|
||||
} else {
|
||||
shouldAnimate.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Trigger animation on mount if requested
|
||||
if (animateOnMount) {
|
||||
onMounted(() => {
|
||||
if (animationDelay > 0) {
|
||||
setTimeout(() => {
|
||||
shouldAnimate.value = true
|
||||
}, animationDelay)
|
||||
} else {
|
||||
shouldAnimate.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Watch for trigger changes to retrigger animation
|
||||
if (trigger) {
|
||||
watch(trigger, () => {
|
||||
retriggerAnimation()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
shouldAnimate,
|
||||
retriggerAnimation
|
||||
}
|
||||
}
|
||||
@@ -1332,9 +1332,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
return 'Token-based'
|
||||
}
|
||||
},
|
||||
GeminiImageNode: {
|
||||
displayPrice: '$0.03 per 1K tokens'
|
||||
},
|
||||
// OpenAI nodes
|
||||
OpenAIChatNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
@@ -1386,38 +1383,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
},
|
||||
ViduStartEndToVideoNode: {
|
||||
displayPrice: '$0.4/Run'
|
||||
},
|
||||
ByteDanceImageNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model'
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget) return 'Token-based'
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
|
||||
if (model.includes('seedream-3-0-t2i')) {
|
||||
return '$0.03/Run'
|
||||
}
|
||||
return 'Token-based'
|
||||
}
|
||||
},
|
||||
ByteDanceImageEditNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model'
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget) return 'Token-based'
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
|
||||
if (model.includes('seededit-3-0-i2i')) {
|
||||
return '$0.03/Run'
|
||||
}
|
||||
return 'Token-based'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1505,10 +1470,7 @@ export const useNodePricing = () => {
|
||||
// Google/Gemini nodes
|
||||
GeminiNode: ['model'],
|
||||
// OpenAI nodes
|
||||
OpenAIChatNode: ['model'],
|
||||
// ByteDance
|
||||
ByteDanceImageNode: ['model'],
|
||||
ByteDanceImageEditNode: ['model']
|
||||
OpenAIChatNode: ['model']
|
||||
}
|
||||
return widgetMap[nodeType] || []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const AiModelIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/ai-model'))
|
||||
)
|
||||
|
||||
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'model-library',
|
||||
icon: 'icon-[comfy--ai-model]',
|
||||
icon: AiModelIcon,
|
||||
title: 'sideToolbar.modelLibrary',
|
||||
tooltip: 'sideToolbar.modelLibrary',
|
||||
label: 'sideToolbar.labels.models',
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
const NodeIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/node'))
|
||||
)
|
||||
|
||||
export const useNodeLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'node-library',
|
||||
icon: 'icon-[comfy--node]',
|
||||
icon: NodeIcon,
|
||||
title: 'sideToolbar.nodeLibrary',
|
||||
tooltip: 'sideToolbar.nodeLibrary',
|
||||
label: 'sideToolbar.labels.nodes',
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
const WorkflowIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/workflow'))
|
||||
)
|
||||
|
||||
export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
return {
|
||||
id: 'workflows',
|
||||
icon: 'icon-[comfy--workflow]',
|
||||
icon: WorkflowIcon,
|
||||
iconBadge: () => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||
|
||||
@@ -298,26 +298,8 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Comfy.Canvas.ToggleLock',
|
||||
icon: 'pi pi-lock',
|
||||
label: 'Canvas Toggle Lock',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
app.canvas.state.readOnly = !app.canvas.state.readOnly
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.Lock',
|
||||
icon: 'pi pi-lock',
|
||||
label: 'Lock Canvas',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
app.canvas.state.readOnly = true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.Unlock',
|
||||
icon: 'pi pi-lock-open',
|
||||
label: 'Unlock Canvas',
|
||||
function: () => {
|
||||
app.canvas.state.readOnly = false
|
||||
app.canvas['read_only'] = !app.canvas['read_only']
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -446,9 +428,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
)
|
||||
group.resizeTo(canvas.selectedItems, padding)
|
||||
canvas.graph?.add(group)
|
||||
|
||||
group.recomputeInsideNodes()
|
||||
|
||||
useTitleEditorStore().titleEditorTarget = group
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export function useZoomControls() {
|
||||
const isModalVisible = ref(false)
|
||||
|
||||
const showModal = () => {
|
||||
isModalVisible.value = true
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
isModalVisible.value = false
|
||||
}
|
||||
|
||||
const toggleModal = () => {
|
||||
isModalVisible.value = !isModalVisible.value
|
||||
}
|
||||
|
||||
const hasActivePopup = computed(() => isModalVisible.value)
|
||||
|
||||
return {
|
||||
isModalVisible,
|
||||
showModal,
|
||||
hideModal,
|
||||
toggleModal,
|
||||
hasActivePopup
|
||||
}
|
||||
}
|
||||
@@ -191,18 +191,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.Unlock'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'h'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.Lock'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Escape'
|
||||
|
||||
@@ -13,7 +13,6 @@ export const CORE_MENU_COMMANDS = [
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
|
||||
[
|
||||
['Help'],
|
||||
[
|
||||
|
||||
@@ -791,11 +791,11 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'combo',
|
||||
options: [
|
||||
{ value: 'standard', text: 'Standard (New)' },
|
||||
{ value: 'legacy', text: 'Drag Navigation' }
|
||||
{ value: 'legacy', text: 'Left-Click Pan (Legacy)' }
|
||||
],
|
||||
versionAdded: '1.25.0',
|
||||
defaultsByInstallVersion: {
|
||||
'1.25.0': 'legacy'
|
||||
'1.25.0': 'standard'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -42,8 +42,6 @@ app.registerExtension({
|
||||
this.graph.add(group)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.change()
|
||||
|
||||
group.recomputeInsideNodes()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import './previewAny'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './saveMesh'
|
||||
import './selectionBorder'
|
||||
import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
import './uploadAudio'
|
||||
|
||||