mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Compare commits
2 Commits
sno-playwr
...
sno-fix-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae586533f | ||
|
|
18c2163625 |
7
.github/workflows/i18n.yaml
vendored
7
.github/workflows/i18n.yaml
vendored
@@ -25,13 +25,6 @@ jobs:
|
||||
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
i18n-tools-cache-${{ runner.os }}-
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
2
.github/workflows/lint-and-format.yaml
vendored
2
.github/workflows/lint-and-format.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
|
||||
163
.github/workflows/pr-playwright-comment.yaml
vendored
Normal file
163
.github/workflows/pr-playwright-comment.yaml
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
name: PR Playwright Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Tests CI']
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
|
||||
|
||||
jobs:
|
||||
comment-summary:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No open PR found for this branch');
|
||||
return null;
|
||||
}
|
||||
|
||||
return pullRequests[0].number;
|
||||
|
||||
- name: Log when no PR found
|
||||
if: steps.pr.outputs.result == 'null'
|
||||
run: |
|
||||
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
|
||||
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
|
||||
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
|
||||
echo "Event: ${{ github.event.workflow_run.event }}"
|
||||
|
||||
- name: Generate comment body for start
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
|
||||
id: comment-body-start
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Tests are starting...** " >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 🚀 Running Tests" >> comment.md
|
||||
echo "- 🧪 **chromium**: Running tests..." >> comment.md
|
||||
echo "- 🧪 **chromium-0.5x**: Running tests..." >> comment.md
|
||||
echo "- 🧪 **chromium-2x**: Running tests..." >> comment.md
|
||||
echo "- 🧪 **mobile-chrome**: Running tests..." >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
echo "⏱️ Please wait while tests are running across all browsers..." >> comment.md
|
||||
|
||||
- name: Download all deployment info
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: deployment-info-*
|
||||
merge-multiple: true
|
||||
path: deployment-info
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate comment body for completion
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
id: comment-body-completed
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
|
||||
# Check if all tests passed
|
||||
ALL_PASSED=true
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
if [ "$exit_code" != "0" ]; then
|
||||
ALL_PASSED=false
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "✅ **All tests passed across all browsers!**" >> comment.md
|
||||
else
|
||||
echo "❌ **Some tests failed!**" >> comment.md
|
||||
fi
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 📊 Test Reports by Browser" >> comment.md
|
||||
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
url=$(echo "$info" | cut -d'|' -f3)
|
||||
|
||||
# Validate URLs before using them in comments
|
||||
sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
|
||||
if [ "$sanitized_url" = "INVALID_URL" ]; then
|
||||
echo "Invalid deployment URL detected: $url"
|
||||
url="#" # Use safe fallback
|
||||
fi
|
||||
|
||||
if [ "$exit_code" = "0" ]; then
|
||||
status="✅"
|
||||
else
|
||||
status="❌"
|
||||
fi
|
||||
|
||||
echo "- $status **$browser**: [View Report]($url)" >> comment.md
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "🎉 Your tests are passing across all browsers!" >> comment.md
|
||||
else
|
||||
echo "⚠️ Please check the test reports for details on failures." >> comment.md
|
||||
fi
|
||||
|
||||
- name: Comment PR - Tests Started
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
|
||||
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr.outputs.result }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
|
||||
- name: Comment PR - Tests Complete
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr.outputs.result }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
241
.github/workflows/pr-playwright-deploy.yaml
vendored
241
.github/workflows/pr-playwright-deploy.yaml
vendored
@@ -1,17 +1,14 @@
|
||||
name: PR Playwright Deploy and Comment
|
||||
name: PR Playwright Deploy
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tests CI"]
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
deploy-reports:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed'
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request'
|
||||
permissions:
|
||||
actions: read
|
||||
strategy:
|
||||
@@ -56,39 +53,14 @@ jobs:
|
||||
fi
|
||||
echo "branch=${{ fromJSON(steps.pr-info.outputs.result).sanitized_branch }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install pnpm
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Download playwright report shards
|
||||
- name: Download playwright report
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: playwright-report-${{ matrix.browser }}-shard-*
|
||||
path: playwright-shards
|
||||
|
||||
- name: Install Playwright
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
run: npm install @playwright/test
|
||||
|
||||
- name: Merge reports
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
run: |
|
||||
npx playwright merge-reports --reporter html ./playwright-shards
|
||||
mv playwright-report merged-report
|
||||
|
||||
- name: Rename merged report
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
run: mv merged-report playwright-report
|
||||
name: playwright-report-${{ matrix.browser }}
|
||||
path: playwright-report
|
||||
|
||||
- name: Install Wrangler
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
@@ -103,20 +75,14 @@ jobs:
|
||||
RETRY_COUNT=0
|
||||
MAX_RETRIES=3
|
||||
SUCCESS=false
|
||||
DEPLOYMENT_URL=""
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
|
||||
|
||||
OUTPUT=$(npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }} 2>&1)
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
if npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
|
||||
SUCCESS=true
|
||||
DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oE 'https://[^ ]+\.pages\.dev' | head -1)
|
||||
echo "Deployment successful on attempt $RETRY_COUNT"
|
||||
echo "URL: $DEPLOYMENT_URL"
|
||||
else
|
||||
echo "Deployment failed on attempt $RETRY_COUNT"
|
||||
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
|
||||
@@ -130,197 +96,6 @@ jobs:
|
||||
echo "All deployment attempts failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
- name: Save deployment info
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
run: |
|
||||
echo "${{ matrix.browser }}|${{ steps.cloudflare-deploy.outcome == 'success' && '0' || '1' }}|${{ steps.cloudflare-deploy.outputs.deployment_url || '#' }}" > deployment-info-${{ matrix.browser }}.txt
|
||||
|
||||
- name: Upload deployment info
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deployment-info-${{ matrix.browser }}
|
||||
path: deployment-info-${{ matrix.browser }}.txt
|
||||
retention-days: 1
|
||||
|
||||
comment-tests-starting:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'requested'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No open PR found for this branch');
|
||||
return null;
|
||||
}
|
||||
|
||||
return pullRequests[0].number;
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate comment body for start
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
id: comment-body-start
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Tests are starting...** " >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 🚀 Running Tests (5 parallel shards per browser)" >> comment.md
|
||||
echo "- 🧪 **chromium**: Running tests in 5 shards..." >> comment.md
|
||||
echo "- 🧪 **chromium-0.5x**: Running tests in 5 shards..." >> comment.md
|
||||
echo "- 🧪 **chromium-2x**: Running tests in 5 shards..." >> comment.md
|
||||
echo "- 🧪 **mobile-chrome**: Running tests in 5 shards..." >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
echo "⏱️ Tests are running in parallel across 20 jobs (4 browsers × 5 shards)..." >> comment.md
|
||||
|
||||
- name: Comment PR - Tests Started
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr.outputs.result }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
|
||||
comment-tests-completed:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy-reports
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed' && always()
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No open PR found for this branch');
|
||||
return null;
|
||||
}
|
||||
|
||||
return pullRequests[0].number;
|
||||
|
||||
- name: Download all deployment info
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: deployment-info-*
|
||||
merge-multiple: true
|
||||
path: deployment-info
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate comment body for completion
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
id: comment-body-completed
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
|
||||
# Check if all tests passed
|
||||
ALL_PASSED=true
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
if [ "$exit_code" != "0" ]; then
|
||||
ALL_PASSED=false
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "✅ **All tests passed across all browsers!**" >> comment.md
|
||||
else
|
||||
echo "❌ **Some tests failed!**" >> comment.md
|
||||
fi
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 📊 Test Reports by Browser" >> comment.md
|
||||
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
url=$(echo "$info" | cut -d'|' -f3)
|
||||
|
||||
# Validate URLs before using them in comments
|
||||
sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
|
||||
if [ "$sanitized_url" = "INVALID_URL" ]; then
|
||||
echo "Invalid deployment URL detected: $url"
|
||||
url="#" # Use safe fallback
|
||||
fi
|
||||
|
||||
if [ "$exit_code" = "0" ]; then
|
||||
status="✅"
|
||||
else
|
||||
status="❌"
|
||||
fi
|
||||
|
||||
echo "- $status **$browser**: [View Report]($url)" >> comment.md
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "🎉 Your tests are passing across all browsers!" >> comment.md
|
||||
else
|
||||
echo "⚠️ Please check the test reports for details on failures." >> comment.md
|
||||
fi
|
||||
|
||||
- name: Comment PR - Tests Complete
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr.outputs.result }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
24
.github/workflows/pr-storybook-comment.yaml
vendored
24
.github/workflows/pr-storybook-comment.yaml
vendored
@@ -8,10 +8,7 @@ on:
|
||||
jobs:
|
||||
comment-storybook:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||
&& github.event.workflow_run.event == 'pull_request'
|
||||
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
@@ -100,17 +97,7 @@ jobs:
|
||||
<!-- STORYBOOK_BUILD_STATUS -->
|
||||
## 🎨 Storybook Build Status
|
||||
|
||||
${{
|
||||
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|
||||
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|
||||
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|
||||
|| '❌'
|
||||
}} **${{
|
||||
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|
||||
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|
||||
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|
||||
|| 'Build failed!'
|
||||
}}**
|
||||
${{ fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅' || '❌' }} **${{ fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
|
||||
|
||||
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
|
||||
|
||||
@@ -118,9 +105,4 @@ jobs:
|
||||
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
|
||||
|
||||
---
|
||||
${{
|
||||
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|
||||
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'ℹ️ Chromatic was skipped for this PR.'
|
||||
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'ℹ️ The Chromatic run was cancelled.'
|
||||
|| '⚠️ Please check the workflow logs for error details.'
|
||||
}}
|
||||
${{ fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}
|
||||
|
||||
7
.github/workflows/test-browser-exp.yaml
vendored
7
.github/workflows/test-browser-exp.yaml
vendored
@@ -11,13 +11,6 @@ jobs:
|
||||
if: github.event.label.name == 'New Browser Test Expectations'
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
57
.github/workflows/test-ui.yaml
vendored
57
.github/workflows/test-ui.yaml
vendored
@@ -83,33 +83,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Chromium tests with 5 shards (optimized distribution)
|
||||
- browser: chromium
|
||||
shard: 1
|
||||
shard-total: 5
|
||||
- browser: chromium
|
||||
shard: 2
|
||||
shard-total: 5
|
||||
- browser: chromium
|
||||
shard: 3
|
||||
shard-total: 5
|
||||
- browser: chromium
|
||||
shard: 4
|
||||
shard-total: 5
|
||||
- browser: chromium
|
||||
shard: 5
|
||||
shard-total: 5
|
||||
# Other browser variants without sharding (faster tests)
|
||||
- browser: chromium-2x
|
||||
shard: 1
|
||||
shard-total: 1
|
||||
- browser: chromium-0.5x
|
||||
shard: 1
|
||||
shard-total: 1
|
||||
- browser: mobile-chrome
|
||||
shard: 1
|
||||
shard-total: 1
|
||||
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
|
||||
steps:
|
||||
- name: Wait for cache propagation
|
||||
run: sleep 10
|
||||
@@ -161,38 +135,15 @@ jobs:
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.browser }}${{ matrix.shard-total > 1 && format(', shard {0}/{1}', matrix.shard, matrix.shard-total) || '' }})
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
id: playwright
|
||||
run: |
|
||||
if [ "${{ matrix.shard-total }}" -gt 1 ]; then
|
||||
npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}/${{ matrix.shard-total }}
|
||||
else
|
||||
npx playwright test --project=${{ matrix.browser }}
|
||||
fi
|
||||
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always() # note: use always() to allow results to be upload/report even tests failed.
|
||||
with:
|
||||
name: playwright-report-${{ matrix.browser }}-shard-${{ matrix.shard }}
|
||||
name: playwright-report-${{ matrix.browser }}
|
||||
path: ComfyUI_frontend/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
merge-reports:
|
||||
needs: playwright-tests
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all workflow artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: playwright-report-*
|
||||
path: all-reports/
|
||||
|
||||
- name: Upload merged report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-merged
|
||||
path: all-reports/
|
||||
retention-days: 30
|
||||
|
||||
|
||||
2
.github/workflows/update-manager-types.yaml
vendored
2
.github/workflows/update-manager-types.yaml
vendored
@@ -121,4 +121,4 @@ jobs:
|
||||
labels: Manager
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
src/types/generatedManagerTypes.ts
|
||||
src/types/generatedManagerTypes.ts
|
||||
|
||||
38
CLAUDE.md
38
CLAUDE.md
@@ -82,44 +82,6 @@ When referencing Comfy-Org repos:
|
||||
2. Use GitHub API for branches/PRs/metadata
|
||||
3. Curl GitHub website if needed
|
||||
|
||||
## Settings and Feature Flags Quick Reference
|
||||
|
||||
### Settings Usage
|
||||
```typescript
|
||||
const settingStore = useSettingStore()
|
||||
const value = settingStore.get('Comfy.SomeSetting') // Get setting
|
||||
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
|
||||
```
|
||||
|
||||
### Dynamic Defaults
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.Setting',
|
||||
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Defaults
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.Feature',
|
||||
defaultValue: 'legacy',
|
||||
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
|
||||
}
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
```typescript
|
||||
if (api.serverSupportsFeature('feature_name')) { // Check capability
|
||||
// Use enhanced feature
|
||||
}
|
||||
const value = api.getServerFeature('config_name', defaultValue) // Get config
|
||||
```
|
||||
|
||||
**Documentation:**
|
||||
- Settings system: `docs/SETTINGS.md`
|
||||
- Feature flags system: `docs/FEATURE_FLAGS.md`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- NEVER use `any` type - use proper TypeScript types
|
||||
|
||||
@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
### Prerequisites & Technology Stack
|
||||
|
||||
- **Required Software**:
|
||||
- Node.js (v18 or later to build; v24 for vite dev server) and pnpm
|
||||
- Node.js (v16 or later; v24 strongly recommended) and pnpm
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
# Playwright Test Sharding Strategy
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the optimized sharding strategy for Playwright tests to achieve balanced execution times across parallel CI jobs.
|
||||
|
||||
## Problem
|
||||
|
||||
The original naive sharding approach (dividing tests equally by file count) resulted in imbalanced execution times:
|
||||
- Shard 5 (chromium): 9 minutes
|
||||
- Other shards: 2-6 minutes
|
||||
|
||||
This was due to `interaction.spec.ts` containing 61 tests with 81 screenshot comparisons, making it significantly heavier than other test files.
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Weighted Test Distribution
|
||||
|
||||
Tests are assigned weights based on:
|
||||
- Number of test cases
|
||||
- Screenshot comparisons (heavy operations)
|
||||
- Test complexity (DOM manipulation, async operations)
|
||||
- Historical execution time
|
||||
|
||||
### 2. Optimized Shard Configuration
|
||||
|
||||
The sharding configuration uses a greedy algorithm to distribute tests:
|
||||
1. Sort tests by weight (heaviest first)
|
||||
2. Assign each test to the shard with lowest total weight
|
||||
3. Result: ~4.5% imbalance vs. previous 80% imbalance
|
||||
|
||||
### 3. Project-Specific Sharding
|
||||
|
||||
- **chromium**: 5 shards with optimized distribution
|
||||
- **chromium-2x, chromium-0.5x**: No sharding (fast enough)
|
||||
- **mobile-chrome**: No sharding (fast enough)
|
||||
|
||||
## Implementation
|
||||
|
||||
### Generated Configuration
|
||||
|
||||
Run `pnpm test:browser:optimize-shards` to regenerate the shard configuration based on current test weights.
|
||||
|
||||
### Files
|
||||
|
||||
- `shardConfig.generated.ts`: Auto-generated shard assignments
|
||||
- `playwright-sharded.config.ts`: Playwright config using optimized shards
|
||||
- `scripts/optimizeSharding.js`: Script to analyze and optimize distribution
|
||||
|
||||
### CI Configuration
|
||||
|
||||
The GitHub workflow uses a matrix strategy with explicit shard configurations:
|
||||
|
||||
```yaml
|
||||
matrix:
|
||||
include:
|
||||
- browser: chromium
|
||||
shard: 1
|
||||
shard-total: 5
|
||||
# ... etc
|
||||
```
|
||||
|
||||
## Shard Distribution (Balanced)
|
||||
|
||||
| Shard | Weight | Key Tests |
|
||||
|-------|--------|-----------|
|
||||
| 1 | 225 | interaction.spec.ts (heavy screenshots) |
|
||||
| 2 | 220 | subgraph.spec.ts, workflows, primitives |
|
||||
| 3 | 225 | widget.spec.ts, nodeLibrary, templates |
|
||||
| 4 | 215 | nodeSearchBox, rightClickMenu, colorPalette |
|
||||
| 5 | 215 | dialog, groupNode, remoteWidgets |
|
||||
|
||||
## Monitoring
|
||||
|
||||
After deployment, monitor CI execution times to ensure shards remain balanced. If new heavy tests are added, re-run the optimization script.
|
||||
|
||||
## Maintenance
|
||||
|
||||
1. When adding new heavy tests, update `TEST_WEIGHTS` in `optimizeSharding.js`
|
||||
2. Run `pnpm test:browser:optimize-shards`
|
||||
3. Commit the updated `shardConfig.generated.ts`
|
||||
|
||||
## Expected Results
|
||||
|
||||
- All chromium shards complete in 3-4 minutes (vs. 2-9 minutes)
|
||||
- Total CI time reduced from 9 minutes to ~4 minutes
|
||||
- Better resource utilization in CI runners
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Custom test runner for optimized sharding
|
||||
* This script determines which tests to run based on shard configuration
|
||||
*/
|
||||
import { spawn } from 'child_process'
|
||||
|
||||
import { NO_SHARD_PROJECTS, getShardTests } from './shardConfig'
|
||||
|
||||
const projectName = process.env.PLAYWRIGHT_PROJECT || 'chromium'
|
||||
const shardInfo = process.env.PLAYWRIGHT_SHARD
|
||||
|
||||
// Parse shard information from environment variable (format: "current/total")
|
||||
let shardIndex = 1
|
||||
let totalShards = 1
|
||||
|
||||
if (shardInfo) {
|
||||
const [current, total] = shardInfo.split('/').map(Number)
|
||||
shardIndex = current
|
||||
totalShards = total
|
||||
}
|
||||
|
||||
// Check if this project should skip sharding
|
||||
if (NO_SHARD_PROJECTS.includes(projectName)) {
|
||||
// For projects that don't need sharding, only run on shard 1
|
||||
if (shardIndex > 1) {
|
||||
console.log(
|
||||
`Skipping shard ${shardIndex}/${totalShards} for project ${projectName} (no sharding needed)`
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
console.log(`Running all tests for project ${projectName} (no sharding)`)
|
||||
} else {
|
||||
console.log(
|
||||
`Running shard ${shardIndex}/${totalShards} for project ${projectName}`
|
||||
)
|
||||
}
|
||||
|
||||
// Get the test files for this shard
|
||||
const shardTests = getShardTests(shardIndex, totalShards, projectName)
|
||||
|
||||
// Build the Playwright command
|
||||
const args = ['playwright', 'test', `--project=${projectName}`]
|
||||
|
||||
if (shardTests && shardTests.length > 0) {
|
||||
// Add specific test files for this shard
|
||||
shardTests.forEach((testFile) => {
|
||||
args.push(`browser_tests/tests/${testFile}`)
|
||||
})
|
||||
} else if (shardTests === null) {
|
||||
// Run all tests (no custom sharding)
|
||||
// Don't add any test file filters
|
||||
} else {
|
||||
// Empty shard - no tests to run
|
||||
console.log(`No tests assigned to shard ${shardIndex}/${totalShards}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Add CI-specific options if running in CI
|
||||
if (process.env.CI) {
|
||||
args.push('--reporter=github')
|
||||
}
|
||||
|
||||
// Execute Playwright with the constructed arguments
|
||||
console.log(`Executing: npx ${args.join(' ')}`)
|
||||
const child = spawn('npx', args, {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
})
|
||||
|
||||
child.on('exit', (code) => {
|
||||
process.exit(code || 0)
|
||||
})
|
||||
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script to analyze test distribution and create optimized shard configurations
|
||||
* Run with: node browser_tests/scripts/optimizeSharding.js
|
||||
*/
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Test weights based on empirical data and test characteristics
|
||||
const TEST_WEIGHTS = {
|
||||
'interaction.spec.ts': 180, // Very heavy - 61 tests with 81 screenshots
|
||||
'subgraph.spec.ts': 60, // Heavy - 23 complex tests
|
||||
'widget.spec.ts': 50, // Medium-heavy - screenshots
|
||||
'nodeSearchBox.spec.ts': 45, // Medium-heavy - screenshots
|
||||
'dialog.spec.ts': 40,
|
||||
'groupNode.spec.ts': 40,
|
||||
'rightClickMenu.spec.ts': 35,
|
||||
'sidebar/workflows.spec.ts': 35,
|
||||
'sidebar/nodeLibrary.spec.ts': 35,
|
||||
'colorPalette.spec.ts': 30,
|
||||
'nodeDisplay.spec.ts': 25,
|
||||
'primitiveNode.spec.ts': 25,
|
||||
'templates.spec.ts': 25,
|
||||
'remoteWidgets.spec.ts': 25,
|
||||
'useSettingSearch.spec.ts': 25,
|
||||
'nodeHelp.spec.ts': 25,
|
||||
'extensionAPI.spec.ts': 20,
|
||||
'bottomPanelShortcuts.spec.ts': 20,
|
||||
'featureFlags.spec.ts': 20,
|
||||
'sidebar/queue.spec.ts': 20,
|
||||
'graphCanvasMenu.spec.ts': 20,
|
||||
'nodeBadge.spec.ts': 20,
|
||||
'noteNode.spec.ts': 15,
|
||||
'domWidget.spec.ts': 15,
|
||||
'selectionToolbox.spec.ts': 15,
|
||||
'execution.spec.ts': 15,
|
||||
'rerouteNode.spec.ts': 15,
|
||||
'copyPaste.spec.ts': 15,
|
||||
'loadWorkflowInMedia.spec.ts': 15,
|
||||
'menu.spec.ts': 15,
|
||||
// Light tests
|
||||
'backgroundImageUpload.spec.ts': 10,
|
||||
'browserTabTitle.spec.ts': 10,
|
||||
'changeTracker.spec.ts': 10,
|
||||
'chatHistory.spec.ts': 10,
|
||||
'commands.spec.ts': 10,
|
||||
'customIcons.spec.ts': 10,
|
||||
'graph.spec.ts': 10,
|
||||
'keybindings.spec.ts': 10,
|
||||
'litegraphEvent.spec.ts': 10,
|
||||
'minimap.spec.ts': 10,
|
||||
'releaseNotifications.spec.ts': 10,
|
||||
'subgraph-rename-dialog.spec.ts': 10,
|
||||
'userSelectView.spec.ts': 10,
|
||||
'versionMismatchWarnings.spec.ts': 10,
|
||||
'workflowTabThumbnail.spec.ts': 10,
|
||||
'actionbar.spec.ts': 10
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all test files from the browser_tests directory
|
||||
*/
|
||||
function getTestFiles() {
|
||||
const testsDir = path.join(__dirname, '..', 'tests')
|
||||
const files = []
|
||||
|
||||
function scanDir(dir, prefix = '') {
|
||||
const items = fs.readdirSync(dir)
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item)
|
||||
const relativePath = prefix ? `${prefix}/${item}` : item
|
||||
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
scanDir(fullPath, relativePath)
|
||||
} else if (item.endsWith('.spec.ts')) {
|
||||
files.push(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDir(testsDir)
|
||||
return files
|
||||
}
|
||||
|
||||
/**
|
||||
* Create balanced shards based on test weights
|
||||
*/
|
||||
function createBalancedShards(testFiles, numShards) {
|
||||
// Create test entries with weights
|
||||
const tests = testFiles.map((file) => ({
|
||||
file,
|
||||
weight: TEST_WEIGHTS[file] || 15 // Default weight for unknown tests
|
||||
}))
|
||||
|
||||
// Sort tests by weight (heaviest first)
|
||||
tests.sort((a, b) => b.weight - a.weight)
|
||||
|
||||
// Initialize shards
|
||||
const shards = Array.from({ length: numShards }, () => ({
|
||||
tests: [],
|
||||
totalWeight: 0
|
||||
}))
|
||||
|
||||
// Distribute tests using a greedy algorithm (assign to shard with least weight)
|
||||
for (const test of tests) {
|
||||
// Find shard with minimum weight
|
||||
let minShard = shards[0]
|
||||
for (const shard of shards) {
|
||||
if (shard.totalWeight < minShard.totalWeight) {
|
||||
minShard = shard
|
||||
}
|
||||
}
|
||||
|
||||
// Add test to the lightest shard
|
||||
minShard.tests.push(test.file)
|
||||
minShard.totalWeight += test.weight
|
||||
}
|
||||
|
||||
return shards
|
||||
}
|
||||
|
||||
/**
|
||||
* Print shard configuration
|
||||
*/
|
||||
function printShardConfig(shards) {
|
||||
console.log('\n=== Optimized Shard Configuration ===\n')
|
||||
|
||||
shards.forEach((shard, index) => {
|
||||
console.log(`Shard ${index + 1} (weight: ${shard.totalWeight})`)
|
||||
console.log(' Tests:')
|
||||
shard.tests.forEach((test) => {
|
||||
const weight = TEST_WEIGHTS[test] || 15
|
||||
console.log(` - ${test} (weight: ${weight})`)
|
||||
})
|
||||
console.log()
|
||||
})
|
||||
|
||||
// Print weight balance analysis
|
||||
const weights = shards.map((s) => s.totalWeight)
|
||||
const maxWeight = Math.max(...weights)
|
||||
const minWeight = Math.min(...weights)
|
||||
const avgWeight = weights.reduce((a, b) => a + b, 0) / weights.length
|
||||
|
||||
console.log('=== Balance Analysis ===')
|
||||
console.log(`Max weight: ${maxWeight}`)
|
||||
console.log(`Min weight: ${minWeight}`)
|
||||
console.log(`Avg weight: ${avgWeight.toFixed(1)}`)
|
||||
console.log(
|
||||
`Imbalance: ${(((maxWeight - minWeight) / avgWeight) * 100).toFixed(1)}%`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TypeScript configuration file
|
||||
*/
|
||||
function generateConfigFile(shards) {
|
||||
const config = `/**
|
||||
* Auto-generated shard configuration for balanced test distribution
|
||||
* Generated on: ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
export const OPTIMIZED_SHARDS = ${JSON.stringify(
|
||||
shards.map((s) => s.tests),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
|
||||
export function getShardTests(shardIndex: number): string[] {
|
||||
return OPTIMIZED_SHARDS[shardIndex - 1] || []
|
||||
}
|
||||
|
||||
export function getShardPattern(shardIndex: number): string[] {
|
||||
return getShardTests(shardIndex).map(test => \`**/\${test}\`)
|
||||
}
|
||||
`
|
||||
|
||||
const configPath = path.join(__dirname, '..', 'shardConfig.generated.ts')
|
||||
fs.writeFileSync(configPath, config)
|
||||
console.log(`\n✅ Generated configuration file: ${configPath}`)
|
||||
}
|
||||
|
||||
// Main execution
|
||||
function main() {
|
||||
const numShards = parseInt(process.argv[2]) || 5
|
||||
|
||||
console.log(`Analyzing test distribution for ${numShards} shards...`)
|
||||
|
||||
const testFiles = getTestFiles()
|
||||
console.log(`Found ${testFiles.length} test files`)
|
||||
|
||||
const shards = createBalancedShards(testFiles, numShards)
|
||||
printShardConfig(shards)
|
||||
generateConfigFile(shards)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Auto-generated shard configuration for balanced test distribution
|
||||
* Generated on: 2025-09-02T16:09:27.236Z
|
||||
*/
|
||||
|
||||
export const OPTIMIZED_SHARDS = [
|
||||
[
|
||||
'interaction.spec.ts',
|
||||
'selectionToolbox.spec.ts',
|
||||
'chatHistory.spec.ts',
|
||||
'litegraphEvent.spec.ts',
|
||||
'versionMismatchWarnings.spec.ts'
|
||||
],
|
||||
[
|
||||
'subgraph.spec.ts',
|
||||
'sidebar/workflows.spec.ts',
|
||||
'primitiveNode.spec.ts',
|
||||
'bottomPanelShortcuts.spec.ts',
|
||||
'nodeBadge.spec.ts',
|
||||
'execution.spec.ts',
|
||||
'rerouteNode.spec.ts',
|
||||
'changeTracker.spec.ts',
|
||||
'keybindings.spec.ts',
|
||||
'userSelectView.spec.ts'
|
||||
],
|
||||
[
|
||||
'widget.spec.ts',
|
||||
'sidebar/nodeLibrary.spec.ts',
|
||||
'nodeHelp.spec.ts',
|
||||
'templates.spec.ts',
|
||||
'featureFlags.spec.ts',
|
||||
'copyPaste.spec.ts',
|
||||
'loadWorkflowInMedia.spec.ts',
|
||||
'actionbar.spec.ts',
|
||||
'commands.spec.ts',
|
||||
'minimap.spec.ts',
|
||||
'workflowTabThumbnail.spec.ts'
|
||||
],
|
||||
[
|
||||
'nodeSearchBox.spec.ts',
|
||||
'rightClickMenu.spec.ts',
|
||||
'colorPalette.spec.ts',
|
||||
'useSettingSearch.spec.ts',
|
||||
'graphCanvasMenu.spec.ts',
|
||||
'domWidget.spec.ts',
|
||||
'menu.spec.ts',
|
||||
'backgroundImageUpload.spec.ts',
|
||||
'customIcons.spec.ts',
|
||||
'releaseNotifications.spec.ts'
|
||||
],
|
||||
[
|
||||
'dialog.spec.ts',
|
||||
'groupNode.spec.ts',
|
||||
'nodeDisplay.spec.ts',
|
||||
'remoteWidgets.spec.ts',
|
||||
'extensionAPI.spec.ts',
|
||||
'sidebar/queue.spec.ts',
|
||||
'noteNode.spec.ts',
|
||||
'browserTabTitle.spec.ts',
|
||||
'graph.spec.ts',
|
||||
'subgraph-rename-dialog.spec.ts'
|
||||
]
|
||||
]
|
||||
|
||||
export function getShardTests(shardIndex: number): string[] {
|
||||
return OPTIMIZED_SHARDS[shardIndex - 1] || []
|
||||
}
|
||||
|
||||
export function getShardPattern(shardIndex: number): string[] {
|
||||
return getShardTests(shardIndex).map((test) => `**/${test}`)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/**
|
||||
* Custom sharding configuration for Playwright tests
|
||||
* Balances test execution time across shards based on test complexity
|
||||
*/
|
||||
|
||||
export interface ShardConfig {
|
||||
testFiles: string[]
|
||||
weight: number // Estimated relative execution time
|
||||
}
|
||||
|
||||
// Group tests by execution characteristics
|
||||
export const HEAVY_SCREENSHOT_TESTS = [
|
||||
'interaction.spec.ts' // 61 tests, 81 screenshots - heaviest test file
|
||||
]
|
||||
|
||||
export const MEDIUM_SCREENSHOT_TESTS = [
|
||||
'widget.spec.ts', // 17 tests with screenshots
|
||||
'rightClickMenu.spec.ts', // 11 tests with screenshots
|
||||
'nodeSearchBox.spec.ts', // 23 tests with screenshots
|
||||
'groupNode.spec.ts' // 17 tests with screenshots
|
||||
]
|
||||
|
||||
export const LIGHT_SCREENSHOT_TESTS = [
|
||||
'colorPalette.spec.ts',
|
||||
'primitiveNode.spec.ts',
|
||||
'nodeDisplay.spec.ts',
|
||||
'graphCanvasMenu.spec.ts',
|
||||
'nodeBadge.spec.ts',
|
||||
'noteNode.spec.ts',
|
||||
'domWidget.spec.ts',
|
||||
'templates.spec.ts',
|
||||
'selectionToolbox.spec.ts',
|
||||
'execution.spec.ts',
|
||||
'rerouteNode.spec.ts',
|
||||
'copyPaste.spec.ts',
|
||||
'loadWorkflowInMedia.spec.ts'
|
||||
]
|
||||
|
||||
export const HEAVY_LOGIC_TESTS = [
|
||||
'subgraph.spec.ts', // 23 tests, complex logic
|
||||
'dialog.spec.ts', // 21 tests
|
||||
'sidebar/workflows.spec.ts', // 18 tests
|
||||
'sidebar/nodeLibrary.spec.ts' // 18 tests
|
||||
]
|
||||
|
||||
export const MEDIUM_LOGIC_TESTS = [
|
||||
'remoteWidgets.spec.ts', // 14 tests
|
||||
'useSettingSearch.spec.ts', // 13 tests
|
||||
'sidebar/queue.spec.ts', // 12 tests
|
||||
'nodeHelp.spec.ts', // 12 tests
|
||||
'extensionAPI.spec.ts', // 11 tests
|
||||
'bottomPanelShortcuts.spec.ts', // 11 tests
|
||||
'featureFlags.spec.ts', // 9 tests
|
||||
'menu.spec.ts' // 9 tests
|
||||
]
|
||||
|
||||
export const LIGHT_LOGIC_TESTS = [
|
||||
'backgroundImageUpload.spec.ts',
|
||||
'browserTabTitle.spec.ts',
|
||||
'changeTracker.spec.ts',
|
||||
'chatHistory.spec.ts',
|
||||
'commands.spec.ts',
|
||||
'customIcons.spec.ts',
|
||||
'graph.spec.ts',
|
||||
'keybindings.spec.ts',
|
||||
'litegraphEvent.spec.ts',
|
||||
'minimap.spec.ts',
|
||||
'releaseNotifications.spec.ts',
|
||||
'subgraph-rename-dialog.spec.ts',
|
||||
'userSelectView.spec.ts',
|
||||
'versionMismatchWarnings.spec.ts',
|
||||
'workflowTabThumbnail.spec.ts',
|
||||
'actionbar.spec.ts'
|
||||
]
|
||||
|
||||
// Optimized shard distribution for chromium tests
|
||||
export const CHROMIUM_SHARDS: ShardConfig[] = [
|
||||
{
|
||||
// Shard 1: Heavy screenshot test (interaction.spec.ts alone)
|
||||
testFiles: HEAVY_SCREENSHOT_TESTS,
|
||||
weight: 100
|
||||
},
|
||||
{
|
||||
// Shard 2: Medium screenshot tests
|
||||
testFiles: MEDIUM_SCREENSHOT_TESTS,
|
||||
weight: 80
|
||||
},
|
||||
{
|
||||
// Shard 3: Light screenshot tests
|
||||
testFiles: LIGHT_SCREENSHOT_TESTS,
|
||||
weight: 70
|
||||
},
|
||||
{
|
||||
// Shard 4: Heavy logic tests
|
||||
testFiles: HEAVY_LOGIC_TESTS,
|
||||
weight: 75
|
||||
},
|
||||
{
|
||||
// Shard 5: Medium and light logic tests
|
||||
testFiles: [...MEDIUM_LOGIC_TESTS, ...LIGHT_LOGIC_TESTS],
|
||||
weight: 65
|
||||
}
|
||||
]
|
||||
|
||||
// No sharding needed for these projects
|
||||
export const NO_SHARD_PROJECTS = [
|
||||
'mobile-chrome',
|
||||
'chromium-0.5x',
|
||||
'chromium-2x'
|
||||
]
|
||||
|
||||
/**
|
||||
* Get the test files for a specific shard
|
||||
* @param shardIndex 1-based shard index
|
||||
* @param totalShards Total number of shards
|
||||
* @param projectName Name of the Playwright project
|
||||
*/
|
||||
export function getShardTests(
|
||||
shardIndex: number,
|
||||
totalShards: number,
|
||||
projectName: string
|
||||
): string[] | null {
|
||||
// For projects that don't need sharding, return null to run all tests
|
||||
if (NO_SHARD_PROJECTS.includes(projectName)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// For chromium project, use custom sharding
|
||||
if (projectName === 'chromium' && totalShards === 5) {
|
||||
const shard = CHROMIUM_SHARDS[shardIndex - 1]
|
||||
return shard ? shard.testFiles : []
|
||||
}
|
||||
|
||||
// Fallback to default sharding for other configurations
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a grep pattern to filter tests for a specific shard
|
||||
* @param shardIndex 1-based shard index
|
||||
* @param totalShards Total number of shards
|
||||
* @param projectName Name of the Playwright project
|
||||
*/
|
||||
export function getShardGrep(
|
||||
shardIndex: number,
|
||||
totalShards: number,
|
||||
projectName: string
|
||||
): RegExp | null {
|
||||
const tests = getShardTests(shardIndex, totalShards, projectName)
|
||||
|
||||
if (!tests || tests.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Create a regex pattern that matches any of the test files
|
||||
const pattern = tests.map((file) => file.replace(/\./g, '\\.')).join('|')
|
||||
return new RegExp(pattern)
|
||||
}
|
||||
@@ -59,6 +59,18 @@ test.describe('Execution error', () => {
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(executionError).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can display Issue Report form', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.getByLabel('Help Fix This').click()
|
||||
const issueReportForm = comfyPage.page.getByText(
|
||||
'Submit Error Report (Optional)'
|
||||
)
|
||||
await expect(issueReportForm).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Missing models warning', () => {
|
||||
@@ -291,16 +303,37 @@ test.describe('Settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Support', () => {
|
||||
test('Should open external zendesk link', async ({ comfyPage }) => {
|
||||
test.describe('Feedback dialog', () => {
|
||||
test('Should open from topmenu help command', async ({ comfyPage }) => {
|
||||
// Open feedback dialog from top menu
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
const pagePromise = comfyPage.page.context().waitForEvent('page')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
|
||||
const newPage = await pagePromise
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
await newPage.waitForLoadState('networkidle')
|
||||
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
|
||||
await newPage.close()
|
||||
// Verify feedback dialog content is visible
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
await expect(feedbackHeader).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should close when close button clicked', async ({ comfyPage }) => {
|
||||
// Open feedback dialog
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
|
||||
// Close feedback dialog
|
||||
await comfyPage.page
|
||||
.getByLabel('', { exact: true })
|
||||
.getByLabel('Close')
|
||||
.click()
|
||||
await feedbackHeader.waitFor({ state: 'hidden' })
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(feedbackHeader).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -15,10 +15,8 @@ test.describe('Load Workflow in Media', () => {
|
||||
'workflow.mp4',
|
||||
'workflow.mov',
|
||||
'workflow.m4v',
|
||||
'workflow.svg'
|
||||
// TODO: Re-enable after fixing test asset to use core nodes only
|
||||
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
|
||||
// 'workflow.avif'
|
||||
'workflow.svg',
|
||||
'workflow.avif'
|
||||
]
|
||||
fileNames.forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 67 KiB |
293
docs/SETTINGS.md
293
docs/SETTINGS.md
@@ -1,293 +0,0 @@
|
||||
# Settings System
|
||||
|
||||
## Overview
|
||||
|
||||
ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration.
|
||||
|
||||
### Settings Architecture
|
||||
- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts`
|
||||
- Registered at app startup, loaded/saved via `useSettingStore` (Pinia)
|
||||
- Persisted per user via backend `/settings` endpoint
|
||||
- If a value hasn't been set by the user, the store returns the computed default
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:105-122
|
||||
function getDefaultValue<K extends keyof Settings>(
|
||||
key: K
|
||||
): Settings[K] | undefined {
|
||||
const param = getSettingById(key)
|
||||
if (param === undefined) return
|
||||
|
||||
const versionedDefault = getVersionedDefaultValue(key, param)
|
||||
if (versionedDefault) {
|
||||
return versionedDefault
|
||||
}
|
||||
|
||||
return typeof param.defaultValue === 'function'
|
||||
? param.defaultValue()
|
||||
: param.defaultValue
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Registration Process
|
||||
|
||||
Settings are registered after server values are loaded:
|
||||
|
||||
```typescript
|
||||
// From src/components/graph/GraphCanvas.vue:311-315
|
||||
CORE_SETTINGS.forEach((setting) => {
|
||||
settingStore.addSetting(setting)
|
||||
})
|
||||
|
||||
await newUserService().initializeIfNewUser(settingStore)
|
||||
```
|
||||
|
||||
## Dynamic and Environment-Based Defaults
|
||||
|
||||
### Computed Defaults
|
||||
You can compute defaults dynamically using function defaults that access runtime context:
|
||||
|
||||
```typescript
|
||||
// From src/constants/coreSettings.ts:94-101
|
||||
{
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
// Default to small if the window is less than 1536px(2xl) wide
|
||||
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// From src/constants/coreSettings.ts:306
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Defaults
|
||||
You can vary defaults by installed frontend version using `defaultsByInstallVersion`:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:129-150
|
||||
function getVersionedDefaultValue<K extends keyof Settings, TValue = Settings[K]>(
|
||||
key: K,
|
||||
param: SettingParams<TValue> | undefined
|
||||
): TValue | null {
|
||||
const defaultsByInstallVersion = param?.defaultsByInstallVersion
|
||||
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
|
||||
const installedVersion = get('Comfy.InstalledVersion')
|
||||
if (installedVersion) {
|
||||
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
|
||||
(a, b) => compareVersions(b, a)
|
||||
)
|
||||
for (const version of sortedVersions) {
|
||||
if (!isSemVer(version)) continue
|
||||
if (compareVersions(installedVersion, version) >= 0) {
|
||||
const versionedDefault = defaultsByInstallVersion[version]
|
||||
return typeof versionedDefault === 'function'
|
||||
? versionedDefault()
|
||||
: versionedDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
Example versioned defaults from codebase:
|
||||
|
||||
```typescript
|
||||
// From src/constants/coreSettings.ts:38-40
|
||||
{
|
||||
id: 'Comfy.Graph.LinkReleaseAction',
|
||||
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
|
||||
defaultsByInstallVersion: {
|
||||
'1.24.1': LinkReleaseTriggerAction.SEARCH_BOX
|
||||
}
|
||||
}
|
||||
|
||||
// Another versioned default example
|
||||
{
|
||||
id: 'Comfy.Graph.LinkReleaseAction.Shift',
|
||||
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,
|
||||
defaultsByInstallVersion: {
|
||||
'1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real Examples from Codebase
|
||||
|
||||
Here are actual settings showing different patterns:
|
||||
|
||||
```typescript
|
||||
// Number setting with validation
|
||||
{
|
||||
id: 'LiteGraph.Node.TooltipDelay',
|
||||
name: 'Tooltip Delay',
|
||||
type: 'number',
|
||||
attrs: {
|
||||
min: 100,
|
||||
max: 3000,
|
||||
step: 50
|
||||
},
|
||||
defaultValue: 500,
|
||||
versionAdded: '1.9.0'
|
||||
}
|
||||
|
||||
// Hidden system setting for tracking
|
||||
{
|
||||
id: 'Comfy.InstalledVersion',
|
||||
name: 'The frontend version that was running when the user first installed ComfyUI',
|
||||
type: 'hidden',
|
||||
defaultValue: null,
|
||||
versionAdded: '1.24.0'
|
||||
}
|
||||
|
||||
// Slider with complex tooltip
|
||||
{
|
||||
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
|
||||
name: 'Low quality rendering zoom threshold',
|
||||
tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
max: 1.0,
|
||||
step: 0.05
|
||||
},
|
||||
defaultValue: 0.5
|
||||
}
|
||||
```
|
||||
|
||||
### New User Version Capture
|
||||
|
||||
The initial installed version is captured for new users to ensure versioned defaults remain stable:
|
||||
|
||||
```typescript
|
||||
// From src/services/newUserService.ts:49-53
|
||||
await settingStore.set(
|
||||
'Comfy.InstalledVersion',
|
||||
__COMFYUI_FRONTEND_VERSION__
|
||||
)
|
||||
```
|
||||
|
||||
## Practical Patterns for Environment-Based Defaults
|
||||
|
||||
### Dynamic Default Patterns
|
||||
```typescript
|
||||
// Device-based default
|
||||
{
|
||||
id: 'Comfy.Example.MobileDefault',
|
||||
type: 'boolean',
|
||||
defaultValue: () => /Mobile/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
// Environment-based default
|
||||
{
|
||||
id: 'Comfy.Example.DevMode',
|
||||
type: 'boolean',
|
||||
defaultValue: () => import.meta.env.DEV
|
||||
}
|
||||
|
||||
// Window size based
|
||||
{
|
||||
id: 'Comfy.Example.CompactUI',
|
||||
type: 'boolean',
|
||||
defaultValue: () => window.innerWidth < 1024
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Rollout Pattern
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.NewFeature',
|
||||
type: 'combo',
|
||||
options: ['legacy', 'enhanced'],
|
||||
defaultValue: 'legacy',
|
||||
defaultsByInstallVersion: {
|
||||
'1.25.0': 'enhanced'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Settings Persistence and Access
|
||||
|
||||
### API Interaction
|
||||
Values are stored per user via the backend. The store writes through API and falls back to defaults when not set:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:73-75
|
||||
onChange(settingsById.value[key], newValue, oldValue)
|
||||
settingValues.value[key] = newValue
|
||||
await api.storeSetting(key, newValue)
|
||||
```
|
||||
|
||||
### Usage in Components
|
||||
```typescript
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// Get setting value (returns computed default if not set by user)
|
||||
const value = settingStore.get('Comfy.SomeSetting')
|
||||
|
||||
// Update setting value
|
||||
await settingStore.set('Comfy.SomeSetting', newValue)
|
||||
```
|
||||
|
||||
|
||||
## Advanced Settings Features
|
||||
|
||||
### Migration and Backward Compatibility
|
||||
|
||||
Settings support migration from deprecated values:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:68-69, 172-175
|
||||
const newValue = tryMigrateDeprecatedValue(
|
||||
settingsById.value[key],
|
||||
clonedValue
|
||||
)
|
||||
|
||||
// Migration happens during addSetting for existing values:
|
||||
if (settingValues.value[setting.id] !== undefined) {
|
||||
settingValues.value[setting.id] = tryMigrateDeprecatedValue(
|
||||
setting,
|
||||
settingValues.value[setting.id]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### onChange Callbacks
|
||||
|
||||
Settings can define onChange callbacks that receive the setting definition, new value, and old value:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:73, 177
|
||||
onChange(settingsById.value[key], newValue, oldValue) // During set()
|
||||
onChange(setting, get(setting.id), undefined) // During addSetting()
|
||||
```
|
||||
|
||||
### Settings UI and Categories
|
||||
|
||||
Settings are automatically grouped for UI based on their `category` or derived from `id`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
category: ['Appearance', 'Sidebar', 'Size'],
|
||||
// UI will group this under Appearance > Sidebar > Size
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Feature flag system: `docs/FEATURE_FLAGS.md`
|
||||
- Settings schema for backend: `src/schemas/apiSchema.ts` (zSettings)
|
||||
- Server configuration (separate from user settings): `src/constants/serverConfig.ts`
|
||||
|
||||
## Summary
|
||||
|
||||
- **Settings**: User preferences with dynamic/versioned defaults, persisted per user
|
||||
- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env)
|
||||
- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases
|
||||
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`
|
||||
@@ -1,82 +0,0 @@
|
||||
# Settings and Feature Flags Sequence Diagram
|
||||
|
||||
This diagram shows the flow of settings initialization, default resolution, persistence, and feature flags exchange.
|
||||
|
||||
This diagram accurately reflects the actual implementation in the ComfyUI frontend codebase.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as User
|
||||
participant Vue as Vue Component
|
||||
participant Store as SettingStore (Pinia)
|
||||
participant API as ComfyApi (WebSocket/REST)
|
||||
participant Backend as Backend
|
||||
participant NewUserSvc as NewUserService
|
||||
|
||||
Note over Vue,Store: App startup (GraphCanvas.vue)
|
||||
Vue->>Store: loadSettingValues()
|
||||
Store->>API: getSettings()
|
||||
API->>Backend: GET /settings
|
||||
Backend-->>API: settings map (per-user)
|
||||
API-->>Store: settings map
|
||||
Store-->>Vue: loaded
|
||||
|
||||
Vue->>Store: register CORE_SETTINGS (addSetting for each)
|
||||
loop For each setting registration
|
||||
Store->>Store: tryMigrateDeprecatedValue(existing value)
|
||||
Store->>Store: onChange(setting, currentValue, undefined)
|
||||
end
|
||||
|
||||
Note over Vue,NewUserSvc: New user detection
|
||||
Vue->>NewUserSvc: initializeIfNewUser(settingStore)
|
||||
NewUserSvc->>NewUserSvc: checkIsNewUser(settingStore)
|
||||
alt New user detected
|
||||
NewUserSvc->>Store: set("Comfy.InstalledVersion", __COMFYUI_FRONTEND_VERSION__)
|
||||
Store->>Store: tryMigrateDeprecatedValue(newValue)
|
||||
Store->>Store: onChange(setting, newValue, oldValue)
|
||||
Store->>API: storeSetting(key, newValue)
|
||||
API->>Backend: POST /settings/{id}
|
||||
else Existing user
|
||||
Note over NewUserSvc: Skip setting installed version
|
||||
end
|
||||
|
||||
Note over Vue,Store: Component reads a setting
|
||||
Vue->>Store: get(key)
|
||||
Store->>Store: exists(key)?
|
||||
alt User value exists
|
||||
Store-->>Vue: return stored user value
|
||||
else Not set by user
|
||||
Store->>Store: getVersionedDefaultValue(key)
|
||||
alt Versioned default matched (defaultsByInstallVersion)
|
||||
Store-->>Vue: return versioned default
|
||||
else No version match
|
||||
Store->>Store: evaluate defaultValue (function or constant)
|
||||
Note over Store: defaultValue can use window size,<br/>locale, env, etc.
|
||||
Store-->>Vue: return computed default
|
||||
end
|
||||
end
|
||||
|
||||
Note over User,Store: User updates a setting
|
||||
User->>Vue: changes setting in UI
|
||||
Vue->>Store: set(key, newValue)
|
||||
Store->>Store: tryMigrateDeprecatedValue(newValue)
|
||||
Store->>Store: check if newValue === oldValue (early return if same)
|
||||
Store->>Store: onChange(setting, newValue, oldValue)
|
||||
Store->>Store: update settingValues[key]
|
||||
Store->>API: storeSetting(key, newValue)
|
||||
API->>Backend: POST /settings/{id}
|
||||
Backend-->>API: 200 OK
|
||||
API-->>Store: ack
|
||||
|
||||
Note over API,Backend: Feature Flags WebSocket Exchange
|
||||
API->>Backend: WS connect
|
||||
API->>Backend: send { type: "feature_flags", data: clientFeatureFlags.json }
|
||||
Backend-->>API: WS send { type: "feature_flags", data: server flags }
|
||||
API->>API: store serverFeatureFlags = data
|
||||
|
||||
Note over Vue,API: Feature flag consumption in UI/logic
|
||||
Vue->>API: serverSupportsFeature(name)
|
||||
API-->>Vue: boolean (true only if flag === true)
|
||||
Vue->>API: getServerFeature(name, default)
|
||||
API-->>Vue: value or default
|
||||
```
|
||||
@@ -12,7 +12,6 @@ const config: KnipConfig = {
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts',
|
||||
'vitest.config.ts',
|
||||
'vitest.litegraph.config.ts',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
project: [
|
||||
@@ -33,8 +32,6 @@ const config: KnipConfig = {
|
||||
'coverage/**',
|
||||
// i18n config
|
||||
'.i18nrc.cjs',
|
||||
// Vitest litegraph config
|
||||
'vitest.litegraph.config.ts',
|
||||
// Test setup files
|
||||
'browser_tests/globalSetup.ts',
|
||||
'browser_tests/globalTeardown.ts',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.8",
|
||||
"version": "1.26.7",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -19,11 +19,8 @@
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:browser": "npx nx e2e",
|
||||
"test:browser:sharded": "playwright test --config=playwright-sharded.config.ts",
|
||||
"test:browser:optimize-shards": "node browser_tests/scripts/optimizeSharding.js",
|
||||
"test:unit": "nx run test tests-ui/tests",
|
||||
"test:component": "nx run test src/components/",
|
||||
"test:litegraph": "vitest run --config vitest.litegraph.config.ts",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
@@ -34,7 +31,7 @@
|
||||
"knip": "knip --cache",
|
||||
"knip:no-cache": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "npx playwright test --config=playwright.i18n.config.ts",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"storybook": "nx storybook -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
@@ -59,7 +56,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -82,7 +78,6 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"ink": "^6.2.2",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"lucide-vue-next": "^0.540.0",
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
import { getShardPattern } from './browser_tests/shardConfig.generated'
|
||||
import baseConfig from './playwright.config'
|
||||
|
||||
/**
|
||||
* Optimized Playwright configuration for CI with balanced sharding
|
||||
* Uses pre-calculated shard distribution for even test execution times
|
||||
*/
|
||||
|
||||
// Parse shard information from Playwright CLI
|
||||
const shardInfo =
|
||||
process.env.SHARD ||
|
||||
process.argv.find((arg) => arg.includes('--shard='))?.split('=')[1]
|
||||
const [currentShard, totalShards] = shardInfo
|
||||
? shardInfo.split('/').map(Number)
|
||||
: [1, 1]
|
||||
|
||||
// Get test patterns for current shard
|
||||
const testMatch = totalShards === 5 ? getShardPattern(currentShard) : undefined
|
||||
|
||||
console.log(`🎯 Shard ${currentShard}/${totalShards} configuration`)
|
||||
if (testMatch) {
|
||||
console.log(`📋 Running tests:`, testMatch)
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
|
||||
// Use optimized test distribution for 5-shard setup
|
||||
...(testMatch && { testMatch }),
|
||||
|
||||
// Optimize parallel execution based on shard content
|
||||
fullyParallel: true,
|
||||
workers: process.env.CI ? (currentShard === 1 ? 2 : 4) : baseConfig.workers,
|
||||
|
||||
// Adjust timeouts for heavy tests
|
||||
timeout: currentShard === 1 ? 20000 : 15000,
|
||||
|
||||
// Optimize retries
|
||||
retries: process.env.CI ? (currentShard === 1 ? 2 : 3) : 0
|
||||
})
|
||||
@@ -31,21 +31,23 @@ export default defineConfig({
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
timeout: 15000,
|
||||
grepInvert: /@mobile/ // Run all tests except those tagged with @mobile
|
||||
grepInvert: /@mobile|collect-i18n/ // Run all tests except mobile and i18n collection tests
|
||||
},
|
||||
|
||||
{
|
||||
name: 'chromium-2x',
|
||||
use: { ...devices['Desktop Chrome'], deviceScaleFactor: 2 },
|
||||
timeout: 15000,
|
||||
grep: /@2x/ // Run all tests tagged with @2x
|
||||
grep: /@2x/, // Run all tests tagged with @2x
|
||||
grepInvert: /collect-i18n/ // Exclude i18n collection tests
|
||||
},
|
||||
|
||||
{
|
||||
name: 'chromium-0.5x',
|
||||
use: { ...devices['Desktop Chrome'], deviceScaleFactor: 0.5 },
|
||||
timeout: 15000,
|
||||
grep: /@0.5x/ // Run all tests tagged with @0.5x
|
||||
grep: /@0.5x/, // Run all tests tagged with @0.5x
|
||||
grepInvert: /collect-i18n/ // Exclude i18n collection tests
|
||||
},
|
||||
|
||||
// {
|
||||
@@ -62,7 +64,8 @@ export default defineConfig({
|
||||
{
|
||||
name: 'mobile-chrome',
|
||||
use: { ...devices['Pixel 5'], hasTouch: true },
|
||||
grep: /@mobile/ // Run only tests tagged with @mobile
|
||||
grep: /@mobile/, // Run only tests tagged with @mobile
|
||||
grepInvert: /collect-i18n/ // Exclude i18n collection tests
|
||||
}
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './scripts',
|
||||
testDir: './scripts/i18n',
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
headless: true
|
||||
@@ -9,4 +9,5 @@ export default defineConfig({
|
||||
reporter: 'list',
|
||||
timeout: 60000,
|
||||
testMatch: /collect-i18n-.*\.ts/
|
||||
// Don't use globalSetup/globalTeardown to avoid ComfyUI path dependencies
|
||||
})
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
import baseConfig from './playwright.config'
|
||||
|
||||
/**
|
||||
* Optimized Playwright configuration with intelligent sharding
|
||||
* This configuration improves test distribution to balance execution time
|
||||
*/
|
||||
|
||||
// Helper to determine if we should apply custom test filtering
|
||||
const shardInfo = process.env.SHARD
|
||||
? process.env.SHARD.split('/').map(Number)
|
||||
: null
|
||||
const currentShard = shardInfo?.[0] || 1
|
||||
const totalShards = shardInfo?.[1] || 1
|
||||
const projectName = process.env.TEST_PROJECT || 'chromium'
|
||||
|
||||
// Define test groups for better distribution
|
||||
const testGroups = {
|
||||
// Heavy tests (run in separate shards)
|
||||
heavy: [
|
||||
'**/interaction.spec.ts' // 61 tests with 81 screenshots
|
||||
],
|
||||
// Medium-heavy tests
|
||||
mediumHeavy: [
|
||||
'**/subgraph.spec.ts', // 23 complex tests
|
||||
'**/widget.spec.ts', // 17 tests with screenshots
|
||||
'**/nodeSearchBox.spec.ts' // 23 tests with screenshots
|
||||
],
|
||||
// Medium tests
|
||||
medium: [
|
||||
'**/dialog.spec.ts',
|
||||
'**/groupNode.spec.ts',
|
||||
'**/rightClickMenu.spec.ts',
|
||||
'**/sidebar/workflows.spec.ts',
|
||||
'**/sidebar/nodeLibrary.spec.ts'
|
||||
],
|
||||
// Light tests
|
||||
light: [
|
||||
'**/colorPalette.spec.ts',
|
||||
'**/primitiveNode.spec.ts',
|
||||
'**/nodeDisplay.spec.ts',
|
||||
'**/graphCanvasMenu.spec.ts',
|
||||
'**/nodeBadge.spec.ts',
|
||||
'**/noteNode.spec.ts',
|
||||
'**/domWidget.spec.ts',
|
||||
'**/templates.spec.ts',
|
||||
'**/selectionToolbox.spec.ts',
|
||||
'**/execution.spec.ts',
|
||||
'**/rerouteNode.spec.ts',
|
||||
'**/copyPaste.spec.ts',
|
||||
'**/loadWorkflowInMedia.spec.ts'
|
||||
],
|
||||
// Very light tests
|
||||
veryLight: [
|
||||
'**/backgroundImageUpload.spec.ts',
|
||||
'**/browserTabTitle.spec.ts',
|
||||
'**/changeTracker.spec.ts',
|
||||
'**/chatHistory.spec.ts',
|
||||
'**/commands.spec.ts',
|
||||
'**/customIcons.spec.ts',
|
||||
'**/graph.spec.ts',
|
||||
'**/keybindings.spec.ts',
|
||||
'**/litegraphEvent.spec.ts',
|
||||
'**/minimap.spec.ts',
|
||||
'**/releaseNotifications.spec.ts',
|
||||
'**/remoteWidgets.spec.ts',
|
||||
'**/useSettingSearch.spec.ts',
|
||||
'**/sidebar/queue.spec.ts',
|
||||
'**/nodeHelp.spec.ts',
|
||||
'**/extensionAPI.spec.ts',
|
||||
'**/bottomPanelShortcuts.spec.ts',
|
||||
'**/featureFlags.spec.ts',
|
||||
'**/menu.spec.ts',
|
||||
'**/subgraph-rename-dialog.spec.ts',
|
||||
'**/userSelectView.spec.ts',
|
||||
'**/versionMismatchWarnings.spec.ts',
|
||||
'**/workflowTabThumbnail.spec.ts',
|
||||
'**/actionbar.spec.ts'
|
||||
]
|
||||
}
|
||||
|
||||
// Custom test patterns for each shard (when running with 5 shards)
|
||||
const shardPatterns: Record<number, string[]> = {
|
||||
1: testGroups.heavy, // Shard 1: Only interaction.spec.ts
|
||||
2: testGroups.mediumHeavy, // Shard 2: Medium-heavy tests
|
||||
3: testGroups.medium, // Shard 3: Medium tests
|
||||
4: testGroups.light, // Shard 4: Light tests
|
||||
5: testGroups.veryLight // Shard 5: Very light tests
|
||||
}
|
||||
|
||||
// Determine which tests to run based on shard
|
||||
let testMatch: string[] | undefined
|
||||
if (
|
||||
projectName === 'chromium' &&
|
||||
totalShards === 5 &&
|
||||
shardPatterns[currentShard]
|
||||
) {
|
||||
testMatch = shardPatterns[currentShard]
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
// Override testMatch if we have custom shard patterns
|
||||
...(testMatch && { testMatch }),
|
||||
|
||||
// Increase workers for lighter test shards
|
||||
use: {
|
||||
...baseConfig.use,
|
||||
// More parallel workers for shards with lighter tests
|
||||
...(currentShard >= 4 &&
|
||||
projectName === 'chromium' && {
|
||||
workers: process.env.CI ? 4 : 2
|
||||
})
|
||||
},
|
||||
|
||||
// Optimize retries based on shard content
|
||||
retries: process.env.CI ? (currentShard === 1 ? 2 : 3) : 0,
|
||||
|
||||
// Project-specific optimizations
|
||||
projects:
|
||||
baseConfig.projects?.map((project) => {
|
||||
// For non-chromium projects that don't need sharding
|
||||
if (
|
||||
['mobile-chrome', 'chromium-0.5x', 'chromium-2x'].includes(
|
||||
project.name || ''
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...project,
|
||||
// These projects should only run when not sharding or on first shard
|
||||
...(totalShards > 1 && currentShard > 1 && { testMatch: [] })
|
||||
}
|
||||
}
|
||||
|
||||
return project
|
||||
}) || []
|
||||
})
|
||||
300
pnpm-lock.yaml
generated
300
pnpm-lock.yaml
generated
@@ -213,9 +213,6 @@ importers:
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
'@types/node':
|
||||
specifier: ^20.14.8
|
||||
version: 20.14.10
|
||||
@@ -282,9 +279,6 @@ importers:
|
||||
jiti:
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
jsdom:
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
knip:
|
||||
specifier: ^5.62.0
|
||||
version: 5.62.0(@types/node@20.14.10)(typescript@5.9.2)
|
||||
@@ -347,7 +341,7 @@ importers:
|
||||
version: 7.7.6(rollup@4.22.4)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2)
|
||||
vue-tsc:
|
||||
specifier: ^2.1.10
|
||||
version: 2.1.10(typescript@5.9.2)
|
||||
@@ -486,9 +480,6 @@ packages:
|
||||
'@anthropic-ai/sdk@0.8.1':
|
||||
resolution: {integrity: sha512-59etePenCizVx1O8Qhi1T1ruE04ISfNzCnyhZNcsss1QljsLmYS83jttarMNEvGYcsUF7rwxw2lzcC3Zbxao7g==}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop@1.3.1':
|
||||
resolution: {integrity: sha512-MptcLppK78B2eplL5fHk93kfCbZ6uCpt33YauBPrOwI5zcHYJhZGeaGEaAXoVAHnSJOdQUhy6kGVVC9qggz2Fg==}
|
||||
|
||||
@@ -1058,34 +1049,6 @@ packages:
|
||||
'@comfyorg/comfyui-electron-types@0.4.43':
|
||||
resolution: {integrity: sha512-o6WFbYn9yAkGbkOwvhPF7pbKDvN0occZ21Tfyhya8CIsIqKpTHLft0aOqo4yhSh+kTxN16FYjsfrTH5Olk4WuA==}
|
||||
|
||||
'@csstools/color-helpers@5.1.0':
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@csstools/css-calc@2.1.4':
|
||||
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-color-parser@3.1.0':
|
||||
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5':
|
||||
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4':
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@emnapi/core@1.4.5':
|
||||
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
|
||||
|
||||
@@ -2544,9 +2507,6 @@ packages:
|
||||
'@types/fs-extra@11.0.4':
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
@@ -2604,9 +2564,6 @@ packages:
|
||||
'@types/three@0.169.0':
|
||||
resolution: {integrity: sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==}
|
||||
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
@@ -2922,10 +2879,6 @@ packages:
|
||||
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -3483,17 +3436,9 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
cssstyle@4.6.0:
|
||||
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
de-indent@1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
|
||||
@@ -3519,9 +3464,6 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
decode-named-character-reference@1.2.0:
|
||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||
|
||||
@@ -3704,10 +3646,6 @@ packages:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4285,10 +4223,6 @@ packages:
|
||||
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
html-minifier-terser@6.1.0:
|
||||
resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4301,10 +4235,6 @@ packages:
|
||||
http-parser-js@0.5.10:
|
||||
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
http-proxy@1.18.1:
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -4314,10 +4244,6 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-signals@5.0.0:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
@@ -4507,9 +4433,6 @@ packages:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
is-promise@2.2.2:
|
||||
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
|
||||
|
||||
@@ -4614,15 +4537,6 @@ packages:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@26.1.0:
|
||||
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsesc@3.0.2:
|
||||
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4815,9 +4729,6 @@ packages:
|
||||
resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.1.0:
|
||||
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5211,9 +5122,6 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
nwsapi@2.2.21:
|
||||
resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==}
|
||||
|
||||
nx@21.4.1:
|
||||
resolution: {integrity: sha512-nD8NjJGYk5wcqiATzlsLauvyrSHV2S2YmM2HBIKqTTwVP2sey07MF3wDB9U2BwxIjboahiITQ6pfqFgB79TF2A==}
|
||||
hasBin: true
|
||||
@@ -5345,9 +5253,6 @@ packages:
|
||||
parse5@7.1.2:
|
||||
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -5870,9 +5775,6 @@ packages:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
rrweb-cssom@0.8.0:
|
||||
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
|
||||
|
||||
run-applescript@7.0.0:
|
||||
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5892,10 +5794,6 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
saxes@6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
@@ -6143,9 +6041,6 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
synckit@0.11.3:
|
||||
resolution: {integrity: sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -6218,13 +6113,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.0.3
|
||||
|
||||
tldts-core@6.1.86:
|
||||
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
||||
|
||||
tldts@6.1.86:
|
||||
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||
hasBin: true
|
||||
|
||||
tmp@0.2.5:
|
||||
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
||||
engines: {node: '>=14.14'}
|
||||
@@ -6244,17 +6132,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
@@ -6701,10 +6581,6 @@ packages:
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
walk-up-path@4.0.0:
|
||||
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -6745,22 +6621,10 @@ packages:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-mimetype@3.0.0:
|
||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
whatwg-mimetype@4.0.0:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@@ -6839,13 +6703,6 @@ packages:
|
||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
xml-name-validator@5.0.0:
|
||||
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7102,14 +6959,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
lru-cache: 10.4.3
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop@1.3.1':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
@@ -7866,26 +7715,6 @@ snapshots:
|
||||
|
||||
'@comfyorg/comfyui-electron-types@0.4.43': {}
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
|
||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 5.1.0
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4': {}
|
||||
|
||||
'@emnapi/core@1.4.5':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.4
|
||||
@@ -8882,7 +8711,7 @@ snapshots:
|
||||
tsconfig-paths: 4.2.0
|
||||
tslib: 2.8.1
|
||||
vite: 5.4.19(@types/node@20.14.10)(terser@5.39.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -9536,12 +9365,6 @@ snapshots:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 20.14.10
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/tough-cookie': 4.0.5
|
||||
parse5: 7.1.2
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
@@ -9606,8 +9429,6 @@ snapshots:
|
||||
fflate: 0.8.2
|
||||
meshoptimizer: 0.18.1
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@@ -9799,7 +9620,7 @@ snapshots:
|
||||
sirv: 3.0.1
|
||||
tinyglobby: 0.2.14
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
@@ -10044,8 +9865,6 @@ snapshots:
|
||||
|
||||
address@1.2.2: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
@@ -10627,18 +10446,8 @@ snapshots:
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
cssstyle@4.6.0:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 3.2.0
|
||||
rrweb-cssom: 0.8.0
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
debounce-fn@6.0.0:
|
||||
@@ -10653,8 +10462,6 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
decode-named-character-reference@1.2.0:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
@@ -10818,8 +10625,6 @@ snapshots:
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
env-paths@3.0.0: {}
|
||||
@@ -11521,10 +11326,6 @@ snapshots:
|
||||
dependencies:
|
||||
whatwg-encoding: 2.0.0
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
html-minifier-terser@6.1.0:
|
||||
dependencies:
|
||||
camel-case: 4.1.2
|
||||
@@ -11545,13 +11346,6 @@ snapshots:
|
||||
|
||||
http-parser-js@0.5.10: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy@1.18.1:
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
@@ -11579,13 +11373,6 @@ snapshots:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
human-signals@8.0.1: {}
|
||||
@@ -11741,8 +11528,6 @@ snapshots:
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
is-promise@2.2.2: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
@@ -11835,33 +11620,6 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@26.1.0:
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.21
|
||||
parse5: 7.3.0
|
||||
rrweb-cssom: 0.8.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.1.2
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
ws: 8.18.3
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
jsesc@3.0.2: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -12064,8 +11822,6 @@ snapshots:
|
||||
|
||||
lru-cache@10.3.0: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.1.0: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -12627,8 +12383,6 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nwsapi@2.2.21: {}
|
||||
|
||||
nx@21.4.1:
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 0.2.4
|
||||
@@ -12831,10 +12585,6 @@ snapshots:
|
||||
dependencies:
|
||||
entities: 4.5.0
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
pascal-case@3.1.2:
|
||||
@@ -13460,8 +13210,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
rrweb-cssom@0.8.0: {}
|
||||
|
||||
run-applescript@7.0.0: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -13476,10 +13224,6 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
scheduler@0.26.0: {}
|
||||
|
||||
section-matter@1.0.0:
|
||||
@@ -13751,8 +13495,6 @@ snapshots:
|
||||
react: 19.1.1
|
||||
use-sync-external-store: 1.5.0(react@19.1.1)
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
synckit@0.11.3:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.2.2
|
||||
@@ -13846,12 +13588,6 @@ snapshots:
|
||||
markdown-it-task-lists: 2.1.1
|
||||
prosemirror-markdown: 1.13.1
|
||||
|
||||
tldts-core@6.1.86: {}
|
||||
|
||||
tldts@6.1.86:
|
||||
dependencies:
|
||||
tldts-core: 6.1.86
|
||||
|
||||
tmp@0.2.5: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
@@ -13864,16 +13600,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
dependencies:
|
||||
tldts: 6.1.86
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@5.1.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
@@ -14223,7 +13951,7 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
terser: 5.39.2
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -14253,7 +13981,6 @@ snapshots:
|
||||
'@types/node': 20.14.10
|
||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||
happy-dom: 15.11.0
|
||||
jsdom: 26.1.0
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
@@ -14358,10 +14085,6 @@ snapshots:
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
walk-up-path@4.0.0: {}
|
||||
|
||||
wcwidth@1.0.1:
|
||||
@@ -14392,19 +14115,8 @@ snapshots:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
whatwg-mimetype@3.0.0: {}
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
dependencies:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
@@ -14468,10 +14180,6 @@ snapshots:
|
||||
|
||||
xml-name-validator@4.0.0: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
||||
import type { FormItem, SettingParams } from '../src/types/settingTypes'
|
||||
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
import { comfyPageFixture as test } from '../../browser_tests/fixtures/ComfyPage'
|
||||
import { CORE_MENU_COMMANDS } from '../../src/constants/coreMenuCommands'
|
||||
import { SERVER_CONFIG_ITEMS } from '../../src/constants/serverConfig'
|
||||
import type { ComfyCommandImpl } from '../../src/stores/commandStore'
|
||||
import type { FormItem, SettingParams } from '../../src/types/settingTypes'
|
||||
import { formatCamelCase, normalizeI18nKey } from '../../src/utils/formatUtil'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
const commandsPath = './src/locales/en/commands.json'
|
||||
@@ -1,33 +1,39 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import type { ComfyNodeDef } from '../src/schemas/nodeDefSchema'
|
||||
import type { ComfyApi } from '../src/scripts/api'
|
||||
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import { normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
import { comfyPageFixture as test } from '../../browser_tests/fixtures/ComfyPage'
|
||||
import { normalizeI18nKey } from '../../src/utils/formatUtil'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
||||
|
||||
test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
// Mock view route
|
||||
comfyPage.page.route('**/view**', async (route) => {
|
||||
await comfyPage.page.route('**/view**', async (route) => {
|
||||
await route.fulfill({
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
})
|
||||
|
||||
const nodeDefs: ComfyNodeDefImpl[] = (
|
||||
Object.values(
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
const api = window['app'].api as ComfyApi
|
||||
return await api.getNodeDefs()
|
||||
const nodeDefs = await comfyPage.page.evaluate(async () => {
|
||||
const api = window['app'].api
|
||||
const defs = await api.getNodeDefs()
|
||||
// Process node definitions in the browser context where ComfyNodeDefImpl is available
|
||||
return Object.values(defs)
|
||||
.filter((def: any) => !def.name.startsWith('DevTools'))
|
||||
.map((def: any) => {
|
||||
// Create ComfyNodeDefImpl in browser context
|
||||
const impl = new (window as any).ComfyNodeDefImpl(def)
|
||||
// Return a plain object with the needed properties
|
||||
return {
|
||||
name: impl.name,
|
||||
display_name: impl.display_name,
|
||||
description: impl.description,
|
||||
category: impl.category,
|
||||
inputs: impl.inputs,
|
||||
outputs: impl.outputs
|
||||
}
|
||||
})
|
||||
) as ComfyNodeDef[]
|
||||
)
|
||||
// Ignore DevTools nodes (used for internal testing)
|
||||
.filter((def) => !def.name.startsWith('DevTools'))
|
||||
.map((def) => new ComfyNodeDefImpl(def))
|
||||
})
|
||||
|
||||
console.log(`Collected ${nodeDefs.length} node definitions`)
|
||||
|
||||
@@ -100,7 +106,7 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
|
||||
const nodeDefLabels = await extractWidgetLabels()
|
||||
|
||||
function extractInputs(nodeDef: ComfyNodeDefImpl) {
|
||||
function extractInputs(nodeDef: any) {
|
||||
const inputs = Object.fromEntries(
|
||||
Object.values(nodeDef.inputs).flatMap((input) => {
|
||||
const name = input.name
|
||||
@@ -124,7 +130,7 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
return Object.keys(inputs).length > 0 ? inputs : undefined
|
||||
}
|
||||
|
||||
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
|
||||
function extractOutputs(nodeDef: any) {
|
||||
const outputs = Object.fromEntries(
|
||||
nodeDef.outputs.flatMap((output, i) => {
|
||||
// Ignore data types if they are already translated in allDataTypesLocale.
|
||||
@@ -15,14 +15,12 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const conflictDetection = useConflictDetection()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
@@ -49,9 +47,5 @@ onMounted(() => {
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
// Initialize conflict detection in background
|
||||
// This runs async and doesn't block UI setup
|
||||
void conflictDetection.initializeConflictDetection()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -107,7 +107,6 @@ const rename = async (
|
||||
}
|
||||
}
|
||||
|
||||
const isRoot = props.item.key === 'root'
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
@@ -121,27 +120,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
command: async () => {
|
||||
await workflowService.duplicateWorkflow(workflowStore.activeWorkflow!)
|
||||
},
|
||||
visible: isRoot
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
visible: isRoot
|
||||
},
|
||||
{
|
||||
label: t('menuLabels.Save'),
|
||||
icon: 'pi pi-save',
|
||||
command: async () => {
|
||||
await useCommandStore().execute('Comfy.SaveWorkflow')
|
||||
},
|
||||
visible: isRoot
|
||||
},
|
||||
{
|
||||
label: t('menuLabels.Save As'),
|
||||
icon: 'pi pi-save',
|
||||
command: async () => {
|
||||
await useCommandStore().execute('Comfy.SaveWorkflowAs')
|
||||
},
|
||||
visible: isRoot
|
||||
visible: props.item.key === 'root'
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
@@ -155,7 +134,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
visible: isRoot
|
||||
visible: props.item.key === 'root'
|
||||
},
|
||||
{
|
||||
label: t('breadcrumbsMenu.deleteWorkflow'),
|
||||
@@ -163,7 +142,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
command: async () => {
|
||||
await workflowService.deleteWorkflow(workflowStore.activeWorkflow!)
|
||||
},
|
||||
visible: isRoot
|
||||
visible: props.item.key === 'root'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -16,14 +16,6 @@ const meta: Meta<typeof IconButton> = {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent']
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
onClick: { action: 'clicked' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -11,7 +11,6 @@ import { computed } from 'vue'
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonTypeClasses,
|
||||
getIconButtonSizeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
@@ -23,8 +22,6 @@ interface IconButtonProps extends BaseButtonProps {
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
onClick
|
||||
} = defineProps<IconButtonProps>()
|
||||
@@ -32,9 +29,7 @@ const {
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} p-0`
|
||||
const sizeClasses = getIconButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -28,14 +28,6 @@ const meta: Meta<typeof IconTextButton> = {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent']
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
iconPosition: {
|
||||
control: { type: 'select' },
|
||||
options: ['left', 'right']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
|
||||
<span>{{ label }}</span>
|
||||
<slot v-if="iconPosition === 'right'" name="icon"></slot>
|
||||
@@ -13,7 +13,6 @@ import { computed } from 'vue'
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonSizeClasses,
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
@@ -27,8 +26,6 @@ interface IconTextButtonProps extends BaseButtonProps {
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'primary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
iconPosition = 'left',
|
||||
label,
|
||||
@@ -38,9 +35,7 @@ const {
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -16,14 +16,6 @@ const meta: Meta<typeof TextButton> = {
|
||||
options: ['sm', 'md'],
|
||||
defaultValue: 'md'
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
|
||||
<span>{{ label }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -11,7 +11,6 @@ import { computed } from 'vue'
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonSizeClasses,
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
@@ -24,8 +23,6 @@ interface TextButtonProps extends BaseButtonProps {
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'primary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
label,
|
||||
onClick
|
||||
@@ -34,9 +31,7 @@ const {
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = getBaseButtonClasses()
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center"
|
||||
:style="{ width: size + 'px', height: size + 'px' }"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
class="animate-spin"
|
||||
:style="{ animationDuration: duration }"
|
||||
>
|
||||
<g clip-path="url(#clip0_776_9582)">
|
||||
<!-- Top dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.25s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.5s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.75s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.125s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.625s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.875s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.375s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_776_9582">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const { size = 24, duration = '2s' } = defineProps<{
|
||||
size?: number
|
||||
duration?: string
|
||||
}>()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const color = computed(() =>
|
||||
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dot-animation {
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,7 +29,7 @@
|
||||
/>
|
||||
|
||||
<template v-if="item.footerComponent" #footer>
|
||||
<component :is="item.footerComponent" v-bind="item.footerProps" />
|
||||
<component :is="item.footerComponent" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -21,9 +21,16 @@
|
||||
@click="showReport"
|
||||
/>
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
v-show="!sendReportOpen"
|
||||
text
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
<Button
|
||||
v-if="authStore.currentUser"
|
||||
v-show="!reportOpen"
|
||||
text
|
||||
:label="$t('issueReport.contactSupportTitle')"
|
||||
@click="showContactSupport"
|
||||
/>
|
||||
</div>
|
||||
@@ -34,6 +41,16 @@
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
:title="$t('issueReport.submitErrorReport')"
|
||||
:error-type="error.reportType ?? 'unknownError'"
|
||||
:extra-fields="[stackTraceField]"
|
||||
:tags="{
|
||||
exceptionMessage: error.exceptionMessage,
|
||||
nodeType: error.nodeType ?? 'UNKNOWN'
|
||||
}"
|
||||
/>
|
||||
<div class="flex gap-4 justify-end">
|
||||
<FindIssueButton
|
||||
:error-message="error.exceptionMessage"
|
||||
@@ -64,12 +81,18 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { ReportField } from '@/types/issueReportTypes'
|
||||
import {
|
||||
type ErrorReportData,
|
||||
generateErrorReport
|
||||
} from '@/utils/errorReportUtil'
|
||||
|
||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const { error } = defineProps<{
|
||||
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
|
||||
/**
|
||||
@@ -91,6 +114,10 @@ const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const sendReportOpen = ref(false)
|
||||
const showSendReport = () => {
|
||||
sendReportOpen.value = true
|
||||
}
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
@@ -99,6 +126,15 @@ const title = computed<string>(
|
||||
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
|
||||
)
|
||||
|
||||
const stackTraceField = computed<ReportField>(() => {
|
||||
return {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
getData: () => error.traceback
|
||||
}
|
||||
})
|
||||
|
||||
const showContactSupport = async () => {
|
||||
await useCommandStore().execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
33
src/components/dialog/content/IssueReportDialogContent.vue
Normal file
33
src/components/dialog/content/IssueReportDialogContent.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="p-2 h-full" aria-labelledby="issue-report-title">
|
||||
<Panel
|
||||
:pt="{
|
||||
root: 'border-none',
|
||||
content: 'p-0'
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<header class="flex flex-col items-center w-full">
|
||||
<h2 id="issue-report-title" class="text-4xl">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
|
||||
</header>
|
||||
</template>
|
||||
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Panel from 'primevue/panel'
|
||||
|
||||
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
|
||||
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
panelProps: IssueReportPanelProps
|
||||
}>()
|
||||
</script>
|
||||
@@ -31,20 +31,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<div v-if="showManagerButtons" class="flex justify-end py-3">
|
||||
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
||||
<PackInstallButton
|
||||
v-if="showInstallAllButton"
|
||||
size="md"
|
||||
:disabled="
|
||||
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
||||
"
|
||||
:is-loading="isLoading"
|
||||
:disabled="isLoading || !!error || missingNodePacks.length === 0"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="
|
||||
isLoading
|
||||
? $t('manager.gettingInfo')
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
variant="black"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<Button label="Open Manager" size="small" outlined @click="openManager" />
|
||||
</div>
|
||||
@@ -54,39 +46,34 @@
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackInstallButton from './manager/button/PackInstallButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
// Check if any of the missing packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
if (!missingNodePacks.value?.length) return false
|
||||
return missingNodePacks.value.some((pack) =>
|
||||
comfyManagerStore.isPackInstalling(pack.id)
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
|
||||
@@ -111,47 +98,10 @@ const uniqueNodes = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const managerStateStore = useManagerStateStore()
|
||||
|
||||
// Show manager buttons unless manager is disabled
|
||||
const showManagerButtons = computed(() => {
|
||||
return managerStateStore.managerUIState !== ManagerUIState.DISABLED
|
||||
})
|
||||
|
||||
// Only show Install All button for NEW_UI (new manager with v4 support)
|
||||
const showInstallAllButton = computed(() => {
|
||||
return managerStateStore.managerUIState === ManagerUIState.NEW_UI
|
||||
})
|
||||
|
||||
const openManager = async () => {
|
||||
const state = managerStateStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
useDialogService().showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch {
|
||||
// If legacy command doesn't exist, show toast
|
||||
const { t } = useI18n()
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
useDialogService().showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
break
|
||||
}
|
||||
const openManager = () => {
|
||||
useDialogService().showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,20 +30,11 @@ const defaultMockTaskLogs = [
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
taskLogs: [...defaultMockTaskLogs],
|
||||
succeededTasksLogs: [...defaultMockTaskLogs],
|
||||
failedTasksLogs: [...defaultMockTaskLogs],
|
||||
managerQueue: { historyCount: 2 },
|
||||
isLoading: false
|
||||
taskLogs: [...defaultMockTaskLogs]
|
||||
})),
|
||||
useManagerProgressDialogStore: vi.fn(() => ({
|
||||
isExpanded: true,
|
||||
activeTabIndex: 0,
|
||||
getActiveTabIndex: vi.fn(() => 0),
|
||||
setActiveTabIndex: vi.fn(),
|
||||
toggle: vi.fn(),
|
||||
collapse: mockCollapse,
|
||||
expand: vi.fn()
|
||||
collapse: mockCollapse
|
||||
}))
|
||||
}))
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
'max-h-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div v-for="(log, index) in focusedLogs" :key="index">
|
||||
<div v-for="(panel, index) in taskPanels" :key="index">
|
||||
<Panel
|
||||
:expanded="collapsedPanels[index] === true"
|
||||
:expanded="collapsedPanels[index] || false"
|
||||
toggleable
|
||||
class="shadow-elevation-1 rounded-lg mt-2 dark-theme:bg-black dark-theme:border-black"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full py-2">
|
||||
<div class="flex flex-col text-sm font-medium leading-normal">
|
||||
<span>{{ log.taskName }}</span>
|
||||
<span>{{ panel.taskName }}</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
isInProgress(index)
|
||||
@@ -52,24 +52,24 @@
|
||||
</template>
|
||||
<div
|
||||
:ref="
|
||||
index === focusedLogs.length - 1
|
||||
index === taskPanels.length - 1
|
||||
? (el) => (lastPanelRef = el as HTMLElement)
|
||||
: undefined
|
||||
"
|
||||
class="overflow-y-auto h-64 rounded-lg bg-black"
|
||||
:class="{
|
||||
'h-64': index !== focusedLogs.length - 1,
|
||||
'flex-grow': index === focusedLogs.length - 1
|
||||
'h-64': index !== taskPanels.length - 1,
|
||||
'flex-grow': index === taskPanels.length - 1
|
||||
}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-for="(logLine, logIndex) in log.logs"
|
||||
v-for="(log, logIndex) in panel.logs"
|
||||
:key="logIndex"
|
||||
class="text-neutral-400 dark-theme:text-muted"
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ logLine }}</pre>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,31 +90,14 @@ import {
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { taskLogs } = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isInProgress = (index: number) => {
|
||||
const log = focusedLogs.value[index]
|
||||
if (!log) return false
|
||||
const isInProgress = (index: number) =>
|
||||
index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0
|
||||
|
||||
// Check if this task is in the running or pending queue
|
||||
const taskQueue = comfyManagerStore.taskQueue
|
||||
if (!taskQueue) return false
|
||||
|
||||
const allQueueTasks = [
|
||||
...(taskQueue.running_queue || []),
|
||||
...(taskQueue.pending_queue || [])
|
||||
]
|
||||
|
||||
return allQueueTasks.some((task) => task.ui_id === log.taskId)
|
||||
}
|
||||
|
||||
const focusedLogs = computed(() => {
|
||||
if (progressDialogContent.getActiveTabIndex() === 0) {
|
||||
return comfyManagerStore.succeededTasksLogs
|
||||
}
|
||||
return comfyManagerStore.failedTasksLogs
|
||||
})
|
||||
const taskPanels = computed(() => taskLogs)
|
||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
||||
const isCollapsed = computed(() => !isExpanded.value)
|
||||
|
||||
@@ -132,7 +115,7 @@ const { y: scrollY } = useScroll(sectionsContainerRef, {
|
||||
|
||||
const lastPanelRef = ref<HTMLElement | null>(null)
|
||||
const isUserScrolling = ref(false)
|
||||
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
|
||||
const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs)
|
||||
|
||||
const isAtBottom = (el: HTMLElement | null) => {
|
||||
if (!el) return false
|
||||
|
||||
338
src/components/dialog/content/error/ReportIssuePanel.spec.ts
Normal file
338
src/components/dialog/content/error/ReportIssuePanel.spec.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMesages from '@/locales/en/main.json'
|
||||
import { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from './ReportIssuePanel.vue'
|
||||
|
||||
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
|
||||
const CUSTOM_FIELDS = [
|
||||
{
|
||||
label: 'Custom Field',
|
||||
value: 'CustomField',
|
||||
optIn: true,
|
||||
getData: () => 'mock data'
|
||||
}
|
||||
]
|
||||
|
||||
async function getSubmittedContext() {
|
||||
const { captureMessage } = (await import('@sentry/core')) as any
|
||||
return captureMessage.mock.calls[0][1]
|
||||
}
|
||||
|
||||
async function submitForm(wrapper: any) {
|
||||
await wrapper.findComponent(Form).trigger('submit')
|
||||
return getSubmittedContext()
|
||||
}
|
||||
|
||||
async function findAndUpdateCheckbox(
|
||||
wrapper: any,
|
||||
value: string,
|
||||
checked = true
|
||||
) {
|
||||
const checkbox = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.find((c: any) => c.props('value') === value)
|
||||
if (!checkbox) throw new Error(`Checkbox with value "${value}" not found`)
|
||||
|
||||
await checkbox.vm.$emit('update:modelValue', checked)
|
||||
return checkbox
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMesages
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: vi.fn(() => ({
|
||||
add: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getLogs: vi.fn().mockResolvedValue('mock logs'),
|
||||
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
|
||||
getSettings: vi.fn().mockResolvedValue('mock settings'),
|
||||
fetchApi: vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({}),
|
||||
text: vi.fn().mockResolvedValue('')
|
||||
}),
|
||||
apiURL: vi.fn().mockReturnValue('https://test.com')
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
asSerialisable: vi.fn().mockReturnValue({})
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@sentry/core', () => ({
|
||||
captureMessage: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@primevue/forms', () => ({
|
||||
Form: {
|
||||
name: 'Form',
|
||||
template:
|
||||
'<form @submit.prevent="onSubmit"><slot :values="formValues" /></form>',
|
||||
props: ['resolver'],
|
||||
data() {
|
||||
return {
|
||||
formValues: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.$emit('submit', {
|
||||
valid: true,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
values: this.formValues
|
||||
})
|
||||
},
|
||||
updateFieldValue(name: string, value: any) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.formValues[name] = value
|
||||
}
|
||||
}
|
||||
},
|
||||
FormField: {
|
||||
name: 'FormField',
|
||||
template:
|
||||
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
|
||||
props: ['name'],
|
||||
data() {
|
||||
return {
|
||||
modelValue: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
updateValue(value) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.modelValue = value
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let parent = this.$parent
|
||||
while (parent && parent.$options.name !== 'Form') {
|
||||
parent = parent.$parent
|
||||
}
|
||||
if (parent) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
parent.updateFieldValue(this.name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
currentUser: {
|
||||
email: 'test@example.com'
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
})
|
||||
|
||||
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
|
||||
return mount(ReportIssuePanel, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n, createPinia()],
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the panel with all required components', () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
expect(wrapper.find('.p-panel').exists()).toBe(true)
|
||||
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates selection when checkboxes are selected', async () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error'
|
||||
})
|
||||
|
||||
const checkboxes = wrapper.findAllComponents(Checkbox)
|
||||
|
||||
for (const field of DEFAULT_FIELDS) {
|
||||
const checkbox = checkboxes.find(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(checkbox) => checkbox.props('value') === field
|
||||
)
|
||||
expect(checkbox).toBeDefined()
|
||||
|
||||
await checkbox?.vm.$emit('update:modelValue', [field])
|
||||
expect(wrapper.vm.selection).toContain(field)
|
||||
}
|
||||
})
|
||||
|
||||
it('updates contactInfo when input is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const input = wrapper.findComponent(InputText)
|
||||
|
||||
await input.vm.$emit('update:modelValue', 'test@example.com')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.user.email).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('updates additional details when textarea is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
|
||||
await textarea.vm.$emit('update:modelValue', 'This is a test detail.')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra.details).toBe('This is a test detail.')
|
||||
})
|
||||
|
||||
it('set contact preferences back to false if email is removed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const input = wrapper.findComponent(InputText)
|
||||
|
||||
// Set a valid email, enabling the contact preferences to be changed
|
||||
await input.vm.$emit('update:modelValue', 'name@example.com')
|
||||
|
||||
// Enable both contact preferences
|
||||
for (const pref of ['followUp', 'notifyOnResolution']) {
|
||||
await findAndUpdateCheckbox(wrapper, pref)
|
||||
}
|
||||
|
||||
// Change the email back to empty
|
||||
await input.vm.$emit('update:modelValue', '')
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
// Check that the contact preferences are back to false automatically
|
||||
expect(context.tags.followUp).toBe(false)
|
||||
expect(context.tags.notifyOnResolution).toBe(false)
|
||||
})
|
||||
|
||||
it('renders with overridden default fields', () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
defaultFields: ['Settings']
|
||||
})
|
||||
|
||||
// Filter out the contact preferences checkboxes
|
||||
const fieldCheckboxes = wrapper.findAllComponents(Checkbox).filter(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(checkbox) =>
|
||||
!['followUp', 'notifyOnResolution'].includes(checkbox.props('value'))
|
||||
)
|
||||
expect(fieldCheckboxes.length).toBe(1)
|
||||
expect(fieldCheckboxes.at(0)?.props('value')).toBe('Settings')
|
||||
})
|
||||
|
||||
it('renders additional fields when extraFields prop is provided', () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
extraFields: CUSTOM_FIELDS
|
||||
})
|
||||
const customCheckbox = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
.find((checkbox) => checkbox.props('value') === 'CustomField')
|
||||
expect(customCheckbox).toBeDefined()
|
||||
})
|
||||
|
||||
it('allows custom fields to be selected', async () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
extraFields: CUSTOM_FIELDS
|
||||
})
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, 'CustomField')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra.CustomField).toBe('mock data')
|
||||
})
|
||||
|
||||
it('does not submit unchecked fields', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
|
||||
// Set details but don't check any field checkboxes
|
||||
await textarea.vm.$emit(
|
||||
'update:modelValue',
|
||||
'Report with only text but no fields selected'
|
||||
)
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
// Verify none of the optional fields were included
|
||||
for (const field of DEFAULT_FIELDS) {
|
||||
expect(context.extra[field]).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
checkbox: 'Logs',
|
||||
apiMethod: 'getLogs',
|
||||
expectedKey: 'Logs',
|
||||
mockValue: 'mock logs'
|
||||
},
|
||||
{
|
||||
checkbox: 'SystemStats',
|
||||
apiMethod: 'getSystemStats',
|
||||
expectedKey: 'SystemStats',
|
||||
mockValue: 'mock stats'
|
||||
},
|
||||
{
|
||||
checkbox: 'Settings',
|
||||
apiMethod: 'getSettings',
|
||||
expectedKey: 'Settings',
|
||||
mockValue: 'mock settings'
|
||||
}
|
||||
])(
|
||||
'submits $checkbox data when checkbox is selected',
|
||||
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { api } = (await import('@/scripts/api')) as any
|
||||
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, checkbox)
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra[expectedKey]).toBe(mockValue)
|
||||
}
|
||||
)
|
||||
|
||||
it('submits workflow when the Workflow checkbox is selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { app } = (await import('@/scripts/app')) as any
|
||||
const mockWorkflow = { nodes: [], edges: [] }
|
||||
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, 'Workflow')
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
expect(context.extra.Workflow).toEqual(mockWorkflow)
|
||||
})
|
||||
})
|
||||
348
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
348
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
:resolver="zodResolver(issueReportSchema)"
|
||||
@submit="submit"
|
||||
>
|
||||
<Panel :pt="$attrs.pt as any">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-4">
|
||||
<Button
|
||||
v-tooltip="!submitted ? $t('g.reportIssueTooltip') : undefined"
|
||||
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
|
||||
:severity="submitted ? 'secondary' : 'primary'"
|
||||
:icon="submitted ? 'pi pi-check' : 'pi pi-send'"
|
||||
:disabled="submitted"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 mt-2 border border-round surface-border shadow-1">
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormField
|
||||
v-slot="$field"
|
||||
name="contactInfo"
|
||||
:initial-value="authStore.currentUser?.email"
|
||||
>
|
||||
<div class="self-stretch inline-flex justify-start items-center">
|
||||
<label for="contactInfo" class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.email')
|
||||
}}</label>
|
||||
</div>
|
||||
<InputText
|
||||
id="contactInfo"
|
||||
v-bind="$field"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched && $field.value !== ''"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.invalidEmail') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="$field" name="helpType">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2.5"
|
||||
>
|
||||
<label for="helpType" class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.whatDoYouNeedHelpWith')
|
||||
}}</label>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-bind="$field"
|
||||
v-model="$field.value"
|
||||
:options="helpTypes"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:placeholder="$t('issueReport.selectIssue')"
|
||||
class="w-full"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.selectIssueType') }}
|
||||
</Message>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2.5"
|
||||
>
|
||||
<span class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.whatCanWeInclude')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3">
|
||||
<div v-for="field in fields" :key="field.value">
|
||||
<FormField
|
||||
v-if="field.optIn"
|
||||
v-slot="$field"
|
||||
:name="field.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
v-model="selection"
|
||||
:input-id="field.value"
|
||||
:value="field.value"
|
||||
/>
|
||||
<label :for="field.value">{{ field.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<FormField v-slot="$field" name="details">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2.5"
|
||||
>
|
||||
<label for="details" class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.describeTheProblem')
|
||||
}}</label>
|
||||
</div>
|
||||
<Textarea
|
||||
v-bind="$field"
|
||||
id="details"
|
||||
class="w-full"
|
||||
rows="5"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{
|
||||
$field.value
|
||||
? t('issueReport.validation.maxLength')
|
||||
: t('issueReport.validation.descriptionRequired')
|
||||
}}
|
||||
</Message>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 mt-2">
|
||||
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
|
||||
<FormField
|
||||
v-slot="$field"
|
||||
:name="checkbox.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
v-model="contactPrefs"
|
||||
:input-id="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
:disabled="
|
||||
$form.contactInfo?.error || !$form.contactInfo?.value
|
||||
"
|
||||
/>
|
||||
<label :for="checkbox.value">{{ checkbox.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { cloneDeep } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
type IssueReportFormData,
|
||||
issueReportSchema
|
||||
} from '@/schemas/issueReportSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type {
|
||||
DefaultField,
|
||||
IssueReportPanelProps,
|
||||
ReportField
|
||||
} from '@/types/issueReportTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
const DEFAULT_ISSUE_NAME = 'User reported issue'
|
||||
|
||||
const props = defineProps<IssueReportPanelProps>()
|
||||
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
|
||||
props
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const selection = ref<string[]>([])
|
||||
const contactPrefs = ref<string[]>([])
|
||||
const submitted = ref(false)
|
||||
|
||||
const contactCheckboxes = [
|
||||
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
|
||||
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
|
||||
]
|
||||
|
||||
const helpTypes = [
|
||||
{
|
||||
label: t('issueReport.helpTypes.billingPayments'),
|
||||
value: 'billingPayments'
|
||||
},
|
||||
{
|
||||
label: t('issueReport.helpTypes.loginAccessIssues'),
|
||||
value: 'loginAccessIssues'
|
||||
},
|
||||
{ label: t('issueReport.helpTypes.giveFeedback'), value: 'giveFeedback' },
|
||||
{ label: t('issueReport.helpTypes.bugReport'), value: 'bugReport' },
|
||||
{ label: t('issueReport.helpTypes.somethingElse'), value: 'somethingElse' }
|
||||
]
|
||||
|
||||
const defaultFieldsConfig: ReportField[] = [
|
||||
{
|
||||
label: t('issueReport.systemStats'),
|
||||
value: 'SystemStats',
|
||||
getData: () => api.getSystemStats(),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.workflow'),
|
||||
value: 'Workflow',
|
||||
getData: () => cloneDeep(app.graph.asSerialisable()),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.logs'),
|
||||
value: 'Logs',
|
||||
getData: () => api.getLogs(),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.settings'),
|
||||
value: 'Settings',
|
||||
getData: () => api.getSettings(),
|
||||
optIn: true
|
||||
}
|
||||
]
|
||||
|
||||
const fields = computed(() => [
|
||||
...defaultFieldsConfig.filter(({ value }) =>
|
||||
defaultFields.includes(value as DefaultField)
|
||||
),
|
||||
...(props.extraFields ?? [])
|
||||
])
|
||||
|
||||
const createUser = (formData: IssueReportFormData): User => ({
|
||||
email: formData.contactInfo || undefined
|
||||
})
|
||||
|
||||
const createExtraData = async (formData: IssueReportFormData) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
const isChecked = (fieldValue: string) => formData[fieldValue]
|
||||
|
||||
await Promise.all(
|
||||
fields.value
|
||||
.filter((field) => !field.optIn || isChecked(field.value))
|
||||
.map(async (field) => {
|
||||
try {
|
||||
result[field.value] = await field.getData()
|
||||
} catch (error) {
|
||||
console.error(`Failed to collect ${field.value}:`, error)
|
||||
result[field.value] = { error: String(error) }
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const createCaptureContext = async (
|
||||
formData: IssueReportFormData
|
||||
): Promise<CaptureContext> => {
|
||||
return {
|
||||
user: createUser(formData),
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType,
|
||||
helpType: formData.helpType,
|
||||
followUp: formData.contactInfo ? formData.followUp : false,
|
||||
notifyOnResolution: formData.contactInfo
|
||||
? formData.notifyOnResolution
|
||||
: false,
|
||||
isElectron: isElectron(),
|
||||
..._.mapValues(props.tags, (tag) => _.trim(tag).replace(/[\n\r\t]/g, ' '))
|
||||
},
|
||||
extra: {
|
||||
details: formData.details,
|
||||
...(await createExtraData(formData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateUniqueTicketId = (type: string) => `${type}-${generateUUID()}`
|
||||
|
||||
const submit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
try {
|
||||
const captureContext = await createCaptureContext(event.values)
|
||||
|
||||
// If it's billing or access issue, generate unique id to be used by customer service ticketing
|
||||
const isValidContactInfo = event.values.contactInfo?.length
|
||||
const isCustomerServiceIssue =
|
||||
isValidContactInfo &&
|
||||
['billingPayments', 'loginAccessIssues'].includes(
|
||||
event.values.helpType || ''
|
||||
)
|
||||
const issueName = isCustomerServiceIssue
|
||||
? `ticket-${generateUniqueTicketId(event.values.helpType || '')}`
|
||||
: DEFAULT_ISSUE_NAME
|
||||
captureMessage(issueName, captureContext)
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -26,35 +26,6 @@
|
||||
}"
|
||||
>
|
||||
<div class="px-6 flex flex-col h-full">
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<p class="text-sm font-bold m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.title') }}
|
||||
</p>
|
||||
<p class="text-xs m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.message') }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm font-bold m-0 cursor-pointer"
|
||||
@click="onClickWarningLink"
|
||||
>
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 w-6 h-6 border-none outline-none bg-transparent flex items-center justify-center text-yellow-600 rounded transition-colors"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:searchQuery="searchQuery"
|
||||
v-model:searchMode="searchMode"
|
||||
@@ -63,7 +34,6 @@
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
:sort-options="sortOptions"
|
||||
:is-update-available-tab="isUpdateAvailableTab"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
@@ -99,9 +69,7 @@
|
||||
:is-selected="
|
||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||
"
|
||||
@click.stop="
|
||||
(event: MouseEvent) => selectNodePack(item, event)
|
||||
"
|
||||
@click.stop="(event) => selectNodePack(item, event)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
@@ -133,8 +101,7 @@ import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect
|
||||
watch
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -152,7 +119,6 @@ import { useManagerStatePersistence } from '@/composables/manager/useManagerStat
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
@@ -167,13 +133,12 @@ const { initialTab } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { getPackById } = useComfyRegistryStore()
|
||||
const conflictAcknowledgment = useConflictAcknowledgment()
|
||||
const persistedState = useManagerStatePersistence()
|
||||
const initialState = persistedState.loadStoredState()
|
||||
|
||||
const GRID_STYLE = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(17rem, 1fr))',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(19rem, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '1.5rem'
|
||||
} as const
|
||||
@@ -184,13 +149,6 @@ const {
|
||||
toggle: toggleSideNav
|
||||
} = useResponsiveCollapse()
|
||||
|
||||
// Use conflict acknowledgment state from composable
|
||||
const {
|
||||
shouldShowManagerBanner,
|
||||
dismissWarningBanner,
|
||||
dismissRedDotNotification
|
||||
} = conflictAcknowledgment
|
||||
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
|
||||
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi-box' },
|
||||
@@ -354,13 +312,6 @@ watch([isAllTab, searchResults], () => {
|
||||
displayPacks.value = searchResults.value
|
||||
})
|
||||
|
||||
const onClickWarningLink = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/troubleshooting/custom-node-issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
const onResultsChange = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
case ManagerTab.Installed:
|
||||
@@ -521,10 +472,6 @@ watch([searchQuery, selectedTab], () => {
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
dismissRedDotNotification()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
persistedState.persistState({
|
||||
selectedTabId: selectedTab.value?.id,
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import ManagerHeader from './ManagerHeader.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMessages
|
||||
}
|
||||
})
|
||||
|
||||
describe('ManagerHeader', () => {
|
||||
const createWrapper = () => {
|
||||
return mount(ManagerHeader, {
|
||||
global: {
|
||||
plugins: [createPinia(), PrimeVue, i18n],
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
components: {
|
||||
Tag
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the component title', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('h2').text()).toBe(
|
||||
enMessages.manager.discoverCommunityContent
|
||||
)
|
||||
})
|
||||
|
||||
it('displays the legacy manager UI tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
|
||||
})
|
||||
|
||||
it('applies info severity to the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('p-tag-info')
|
||||
})
|
||||
|
||||
it('displays info icon in the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const icon = wrapper.find('.pi-info-circle')
|
||||
expect(icon.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has cursor-help class on the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('cursor-help')
|
||||
})
|
||||
|
||||
it('has proper structure with flex container', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
|
||||
expect(flexContainer.exists()).toBe(true)
|
||||
|
||||
const tag = flexContainer.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -4,22 +4,6 @@
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
<div class="flex justify-end ml-auto pr-4 pl-2">
|
||||
<Tag
|
||||
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
:value="$t('manager.legacyManagerUI')"
|
||||
class="cursor-help ml-2"
|
||||
:pt="{
|
||||
root: { class: 'text-xs' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
</script>
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
<template>
|
||||
<div class="w-[552px] flex flex-col">
|
||||
<ContentDivider :width="1" />
|
||||
<div class="px-4 py-6 w-full h-full flex flex-col gap-2">
|
||||
<!-- Description -->
|
||||
<div v-if="showAfterWhatsNew">
|
||||
<p
|
||||
class="text-sm leading-4 text-neutral-800 dark-theme:text-white m-0 mb-4"
|
||||
>
|
||||
{{ $t('manager.conflicts.description') }}
|
||||
<br /><br />
|
||||
{{ $t('manager.conflicts.info') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Import Failed List Wrapper -->
|
||||
<div
|
||||
v-if="importFailedConflicts.length > 0"
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleImportFailedPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ importFailedConflicts.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.importFailedExtensions') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
importFailedExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Import failed list -->
|
||||
<div
|
||||
v-if="importFailedExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="(packageName, i) in importFailedConflicts"
|
||||
:key="i"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
|
||||
{{ packageName }}
|
||||
</span>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Conflict List Wrapper -->
|
||||
<div
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleConflictsPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ allConflictDetails.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.conflicts') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
conflictsExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Conflicts list -->
|
||||
<div
|
||||
v-if="conflictsExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="(conflict, i) in allConflictDetails"
|
||||
:key="i"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span
|
||||
class="text-xs text-neutral-600 dark-theme:text-neutral-300"
|
||||
>{{ getConflictMessage(conflict, t) }}</span
|
||||
>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Extension List Wrapper -->
|
||||
<div
|
||||
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
|
||||
@click="toggleExtensionsPanel"
|
||||
>
|
||||
<div class="flex-1 flex">
|
||||
<span
|
||||
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
|
||||
>{{ conflictData.length }}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
|
||||
>{{ $t('manager.conflicts.extensionAtRisk') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="
|
||||
extensionsExpanded
|
||||
? 'pi pi-chevron-down text-xs'
|
||||
: 'pi pi-chevron-right text-xs'
|
||||
"
|
||||
text
|
||||
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Extension list -->
|
||||
<div
|
||||
v-if="extensionsExpanded"
|
||||
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
v-for="conflictResult in conflictData"
|
||||
:key="conflictResult.package_id"
|
||||
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
|
||||
>
|
||||
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
|
||||
{{ conflictResult.package_name }}
|
||||
</span>
|
||||
<span class="pi pi-info-circle text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContentDivider :width="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { filter, flatMap, map, some } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import {
|
||||
ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
const { showAfterWhatsNew = false, conflictedPackages } = defineProps<{
|
||||
showAfterWhatsNew?: boolean
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { conflictedPackages: globalConflictPackages } = useConflictDetection()
|
||||
|
||||
const conflictsExpanded = ref<boolean>(false)
|
||||
const extensionsExpanded = ref<boolean>(false)
|
||||
const importFailedExpanded = ref<boolean>(false)
|
||||
|
||||
const conflictData = computed(
|
||||
() => conflictedPackages || globalConflictPackages.value
|
||||
)
|
||||
|
||||
const allConflictDetails = computed(() => {
|
||||
const allConflicts = flatMap(
|
||||
conflictData.value,
|
||||
(result: ConflictDetectionResult) => result.conflicts
|
||||
)
|
||||
return filter(
|
||||
allConflicts,
|
||||
(conflict: ConflictDetail) => conflict.type !== 'import_failed'
|
||||
)
|
||||
})
|
||||
|
||||
const packagesWithImportFailed = computed(() => {
|
||||
return filter(conflictData.value, (result: ConflictDetectionResult) =>
|
||||
some(
|
||||
result.conflicts,
|
||||
(conflict: ConflictDetail) => conflict.type === 'import_failed'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const importFailedConflicts = computed(() => {
|
||||
return map(
|
||||
packagesWithImportFailed.value,
|
||||
(result: ConflictDetectionResult) =>
|
||||
result.package_name || result.package_id
|
||||
)
|
||||
})
|
||||
|
||||
const toggleImportFailedPanel = () => {
|
||||
importFailedExpanded.value = !importFailedExpanded.value
|
||||
conflictsExpanded.value = false
|
||||
extensionsExpanded.value = false
|
||||
}
|
||||
|
||||
const toggleConflictsPanel = () => {
|
||||
conflictsExpanded.value = !conflictsExpanded.value
|
||||
extensionsExpanded.value = false
|
||||
importFailedExpanded.value = false
|
||||
}
|
||||
|
||||
const toggleExtensionsPanel = () => {
|
||||
extensionsExpanded.value = !extensionsExpanded.value
|
||||
conflictsExpanded.value = false
|
||||
importFailedExpanded.value = false
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.conflict-list-item:hover {
|
||||
background-color: rgba(0, 122, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full px-3 py-4">
|
||||
<div class="w-full flex items-center justify-between gap-2 pr-1">
|
||||
<Button
|
||||
:label="$t('manager.conflicts.conflictInfoTitle')"
|
||||
text
|
||||
severity="secondary"
|
||||
size="small"
|
||||
icon="pi pi-info-circle"
|
||||
:pt="{
|
||||
label: { class: 'text-sm' }
|
||||
}"
|
||||
@click="handleConflictInfoClick"
|
||||
/>
|
||||
<Button
|
||||
v-if="props.buttonText"
|
||||
:label="props.buttonText"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
@click="handleButtonClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
interface Props {
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttonText: undefined,
|
||||
onButtonClick: undefined
|
||||
})
|
||||
const dialogStore = useDialogStore()
|
||||
const handleConflictInfoClick = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/troubleshooting/custom-node-issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
const handleButtonClick = () => {
|
||||
// Close the conflict dialog
|
||||
dialogStore.closeDialog({ key: 'global-node-conflict' })
|
||||
// Execute the custom button action if provided
|
||||
if (props.onButtonClick) {
|
||||
props.onButtonClick()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<div class="h-12 flex items-center justify-between w-full pl-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Warning Icon -->
|
||||
<i class="pi pi-exclamation-triangle text-lg"></i>
|
||||
<!-- Title -->
|
||||
<p class="text-base font-bold">
|
||||
{{ $t('manager.conflicts.title') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,10 +17,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { computed, inject } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
type PackStatus = components['schemas']['NodeStatus']
|
||||
@@ -33,15 +32,10 @@ type StatusProps = {
|
||||
severity: MessageSeverity
|
||||
}
|
||||
|
||||
const { statusType, hasCompatibilityIssues } = defineProps<{
|
||||
const { statusType } = defineProps<{
|
||||
statusType: Status
|
||||
hasCompatibilityIssues?: boolean
|
||||
}>()
|
||||
|
||||
// Inject import failed context from parent
|
||||
const importFailedContext = inject(ImportFailedKey)
|
||||
const importFailed = importFailedContext?.importFailed
|
||||
|
||||
const statusPropsMap: Record<Status, StatusProps> = {
|
||||
NodeStatusActive: {
|
||||
label: 'active',
|
||||
@@ -77,13 +71,10 @@ const statusPropsMap: Record<Status, StatusProps> = {
|
||||
}
|
||||
}
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (importFailed?.value) return 'importFailed'
|
||||
if (hasCompatibilityIssues) return 'conflicting'
|
||||
return statusPropsMap[statusType]?.label || 'unknown'
|
||||
})
|
||||
const statusSeverity = computed(() => {
|
||||
if (hasCompatibilityIssues || importFailed?.value) return 'error'
|
||||
return statusPropsMap[statusType]?.severity || 'secondary'
|
||||
})
|
||||
const statusLabel = computed(
|
||||
() => statusPropsMap[statusType]?.label || 'unknown'
|
||||
)
|
||||
const statusSeverity = computed(
|
||||
() => statusPropsMap[statusType]?.severity || 'secondary'
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -6,18 +6,11 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Mock config to prevent __COMFYUI_FRONTEND_VERSION__ error
|
||||
vi.mock('@/config', () => ({
|
||||
default: {
|
||||
app_title: 'ComfyUI',
|
||||
app_version: '1.0.0'
|
||||
}
|
||||
}))
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
@@ -127,7 +120,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
|
||||
it('falls back to NIGHTLY when nodePack.id is missing', () => {
|
||||
@@ -141,7 +134,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
|
||||
it('toggles the popover when button is clicked', async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill }"
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer px-2 py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700': fill }"
|
||||
aria-haspopup="true"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -12,16 +12,17 @@
|
||||
>
|
||||
<i
|
||||
v-if="isUpdateAvailable"
|
||||
class="pi pi-arrow-circle-up text-blue-600 text-xs"
|
||||
class="pi pi-arrow-circle-up text-blue-600"
|
||||
style="font-size: 8px"
|
||||
/>
|
||||
<span>{{ installedVersion }}</span>
|
||||
<i class="pi pi-chevron-right text-xxs" />
|
||||
<i class="pi pi-chevron-right" style="font-size: 8px" />
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
:pt="{
|
||||
content: { class: 'p-0 shadow-lg' }
|
||||
content: { class: 'px-0' }
|
||||
}"
|
||||
>
|
||||
<PackVersionSelectorPopover
|
||||
@@ -41,7 +42,8 @@ import { computed, ref, watch } from 'vue'
|
||||
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const TRUNCATED_HASH_LENGTH = 7
|
||||
@@ -62,11 +64,11 @@ const popoverRef = ref()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedVersion = computed(() => {
|
||||
if (!nodePack.id) return 'nightly'
|
||||
if (!nodePack.id) return SelectedVersion.NIGHTLY
|
||||
const version =
|
||||
managerStore.installedPacks[nodePack.id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
'nightly'
|
||||
SelectedVersion.NIGHTLY
|
||||
|
||||
// If Git hash, truncate to 7 characters
|
||||
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
|
||||
|
||||
@@ -3,32 +3,18 @@ import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import Select from 'primevue/select'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
// SelectedVersion is now using direct strings instead of enum
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Default mock versions for reference
|
||||
const defaultMockVersions = [
|
||||
{
|
||||
version: '1.0.0',
|
||||
createdAt: '2023-01-01',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
},
|
||||
{ version: '1.0.0', createdAt: '2023-01-01' },
|
||||
{ version: '0.9.0', createdAt: '2022-12-01' },
|
||||
{ version: '0.8.0', createdAt: '2022-11-01' }
|
||||
]
|
||||
@@ -36,24 +22,13 @@ const defaultMockVersions = [
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
},
|
||||
repository: 'https://github.com/user/repo',
|
||||
has_registry_data: true
|
||||
latest_version: { version: '1.0.0' },
|
||||
repository: 'https://github.com/user/repo'
|
||||
}
|
||||
|
||||
// Create mock functions
|
||||
const mockGetPackVersions = vi.fn()
|
||||
const mockInstallPack = vi.fn().mockResolvedValue(undefined)
|
||||
const mockCheckNodeCompatibility = vi.fn()
|
||||
|
||||
// Mock the registry service
|
||||
vi.mock('@/services/comfyRegistryService', () => ({
|
||||
@@ -74,13 +49,6 @@ vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the conflict detection composable
|
||||
vi.mock('@/composables/useConflictDetection', () => ({
|
||||
useConflictDetection: vi.fn(() => ({
|
||||
checkNodeCompatibility: mockCheckNodeCompatibility
|
||||
}))
|
||||
}))
|
||||
|
||||
const waitForPromises = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
await nextTick()
|
||||
@@ -91,9 +59,6 @@ describe('PackVersionSelectorPopover', () => {
|
||||
vi.clearAllMocks()
|
||||
mockGetPackVersions.mockReset()
|
||||
mockInstallPack.mockReset().mockResolvedValue(undefined)
|
||||
mockCheckNodeCompatibility
|
||||
.mockReset()
|
||||
.mockReturnValue({ hasConflict: false, conflicts: [] })
|
||||
})
|
||||
|
||||
const mountComponent = ({
|
||||
@@ -113,12 +78,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: {
|
||||
Listbox,
|
||||
VerifiedIcon,
|
||||
Select
|
||||
},
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
Listbox
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -160,15 +120,14 @@ describe('PackVersionSelectorPopover', () => {
|
||||
|
||||
const options = listbox.props('options')!
|
||||
// Check that we have both special options and version options
|
||||
// Latest version (1.0.0) should be excluded from the version list to avoid duplication
|
||||
expect(options.length).toBe(defaultMockVersions.length + 1) // 2 special options + version options minus 1 duplicate
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
|
||||
|
||||
// Check that special options exist
|
||||
expect(options.some((o) => o.value === 'nightly')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'latest')).toBe(true)
|
||||
expect(options.some((o) => o.value === SelectedVersion.NIGHTLY)).toBe(true)
|
||||
expect(options.some((o) => o.value === SelectedVersion.LATEST)).toBe(true)
|
||||
|
||||
// Check that version options exist (excluding latest version 1.0.0)
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(false) // Should be excluded as it's the latest
|
||||
// Check that version options exist
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
|
||||
expect(options.some((o) => o.value === '0.9.0')).toBe(true)
|
||||
expect(options.some((o) => o.value === '0.8.0')).toBe(true)
|
||||
})
|
||||
@@ -345,7 +304,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
|
||||
it('defaults to nightly when publisher name is "Unclaimed"', async () => {
|
||||
@@ -366,343 +325,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
})
|
||||
|
||||
describe('version compatibility checking', () => {
|
||||
it('shows warning icon for incompatible versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return conflict for specific version
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.supported_os?.includes('linux')) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'os',
|
||||
current_value: 'windows',
|
||||
required_value: 'linux'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['linux'],
|
||||
supported_accelerators: ['CUDA']
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for incompatible versions
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows verified icon for compatible versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return no conflicts
|
||||
mockCheckNodeCompatibility.mockReturnValue({
|
||||
hasConflict: false,
|
||||
conflicts: []
|
||||
})
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The verified icon should be shown for compatible versions
|
||||
// Look for the VerifiedIcon component or SVG elements
|
||||
const verifiedIcons = wrapper.findAll('svg')
|
||||
expect(verifiedIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('calls checkVersionCompatibility with correct version data', async () => {
|
||||
// Set up the mock for versions with specific supported data
|
||||
const versionsWithCompatibility = [
|
||||
{
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CUDA', 'CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0'
|
||||
}
|
||||
]
|
||||
mockGetPackVersions.mockResolvedValueOnce(versionsWithCompatibility)
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'], // latest_version data takes precedence
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Clear previous calls from component mounting/rendering
|
||||
mockCheckNodeCompatibility.mockClear()
|
||||
|
||||
// Trigger compatibility check by accessing getVersionCompatibility
|
||||
const vm = wrapper.vm as any
|
||||
vm.getVersionCompatibility('1.0.0')
|
||||
|
||||
// Verify that checkNodeCompatibility was called with correct data
|
||||
// Since 1.0.0 is the latest version, it should use latest_version data
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows', 'linux'],
|
||||
supported_accelerators: ['CPU'], // latest_version data takes precedence
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true,
|
||||
version: '1.0.0'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows version conflict warnings for ComfyUI and frontend versions', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return version conflicts
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
const conflicts = []
|
||||
if (versionData.supported_comfyui_version) {
|
||||
conflicts.push({
|
||||
type: 'comfyui_version',
|
||||
current_value: '0.5.0',
|
||||
required_value: versionData.supported_comfyui_version
|
||||
})
|
||||
}
|
||||
if (versionData.supported_comfyui_frontend_version) {
|
||||
conflicts.push({
|
||||
type: 'frontend_version',
|
||||
current_value: '1.0.0',
|
||||
required_value: versionData.supported_comfyui_frontend_version
|
||||
})
|
||||
}
|
||||
return {
|
||||
hasConflict: conflicts.length > 0,
|
||||
conflicts
|
||||
}
|
||||
})
|
||||
|
||||
const nodePackWithVersionRequirements = {
|
||||
...mockNodePack,
|
||||
supported_comfyui_version: '>=1.0.0',
|
||||
supported_comfyui_frontend_version: '>=2.0.0'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithVersionRequirements }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for version incompatible packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('handles latest and nightly versions using nodePack data', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
const nodePackWithCompatibility = {
|
||||
...mockNodePack,
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
supported_os: ['windows'], // Match nodePack data for test consistency
|
||||
supported_accelerators: ['CPU'], // Match nodePack data for test consistency
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: nodePackWithCompatibility }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
const vm = wrapper.vm as any
|
||||
|
||||
// Clear previous calls from component mounting/rendering
|
||||
mockCheckNodeCompatibility.mockClear()
|
||||
|
||||
// Test latest version
|
||||
vm.getVersionCompatibility('latest')
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true,
|
||||
version: '1.0.0'
|
||||
})
|
||||
|
||||
// Clear for next test call
|
||||
mockCheckNodeCompatibility.mockClear()
|
||||
|
||||
// Test nightly version
|
||||
vm.getVersionCompatibility('nightly')
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
|
||||
id: 'test-pack',
|
||||
name: 'Test Pack',
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0',
|
||||
repository: 'https://github.com/user/repo',
|
||||
has_registry_data: true,
|
||||
latest_version: {
|
||||
supported_os: ['windows'],
|
||||
supported_accelerators: ['CPU'],
|
||||
supported_python_version: '>=3.8',
|
||||
is_banned: false,
|
||||
has_registry_data: true,
|
||||
version: '1.0.0',
|
||||
supported_comfyui_version: '>=0.1.0',
|
||||
supported_comfyui_frontend_version: '>=1.0.0'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('shows banned package warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return banned conflicts
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.is_banned === true) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'banned',
|
||||
current_value: 'installed',
|
||||
required_value: 'not_banned'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const bannedNodePack = {
|
||||
...mockNodePack,
|
||||
is_banned: true,
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
is_banned: true
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: bannedNodePack }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// Open the dropdown to see the options
|
||||
const select = wrapper.find('.p-select')
|
||||
if (!select.exists()) {
|
||||
// Try alternative selector
|
||||
const selectButton = wrapper.find('[aria-haspopup="listbox"]')
|
||||
if (selectButton.exists()) {
|
||||
await selectButton.trigger('click')
|
||||
}
|
||||
} else {
|
||||
await select.trigger('click')
|
||||
}
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The warning icon should be shown for banned packages in the dropdown options
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows security pending warnings', async () => {
|
||||
// Set up the mock for versions
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
// Mock compatibility check to return security pending conflicts
|
||||
mockCheckNodeCompatibility.mockImplementation((versionData) => {
|
||||
if (versionData.has_registry_data === false) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'pending',
|
||||
current_value: 'no_registry_data',
|
||||
required_value: 'registry_data_available'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return { hasConflict: false, conflicts: [] }
|
||||
})
|
||||
|
||||
const securityPendingNodePack = {
|
||||
...mockNodePack,
|
||||
has_registry_data: false,
|
||||
latest_version: {
|
||||
...mockNodePack.latest_version,
|
||||
has_registry_data: false
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
props: { nodePack: securityPendingNodePack }
|
||||
})
|
||||
await waitForPromises()
|
||||
|
||||
// Check that compatibility checking function was called
|
||||
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
|
||||
|
||||
// The warning icon should be shown for security pending packages
|
||||
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
|
||||
expect(warningIcons.length).toBeGreaterThan(0)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<div class="w-64 pt-1">
|
||||
<div class="py-2">
|
||||
<span class="pl-3 text-md font-semibold text-neutral-500">
|
||||
{{ $t('manager.selectVersion') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-64 mt-2">
|
||||
<span class="pl-3 text-muted text-md font-semibold opacity-70">
|
||||
{{ $t('manager.selectVersion') }}
|
||||
</span>
|
||||
<div
|
||||
v-if="isLoadingVersions || isQueueing"
|
||||
class="text-center text-muted py-4 flex flex-col items-center"
|
||||
@@ -25,44 +23,24 @@
|
||||
v-model="selectedVersion"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="processedVersionOptions"
|
||||
:options="versionOptions"
|
||||
:highlight-on-select="false"
|
||||
class="w-full max-h-[50vh] border-none shadow-none rounded-md"
|
||||
:pt="{
|
||||
listContainer: { class: 'scrollbar-hide' }
|
||||
}"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex justify-between items-center w-full p-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="slotProps.option.value === 'nightly'">
|
||||
<div class="w-4"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i
|
||||
v-if="slotProps.option.hasConflict"
|
||||
v-tooltip="{
|
||||
value: slotProps.option.conflictMessage,
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-exclamation-triangle text-yellow-500"
|
||||
/>
|
||||
<VerifiedIcon v-else :size="20" class="relative right-0.5" />
|
||||
</template>
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
</div>
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
<i
|
||||
v-if="slotProps.option.isSelected"
|
||||
v-if="selectedVersion === slotProps.option.value"
|
||||
class="pi pi-check text-highlight"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
<ContentDivider class="my-2" />
|
||||
<div class="flex justify-end gap-2 py-1 px-3">
|
||||
<div class="flex justify-end gap-2 p-1 px-3">
|
||||
<Button
|
||||
text
|
||||
class="text-sm"
|
||||
severity="secondary"
|
||||
:label="$t('g.cancel')"
|
||||
:disabled="isQueueing"
|
||||
@@ -71,7 +49,7 @@
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('g.install')"
|
||||
class="py-2.5 px-4 text-sm dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
|
||||
class="py-3 px-4 dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
|
||||
:disabled="isQueueing"
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
@@ -84,42 +62,21 @@ import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
|
||||
type ManagerDatabaseSource =
|
||||
ManagerComponents['schemas']['ManagerDatabaseSource']
|
||||
type SelectedVersion = ManagerComponents['schemas']['SelectedVersion']
|
||||
|
||||
// Enum values for runtime use
|
||||
const SelectedVersionValues = {
|
||||
LATEST: 'latest' as SelectedVersion,
|
||||
NIGHTLY: 'nightly' as SelectedVersion
|
||||
}
|
||||
|
||||
const ManagerChannelValues: Record<string, ManagerChannel> = {
|
||||
DEFAULT: 'default',
|
||||
DEV: 'dev'
|
||||
}
|
||||
|
||||
const ManagerDatabaseSourceValues: Record<string, ManagerDatabaseSource> = {
|
||||
CACHE: 'cache',
|
||||
REMOTE: 'remote',
|
||||
LOCAL: 'local'
|
||||
}
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
@@ -132,25 +89,22 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const registryService = useComfyRegistryService()
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
|
||||
const isQueueing = ref(false)
|
||||
|
||||
const selectedVersion = ref<string>(SelectedVersionValues.LATEST)
|
||||
const selectedVersion = ref<string>(SelectedVersion.LATEST)
|
||||
onMounted(() => {
|
||||
const initialVersion =
|
||||
getInitialSelectedVersion() ?? SelectedVersionValues.LATEST
|
||||
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
|
||||
selectedVersion.value =
|
||||
// Use NIGHTLY when version is a Git hash
|
||||
isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
|
||||
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
|
||||
})
|
||||
|
||||
const getInitialSelectedVersion = () => {
|
||||
if (!nodePack.id) return
|
||||
|
||||
// If unclaimed, set selected version to nightly
|
||||
if (nodePack.publisher?.name === 'Unclaimed')
|
||||
return SelectedVersionValues.NIGHTLY
|
||||
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
|
||||
|
||||
// If node pack is installed, set selected version to the installed version
|
||||
if (managerStore.isPackInstalled(nodePack.id))
|
||||
@@ -172,8 +126,6 @@ const versionOptions = ref<
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const fetchedVersions = ref<components['schemas']['NodeVersion'][]>([])
|
||||
|
||||
const isLoadingVersions = ref(false)
|
||||
|
||||
const onNodePackChange = async () => {
|
||||
@@ -181,34 +133,25 @@ const onNodePackChange = async () => {
|
||||
|
||||
// Fetch versions from the registry
|
||||
const versions = await fetchVersions()
|
||||
fetchedVersions.value = versions
|
||||
|
||||
const latestVersionNumber = nodePack.latest_version?.version
|
||||
|
||||
const availableVersionOptions = versions
|
||||
.map((version) => ({
|
||||
value: version.version ?? '',
|
||||
label: version.version ?? ''
|
||||
}))
|
||||
.filter((option) => option.value && option.value !== latestVersionNumber) // Exclude latest version from the list
|
||||
|
||||
// Add Latest option with actual version number
|
||||
const latestLabel = latestVersionNumber
|
||||
? `${t('manager.latestVersion')} (${latestVersionNumber})`
|
||||
: t('manager.latestVersion')
|
||||
.filter((option) => option.value)
|
||||
|
||||
// Add Latest option
|
||||
const defaultVersions = [
|
||||
{
|
||||
value: SelectedVersionValues.LATEST,
|
||||
label: latestLabel
|
||||
value: SelectedVersion.LATEST,
|
||||
label: t('manager.latestVersion')
|
||||
}
|
||||
]
|
||||
|
||||
// Add Nightly option if there is a non-empty `repository` field
|
||||
if (nodePack.repository?.length) {
|
||||
defaultVersions.push({
|
||||
value: SelectedVersionValues.NIGHTLY,
|
||||
value: SelectedVersion.NIGHTLY,
|
||||
label: t('manager.nightlyVersion')
|
||||
})
|
||||
}
|
||||
@@ -229,86 +172,16 @@ whenever(
|
||||
|
||||
const handleSubmit = async () => {
|
||||
isQueueing.value = true
|
||||
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
// Convert 'latest' to actual version number for installation
|
||||
const actualVersion =
|
||||
selectedVersion.value === 'latest'
|
||||
? nodePack.latest_version?.version ?? 'latest'
|
||||
: selectedVersion.value
|
||||
|
||||
await managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannelValues.DEFAULT,
|
||||
mode: ManagerDatabaseSourceValues.CACHE,
|
||||
version: actualVersion,
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
version: selectedVersion.value,
|
||||
selected_version: selectedVersion.value
|
||||
})
|
||||
|
||||
isQueueing.value = false
|
||||
emit('submit')
|
||||
}
|
||||
|
||||
const getVersionData = (version: string) => {
|
||||
const latestVersionNumber = nodePack.latest_version?.version
|
||||
const useLatestVersionData =
|
||||
version === 'latest' || version === latestVersionNumber
|
||||
if (useLatestVersionData) {
|
||||
const latestVersionData = nodePack.latest_version
|
||||
return {
|
||||
...latestVersionData
|
||||
}
|
||||
}
|
||||
const versionData = fetchedVersions.value.find((v) => v.version === version)
|
||||
if (versionData) {
|
||||
return {
|
||||
...versionData
|
||||
}
|
||||
}
|
||||
// Fallback to nodePack data
|
||||
return {
|
||||
...nodePack
|
||||
}
|
||||
}
|
||||
// Main function to get version compatibility info
|
||||
const getVersionCompatibility = (version: string) => {
|
||||
const versionData = getVersionData(version)
|
||||
const compatibility = checkNodeCompatibility(versionData)
|
||||
const conflictMessage = compatibility.hasConflict
|
||||
? getJoinedConflictMessages(compatibility.conflicts, t)
|
||||
: ''
|
||||
return {
|
||||
hasConflict: compatibility.hasConflict,
|
||||
conflictMessage
|
||||
}
|
||||
}
|
||||
// Helper to determine if an option is selected.
|
||||
const isOptionSelected = (optionValue: string) => {
|
||||
if (selectedVersion.value === optionValue) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
optionValue === 'latest' &&
|
||||
selectedVersion.value === nodePack.latest_version?.version
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Checks if an option is selected, treating 'latest' as an alias for the actual latest version number.
|
||||
const processedVersionOptions = computed(() => {
|
||||
return versionOptions.value.map((option) => {
|
||||
const compatibility = getVersionCompatibility(option.value)
|
||||
const isSelected = isOptionSelected(option.value)
|
||||
return {
|
||||
...option,
|
||||
hasConflict: compatibility.hasConflict,
|
||||
conflictMessage: compatibility.conflictMessage,
|
||||
isSelected: isSelected
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<Button
|
||||
outlined
|
||||
class="!m-0 p-0 rounded-lg text-gray-900 dark-theme:text-gray-50"
|
||||
:class="[
|
||||
variant === 'black'
|
||||
? 'bg-neutral-900 text-white border-neutral-900'
|
||||
: 'border-neutral-700',
|
||||
fullWidth ? 'w-full' : 'w-min-content'
|
||||
]"
|
||||
:disabled="loading"
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<span class="py-2 px-3 whitespace-nowrap">
|
||||
<template v-if="loading">
|
||||
{{ loadingMessage ?? $t('g.loading') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ label }}
|
||||
</template>
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const {
|
||||
label,
|
||||
loadingMessage,
|
||||
fullWidth = false,
|
||||
variant = 'default'
|
||||
} = defineProps<{
|
||||
label: string
|
||||
loading?: boolean
|
||||
loadingMessage?: string
|
||||
fullWidth?: boolean
|
||||
variant?: 'default' | 'black'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: []
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const onClick = (): void => {
|
||||
emit('action')
|
||||
}
|
||||
</script>
|
||||
@@ -12,13 +12,9 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('es-toolkit/compat', async () => {
|
||||
const actual = await vi.importActual('es-toolkit/compat')
|
||||
return {
|
||||
...actual,
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}
|
||||
})
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}))
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="hasConflict"
|
||||
v-tooltip="{
|
||||
value: $t('manager.conflicts.warningTooltip'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="flex items-center justify-center w-6 h-6 cursor-pointer"
|
||||
@click="showConflictModal(true)"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-500 text-xl"></i>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ToggleSwitch
|
||||
v-if="!canToggleDirectly"
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
:readonly="!canToggleDirectly"
|
||||
aria-label="Enable or disable pack"
|
||||
@focus="handleToggleInteraction"
|
||||
/>
|
||||
<ToggleSwitch
|
||||
v-else
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
aria-label="Enable or disable pack"
|
||||
@@ -28,110 +8,57 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import {
|
||||
InstallPackParams,
|
||||
ManagerChannel,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
const { nodePack, hasConflict } = defineProps<{
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
hasConflict?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isPackEnabled, enablePack, disablePack, installedPacks } =
|
||||
useComfyManagerStore()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { acknowledgmentState, markConflictsAsSeen } = useConflictAcknowledgment()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const isEnabled = computed(() => isPackEnabled(nodePack.id))
|
||||
const version = computed(() => {
|
||||
const id = nodePack.id
|
||||
if (!id) return 'nightly' as ManagerComponents['schemas']['SelectedVersion']
|
||||
if (!id) return SelectedVersion.NIGHTLY
|
||||
return (
|
||||
installedPacks[id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
SelectedVersion.NIGHTLY
|
||||
)
|
||||
})
|
||||
|
||||
const packageConflict = computed(() =>
|
||||
getConflictsForPackageByID(nodePack.id || '')
|
||||
)
|
||||
const canToggleDirectly = computed(() => {
|
||||
return !(
|
||||
hasConflict &&
|
||||
!acknowledgmentState.value.modal_dismissed &&
|
||||
packageConflict.value
|
||||
)
|
||||
})
|
||||
|
||||
const showConflictModal = (skipModalDismissed: boolean) => {
|
||||
let modal_dismissed = acknowledgmentState.value.modal_dismissed
|
||||
if (skipModalDismissed) modal_dismissed = false
|
||||
if (packageConflict.value && !modal_dismissed) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [packageConflict.value],
|
||||
buttonText: !isEnabled.value
|
||||
? t('manager.conflicts.enableAnyway')
|
||||
: t('manager.conflicts.understood'),
|
||||
onButtonClick: async () => {
|
||||
if (!isEnabled.value) {
|
||||
await handleEnable()
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for enabling')
|
||||
}
|
||||
return enablePack.call({
|
||||
const handleEnable = () =>
|
||||
enablePack.call({
|
||||
id: nodePack.id,
|
||||
version:
|
||||
version.value ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']),
|
||||
selected_version:
|
||||
version.value ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']),
|
||||
version: version.value,
|
||||
selected_version: version.value,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
skip_post_install: false
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: 'default' as InstallPackParams['mode']
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for disabling')
|
||||
}
|
||||
return disablePack({
|
||||
const handleDisable = () =>
|
||||
disablePack({
|
||||
id: nodePack.id,
|
||||
version:
|
||||
version.value ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
version: version.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
@@ -140,22 +67,10 @@ const handleToggle = async (enable: boolean) => {
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
await handleDisable()
|
||||
handleDisable()
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const onToggle = debounce(
|
||||
(enable: boolean) => {
|
||||
void handleToggle(enable)
|
||||
},
|
||||
TOGGLE_DEBOUNCE_MS,
|
||||
{ trailing: true }
|
||||
)
|
||||
const handleToggleInteraction = async (event: Event) => {
|
||||
if (!canToggleDirectly.value) {
|
||||
event.preventDefault()
|
||||
showConflictModal(false)
|
||||
}
|
||||
}
|
||||
const onToggle = debounce(handleToggle, TOGGLE_DEBOUNCE_MS, { trailing: true })
|
||||
</script>
|
||||
|
||||
@@ -1,87 +1,59 @@
|
||||
<template>
|
||||
<IconTextButton
|
||||
<PackActionButton
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
:label="computedLabel"
|
||||
:border="true"
|
||||
:size="size"
|
||||
:disabled="isLoading || isInstalling"
|
||||
@click="installAllPacks"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="hasConflict && !isInstalling && !isLoading"
|
||||
class="pi pi-exclamation-triangle text-yellow-500"
|
||||
/>
|
||||
<DotSpinner
|
||||
v-else-if="isLoading || isInstalling"
|
||||
duration="1s"
|
||||
:size="size === 'sm' ? 12 : 16"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
:label="
|
||||
label ??
|
||||
(nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install'))
|
||||
"
|
||||
:severity="variant === 'black' ? undefined : 'secondary'"
|
||||
:variant="variant"
|
||||
:loading="isInstalling"
|
||||
:loading-message="$t('g.installing')"
|
||||
@action="installAllPacks"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { inject, ref } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { ButtonSize } from '@/types/buttonTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import {
|
||||
type ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
IsInstallingKey,
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const {
|
||||
nodePacks,
|
||||
isLoading = false,
|
||||
label = 'Install',
|
||||
size = 'sm',
|
||||
hasConflict,
|
||||
conflictInfo
|
||||
} = defineProps<{
|
||||
const { nodePacks, variant, label } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
isLoading?: boolean
|
||||
variant?: 'default' | 'black'
|
||||
label?: string
|
||||
size?: ButtonSize
|
||||
hasConflict?: boolean
|
||||
conflictInfo?: ConflictDetail[]
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||
|
||||
// Check if any of the packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
if (!nodePacks?.length) return false
|
||||
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
|
||||
})
|
||||
const onClick = (): void => {
|
||||
isInstalling.value = true
|
||||
}
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
if (!installItem.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
? SelectedVersion.NIGHTLY
|
||||
: installItem.latest_version?.version ?? SelectedVersion.LATEST
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
repository: installItem.repository ?? '',
|
||||
channel: 'dev' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: versionToInstall,
|
||||
version: versionToInstall
|
||||
}
|
||||
@@ -93,54 +65,14 @@ const installPack = (item: NodePack) =>
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
if (hasConflict && conflictInfo) {
|
||||
// Check each package individually for conflicts
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const conflictedPackages: ConflictDetectionResult[] = nodePacks
|
||||
.map((pack) => {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
return {
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: compatibilityCheck.hasConflict,
|
||||
conflicts: compatibilityCheck.conflicts,
|
||||
is_compatible: !compatibilityCheck.hasConflict
|
||||
}
|
||||
})
|
||||
.filter((result) => result.has_conflict) // Only show packages with conflicts
|
||||
isInstalling.value = true
|
||||
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages,
|
||||
buttonText: t('manager.conflicts.installAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// Proceed with installation of uninstalled packages
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// No conflicts or conflicts acknowledged - proceed with installation
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
|
||||
const performInstallation = async (packs: NodePack[]) => {
|
||||
await Promise.all(packs.map(installPack))
|
||||
await Promise.all(uninstalledPacks.map(installPack))
|
||||
managerStore.installPack.clear()
|
||||
}
|
||||
|
||||
const computedLabel = computed(() =>
|
||||
isInstalling.value
|
||||
? t('g.installing')
|
||||
: label ??
|
||||
(nodePacks.length > 1 ? t('manager.installSelected') : t('g.install'))
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
<template>
|
||||
<IconTextButton
|
||||
<PackActionButton
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
:label="
|
||||
nodePacks.length > 1
|
||||
? $t('manager.uninstallSelected')
|
||||
: $t('manager.uninstall')
|
||||
"
|
||||
:border="true"
|
||||
:size="size"
|
||||
class="border-red-500"
|
||||
@click="uninstallItems"
|
||||
severity="danger"
|
||||
:loading-message="$t('manager.uninstalling')"
|
||||
@action="uninstallItems"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { ButtonSize } from '@/types/buttonTypes'
|
||||
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks, size } = defineProps<{
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
size?: ButtonSize
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (
|
||||
uninstallItem: NodePack
|
||||
): ManagerComponents['schemas']['ManagerPackInfo'] => {
|
||||
if (!uninstallItem.id) {
|
||||
throw new Error('Node ID is required for uninstallation')
|
||||
}
|
||||
|
||||
const createPayload = (uninstallItem: NodePack): ManagerPackInfo => {
|
||||
return {
|
||||
id: uninstallItem.id,
|
||||
version: uninstallItem.latest_version?.version || 'unknown'
|
||||
version: uninstallItem.latest_version?.version
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<IconTextButton
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
:label="$t('manager.updateAll')"
|
||||
:border="true"
|
||||
size="sm"
|
||||
:disabled="isUpdating"
|
||||
@click="updateAllPacks"
|
||||
>
|
||||
<template v-if="isUpdating" #icon>
|
||||
<DotSpinner duration="1s" :size="12" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
}>()
|
||||
|
||||
const isUpdating = ref<boolean>(false)
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (updateItem: NodePack) => {
|
||||
return {
|
||||
id: updateItem.id!,
|
||||
version: updateItem.latest_version!.version!
|
||||
}
|
||||
}
|
||||
|
||||
const updatePack = async (item: NodePack) => {
|
||||
if (!item.id || !item.latest_version?.version) {
|
||||
console.warn('Pack missing required id or version:', item)
|
||||
return
|
||||
}
|
||||
await managerStore.updatePack.call(createPayload(item))
|
||||
}
|
||||
|
||||
const updateAllPacks = async () => {
|
||||
if (!nodePacks?.length) {
|
||||
console.warn('No packs provided for update')
|
||||
return
|
||||
}
|
||||
isUpdating.value = true
|
||||
const updatablePacks = nodePacks.filter((pack) =>
|
||||
managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!updatablePacks.length) {
|
||||
console.info('No installed packs available for update')
|
||||
isUpdating.value = false
|
||||
return
|
||||
}
|
||||
console.info(`Starting update of ${updatablePacks.length} packs`)
|
||||
try {
|
||||
await Promise.all(updatablePacks.map(updatePack))
|
||||
managerStore.updatePack.clear()
|
||||
console.info('All packs updated successfully')
|
||||
} catch (error) {
|
||||
console.error('Pack update failed:', error)
|
||||
console.error(
|
||||
'Failed packs info:',
|
||||
updatablePacks.map((p) => p.id)
|
||||
)
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,26 +2,20 @@
|
||||
<template v-if="nodePack">
|
||||
<div class="flex flex-col h-full z-40 overflow-hidden relative">
|
||||
<div class="top-0 z-10 px-6 pt-6 w-full">
|
||||
<InfoPanelHeader
|
||||
:node-packs="[nodePack]"
|
||||
:has-conflict="hasCompatibilityIssues"
|
||||
/>
|
||||
<InfoPanelHeader :node-packs="[nodePack]" />
|
||||
</div>
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="p-6 pt-2 overflow-y-auto flex-1 text-sm scrollbar-hide"
|
||||
class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<MetadataRow
|
||||
v-if="!importFailed && isPackInstalled(nodePack.id)"
|
||||
v-if="isPackInstalled(nodePack.id)"
|
||||
:label="t('manager.filter.enabled')"
|
||||
class="flex"
|
||||
style="align-items: center"
|
||||
>
|
||||
<PackEnableToggle
|
||||
:node-pack="nodePack"
|
||||
:has-conflict="hasCompatibilityIssues"
|
||||
/>
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
v-for="item in infoItems"
|
||||
@@ -35,7 +29,6 @@
|
||||
:status-type="
|
||||
nodePack.status as components['schemas']['NodeVersionStatus']
|
||||
"
|
||||
:has-compatibility-issues="hasCompatibilityIssues"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow :label="t('manager.version')">
|
||||
@@ -43,11 +36,7 @@
|
||||
</MetadataRow>
|
||||
</div>
|
||||
<div class="mb-6 overflow-hidden">
|
||||
<InfoTabs
|
||||
:node-pack="nodePack"
|
||||
:has-compatibility-issues="hasCompatibilityIssues"
|
||||
:conflict-result="conflictResult"
|
||||
/>
|
||||
<InfoTabs :node-pack="nodePack" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,14 +59,9 @@ import PackEnableToggle from '@/components/dialog/content/manager/button/PackEna
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
interface InfoItem {
|
||||
key: string
|
||||
@@ -91,55 +75,18 @@ const { nodePack } = defineProps<{
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack.id))
|
||||
const managerStore = useComfyManagerStore()
|
||||
const isInstalled = computed(() => managerStore.isPackInstalled(nodePack.id))
|
||||
const isInstalling = ref(false)
|
||||
provide(IsInstallingKey, isInstalling)
|
||||
whenever(isInstalled, () => {
|
||||
isInstalling.value = false
|
||||
})
|
||||
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
|
||||
const { t, d, n } = useI18n()
|
||||
|
||||
// Check compatibility once and pass to children
|
||||
const conflictResult = computed((): ConflictDetectionResult | null => {
|
||||
// For installed packages, use stored conflict data
|
||||
if (isInstalled.value && nodePack.id) {
|
||||
return getConflictsForPackageByID(nodePack.id) || null
|
||||
}
|
||||
|
||||
// For non-installed packages, perform compatibility check
|
||||
const compatibility = checkNodeCompatibility(nodePack)
|
||||
|
||||
if (compatibility.hasConflict) {
|
||||
return {
|
||||
package_id: nodePack.id || '',
|
||||
package_name: nodePack.name || '',
|
||||
has_conflict: true,
|
||||
conflicts: compatibility.conflicts,
|
||||
is_compatible: false
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const hasCompatibilityIssues = computed(() => {
|
||||
return conflictResult.value?.has_conflict
|
||||
})
|
||||
|
||||
const packageId = computed(() => nodePack.id || '')
|
||||
const { importFailed, showImportFailedDialog } =
|
||||
useImportFailedDetection(packageId)
|
||||
|
||||
provide(ImportFailedKey, {
|
||||
importFailed,
|
||||
showImportFailedDialog
|
||||
})
|
||||
|
||||
const infoItems = computed<InfoItem[]>(() => [
|
||||
{
|
||||
key: 'publisher',
|
||||
@@ -181,3 +128,17 @@ whenever(
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
<style scoped>
|
||||
.hidden-scrollbar {
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,28 @@
|
||||
<template>
|
||||
<div v-if="nodePacks?.length" class="flex flex-col items-center">
|
||||
<div v-if="nodePacks?.length" class="flex flex-col items-center mb-6">
|
||||
<slot name="thumbnail">
|
||||
<PackIcon :node-pack="nodePacks[0]" width="204" height="106" />
|
||||
<PackIcon :node-pack="nodePacks[0]" width="24" height="24" />
|
||||
</slot>
|
||||
<h2
|
||||
class="text-2xl font-bold text-center mt-4 mb-2"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
<slot name="title">
|
||||
<span class="inline-block text-base">{{ nodePacks[0].name }}</span>
|
||||
{{ nodePacks[0].name }}
|
||||
</slot>
|
||||
</h2>
|
||||
<div
|
||||
v-if="!importFailed"
|
||||
class="mt-2 mb-4 w-full max-w-xs flex justify-center"
|
||||
>
|
||||
<div class="mt-2 mb-4 w-full max-w-xs flex justify-center">
|
||||
<slot name="install-button">
|
||||
<PackUninstallButton
|
||||
v-if="isAllInstalled"
|
||||
v-bind="$attrs"
|
||||
size="md"
|
||||
:node-packs="nodePacks"
|
||||
/>
|
||||
<PackInstallButton
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
size="md"
|
||||
:node-packs="nodePacks"
|
||||
:has-conflict="hasConflict || computedHasConflict"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
<PackInstallButton v-else v-bind="$attrs" :node-packs="nodePacks" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center">
|
||||
<div v-else class="flex flex-col items-center mb-6">
|
||||
<NoResultsPlaceholder
|
||||
:message="$t('manager.status.unknown')"
|
||||
:title="$t('manager.tryAgainLater')"
|
||||
@@ -42,29 +31,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePacks, hasConflict } = defineProps<{
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
hasConflict?: boolean
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
// Inject import failed context from parent
|
||||
const importFailedContext = inject(ImportFailedKey)
|
||||
const importFailed = importFailedContext?.importFailed
|
||||
|
||||
const isAllInstalled = ref(false)
|
||||
watch(
|
||||
[() => nodePacks, () => managerStore.installedPacks],
|
||||
@@ -75,23 +56,4 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Add conflict detection for install button dialog
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
|
||||
// Compute conflict info for all node packs
|
||||
const conflictInfo = computed<ConflictDetail[]>(() => {
|
||||
if (!nodePacks?.length) return []
|
||||
|
||||
const allConflicts: ConflictDetail[] = []
|
||||
for (const nodePack of nodePacks) {
|
||||
const compatibilityCheck = checkNodeCompatibility(nodePack)
|
||||
if (compatibilityCheck.conflicts) {
|
||||
allConflicts.push(...compatibilityCheck.conflicts)
|
||||
}
|
||||
}
|
||||
return allConflicts
|
||||
})
|
||||
|
||||
const computedHasConflict = computed(() => conflictInfo.value.length > 0)
|
||||
</script>
|
||||
|
||||
@@ -6,40 +6,16 @@
|
||||
<PackIconStacked :node-packs="nodePacks" />
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="mt-5">
|
||||
<span class="inline-block mr-2 text-blue-500 text-base">{{
|
||||
nodePacks.length
|
||||
}}</span>
|
||||
<span class="text-base">{{ $t('manager.packsSelected') }}</span>
|
||||
</div>
|
||||
{{ nodePacks.length }}
|
||||
{{ $t('manager.packsSelected') }}
|
||||
</template>
|
||||
<template #install-button>
|
||||
<!-- Mixed: Don't show any button -->
|
||||
<div v-if="isMixed" class="text-sm text-neutral-500">
|
||||
{{ $t('manager.mixedSelectionMessage') }}
|
||||
</div>
|
||||
<!-- All installed: Show uninstall button -->
|
||||
<PackUninstallButton
|
||||
v-else-if="isAllInstalled"
|
||||
size="md"
|
||||
:node-packs="installedPacks"
|
||||
/>
|
||||
<!-- None installed: Show install button -->
|
||||
<PackInstallButton
|
||||
v-else-if="isNoneInstalled"
|
||||
size="md"
|
||||
:node-packs="notInstalledPacks"
|
||||
:has-conflict="hasConflicts"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
<PackInstallButton :full-width="true" :node-packs="nodePacks" />
|
||||
</template>
|
||||
</InfoPanelHeader>
|
||||
<div class="mb-6">
|
||||
<MetadataRow :label="$t('g.status')">
|
||||
<PackStatusMessage
|
||||
:status-type="overallStatus"
|
||||
:has-compatibility-issues="hasConflicts"
|
||||
/>
|
||||
<PackStatusMessage status-type="NodeVersionStatusActive" />
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
:label="$t('manager.totalNodes')"
|
||||
@@ -55,80 +31,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { computed, onUnmounted, provide, toRef } from 'vue'
|
||||
import { computed, onUnmounted } from 'vue'
|
||||
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
|
||||
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
|
||||
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const nodePacksRef = toRef(() => nodePacks)
|
||||
|
||||
// Use new composables for cleaner code
|
||||
const {
|
||||
installedPacks,
|
||||
notInstalledPacks,
|
||||
isAllInstalled,
|
||||
isNoneInstalled,
|
||||
isMixed
|
||||
} = usePacksSelection(nodePacksRef)
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef)
|
||||
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const { getNodeDefs } = useComfyRegistryStore()
|
||||
|
||||
// Provide import failed context for PackStatusMessage
|
||||
provide(ImportFailedKey, {
|
||||
importFailed: hasImportFailed,
|
||||
showImportFailedDialog: () => {} // No-op for multi-selection
|
||||
})
|
||||
|
||||
// Check for conflicts in not-installed packages - keep original logic but simplified
|
||||
const packageConflicts = computed(() => {
|
||||
const conflictsByPackage = new Map<string, ConflictDetail[]>()
|
||||
|
||||
for (const pack of notInstalledPacks.value) {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
if (compatibilityCheck.hasConflict && pack.id) {
|
||||
conflictsByPackage.set(pack.id, compatibilityCheck.conflicts)
|
||||
}
|
||||
}
|
||||
|
||||
return conflictsByPackage
|
||||
})
|
||||
|
||||
// Aggregate all unique conflicts for display
|
||||
const conflictInfo = computed<ConflictDetail[]>(() => {
|
||||
const conflictMap = new Map<string, ConflictDetail>()
|
||||
|
||||
packageConflicts.value.forEach((conflicts) => {
|
||||
conflicts.forEach((conflict) => {
|
||||
const key = `${conflict.type}-${conflict.current_value}-${conflict.required_value}`
|
||||
if (!conflictMap.has(key)) {
|
||||
conflictMap.set(key, conflict)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(conflictMap.values())
|
||||
})
|
||||
|
||||
const hasConflicts = computed(() => conflictInfo.value.length > 0)
|
||||
|
||||
const getPackNodes = async (pack: components['schemas']['Node']) => {
|
||||
if (!pack.latest_version?.version) return []
|
||||
const nodeDefs = await getNodeDefs.call({
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<Tabs :value="activeTab">
|
||||
<TabList class="overflow-x-auto scrollbar-hide">
|
||||
<Tab v-if="hasCompatibilityIssues" value="warning" class="p-2 mr-6">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>⚠️</span>
|
||||
{{ importFailed ? $t('g.error') : $t('g.warning') }}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="description" class="p-2 mr-6">
|
||||
<TabList>
|
||||
<Tab value="description">
|
||||
{{ $t('g.description') }}
|
||||
</Tab>
|
||||
<Tab value="nodes" class="p-2">
|
||||
<Tab value="nodes">
|
||||
{{ $t('g.nodes') }}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="overflow-auto py-4 px-2">
|
||||
<TabPanel
|
||||
v-if="hasCompatibilityIssues"
|
||||
value="warning"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<WarningTabPanel
|
||||
:node-pack="nodePack"
|
||||
:conflict-result="conflictResult"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanels class="overflow-auto">
|
||||
<TabPanel value="description">
|
||||
<DescriptionTabPanel :node-pack="nodePack" />
|
||||
</TabPanel>
|
||||
@@ -43,25 +27,16 @@ import TabList from 'primevue/tablist'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, inject, ref, watchEffect } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
|
||||
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
|
||||
import WarningTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/WarningTabPanel.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
hasCompatibilityIssues?: boolean
|
||||
conflictResult?: ConflictDetectionResult | null
|
||||
}>()
|
||||
|
||||
// Inject import failed context from parent
|
||||
const importFailedContext = inject(ImportFailedKey)
|
||||
const importFailed = importFailedContext?.importFailed
|
||||
|
||||
const nodeNames = computed(() => {
|
||||
// @ts-expect-error comfy_nodes is an Algolia-specific field
|
||||
const { comfy_nodes } = nodePack
|
||||
@@ -69,17 +44,4 @@ const nodeNames = computed(() => {
|
||||
})
|
||||
|
||||
const activeTab = ref('description')
|
||||
|
||||
// Watch for compatibility issues and automatically switch to warning tab
|
||||
watchEffect(
|
||||
() => {
|
||||
if (hasCompatibilityIssues) {
|
||||
activeTab.value = 'warning'
|
||||
} else if (activeTab.value === 'warning') {
|
||||
// If currently on warning tab but no issues, switch to description
|
||||
activeTab.value = 'description'
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 text-sm">
|
||||
<div v-for="(section, index) in sections" :key="index" class="mb-4">
|
||||
<div class="mb-3">
|
||||
<div class="mb-1">
|
||||
{{ section.title }}
|
||||
</div>
|
||||
<div class="text-muted break-words">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex py-1.5 text-xs">
|
||||
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}</div>
|
||||
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}:</div>
|
||||
<div class="w-2/3">
|
||||
<slot>{{ value }}</slot>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<div class="mt-4 overflow-hidden">
|
||||
<InfoTextSection
|
||||
v-if="nodePack?.description"
|
||||
:sections="descriptionSections"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 text-sm">
|
||||
<div class="flex flex-col gap-4 mt-4 text-sm">
|
||||
<template v-if="mappedNodeDefs?.length">
|
||||
<div
|
||||
v-for="nodeDef in mappedNodeDefs"
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
v-if="importFailedInfo"
|
||||
class="cursor-pointer outline-none border-none inline-flex items-center justify-end bg-transparent gap-1"
|
||||
@click="showImportFailedDialog"
|
||||
>
|
||||
<i class="pi pi-code text-base"></i>
|
||||
<span class="dark-theme:text-white text-sm">{{
|
||||
t('serverStart.openLogs')
|
||||
}}</span>
|
||||
</button>
|
||||
<div
|
||||
v-for="(conflict, index) in conflictResult?.conflicts || []"
|
||||
:key="index"
|
||||
class="p-3 bg-yellow-800/20 rounded-md"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm break-words flex-1">
|
||||
{{ getConflictMessage(conflict, $t) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import { t } from '@/i18n'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import { getConflictMessage } from '@/utils/conflictMessageUtil'
|
||||
|
||||
const { nodePack, conflictResult } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
conflictResult: ConflictDetectionResult | null | undefined
|
||||
}>()
|
||||
const packageId = computed(() => nodePack?.id || '')
|
||||
const { importFailedInfo, showImportFailedDialog } =
|
||||
useImportFailedDetection(packageId)
|
||||
</script>
|
||||
@@ -21,58 +21,73 @@
|
||||
<PackBanner :node-pack="nodePack" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="pt-4 px-4 pb-3 w-full h-full">
|
||||
<div class="flex flex-col gap-y-1 w-full h-full">
|
||||
<span
|
||||
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
|
||||
<template v-if="isInstalling">
|
||||
<div
|
||||
class="self-stretch inline-flex flex-col justify-center items-center gap-2 h-full"
|
||||
>
|
||||
<ProgressSpinner />
|
||||
<div
|
||||
class="self-stretch text-center justify-start text-sm font-medium leading-none"
|
||||
>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
{{ $t('g.installing') }}...
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="pt-4 px-4 pb-3 w-full h-full">
|
||||
<div class="flex flex-col gap-y-1 w-full h-full">
|
||||
<span
|
||||
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
|
||||
>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
</div>
|
||||
<PackVersionBadge
|
||||
:node-pack="nodePack"
|
||||
:is-selected="isSelected"
|
||||
:fill="false"
|
||||
/>
|
||||
<div
|
||||
v-if="formattedLatestVersionDate"
|
||||
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
|
||||
>
|
||||
{{ formattedLatestVersionDate }}
|
||||
</div>
|
||||
</div>
|
||||
<PackVersionBadge
|
||||
:node-pack="nodePack"
|
||||
:is-selected="isSelected"
|
||||
:fill="false"
|
||||
:class="isInstalling ? 'pointer-events-none' : ''"
|
||||
/>
|
||||
<div
|
||||
v-if="formattedLatestVersionDate"
|
||||
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
|
||||
>
|
||||
{{ formattedLatestVersionDate }}
|
||||
<div class="flex">
|
||||
<span
|
||||
v-if="publisherName"
|
||||
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
|
||||
>
|
||||
{{ publisherName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-if="publisherName"
|
||||
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
|
||||
>
|
||||
{{ publisherName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template #footer>
|
||||
<PackCardFooter :node-pack="nodePack" :is-installing="isInstalling" />
|
||||
<PackCardFooter :node-pack="nodePack" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Card from 'primevue/card'
|
||||
import { computed, provide } from 'vue'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
@@ -99,17 +114,18 @@ const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const { isPackInstalled, isPackEnabled, isPackInstalling } =
|
||||
useComfyManagerStore()
|
||||
|
||||
const isInstalling = computed(() => isPackInstalling(nodePack?.id))
|
||||
const isInstalling = ref(false)
|
||||
provide(IsInstallingKey, isInstalling)
|
||||
|
||||
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
|
||||
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isDisabled = computed(
|
||||
() => isInstalled.value && !isPackEnabled(nodePack?.id)
|
||||
)
|
||||
|
||||
whenever(isInstalled, () => (isInstalling.value = false))
|
||||
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
)
|
||||
|
||||
@@ -6,28 +6,19 @@
|
||||
<i class="pi pi-download text-muted"></i>
|
||||
<span>{{ formattedDownloads }}</span>
|
||||
</div>
|
||||
<PackInstallButton
|
||||
v-if="!isInstalled"
|
||||
:node-packs="[nodePack]"
|
||||
:is-installing="isInstalling"
|
||||
:has-conflict="hasConflicts"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
|
||||
<PackEnableToggle v-else :node-pack="nodePack" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
@@ -35,23 +26,10 @@ const { nodePack } = defineProps<{
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isInstalling = inject(IsInstallingKey)
|
||||
|
||||
const { n } = useI18n()
|
||||
|
||||
const formattedDownloads = computed(() =>
|
||||
nodePack.downloads ? n(nodePack.downloads) : ''
|
||||
)
|
||||
|
||||
// Add conflict detection for the card button
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
|
||||
// Check for conflicts with this specific node pack
|
||||
const conflictInfo = computed<ConflictDetail[]>(() => {
|
||||
if (!nodePack) return []
|
||||
const compatibilityCheck = checkNodeCompatibility(nodePack)
|
||||
return compatibilityCheck.conflicts || []
|
||||
})
|
||||
|
||||
const hasConflicts = computed(() => conflictInfo.value.length > 0)
|
||||
</script>
|
||||
|
||||
@@ -1,37 +1,11 @@
|
||||
<template>
|
||||
<div class="w-full max-w-[204] aspect-[2/1] rounded-lg overflow-hidden">
|
||||
<!-- default banner show -->
|
||||
<div v-if="showDefaultBanner" class="w-full h-full">
|
||||
<img
|
||||
:src="DEFAULT_BANNER"
|
||||
alt="default banner"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- banner_url or icon show -->
|
||||
<div v-else class="relative w-full h-full">
|
||||
<!-- blur background -->
|
||||
<div
|
||||
v-if="imgSrc"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
backgroundImage: `url(${imgSrc})`,
|
||||
filter: 'blur(10px)'
|
||||
}"
|
||||
></div>
|
||||
<!-- image -->
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_BANNER : imgSrc"
|
||||
:alt="nodePack.name + ' banner'"
|
||||
:class="
|
||||
isImageError
|
||||
? 'relative w-full h-full object-cover z-10'
|
||||
: 'relative w-full h-full object-contain z-10'
|
||||
"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_ICON : imgSrc"
|
||||
:alt="nodePack.name + ' icon'"
|
||||
class="object-contain rounded-lg max-h-72 max-w-72"
|
||||
:style="{ width: cssWidth, height: cssHeight }"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -39,14 +13,29 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
|
||||
const DEFAULT_ICON = '/assets/images/fallback-gradient-avatar.svg'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
const {
|
||||
nodePack,
|
||||
width = '4.5rem',
|
||||
height = '4.5rem'
|
||||
} = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
width?: string
|
||||
height?: string
|
||||
}>()
|
||||
|
||||
const isImageError = ref(false)
|
||||
const shouldShowFallback = computed(
|
||||
() => !nodePack.icon || nodePack.icon.trim() === '' || isImageError.value
|
||||
)
|
||||
const imgSrc = computed(() =>
|
||||
shouldShowFallback.value ? DEFAULT_ICON : nodePack.icon
|
||||
)
|
||||
|
||||
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
|
||||
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
|
||||
const convertToCssValue = (value: string | number) =>
|
||||
typeof value === 'number' ? `${value}rem` : value
|
||||
|
||||
const cssWidth = computed(() => convertToCssValue(width))
|
||||
const cssHeight = computed(() => convertToCssValue(height))
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<template>
|
||||
<div class="relative w-[224px] h-[104px] shadow-xl">
|
||||
<div class="relative w-24 h-24">
|
||||
<div
|
||||
v-for="(pack, index) in nodePacks.slice(0, maxVisible)"
|
||||
:key="pack.id"
|
||||
class="absolute w-[210px] h-[90px]"
|
||||
class="absolute"
|
||||
:style="{
|
||||
bottom: `${index * offset}px`,
|
||||
right: `${index * offset}px`,
|
||||
zIndex: maxVisible - index
|
||||
}"
|
||||
>
|
||||
<div class="border rounded-lg shadow-lg p-0.5">
|
||||
<PackIcon :node-pack="pack" />
|
||||
<div class="border rounded-lg p-0.5">
|
||||
<PackIcon :node-pack="pack" width="4.5rem" height="4.5rem" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="nodePacks.length > maxVisible"
|
||||
class="absolute -top-2 -right-2 bg-primary rounded-full w-7 h-7 flex items-center justify-center text-xs font-bold shadow-md z-10"
|
||||
>
|
||||
+{{ nodePacks.length - maxVisible }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -28,14 +28,11 @@
|
||||
</div>
|
||||
<PackInstallButton
|
||||
v-if="isMissingTab && missingNodePacks.length > 0"
|
||||
variant="black"
|
||||
:disabled="isLoading || !!error"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
:node-packs="updateAvailableNodePacks"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
@@ -68,10 +65,8 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUpdateButton from '@/components/dialog/content/manager/button/PackUpdateButton.vue'
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import {
|
||||
type SearchOption,
|
||||
SortableAlgoliaField
|
||||
@@ -88,7 +83,6 @@ const { searchResults, sortOptions } = defineProps<{
|
||||
suggestions?: QuerySuggestion[]
|
||||
sortOptions?: SortableField[]
|
||||
isMissingTab?: boolean
|
||||
isUpdateAvailableTab?: boolean
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
@@ -102,10 +96,6 @@ const { t } = useI18n()
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
|
||||
// Use the composable to get update available nodes
|
||||
const { hasUpdateAvailable, updateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
const hasResults = computed(
|
||||
() => searchQuery.value?.trim() && searchResults?.length
|
||||
)
|
||||
|
||||
@@ -112,12 +112,12 @@ import Divider from 'primevue/divider'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -128,10 +128,10 @@ interface CreditHistoryItemData {
|
||||
isPositive: boolean
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
@@ -160,8 +160,15 @@ const handleCreditsHistoryClick = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
const handleMessageSupport = () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('issueReport.contactSupportTitle'),
|
||||
subtitle: t('issueReport.contactSupportDescription'),
|
||||
panelProps: {
|
||||
errorType: 'BillingSupport',
|
||||
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleFaqClick = () => {
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-2 shadow-lg flex items-center justify-between"
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center text-base leading-none">
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isInProgress">
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<span class="mr-2">🎉</span>
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="mr-2">✅</span>
|
||||
<span>{{ $t('manager.restartToApplyChanges') }}</span>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span v-if="isInProgress" class="text-sm text-neutral-700">
|
||||
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ totalTasksCount }}
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress && !isRestartCompleted"
|
||||
v-if="!isInProgress"
|
||||
rounded
|
||||
outlined
|
||||
class="mr-4 rounded-md border-2 px-3 text-neutral-600 border-neutral-900 hover:bg-neutral-100 !dark-theme:bg-transparent dark-theme:text-white dark-theme:border-white dark-theme:hover:bg-neutral-800"
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('manager.applyChanges') }}
|
||||
{{ $t('g.restart') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="!isRestartCompleted"
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
? 'pi pi-chevron-up'
|
||||
: 'pi pi-chevron-down'
|
||||
: 'pi pi-chevron-right'
|
||||
"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
@@ -57,7 +53,6 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
@@ -70,11 +65,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
@@ -84,107 +77,41 @@ import {
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { performConflictDetection } = useConflictDetection()
|
||||
|
||||
// State management for restart process
|
||||
const isRestarting = ref<boolean>(false)
|
||||
const isRestartCompleted = ref<boolean>(false)
|
||||
|
||||
const isInProgress = computed(
|
||||
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
||||
)
|
||||
|
||||
const completedTasksCount = computed(() => {
|
||||
return (
|
||||
comfyManagerStore.succeededTasksIds.length +
|
||||
comfyManagerStore.failedTasksIds.length
|
||||
)
|
||||
})
|
||||
|
||||
const totalTasksCount = computed(() => {
|
||||
const completedTasks = Object.keys(comfyManagerStore.taskHistory).length
|
||||
const taskQueue = comfyManagerStore.taskQueue
|
||||
const queuedTasks = taskQueue
|
||||
? (taskQueue.running_queue?.length || 0) +
|
||||
(taskQueue.pending_queue?.length || 0)
|
||||
: 0
|
||||
return completedTasks + queuedTasks
|
||||
})
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('manager.installingDependencies')
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const currentTaskName = computed(() => {
|
||||
if (isRestarting.value) {
|
||||
return t('manager.restartingBackend')
|
||||
}
|
||||
if (isRestartCompleted.value) {
|
||||
return t('manager.extensionsSuccessfullyInstalled')
|
||||
}
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
return task?.taskName ?? fallbackTaskName
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
// Store original toast setting value
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
const onReconnect = async () => {
|
||||
// Refresh manager state
|
||||
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
comfyManagerStore.clearLogs()
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
isRestarting.value = true
|
||||
// Refresh node definitions
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
|
||||
// Run conflict detection after restart completion
|
||||
await performConflictDetection()
|
||||
} finally {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
closeDialog()
|
||||
comfyManagerStore.resetTaskState()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
} catch (error) {
|
||||
// If restart fails, restore settings and reset state
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
closeDialog() // Close dialog on error
|
||||
throw error
|
||||
// Reload workflow
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,27 +18,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { computed } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const activeTabIndex = computed({
|
||||
get: () => progressDialogContent.getActiveTabIndex(),
|
||||
set: (value) => progressDialogContent.setActiveTabIndex(value)
|
||||
})
|
||||
const activeTabIndex = ref(0)
|
||||
const { t } = useI18n()
|
||||
const tabs = computed(() => [
|
||||
const tabs = [
|
||||
{ label: t('manager.installationQueue') },
|
||||
{
|
||||
label: t('manager.failed', {
|
||||
count: comfyManagerStore.failedTasksIds.length
|
||||
})
|
||||
}
|
||||
])
|
||||
{ label: t('manager.failed', { count: 0 }) }
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -14,17 +14,7 @@
|
||||
@mouseenter="onMenuItemHover(menuItem.key, $event)"
|
||||
@mouseleave="onMenuItemLeave(menuItem.key)"
|
||||
>
|
||||
<div class="help-menu-icon-container">
|
||||
<div class="help-menu-icon">
|
||||
<component
|
||||
:is="menuItem.icon"
|
||||
v-if="typeof menuItem.icon === 'object'"
|
||||
:size="16"
|
||||
/>
|
||||
<i v-else :class="menuItem.icon" />
|
||||
</div>
|
||||
<div v-if="menuItem.showRedDot" class="menu-red-dot" />
|
||||
</div>
|
||||
<i :class="menuItem.icon" class="help-menu-icon" />
|
||||
<span class="menu-label">{{ menuItem.label }}</span>
|
||||
<i v-if="menuItem.key === 'more'" class="pi pi-chevron-right" />
|
||||
</button>
|
||||
@@ -130,19 +120,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Component,
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
@@ -153,13 +133,12 @@ import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
// Types
|
||||
interface MenuItem {
|
||||
key: string
|
||||
icon?: string | Component
|
||||
icon?: string
|
||||
label?: string
|
||||
action?: () => void
|
||||
visible?: boolean
|
||||
type?: 'item' | 'divider'
|
||||
items?: MenuItem[]
|
||||
showRedDot?: boolean
|
||||
}
|
||||
|
||||
// Constants
|
||||
@@ -191,7 +170,6 @@ const { t, locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -210,10 +188,6 @@ const showVersionUpdates = computed(() =>
|
||||
settingStore.get('Comfy.Notification.ShowVersionUpdates')
|
||||
)
|
||||
|
||||
// Use conflict acknowledgment state from composable
|
||||
const { shouldShowRedDot: shouldShowManagerRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const moreItems = computed<MenuItem[]>(() => {
|
||||
const allMoreItems: MenuItem[] = [
|
||||
{
|
||||
@@ -303,18 +277,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: 'pi pi-question-circle',
|
||||
label: t('helpCenter.helpFeedback'),
|
||||
action: () => {
|
||||
void commandStore.execute('Comfy.ContactSupport')
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'manager',
|
||||
type: 'item',
|
||||
icon: PuzzleIcon,
|
||||
label: t('helpCenter.managerExtension'),
|
||||
showRedDot: shouldShowManagerRedDot.value,
|
||||
action: () => {
|
||||
dialogService.showManagerDialog()
|
||||
void commandStore.execute('Comfy.Feedback')
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
@@ -553,13 +516,6 @@ onMounted(async () => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.help-menu-icon-container {
|
||||
position: relative;
|
||||
margin-right: 0.75rem;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.help-menu-icon {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1rem;
|
||||
@@ -567,26 +523,9 @@ onMounted(async () => {
|
||||
width: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.help-menu-icon svg {
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
|
||||
.menu-red-dot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ff3b30;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--p-content-background);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -75,11 +75,6 @@ import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
const { locale, t } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
|
||||
// Define emits
|
||||
const emit = defineEmits<{
|
||||
'whats-new-dismissed': []
|
||||
}>()
|
||||
|
||||
// Local state for dismissed status
|
||||
const isDismissed = ref(false)
|
||||
|
||||
@@ -131,7 +126,6 @@ const show = () => {
|
||||
|
||||
const hide = () => {
|
||||
isDismissed.value = true
|
||||
emit('whats-new-dismissed')
|
||||
}
|
||||
|
||||
const closePopup = async () => {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
:class="iconClass"
|
||||
>
|
||||
<g clip-path="url(#clip0_1099_16244)">
|
||||
<path
|
||||
d="M4.99992 3.00016C4.99992 2.07969 5.74611 1.3335 6.66658 1.3335C7.58706 1.3335 8.33325 2.07969 8.33325 3.00016V4.00016H8.99992C9.9318 4.00016 10.3977 4.00016 10.7653 4.1524C11.2553 4.35539 11.6447 4.74474 11.8477 5.2348C11.9999 5.60234 11.9999 6.06828 11.9999 7.00016H12.9999C13.9204 7.00016 14.6666 7.74635 14.6666 8.66683C14.6666 9.5873 13.9204 10.3335 12.9999 10.3335H11.9999V11.4668C11.9999 12.5869 11.9999 13.147 11.7819 13.5748C11.5902 13.9511 11.2842 14.2571 10.9079 14.4488C10.4801 14.6668 9.92002 14.6668 8.79992 14.6668H8.33325V13.5002C8.33325 12.6717 7.66168 12.0002 6.83325 12.0002C6.00482 12.0002 5.33325 12.6717 5.33325 13.5002V14.6668H4.53325C3.41315 14.6668 2.85309 14.6668 2.42527 14.4488C2.04895 14.2571 1.74299 13.9511 1.55124 13.5748C1.33325 13.147 1.33325 12.5869 1.33325 11.4668V10.3335H2.33325C3.25373 10.3335 3.99992 9.5873 3.99992 8.66683C3.99992 7.74635 3.25373 7.00016 2.33325 7.00016H1.33325C1.33325 6.06828 1.33325 5.60234 1.48549 5.2348C1.68848 4.74474 2.07783 4.35539 2.56789 4.1524C2.93543 4.00016 3.40137 4.00016 4.33325 4.00016H4.99992V3.00016Z"
|
||||
:stroke="color"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1099_16244">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
class?: string
|
||||
}
|
||||
const {
|
||||
size = 16,
|
||||
color = 'currentColor',
|
||||
class: className
|
||||
} = defineProps<Props>()
|
||||
const iconClass = computed(() => className || '')
|
||||
</script>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
:class="iconClass"
|
||||
>
|
||||
<path
|
||||
d="M8.00049 1.3335C8.73661 1.33367 9.33332 1.93038 9.3335 2.6665V2.83447C9.82278 2.96041 10.2851 3.15405 10.7095 3.40479L10.8286 3.28564C11.3493 2.76525 12.1937 2.76519 12.7144 3.28564C13.235 3.80626 13.2348 4.65067 12.7144 5.17139L12.5952 5.29053C12.846 5.71486 13.0396 6.17725 13.1655 6.6665H13.3335C14.0699 6.6665 14.6665 7.26411 14.6665 8.00049C14.6663 8.73672 14.0698 9.3335 13.3335 9.3335H13.1655C13.0396 9.82284 12.846 10.2851 12.5952 10.7095L12.7144 10.8286C13.235 11.3493 13.235 12.1937 12.7144 12.7144C12.1937 13.235 11.3493 13.235 10.8286 12.7144L10.7095 12.5952C10.2851 12.846 9.82284 13.0396 9.3335 13.1655V13.3335C9.3335 14.0698 8.73672 14.6663 8.00049 14.6665C7.26411 14.6665 6.6665 14.0699 6.6665 13.3335V13.1655C6.17725 13.0396 5.71486 12.846 5.29053 12.5952L5.17139 12.7144C4.65067 13.2348 3.80626 13.235 3.28564 12.7144C2.76519 12.1937 2.76525 11.3493 3.28564 10.8286L3.40479 10.7095C3.15405 10.2851 2.96041 9.82278 2.83447 9.3335H2.6665C1.93038 9.33332 1.33367 8.73661 1.3335 8.00049C1.3335 7.26422 1.93027 6.66668 2.6665 6.6665H2.83447C2.96043 6.17722 3.15403 5.71488 3.40479 5.29053L3.28564 5.17139C2.76536 4.65065 2.76508 3.80621 3.28564 3.28564C3.80621 2.76508 4.65065 2.76536 5.17139 3.28564L5.29053 3.40479C5.71488 3.15403 6.17722 2.96043 6.6665 2.83447V2.6665C6.66668 1.93027 7.26422 1.3335 8.00049 1.3335ZM7.3335 8.00049L6.00049 6.6665L4.6665 8.00049L7.3335 10.6665L11.3335 6.6665L10.0005 5.3335L7.3335 8.00049Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
class?: string
|
||||
}
|
||||
const { size = 16, color = '#60A5FA', class: className } = defineProps<Props>()
|
||||
const iconClass = computed(() => className || '')
|
||||
</script>
|
||||
@@ -42,7 +42,6 @@
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
}"
|
||||
@whats-new-dismissed="handleWhatsNewDismissed"
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
@@ -64,9 +63,6 @@ import { computed, onMounted } from 'vue'
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -76,22 +72,8 @@ import SidebarIcon from './SidebarIcon.vue'
|
||||
const settingStore = useSettingStore()
|
||||
const releaseStore = useReleaseStore()
|
||||
const helpCenterStore = useHelpCenterStore()
|
||||
const { shouldShowRedDot } = storeToRefs(releaseStore)
|
||||
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
|
||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||
|
||||
const conflictDetection = useConflictDetection()
|
||||
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
|
||||
// Use conflict acknowledgment state from composable - call only once
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed(() => {
|
||||
const releaseRedDot = showReleaseRedDot
|
||||
return releaseRedDot || shouldShowConflictRedDot.value
|
||||
})
|
||||
|
||||
const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
@@ -107,36 +89,6 @@ const closeHelpCenter = () => {
|
||||
helpCenterStore.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle What's New popup dismissal
|
||||
* Check if conflict modal should be shown after ComfyUI update
|
||||
*/
|
||||
const handleWhatsNewDismissed = async () => {
|
||||
try {
|
||||
// Check if conflict modal should be shown after update
|
||||
const shouldShow =
|
||||
await conflictDetection.shouldShowConflictModalAfterUpdate()
|
||||
if (shouldShow) {
|
||||
showConflictModal()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HelpCenter] Error checking conflict modal:', error)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Show the node conflict dialog with current conflict data
|
||||
*/
|
||||
const showConflictModal = () => {
|
||||
showNodeConflictDialog({
|
||||
showAfterWhatsNew: true,
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize release store on mount
|
||||
onMounted(async () => {
|
||||
// Initialize release store to fetch releases for toast and popup
|
||||
|
||||
@@ -107,12 +107,9 @@ import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
@@ -124,6 +121,7 @@ const colorPaletteStore = useColorPaletteStore()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -159,28 +157,23 @@ const showSettings = (defaultPanel?: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const managerStateStore = useManagerStateStore()
|
||||
// Temporary duplicated from LoadWorkflowWarning.vue
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
|
||||
const showManageExtensions = async () => {
|
||||
const state = managerStateStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
showSettings('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
showSettings('extension')
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
useDialogService().showManagerDialog()
|
||||
break
|
||||
const showManageExtensions = () => {
|
||||
if (isManagerInstalled.value) {
|
||||
useDialogService().showManagerDialog()
|
||||
} else {
|
||||
showSettings('extension')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
/**
|
||||
@@ -179,12 +179,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const numImagesWidget = node.widgets?.find(
|
||||
(w) => w.name === 'num_images'
|
||||
) as IComboWidget
|
||||
const characterInput = node.inputs?.find(
|
||||
(i) => i.name === 'character_image'
|
||||
) as INodeInputSlot
|
||||
const hasCharacter =
|
||||
typeof characterInput?.link !== 'undefined' &&
|
||||
characterInput.link != null
|
||||
|
||||
if (!renderingSpeedWidget)
|
||||
return '$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)'
|
||||
@@ -194,23 +188,11 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
|
||||
const renderingSpeed = String(renderingSpeedWidget.value)
|
||||
if (renderingSpeed.toLowerCase().includes('quality')) {
|
||||
if (hasCharacter) {
|
||||
basePrice = 0.2
|
||||
} else {
|
||||
basePrice = 0.09
|
||||
}
|
||||
} else if (renderingSpeed.toLowerCase().includes('default')) {
|
||||
if (hasCharacter) {
|
||||
basePrice = 0.15
|
||||
} else {
|
||||
basePrice = 0.06
|
||||
}
|
||||
basePrice = 0.09
|
||||
} else if (renderingSpeed.toLowerCase().includes('balanced')) {
|
||||
basePrice = 0.06
|
||||
} else if (renderingSpeed.toLowerCase().includes('turbo')) {
|
||||
if (hasCharacter) {
|
||||
basePrice = 0.1
|
||||
} else {
|
||||
basePrice = 0.03
|
||||
}
|
||||
basePrice = 0.03
|
||||
}
|
||||
|
||||
const totalCost = (basePrice * numImages).toFixed(2)
|
||||
@@ -413,12 +395,7 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
|
||||
// Same pricing matrix as KlingTextToVideoNode
|
||||
if (modeValue.includes('v2-1')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$0.98/Run' // pro, 10s
|
||||
}
|
||||
return '$0.49/Run' // pro, 5s default
|
||||
} else if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run'
|
||||
}
|
||||
@@ -1485,7 +1462,7 @@ export const useNodePricing = () => {
|
||||
OpenAIGPTImage1: ['quality', 'n'],
|
||||
IdeogramV1: ['num_images', 'turbo'],
|
||||
IdeogramV2: ['num_images', 'turbo'],
|
||||
IdeogramV3: ['rendering_speed', 'num_images', 'character_image'],
|
||||
IdeogramV3: ['rendering_speed', 'num_images'],
|
||||
FluxProKontextProNode: [],
|
||||
FluxProKontextMaxNode: [],
|
||||
VeoVideoGenerationNode: ['duration_seconds'],
|
||||
|
||||
@@ -75,29 +75,6 @@ export const useComputedWithWidgetWatch = (
|
||||
}
|
||||
})
|
||||
})
|
||||
if (widgetNames && widgetNames.length > widgetsToObserve.length) {
|
||||
//Inputs have been included
|
||||
const indexesToObserve = widgetNames
|
||||
.map((name) =>
|
||||
widgetsToObserve.some((w) => w.name == name)
|
||||
? -1
|
||||
: node.inputs.findIndex((i) => i.name == name)
|
||||
)
|
||||
.filter((i) => i >= 0)
|
||||
node.onConnectionsChange = useChainCallback(
|
||||
node.onConnectionsChange,
|
||||
(_type: unknown, index: number, isConnected: boolean) => {
|
||||
if (!indexesToObserve.includes(index)) return
|
||||
widgetValues.value = {
|
||||
...widgetValues.value,
|
||||
[indexesToObserve[index]]: isConnected
|
||||
}
|
||||
if (triggerCanvasRedraw) {
|
||||
node.graph?.setDirtyCanvas(true, true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that creates a computed that responds to widget changes.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, onUnmounted } from 'vue'
|
||||
|
||||
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
@@ -9,10 +9,6 @@ import type { components } from '@/types/comfyRegistryTypes'
|
||||
export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
// Flag to prevent duplicate fetches during initialization
|
||||
const isInitializing = ref(false)
|
||||
const lastFetchedIds = ref<string>('')
|
||||
|
||||
const installedPackIds = computed(() =>
|
||||
Array.from(comfyManagerStore.installedPacksIds)
|
||||
)
|
||||
@@ -24,59 +20,24 @@ export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
|
||||
packs.filter((pack) => comfyManagerStore.isPackInstalled(pack.id))
|
||||
|
||||
const startFetchInstalled = async () => {
|
||||
// Prevent duplicate calls during initialization
|
||||
if (isInitializing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isInitializing.value = true
|
||||
try {
|
||||
if (comfyManagerStore.installedPacksIds.size === 0) {
|
||||
await comfyManagerStore.refreshInstalledList()
|
||||
}
|
||||
await startFetch()
|
||||
} finally {
|
||||
isInitializing.value = false
|
||||
}
|
||||
await comfyManagerStore.refreshInstalledList()
|
||||
await startFetch()
|
||||
}
|
||||
|
||||
// When installedPackIds changes, we need to update the nodePacks
|
||||
// But only if the IDs actually changed (not just array reference)
|
||||
whenever(installedPackIds, async (newIds) => {
|
||||
const newIdsStr = newIds.sort().join(',')
|
||||
if (newIdsStr !== lastFetchedIds.value && !isInitializing.value) {
|
||||
lastFetchedIds.value = newIdsStr
|
||||
await startFetch()
|
||||
}
|
||||
whenever(installedPackIds, async () => {
|
||||
await startFetch()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// Create a computed property that provides installed pack info with versions
|
||||
const installedPacksWithVersions = computed(() => {
|
||||
const result: Array<{ id: string; version: string }> = []
|
||||
|
||||
for (const pack of Object.values(comfyManagerStore.installedPacks)) {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
if (id) {
|
||||
result.push({
|
||||
id,
|
||||
version: pack.ver ?? ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
isReady,
|
||||
installedPacks: nodePacks,
|
||||
installedPacksWithVersions,
|
||||
startFetchInstalled,
|
||||
filterInstalledPack
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
export type SelectionState = 'all-installed' | 'none-installed' | 'mixed'
|
||||
|
||||
/**
|
||||
* Composable for managing multi-package selection states
|
||||
* Handles installation status tracking and selection state determination
|
||||
*/
|
||||
export function usePacksSelection(nodePacks: Ref<NodePack[]>) {
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedPacks = computed(() =>
|
||||
nodePacks.value.filter((pack) => managerStore.isPackInstalled(pack.id))
|
||||
)
|
||||
|
||||
const notInstalledPacks = computed(() =>
|
||||
nodePacks.value.filter((pack) => !managerStore.isPackInstalled(pack.id))
|
||||
)
|
||||
|
||||
const isAllInstalled = computed(
|
||||
() => installedPacks.value.length === nodePacks.value.length
|
||||
)
|
||||
|
||||
const isNoneInstalled = computed(
|
||||
() => notInstalledPacks.value.length === nodePacks.value.length
|
||||
)
|
||||
|
||||
const isMixed = computed(
|
||||
() => installedPacks.value.length > 0 && notInstalledPacks.value.length > 0
|
||||
)
|
||||
|
||||
const selectionState = computed<SelectionState>(() => {
|
||||
if (isAllInstalled.value) return 'all-installed'
|
||||
if (isNoneInstalled.value) return 'none-installed'
|
||||
return 'mixed'
|
||||
})
|
||||
|
||||
return {
|
||||
installedPacks,
|
||||
notInstalledPacks,
|
||||
isAllInstalled,
|
||||
isNoneInstalled,
|
||||
isMixed,
|
||||
selectionState
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
type NodeStatus = components['schemas']['NodeStatus']
|
||||
type NodeVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
const STATUS_PRIORITY = [
|
||||
'NodeStatusBanned',
|
||||
'NodeVersionStatusBanned',
|
||||
'NodeStatusDeleted',
|
||||
'NodeVersionStatusDeleted',
|
||||
'NodeVersionStatusFlagged',
|
||||
'NodeVersionStatusPending',
|
||||
'NodeStatusActive',
|
||||
'NodeVersionStatusActive'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Composable for managing package status with priority
|
||||
* Handles import failures and determines the most important status
|
||||
*/
|
||||
export function usePacksStatus(nodePacks: Ref<NodePack[]>) {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
const hasImportFailed = computed(() => {
|
||||
return nodePacks.value.some((pack) => {
|
||||
if (!pack.id) return false
|
||||
const conflicts = conflictDetectionStore.getConflictsForPackageByID(
|
||||
pack.id
|
||||
)
|
||||
return (
|
||||
conflicts?.conflicts?.some((c) => c.type === 'import_failed') || false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const overallStatus = computed<NodeStatus | NodeVersionStatus>(() => {
|
||||
// Check for import failed first (highest priority for installed packages)
|
||||
if (hasImportFailed.value) {
|
||||
// Import failed doesn't have a specific status enum, so we return active
|
||||
// but the PackStatusMessage will handle it via hasImportFailed prop
|
||||
return 'NodeVersionStatusActive' as NodeVersionStatus
|
||||
}
|
||||
|
||||
// Find the highest priority status from all packages
|
||||
for (const priorityStatus of STATUS_PRIORITY) {
|
||||
if (nodePacks.value.some((pack) => pack.status === priorityStatus)) {
|
||||
return priorityStatus as NodeStatus | NodeVersionStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Default to active if no specific status found
|
||||
return 'NodeVersionStatusActive' as NodeVersionStatus
|
||||
})
|
||||
|
||||
return {
|
||||
hasImportFailed,
|
||||
overallStatus
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable to find NodePacks that have updates available
|
||||
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||
* Automatically fetches installed pack data when initialized
|
||||
*/
|
||||
export const useUpdateAvailableNodes = () => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { installedPacks, isLoading, error, startFetchInstalled } =
|
||||
useInstalledPacks()
|
||||
|
||||
// Check if a pack has updates available (same logic as usePackUpdateStatus)
|
||||
const isOutdatedPack = (pack: components['schemas']['Node']) => {
|
||||
const isInstalled = comfyManagerStore.isPackInstalled(pack?.id)
|
||||
if (!isInstalled) return false
|
||||
|
||||
const installedVersion = comfyManagerStore.getInstalledPackVersion(
|
||||
pack.id ?? ''
|
||||
)
|
||||
const latestVersion = pack.latest_version?.version
|
||||
|
||||
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion)
|
||||
|
||||
if (isNightlyPack || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
return compareVersions(latestVersion, installedVersion) > 0
|
||||
}
|
||||
|
||||
// Same filtering logic as ManagerDialogContent.vue
|
||||
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter(isOutdatedPack)
|
||||
|
||||
// Filter only outdated packs from installed packs
|
||||
const updateAvailableNodePacks = computed(() => {
|
||||
if (!installedPacks.value.length) return []
|
||||
return filterOutdatedPacks(installedPacks.value)
|
||||
})
|
||||
|
||||
// Check if there are any outdated packs
|
||||
const hasUpdateAvailable = computed(() => {
|
||||
return updateAvailableNodePacks.value.length > 0
|
||||
})
|
||||
|
||||
// Automatically fetch installed pack data when composable is used
|
||||
onMounted(async () => {
|
||||
if (!installedPacks.value.length && !isLoading.value) {
|
||||
await startFetchInstalled()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
updateAvailableNodePacks,
|
||||
hasUpdateAvailable,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { app } from '@/scripts/app'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
@@ -66,7 +66,8 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
return {
|
||||
id: CORE_NODES_PACK_NAME,
|
||||
version:
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ?? 'nightly'
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
if (pack) {
|
||||
return {
|
||||
id: pack.id,
|
||||
version: pack.latest_version?.version ?? 'nightly'
|
||||
version: pack.latest_version?.version ?? SelectedVersion.NIGHTLY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
|
||||
/**
|
||||
* LocalStorage keys for conflict acknowledgment tracking
|
||||
*/
|
||||
const STORAGE_KEYS = {
|
||||
CONFLICT_MODAL_DISMISSED: 'Comfy.ConflictModalDismissed',
|
||||
CONFLICT_RED_DOT_DISMISSED: 'Comfy.ConflictRedDotDismissed',
|
||||
CONFLICT_WARNING_BANNER_DISMISSED: 'Comfy.ConflictWarningBannerDismissed'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Interface for conflict acknowledgment state
|
||||
*/
|
||||
interface ConflictAcknowledgmentState {
|
||||
modal_dismissed: boolean
|
||||
red_dot_dismissed: boolean
|
||||
warning_banner_dismissed: boolean
|
||||
}
|
||||
|
||||
// Shared state - initialized once and reused across all composable calls
|
||||
const modalDismissed = useStorage(STORAGE_KEYS.CONFLICT_MODAL_DISMISSED, false)
|
||||
const redDotDismissed = useStorage(
|
||||
STORAGE_KEYS.CONFLICT_RED_DOT_DISMISSED,
|
||||
false
|
||||
)
|
||||
const warningBannerDismissed = useStorage(
|
||||
STORAGE_KEYS.CONFLICT_WARNING_BANNER_DISMISSED,
|
||||
false
|
||||
)
|
||||
|
||||
/**
|
||||
* Composable for managing conflict acknowledgment state in localStorage
|
||||
*
|
||||
* This handles:
|
||||
* - Tracking whether conflict modal has been dismissed
|
||||
* - Tracking whether red dot notification has been cleared
|
||||
* - Managing per-package conflict acknowledgments
|
||||
* - Detecting ComfyUI version changes to reset acknowledgment state
|
||||
*/
|
||||
export function useConflictAcknowledgment() {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
// Create computed state object for backward compatibility
|
||||
const state = computed<ConflictAcknowledgmentState>(() => ({
|
||||
modal_dismissed: modalDismissed.value,
|
||||
red_dot_dismissed: redDotDismissed.value,
|
||||
warning_banner_dismissed: warningBannerDismissed.value
|
||||
}))
|
||||
|
||||
/**
|
||||
* Mark red dot notification as dismissed
|
||||
*/
|
||||
function dismissRedDotNotification(): void {
|
||||
redDotDismissed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark manager warning banner as dismissed
|
||||
*/
|
||||
function dismissWarningBanner(): void {
|
||||
warningBannerDismissed.value = true
|
||||
redDotDismissed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark conflicts as seen (unified function for help center and manager)
|
||||
*/
|
||||
function markConflictsAsSeen(): void {
|
||||
redDotDismissed.value = true
|
||||
modalDismissed.value = true
|
||||
warningBannerDismissed.value = true
|
||||
}
|
||||
|
||||
const hasConflicts = computed(() => conflictDetectionStore.hasConflicts)
|
||||
const shouldShowConflictModal = computed(() => !modalDismissed.value)
|
||||
const shouldShowRedDot = computed(() => {
|
||||
if (!hasConflicts.value) return false
|
||||
if (redDotDismissed.value) return false
|
||||
return true
|
||||
})
|
||||
const shouldShowManagerBanner = computed(() => {
|
||||
return hasConflicts.value && !warningBannerDismissed.value
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
acknowledgmentState: state,
|
||||
shouldShowConflictModal,
|
||||
shouldShowRedDot,
|
||||
shouldShowManagerBanner,
|
||||
|
||||
// Methods
|
||||
dismissRedDotNotification,
|
||||
dismissWarningBanner,
|
||||
markConflictsAsSeen
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,15 +20,10 @@ import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
@@ -38,7 +33,6 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import {
|
||||
getAllNonIoNodesInSubgraph,
|
||||
getExecutionIdsForSelectedNodes
|
||||
@@ -685,13 +679,36 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
await workflowService.closeWorkflow(workflowStore.activeWorkflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Feedback',
|
||||
icon: 'pi pi-megaphone',
|
||||
label: 'Give Feedback',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('g.feedback'),
|
||||
subtitle: t('issueReport.feedbackTitle'),
|
||||
panelProps: {
|
||||
errorType: 'Feedback',
|
||||
defaultFields: ['SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ContactSupport',
|
||||
icon: 'pi pi-question',
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
window.open('https://support.comfy.org/', '_blank')
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('issueReport.contactSupportTitle'),
|
||||
subtitle: t('issueReport.contactSupportDescription'),
|
||||
panelProps: {
|
||||
errorType: 'ContactSupport',
|
||||
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -715,107 +732,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
id: 'Comfy.Manager.CustomNodesManager',
|
||||
icon: 'pi pi-puzzle',
|
||||
label: 'Custom Nodes Manager',
|
||||
label: 'Toggle the Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: async () => {
|
||||
const managerState = useManagerStateStore().managerUIState
|
||||
|
||||
switch (managerState) {
|
||||
case ManagerUIState.DISABLED:
|
||||
dialogService.showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
|
||||
icon: 'pi pi-sync',
|
||||
label: 'Check for Custom Node Updates',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
const managerStore = useManagerStateStore()
|
||||
const state = managerStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.notAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
useCommandStore()
|
||||
.execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
.catch(() => {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
dialogService.showSettingsDialog('extension')
|
||||
})
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowMissingPacks',
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
label: 'Install Missing Custom Nodes',
|
||||
versionAdded: '1.17.0',
|
||||
function: async () => {
|
||||
const managerStore = useManagerStateStore()
|
||||
const state = managerStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
// When manager is disabled, open the extensions panel in settings
|
||||
dialogService.showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility'
|
||||
)
|
||||
} catch {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
dialogService.showSettingsDialog('extension')
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
break
|
||||
}
|
||||
dialogService.toggleManagerDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -894,7 +816,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
canvasStore.updateSelectedItems()
|
||||
@@ -980,84 +901,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const modelSelectorDialog = useModelSelectorDialog()
|
||||
modelSelectorDialog.show()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
|
||||
icon: 'pi pi-bars',
|
||||
label: 'Custom Nodes (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowLegacyManagerMenu',
|
||||
icon: 'mdi mdi-puzzle',
|
||||
label: 'Manager Menu (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModels',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: false })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models and Execution Cache',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: true })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
/**
|
||||
* Known server feature flags (top-level, not extensions)
|
||||
*/
|
||||
export enum ServerFeatureFlag {
|
||||
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size',
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4'
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive access to feature flags
|
||||
*/
|
||||
export function useFeatureFlags() {
|
||||
// Create reactive state that tracks server feature flags
|
||||
const flags = reactive({
|
||||
get supportsPreviewMetadata() {
|
||||
return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||
},
|
||||
get maxUploadSize() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
},
|
||||
get supportsManagerV4() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4)
|
||||
}
|
||||
})
|
||||
|
||||
// Create a reactive computed for any feature flag
|
||||
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
|
||||
return computed(() => api.getServerFeature(featurePath, defaultValue))
|
||||
}
|
||||
|
||||
return {
|
||||
flags: readonly(flags),
|
||||
featureFlag
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { type ComputedRef, computed, unref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
|
||||
/**
|
||||
* Extracting import failed conflicts from conflict list
|
||||
*/
|
||||
function extractImportFailedConflicts(conflicts?: ConflictDetail[] | null) {
|
||||
if (!conflicts) return null
|
||||
|
||||
const importFailedConflicts = conflicts.filter(
|
||||
(item): item is ConflictDetail => item.type === 'import_failed'
|
||||
)
|
||||
|
||||
return importFailedConflicts.length > 0 ? importFailedConflicts : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating import failed dialog
|
||||
*/
|
||||
function createImportFailedDialog() {
|
||||
const { t } = useI18n()
|
||||
const { showErrorDialog } = useDialogService()
|
||||
|
||||
return (importFailedInfo: ConflictDetail[] | null) => {
|
||||
if (importFailedInfo) {
|
||||
const errorMessage =
|
||||
importFailedInfo
|
||||
.map((conflict) => conflict.required_value)
|
||||
.filter(Boolean)
|
||||
.join('\n') || t('manager.importFailedGenericError')
|
||||
|
||||
const error = new Error(errorMessage)
|
||||
|
||||
showErrorDialog(error, {
|
||||
title: t('manager.failedToInstall'),
|
||||
reportType: 'importFailedError'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for detecting and handling import failed conflicts
|
||||
* @param packageId - Package ID string or computed ref
|
||||
* @returns Object with import failed detection and dialog handler
|
||||
*/
|
||||
export function useImportFailedDetection(
|
||||
packageId?: string | ComputedRef<string> | null
|
||||
) {
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
|
||||
const isInstalled = computed(() =>
|
||||
packageId ? isPackInstalled(unref(packageId)) : false
|
||||
)
|
||||
|
||||
const conflicts = computed(() => {
|
||||
const currentPackageId = unref(packageId)
|
||||
if (!currentPackageId || !isInstalled.value) return null
|
||||
return getConflictsForPackageByID(currentPackageId) || null
|
||||
})
|
||||
|
||||
const importFailedInfo = computed(() => {
|
||||
return extractImportFailedConflicts(conflicts.value?.conflicts)
|
||||
})
|
||||
|
||||
const importFailed = computed(() => {
|
||||
return importFailedInfo.value !== null
|
||||
})
|
||||
|
||||
const showImportFailedDialog = createImportFailedDialog()
|
||||
|
||||
return {
|
||||
importFailedInfo,
|
||||
importFailed,
|
||||
showImportFailedDialog: () =>
|
||||
showImportFailedDialog(importFailedInfo.value),
|
||||
isInstalled
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,101 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { pickBy } from 'es-toolkit/compat'
|
||||
import { Ref, computed, ref } from 'vue'
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ManagerWsQueueStatus } from '@/types/comfyManagerTypes'
|
||||
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
components['schemas']['TaskHistoryItem']
|
||||
>
|
||||
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
type QueueTaskItem = components['schemas']['QueueTaskItem']
|
||||
type HistoryTaskItem = components['schemas']['TaskHistoryItem']
|
||||
type Task = QueueTaskItem | HistoryTaskItem
|
||||
type QueuedTask<T> = {
|
||||
task: () => Promise<T>
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
const MANAGER_WS_MSG_TYPE = 'cm-queue-status'
|
||||
|
||||
export const useManagerQueue = (
|
||||
taskHistory: Ref<ManagerTaskHistory>,
|
||||
taskQueue: Ref<ManagerTaskQueue>,
|
||||
installedPacks: Ref<Record<string, any>>
|
||||
) => {
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
// Task queue state (read-only from server)
|
||||
const maxHistoryItems = ref(64)
|
||||
const isLoading = ref(false)
|
||||
const isProcessing = ref(false)
|
||||
|
||||
// Computed values
|
||||
const currentQueueLength = computed(
|
||||
() =>
|
||||
taskQueue.value.running_queue.length +
|
||||
taskQueue.value.pending_queue.length
|
||||
export const useManagerQueue = () => {
|
||||
const clientQueueItems = ref<QueuedTask<unknown>[]>([])
|
||||
const clientQueueLength = computed(() => clientQueueItems.value.length)
|
||||
const onCompletedQueue = ref<((() => void) | undefined)[]>([])
|
||||
const onCompleteWaitingCount = ref(0)
|
||||
const uncompletedCount = computed(
|
||||
() => clientQueueLength.value + onCompleteWaitingCount.value
|
||||
)
|
||||
|
||||
/**
|
||||
* Update the processing state based on the current queue length.
|
||||
* If the queue is empty, or all tasks in the queue are associated
|
||||
* with different clients, then this client is not processing any tasks.
|
||||
*/
|
||||
const updateProcessingState = (): void => {
|
||||
isProcessing.value = currentQueueLength.value > 0
|
||||
}
|
||||
const serverQueueStatus = ref<ManagerWsQueueStatus>(ManagerWsQueueStatus.DONE)
|
||||
const isServerIdle = computed(
|
||||
() => serverQueueStatus.value === ManagerWsQueueStatus.DONE
|
||||
)
|
||||
|
||||
const allTasksDone = computed(() => currentQueueLength.value === 0)
|
||||
const historyCount = computed(() => Object.keys(taskHistory.value).length)
|
||||
const allTasksDone = computed(
|
||||
() => isServerIdle.value && clientQueueLength.value === 0
|
||||
)
|
||||
const nextTaskReady = computed(
|
||||
() => isServerIdle.value && clientQueueLength.value > 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if a task is associated with this client.
|
||||
* Task can be from running queue, pending queue, or history.
|
||||
* @param task - The task to check
|
||||
* @returns True if the task belongs to this client
|
||||
*/
|
||||
const isTaskFromThisClient = (task: Task): boolean =>
|
||||
task.client_id === app.api.clientId
|
||||
|
||||
/**
|
||||
* Check if a history task is associated with this client.
|
||||
* @param task - The history task to check
|
||||
* @returns True if the task belongs to this client
|
||||
*/
|
||||
const isHistoryTaskFromThisClient = (task: HistoryTaskItem): boolean =>
|
||||
task.client_id === app.api.clientId
|
||||
|
||||
/**
|
||||
* Filter queue tasks by client id.
|
||||
* Ensures that only tasks associated with this client are processed and
|
||||
* added to client state.
|
||||
* @param tasks - Array of queue tasks to filter
|
||||
* @returns Filtered array containing only tasks from this client
|
||||
*/
|
||||
const filterQueueByClientId = (tasks: QueueTaskItem[]): QueueTaskItem[] =>
|
||||
tasks.filter(isTaskFromThisClient)
|
||||
|
||||
/**
|
||||
* Filter history tasks by client id using pickBy for optimal performance.
|
||||
* Returns a new object containing only tasks associated with this client.
|
||||
* @param history - The history object to filter
|
||||
* @returns Filtered history object containing only tasks from this client
|
||||
*/
|
||||
const filterHistoryByClientId = (history: ManagerTaskHistory) =>
|
||||
pickBy(history, isHistoryTaskFromThisClient)
|
||||
|
||||
/**
|
||||
* Update task queue and history state with filtered data from server.
|
||||
* Ensures only tasks from this client are stored in local state.
|
||||
* @param state - The task state message from the server
|
||||
*/
|
||||
const updateTaskState = (state: ManagerTaskQueue) => {
|
||||
taskQueue.value.running_queue = filterQueueByClientId(state.running_queue)
|
||||
taskQueue.value.pending_queue = filterQueueByClientId(state.pending_queue)
|
||||
taskHistory.value = filterHistoryByClientId(state.history)
|
||||
|
||||
if (state.installed_packs) {
|
||||
installedPacks.value = state.installed_packs
|
||||
}
|
||||
updateProcessingState()
|
||||
}
|
||||
|
||||
// WebSocket event listener for task done
|
||||
const cleanupTaskDoneListener = useEventListener(
|
||||
app.api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
(event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) {
|
||||
updateTaskState(event.detail.state)
|
||||
|
||||
// If no more tasks are running/pending, hide the progress dialog
|
||||
if (allTasksDone.value) {
|
||||
setTimeout(() => {
|
||||
if (allTasksDone.value) {
|
||||
showManagerProgressDialog()
|
||||
}
|
||||
}, 1000) // Small delay to let users see completion
|
||||
}
|
||||
const cleanupListener = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_MSG_TYPE,
|
||||
(event: CustomEvent<{ status: ManagerWsQueueStatus }>) => {
|
||||
if (event?.type === MANAGER_WS_MSG_TYPE && event.detail?.status) {
|
||||
serverQueueStatus.value = event.detail.status
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// WebSocket event listener for task started
|
||||
const cleanupTaskStartedListener = useEventListener(
|
||||
app.api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
(event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) {
|
||||
updateTaskState(event.detail.state)
|
||||
const startNextTask = () => {
|
||||
const nextTask = clientQueueItems.value.shift()
|
||||
if (!nextTask) return
|
||||
|
||||
// Show progress dialog when a task starts
|
||||
showManagerProgressDialog()
|
||||
}
|
||||
const { task, onComplete } = nextTask
|
||||
if (onComplete) {
|
||||
// Set the task's onComplete to be executed the next time the server is idle
|
||||
onCompletedQueue.value.push(onComplete)
|
||||
onCompleteWaitingCount.value++
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Cleanup function to remove event listeners and reset state
|
||||
*/
|
||||
task().catch((e) => {
|
||||
const message = `Error enqueuing task for ComfyUI Manager: ${e}`
|
||||
console.error(message)
|
||||
})
|
||||
}
|
||||
|
||||
const enqueueTask = <T>(task: QueuedTask<T>): void => {
|
||||
clientQueueItems.value.push(task)
|
||||
}
|
||||
|
||||
const clearQueue = () => {
|
||||
clientQueueItems.value = []
|
||||
onCompletedQueue.value = []
|
||||
onCompleteWaitingCount.value = 0
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
cleanupTaskDoneListener()
|
||||
cleanupTaskStartedListener()
|
||||
|
||||
// Reset state
|
||||
isProcessing.value = false
|
||||
isLoading.value = false
|
||||
clearQueue()
|
||||
cleanupListener()
|
||||
}
|
||||
|
||||
whenever(nextTaskReady, startNextTask)
|
||||
whenever(isServerIdle, () => {
|
||||
if (onCompletedQueue.value?.length) {
|
||||
while (
|
||||
onCompleteWaitingCount.value > 0 &&
|
||||
onCompletedQueue.value.length > 0
|
||||
) {
|
||||
const onComplete = onCompletedQueue.value.shift()
|
||||
onComplete?.()
|
||||
onCompleteWaitingCount.value--
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
isProcessing,
|
||||
maxHistoryItems,
|
||||
|
||||
// Computed
|
||||
allTasksDone,
|
||||
historyCount,
|
||||
currentQueueLength,
|
||||
statusMessage: readonly(serverQueueStatus),
|
||||
queueLength: clientQueueLength,
|
||||
uncompletedCount,
|
||||
|
||||
// Actions
|
||||
updateTaskState,
|
||||
enqueueTask,
|
||||
clearQueue,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,22 @@ import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import { LogsWsMessage } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
const LOGS_MESSAGE_TYPE = 'logs'
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
|
||||
interface UseServerLogsOptions {
|
||||
ui_id?: string
|
||||
immediate?: boolean
|
||||
messageFilter?: (message: string) => boolean
|
||||
}
|
||||
|
||||
export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
const {
|
||||
ui_id,
|
||||
immediate = false,
|
||||
messageFilter = (msg: string) => Boolean(msg.trim())
|
||||
} = options
|
||||
|
||||
const logs = ref<string[]>([])
|
||||
const isTaskStarted = ref(!ui_id) // If no ui_id, capture all logs immediately
|
||||
let stopLogs: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskDone: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskStarted: ReturnType<typeof useEventListener> | null = null
|
||||
let stop: ReturnType<typeof useEventListener> | null = null
|
||||
|
||||
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
|
||||
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
|
||||
@@ -38,54 +27,19 @@ export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
event.detail.entries.map((e) => e.m).filter(messageFilter)
|
||||
|
||||
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
|
||||
// Only capture logs if this task has started
|
||||
if (!isTaskStarted.value) return
|
||||
|
||||
if (isValidLogEvent(event)) {
|
||||
const messages = parseLogMessage(event)
|
||||
if (messages.length > 0) {
|
||||
logs.value.push(...messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskStarted = (event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (ui_id && event?.detail?.ui_id === ui_id) {
|
||||
isTaskStarted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskDone = (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (ui_id && event?.detail?.ui_id === ui_id) {
|
||||
isTaskStarted.value = false
|
||||
logs.value.push(...parseLogMessage(event))
|
||||
}
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
await api.subscribeLogs(true)
|
||||
stopLogs = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
|
||||
if (ui_id) {
|
||||
stopTaskStarted = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
handleTaskStarted
|
||||
)
|
||||
stopTaskDone = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
handleTaskDone
|
||||
)
|
||||
}
|
||||
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
}
|
||||
|
||||
const stopListening = async () => {
|
||||
stopLogs?.()
|
||||
stopTaskStarted?.()
|
||||
stopTaskDone?.()
|
||||
stopLogs = null
|
||||
stopTaskStarted = null
|
||||
stopTaskDone = null
|
||||
stop?.()
|
||||
stop = null
|
||||
await api.subscribeLogs(false)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user