Merge branch 'main' into copilot/standardize-triggering-and-docs
@@ -458,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
|
||||
```bash
|
||||
sleep 10
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
```
|
||||
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
|
||||
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
|
||||
- Create a `core/x.yy` branch for the PREVIOUS minor version
|
||||
- Apply branch protection rules
|
||||
- Document the feature freeze policy
|
||||
```bash
|
||||
# Monitor branch creation (for minor/major releases)
|
||||
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
|
||||
gh run list --workflow=release-branch-create.yaml --limit=1
|
||||
```
|
||||
4. If workflow didn't trigger due to [skip ci]:
|
||||
```bash
|
||||
@@ -477,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
5. If workflow triggered, monitor execution:
|
||||
```bash
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ For each commit:
|
||||
3. Merge the PR: `gh pr merge --merge`
|
||||
4. Monitor release workflow:
|
||||
```bash
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
gh run watch
|
||||
```
|
||||
5. Track progress:
|
||||
|
||||
99
.github/workflows/pr-backport.yaml
vendored
@@ -96,41 +96,61 @@ jobs:
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Extract version labels
|
||||
- name: Collect backport targets
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: versions
|
||||
id: targets
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
|
||||
TARGETS=()
|
||||
declare -A SEEN=()
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# For manual triggers, get labels from the PR
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
# For automatic triggers, extract from PR event
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
for label in $LABELS; do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
|
||||
VERSIONS="${VERSIONS}${label} "
|
||||
else
|
||||
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VERSIONS" ]; then
|
||||
echo "::error::No version labels found (e.g., 1.24, 1.22)"
|
||||
add_target() {
|
||||
local label="$1"
|
||||
local target="$2"
|
||||
|
||||
if [ -z "$target" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
target=$(echo "$target" | xargs)
|
||||
|
||||
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
|
||||
TARGETS+=("$target")
|
||||
SEEN["$target"]=1
|
||||
else
|
||||
echo "::warning::Label '${label}' references missing branch '${target}'"
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r label; do
|
||||
[ -z "$label" ] && continue
|
||||
|
||||
if [[ "$label" =~ ^branch:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
add_target "$label" "core/${label}"
|
||||
fi
|
||||
done <<< "$LABELS"
|
||||
|
||||
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
||||
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
|
||||
echo "Found backport targets: ${TARGETS[*]}"
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
@@ -151,16 +171,17 @@ jobs:
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
for target in ${{ steps.targets.outputs.targets }}; do
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
|
||||
TARGET_BRANCH="core/${version}"
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
|
||||
echo "::group::Backporting to ${TARGET_BRANCH}"
|
||||
|
||||
# Fetch target branch (fail if doesn't exist)
|
||||
if ! git fetch origin "${TARGET_BRANCH}"; then
|
||||
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
|
||||
FAILED="${FAILED}${version}:branch-missing "
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
@@ -171,7 +192,7 @@ jobs:
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
git checkout main
|
||||
@@ -181,7 +202,7 @@ jobs:
|
||||
git cherry-pick --abort
|
||||
|
||||
echo "::error::Cherry-pick failed due to conflicts"
|
||||
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
|
||||
|
||||
# Clean up the failed branch
|
||||
git checkout main
|
||||
@@ -215,13 +236,13 @@ jobs:
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
IFS=':' read -r target branch <<< "${backport}"
|
||||
|
||||
if PR_URL=$(gh pr create \
|
||||
--base "core/${version}" \
|
||||
--base "${target}" \
|
||||
--head "${branch}" \
|
||||
--title "[backport ${version}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--title "[backport ${target}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--label "backport" 2>&1); then
|
||||
|
||||
# Extract PR number from URL
|
||||
@@ -231,9 +252,9 @@ jobs:
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
|
||||
fi
|
||||
else
|
||||
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
|
||||
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
|
||||
# Still try to comment on the original PR about the failure
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -254,16 +275,16 @@ jobs:
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
IFS=':' read -r target reason conflicts <<< "${failure}"
|
||||
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
# Convert comma-separated conflicts back to newlines for display
|
||||
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
||||
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
fi
|
||||
done
|
||||
|
||||
1
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -45,6 +45,7 @@ jobs:
|
||||
contents: read
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ENABLE_MINIFY: 'true'
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
env:
|
||||
|
||||
1
.github/workflows/release-draft-create.yaml
vendored
@@ -56,6 +56,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
1
.github/workflows/release-pypi-dev.yaml
vendored
@@ -45,6 +45,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
27
.github/workflows/release-version-bump.yaml
vendored
@@ -15,6 +15,11 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
@@ -26,6 +31,24 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -59,7 +82,9 @@ jobs:
|
||||
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
52
.github/workflows/size-data.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: size data
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
collect:
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Collect size data
|
||||
run: node scripts/size-collect.js
|
||||
|
||||
- name: Save PR number & base branch
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
echo ${{ github.event.number }} > ./temp/size/number.txt
|
||||
echo ${{ github.base_ref }} > ./temp/size/base.txt
|
||||
|
||||
- name: Upload size data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: size-data
|
||||
path: temp/size
|
||||
104
.github/workflows/size-report.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: size report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['size data']
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to report on'
|
||||
required: true
|
||||
type: number
|
||||
run_id:
|
||||
description: 'Size data workflow run ID'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size-report:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
(
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
)
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
name: size-data
|
||||
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
|
||||
path: temp/size
|
||||
|
||||
- name: Set PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set base branch
|
||||
id: pr-base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=main" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download previous size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: size-data.yml
|
||||
event: push
|
||||
name: size-data
|
||||
path: temp/size-prev
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Generate size report
|
||||
run: node scripts/size-report.js > size-report.md
|
||||
|
||||
- name: Read size report
|
||||
id: size-report
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./size-report.md
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.pr-number.outputs.content }}
|
||||
body: |
|
||||
${{ steps.size-report.outputs.content }}
|
||||
<!-- COMFYUI_FRONTEND_SIZE -->
|
||||
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'
|
||||
26
.github/workflows/version-bump-desktop-ui.yaml
vendored
@@ -15,6 +15,11 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-desktop-ui:
|
||||
@@ -27,8 +32,25 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -65,8 +87,10 @@ jobs:
|
||||
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div max-w-48 min-h-52 grid relative"
|
||||
class="task-div relative grid min-h-52 max-w-48"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
>
|
||||
<Card
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
class="relative h-full max-w-48 overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="object-contain w-full h-full opacity-25 pt-4 px-4"
|
||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -27,7 +27,7 @@
|
||||
{{ description }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<div class="mt-1 flex gap-4">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
@@ -73,7 +73,7 @@ defineEmits<{
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
runner.value.state === 'error'
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
? (props.task.errorDescription ?? props.task.shortDescription)
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
28
browser_tests/tests/recordAudio.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Record Audio Node', () => {
|
||||
test('should add a record audio node and take a screenshot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open the search box by double clicking on the canvas
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
|
||||
// Search for and add the RecordAudio node
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the RecordAudio node was added
|
||||
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
|
||||
expect(recordAudioNodes.length).toBe(1)
|
||||
|
||||
// Take a screenshot of the canvas with the RecordAudio node
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
@@ -12,21 +12,26 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
test.fixme(
|
||||
'should allow toggling bypass on a selected node with hotkey',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
}
|
||||
)
|
||||
|
||||
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 82 KiB |
@@ -1,6 +1,7 @@
|
||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import pluginJs from '@eslint/js'
|
||||
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
@@ -23,10 +24,17 @@ const commonGlobals = {
|
||||
} as const
|
||||
|
||||
const settings = {
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
node: true
|
||||
},
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({
|
||||
alwaysTryTypes: true,
|
||||
project: [
|
||||
'./tsconfig.json',
|
||||
'./apps/*/tsconfig.json',
|
||||
'./packages/*/tsconfig.json'
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
],
|
||||
tailwindcss: {
|
||||
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
functions: ['cn', 'clsx', 'tw']
|
||||
@@ -246,5 +254,17 @@ export default defineConfig([
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.30.0",
|
||||
"version": "1.30.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -13,6 +13,8 @@
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build",
|
||||
"size:collect": "node scripts/size-collect.js",
|
||||
"size:report": "node scripts/size-report.js",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"dev:desktop": "nx dev @comfyorg/desktop-ui",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
@@ -87,9 +89,12 @@
|
||||
"jsdom": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-table": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"pretty-bytes": "catalog:",
|
||||
"rollup-plugin-visualizer": "catalog:",
|
||||
"storybook": "catalog:",
|
||||
"stylelint": "catalog:",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
--color-sand-200: #d6cfc2;
|
||||
--color-sand-300: #888682;
|
||||
|
||||
--color-pure-black: #000000;
|
||||
--color-pure-white: #ffffff;
|
||||
|
||||
--color-slate-100: #9c9eab;
|
||||
@@ -144,6 +145,9 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-pure-white);
|
||||
--button-surface-contrast: var(--color-pure-black);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
@@ -155,6 +159,7 @@
|
||||
--backdrop: var(--color-white);
|
||||
--button-hover-surface: var(--color-gray-200);
|
||||
--button-active-surface: var(--color-gray-400);
|
||||
--button-icon: var(--color-gray-600);
|
||||
--dialog-surface: var(--color-neutral-200);
|
||||
--interface-menu-component-surface-hovered: var(--color-gray-200);
|
||||
--interface-menu-component-surface-selected: var(--color-gray-400);
|
||||
@@ -201,8 +206,11 @@
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-pure-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-gray-800);
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
|
||||
--interface-menu-component-surface-selected: var(--color-charcoal-300);
|
||||
@@ -244,8 +252,11 @@
|
||||
|
||||
@theme inline {
|
||||
--color-backdrop: var(--backdrop);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-active-surface: var(--button-active-surface);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered);
|
||||
--color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 16 16" fill="none">
|
||||
<g clip-path="url(#clip0_704_2695)">
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="#9C9EAB" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_704_2695">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 938 B After Width: | Height: | Size: 964 B |
151
pnpm-lock.yaml
generated
@@ -183,9 +183,15 @@ catalogs:
|
||||
lint-staged:
|
||||
specifier: ^15.2.7
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
nx:
|
||||
specifier: 21.4.1
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.2.2
|
||||
@@ -193,8 +199,11 @@ catalogs:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
primeicons:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
@@ -254,7 +263,7 @@ catalogs:
|
||||
version: 3.5.13
|
||||
vue-component-type-helpers:
|
||||
specifier: ^3.0.7
|
||||
version: 3.1.0
|
||||
version: 3.1.1
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
@@ -479,7 +488,7 @@ importers:
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/storybook':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
'@nx/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vitest@3.2.4)
|
||||
@@ -491,19 +500,19 @@ importers:
|
||||
version: 1.52.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/vue3':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.12(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.0
|
||||
@@ -551,10 +560,10 @@ importers:
|
||||
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2)
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0-beta.0(tailwindcss@4.1.12)
|
||||
@@ -588,21 +597,30 @@ importers:
|
||||
lint-staged:
|
||||
specifier: 'catalog:'
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
postcss-html:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
rollup-plugin-visualizer:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.4(rollup@4.22.4)
|
||||
storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
stylelint:
|
||||
specifier: 'catalog:'
|
||||
version: 16.24.0(typescript@5.9.2)
|
||||
@@ -650,7 +668,7 @@ importers:
|
||||
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)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-type-helpers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
version: 3.1.1
|
||||
vue-eslint-parser:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
|
||||
@@ -4898,9 +4916,6 @@ packages:
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -6005,11 +6020,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.5:
|
||||
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
||||
engines: {node: ^18 || >=20}
|
||||
@@ -6357,10 +6367,6 @@ packages:
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.1:
|
||||
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -6373,11 +6379,15 @@ packages:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
prettier@3.3.2:
|
||||
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-bytes@7.1.0:
|
||||
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -7479,9 +7489,6 @@ packages:
|
||||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.1.0:
|
||||
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
|
||||
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
@@ -9674,7 +9681,7 @@ snapshots:
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@nx/cypress': 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/devkit': 21.4.1(nx@21.4.1)
|
||||
@@ -9682,7 +9689,7 @@ snapshots:
|
||||
'@nx/js': 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)
|
||||
'@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.2)
|
||||
semver: 7.7.2
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
@@ -10033,29 +10040,29 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1)
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
unplugin: 1.16.1
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
@@ -10065,19 +10072,19 @@ snapshots:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
find-package-json: 1.2.0
|
||||
magic-string: 0.30.19
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
typescript: 5.9.2
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-meta: 2.2.12(typescript@5.9.2)
|
||||
@@ -10085,10 +10092,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.1
|
||||
@@ -10353,7 +10360,7 @@ snapshots:
|
||||
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
|
||||
'@tiptap/pm': 2.10.4
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)':
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.3
|
||||
'@babel/parser': 7.28.4
|
||||
@@ -10361,7 +10368,7 @@ snapshots:
|
||||
'@babel/types': 7.28.4
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
transitivePeerDependencies:
|
||||
@@ -12139,20 +12146,20 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2):
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.11.11
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.4.2))
|
||||
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -12591,10 +12598,6 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -13889,8 +13892,6 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
nanoid@5.1.5: {}
|
||||
|
||||
napi-postinstall@0.3.3: {}
|
||||
@@ -14270,14 +14271,14 @@ snapshots:
|
||||
dependencies:
|
||||
htmlparser2: 8.0.2
|
||||
js-tokens: 9.0.1
|
||||
postcss: 8.5.1
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.1)
|
||||
postcss: 8.5.6
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.6)
|
||||
|
||||
postcss-resolve-nested-selector@0.1.6: {}
|
||||
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.1):
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.6):
|
||||
dependencies:
|
||||
postcss: 8.5.1
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.6):
|
||||
dependencies:
|
||||
@@ -14295,12 +14296,6 @@ snapshots:
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.1:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -14313,7 +14308,9 @@ snapshots:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier@3.3.2: {}
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
@@ -14990,7 +14987,7 @@ snapshots:
|
||||
internal-slot: 1.1.0
|
||||
optional: true
|
||||
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@testing-library/jest-dom': 6.6.4
|
||||
@@ -15005,7 +15002,7 @@ snapshots:
|
||||
semver: 7.7.2
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
transitivePeerDependencies:
|
||||
- '@testing-library/dom'
|
||||
- bufferutil
|
||||
@@ -15327,7 +15324,7 @@ snapshots:
|
||||
tsx@4.19.4:
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
get-tsconfig: 4.7.5
|
||||
get-tsconfig: 4.10.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
@@ -15679,7 +15676,7 @@ snapshots:
|
||||
vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.1
|
||||
postcss: 8.5.6
|
||||
rollup: 4.22.4
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.10
|
||||
@@ -15744,8 +15741,6 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.1.0: {}
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
|
||||
@@ -62,10 +62,13 @@ catalog:
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
markdown-table: ^3.0.4
|
||||
nx: 21.4.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.3.2
|
||||
prettier: ^3.6.2
|
||||
pretty-bytes: ^7.1.0
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
rollup-plugin-visualizer: ^6.0.4
|
||||
|
||||
93
scripts/bundle-categories.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Bundle categorization configuration
|
||||
*
|
||||
* This file defines how bundles are categorized in size reports.
|
||||
* Categories help identify which parts of the application are growing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleCategory
|
||||
* @property {string} name - Display name of the category
|
||||
* @property {string} description - Description of what this category includes
|
||||
* @property {RegExp[]} patterns - Regex patterns to match bundle files
|
||||
* @property {number} order - Sort order for display (lower = first)
|
||||
*/
|
||||
|
||||
/** @type {BundleCategory[]} */
|
||||
export const BUNDLE_CATEGORIES = [
|
||||
{
|
||||
name: 'App Entry Points',
|
||||
description: 'Main application bundles',
|
||||
patterns: [/^index-.*\.js$/],
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
name: 'Core Views',
|
||||
description: 'Major application views and screens',
|
||||
patterns: [/GraphView-.*\.js$/, /UserSelectView-.*\.js$/],
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
name: 'UI Panels',
|
||||
description: 'Settings and configuration panels',
|
||||
patterns: [/.*Panel-.*\.js$/],
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
name: 'UI Components',
|
||||
description: 'Reusable UI components',
|
||||
patterns: [/Avatar-.*\.js$/, /Badge-.*\.js$/],
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
name: 'Services',
|
||||
description: 'Business logic and services',
|
||||
patterns: [/.*Service-.*\.js$/, /.*Store-.*\.js$/],
|
||||
order: 5
|
||||
},
|
||||
{
|
||||
name: 'Utilities',
|
||||
description: 'Helper functions and utilities',
|
||||
patterns: [/.*[Uu]til.*\.js$/],
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
description: 'Uncategorized bundles',
|
||||
patterns: [/.*/], // Catch-all pattern
|
||||
order: 99
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Categorize a bundle file based on its name
|
||||
*
|
||||
* @param {string} fileName - The bundle file name (e.g., "assets/GraphView-BnV6iF9h.js")
|
||||
* @returns {string} - The category name
|
||||
*/
|
||||
export function categorizeBundle(fileName) {
|
||||
// Extract just the file name without path
|
||||
const baseName = fileName.split('/').pop() || fileName
|
||||
|
||||
// Find the first matching category
|
||||
for (const category of BUNDLE_CATEGORIES) {
|
||||
for (const pattern of category.patterns) {
|
||||
if (pattern.test(baseName)) {
|
||||
return category.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category metadata by name
|
||||
*
|
||||
* @param {string} categoryName - The category name
|
||||
* @returns {BundleCategory | undefined} - The category metadata
|
||||
*/
|
||||
export function getCategoryMetadata(categoryName) {
|
||||
return BUNDLE_CATEGORIES.find((cat) => cat.name === categoryName)
|
||||
}
|
||||
90
scripts/size-collect.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// @ts-check
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { brotliCompressSync, gzipSync } from 'node:zlib'
|
||||
import pico from 'picocolors'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { categorizeBundle } from './bundle-categories.js'
|
||||
|
||||
const distDir = path.resolve('dist')
|
||||
const sizeDir = path.resolve('temp/size')
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {string} file
|
||||
* @property {string} category
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to collect bundle size data
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(distDir)) {
|
||||
console.error(pico.red('Error: dist directory does not exist'))
|
||||
console.error(pico.yellow('Please run "pnpm build" first'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(pico.blue('\nCollecting bundle size data...\n'))
|
||||
|
||||
// Collect main bundle files from dist/assets
|
||||
const assetsDir = path.join(distDir, 'assets')
|
||||
const bundles = []
|
||||
|
||||
if (existsSync(assetsDir)) {
|
||||
const files = await readdir(assetsDir)
|
||||
const jsFiles = files.filter(
|
||||
(file) => file.endsWith('.js') && !file.includes('legacy')
|
||||
)
|
||||
|
||||
for (const file of jsFiles) {
|
||||
const filePath = path.join(assetsDir, file)
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
const size = Buffer.byteLength(content)
|
||||
const gzip = gzipSync(content).length
|
||||
const brotli = brotliCompressSync(content).length
|
||||
const fileName = `assets/${file}`
|
||||
const category = categorizeBundle(fileName)
|
||||
|
||||
bundles.push({
|
||||
file: fileName,
|
||||
category,
|
||||
size,
|
||||
gzip,
|
||||
brotli
|
||||
})
|
||||
|
||||
console.log(
|
||||
`${pico.green(file)} ${pico.dim(`[${category}]`)} - ` +
|
||||
`Size: ${prettyBytes(size)} / ` +
|
||||
`Gzip: ${prettyBytes(gzip)} / ` +
|
||||
`Brotli: ${prettyBytes(brotli)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp/size directory
|
||||
await mkdir(sizeDir, { recursive: true })
|
||||
|
||||
// Write individual bundle files
|
||||
for (const bundle of bundles) {
|
||||
const fileName = bundle.file.replace(/[/\\]/g, '_').replace('.js', '.json')
|
||||
await writeFile(
|
||||
path.join(sizeDir, fileName),
|
||||
JSON.stringify(bundle, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
pico.green(`\n✓ Collected size data for ${bundles.length} bundles\n`)
|
||||
)
|
||||
console.log(pico.blue(`Data saved to: ${sizeDir}\n`))
|
||||
}
|
||||
162
scripts/size-report.js
Normal file
@@ -0,0 +1,162 @@
|
||||
// @ts-check
|
||||
import { markdownTable } from 'markdown-table'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { getCategoryMetadata } from './bundle-categories.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SizeResult & { file: string, category?: string }} BundleResult
|
||||
*/
|
||||
|
||||
const currDir = path.resolve('temp/size')
|
||||
const prevDir = path.resolve('temp/size-prev')
|
||||
let output = '## Bundle Size Report\n\n'
|
||||
const sizeHeaders = ['Size', 'Gzip', 'Brotli']
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to generate the size report
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(currDir)) {
|
||||
console.error('Error: temp/size directory does not exist')
|
||||
console.error('Please run "pnpm size:collect" first')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await renderFiles()
|
||||
process.stdout.write(output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders file sizes and diffs between current and previous versions
|
||||
*/
|
||||
async function renderFiles() {
|
||||
/**
|
||||
* @param {string[]} files
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
|
||||
|
||||
const curr = filterFiles(await readdir(currDir))
|
||||
const prev = existsSync(prevDir) ? filterFiles(await readdir(prevDir)) : []
|
||||
const fileList = new Set([...curr, ...prev])
|
||||
|
||||
// Group bundles by category
|
||||
/** @type {Map<string, Array<{fileName: string, curr: BundleResult | undefined, prev: BundleResult | undefined}>>} */
|
||||
const bundlesByCategory = new Map()
|
||||
|
||||
for (const file of fileList) {
|
||||
const currPath = path.resolve(currDir, file)
|
||||
const prevPath = path.resolve(prevDir, file)
|
||||
|
||||
const curr = await importJSON(currPath)
|
||||
const prev = await importJSON(prevPath)
|
||||
const fileName = curr?.file || prev?.file || ''
|
||||
const category = curr?.category || prev?.category || 'Other'
|
||||
|
||||
if (!bundlesByCategory.has(category)) {
|
||||
bundlesByCategory.set(category, [])
|
||||
}
|
||||
|
||||
// @ts-expect-error - get is valid
|
||||
bundlesByCategory.get(category).push({ fileName, curr, prev })
|
||||
}
|
||||
|
||||
// Sort categories by their order
|
||||
const sortedCategories = Array.from(bundlesByCategory.keys()).sort((a, b) => {
|
||||
const metaA = getCategoryMetadata(a)
|
||||
const metaB = getCategoryMetadata(b)
|
||||
return (metaA?.order ?? 99) - (metaB?.order ?? 99)
|
||||
})
|
||||
|
||||
let totalSize = 0
|
||||
let totalCount = 0
|
||||
|
||||
// Render each category
|
||||
for (const category of sortedCategories) {
|
||||
const bundles = bundlesByCategory.get(category) || []
|
||||
if (bundles.length === 0) continue
|
||||
|
||||
const categoryMeta = getCategoryMetadata(category)
|
||||
output += `### ${category}\n\n`
|
||||
if (categoryMeta?.description) {
|
||||
output += `_${categoryMeta.description}_\n\n`
|
||||
}
|
||||
|
||||
const rows = []
|
||||
let categorySize = 0
|
||||
|
||||
for (const { fileName, curr, prev } of bundles) {
|
||||
if (!curr) {
|
||||
// File was deleted
|
||||
rows.push([`~~${fileName}~~`])
|
||||
} else {
|
||||
rows.push([
|
||||
fileName,
|
||||
`${prettyBytes(curr.size)}${getDiff(curr.size, prev?.size)}`,
|
||||
`${prettyBytes(curr.gzip)}${getDiff(curr.gzip, prev?.gzip)}`,
|
||||
`${prettyBytes(curr.brotli)}${getDiff(curr.brotli, prev?.brotli)}`
|
||||
])
|
||||
categorySize += curr.size
|
||||
totalSize += curr.size
|
||||
totalCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Sort rows by file name within category
|
||||
rows.sort((a, b) => {
|
||||
const fileA = a[0].replace(/~~/g, '')
|
||||
const fileB = b[0].replace(/~~/g, '')
|
||||
return fileA.localeCompare(fileB)
|
||||
})
|
||||
|
||||
output += markdownTable([['File', ...sizeHeaders], ...rows])
|
||||
output += `\n\n**Category Total:** ${prettyBytes(categorySize)}\n\n`
|
||||
}
|
||||
|
||||
// Add overall summary
|
||||
if (totalCount > 0) {
|
||||
output += '---\n\n'
|
||||
output += `**Overall Total Size:** ${prettyBytes(totalSize)}\n`
|
||||
output += `**Total Bundle Count:** ${totalCount}\n`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports JSON data from a specified path
|
||||
*
|
||||
* @template T
|
||||
* @param {string} filePath - Path to the JSON file
|
||||
* @returns {Promise<T | undefined>} The JSON content or undefined if the file does not exist
|
||||
*/
|
||||
async function importJSON(filePath) {
|
||||
if (!existsSync(filePath)) return undefined
|
||||
return (await import(filePath, { with: { type: 'json' } })).default
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the difference between the current and previous sizes
|
||||
*
|
||||
* @param {number} curr - The current size
|
||||
* @param {number} [prev] - The previous size
|
||||
* @returns {string} The difference in pretty format
|
||||
*/
|
||||
function getDiff(curr, prev) {
|
||||
if (prev === undefined) return ''
|
||||
const diff = curr - prev
|
||||
if (diff === 0) return ''
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return ` (**${sign}${prettyBytes(diff)}**)`
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
'flex-row-reverse': sidebarLocation === 'right'
|
||||
}"
|
||||
>
|
||||
<div class="side-toolbar-container pointer-events-auto">
|
||||
<div class="side-toolbar-container">
|
||||
<slot name="side-toolbar" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!workspaceStore.focusMode"
|
||||
class="pointer-events-none ml-2 flex pt-2"
|
||||
>
|
||||
<div class="pointer-events-auto min-w-0 flex-1">
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
ref="breadcrumbRef"
|
||||
class="w-fit rounded-lg p-0"
|
||||
:model="items"
|
||||
:pt="{ item: { class: 'pointer-events-auto' } }"
|
||||
aria-label="Graph navigation"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
class="absolute right-2 bottom-2 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
|
||||
@@ -75,7 +75,7 @@ let promptInput = findPromptInput()
|
||||
const previousPromptInput = ref<string | null>(null)
|
||||
|
||||
const getPreviousResponseId = (index: number) =>
|
||||
index > 0 ? parsedHistory.value[index - 1]?.response_id ?? '' : ''
|
||||
index > 0 ? (parsedHistory.value[index - 1]?.response_id ?? '') : ''
|
||||
|
||||
const storePromptInput = () => {
|
||||
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
@@ -106,7 +106,7 @@ const getLabel = (val: string | null | undefined) => {
|
||||
if (val == null) return label ?? ''
|
||||
if (!options) return label ?? ''
|
||||
const found = options.find((o) => o.value === val)
|
||||
return found ? found.name : label ?? ''
|
||||
return found ? found.name : (label ?? '')
|
||||
}
|
||||
|
||||
// Extract complex style logic from template
|
||||
|
||||
@@ -113,7 +113,7 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
const isOverflowing = ref(false)
|
||||
const groupClasses = computed(() =>
|
||||
cn(
|
||||
'sidebar-item-group flex flex-col items-center overflow-hidden flex-shrink-0' +
|
||||
'sidebar-item-group pointer-events-auto flex flex-col items-center overflow-hidden flex-shrink-0' +
|
||||
(isConnected.value ? '' : ' rounded-lg shadow-md')
|
||||
)
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ const emit = defineEmits<{
|
||||
(e: 'click', event: MouseEvent): void
|
||||
}>()
|
||||
const overlayValue = computed(() =>
|
||||
typeof iconBadge === 'function' ? iconBadge() ?? '' : iconBadge
|
||||
typeof iconBadge === 'function' ? (iconBadge() ?? '') : iconBadge
|
||||
)
|
||||
const shouldShowBadge = computed(() => !!overlayValue.value)
|
||||
const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
|
||||
@@ -100,9 +100,9 @@ const coverResult = flatOutputs.length
|
||||
// Using `==` instead of `===` because NodeId can be a string or a number
|
||||
const node: ComfyNode | null =
|
||||
flatOutputs.length && props.task.workflow
|
||||
? props.task.workflow.nodes.find(
|
||||
? (props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult?.nodeId
|
||||
) ?? null
|
||||
) ?? null)
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="workflow-tabs-container flex h-full max-w-full flex-auto flex-row overflow-hidden"
|
||||
:class="{ 'workflow-tabs-container-desktop': isDesktop }"
|
||||
>
|
||||
@@ -13,7 +14,6 @@
|
||||
@mousedown="whileMouseDown($event, () => scroll(-1))"
|
||||
/>
|
||||
<ScrollPanel
|
||||
ref="scrollPanelRef"
|
||||
class="no-drag overflow-hidden"
|
||||
:pt:content="{
|
||||
class: 'p-0 w-full flex',
|
||||
@@ -74,6 +74,7 @@ import ContextMenu from 'primevue/contextmenu'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, nextTick, onUpdated, ref, watch } from 'vue'
|
||||
import type { WatchStopHandle } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||
@@ -108,7 +109,7 @@ const workflowService = useWorkflowService()
|
||||
|
||||
const rightClickedTab = ref<WorkflowOption | undefined>()
|
||||
const menu = ref()
|
||||
const scrollPanelRef = ref()
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const showOverflowArrows = ref(false)
|
||||
const leftArrowEnabled = ref(false)
|
||||
const rightArrowEnabled = ref(false)
|
||||
@@ -221,75 +222,98 @@ const handleWheel = (event: WheelEvent) => {
|
||||
})
|
||||
}
|
||||
|
||||
const scrollContent = computed(
|
||||
() =>
|
||||
(containerRef.value?.querySelector(
|
||||
'.p-scrollpanel-content'
|
||||
) as HTMLElement | null) ?? null
|
||||
)
|
||||
|
||||
const scroll = (direction: number) => {
|
||||
const scrollElement = scrollPanelRef.value.$el.querySelector(
|
||||
'.p-scrollpanel-content'
|
||||
) as HTMLElement
|
||||
scrollElement.scrollBy({ left: direction * 20 })
|
||||
const el = scrollContent.value
|
||||
if (!el) return
|
||||
el.scrollBy({ left: direction * 20 })
|
||||
}
|
||||
|
||||
const ensureActiveTabVisible = async (
|
||||
options: { waitForDom?: boolean } = {}
|
||||
) => {
|
||||
if (!selectedWorkflow.value) return
|
||||
|
||||
if (options.waitForDom !== false) {
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const containerElement = containerRef.value
|
||||
if (!containerElement) return
|
||||
|
||||
const activeTabElement = containerElement.querySelector(
|
||||
'.p-togglebutton-checked'
|
||||
)
|
||||
if (!activeTabElement) return
|
||||
|
||||
activeTabElement.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
||||
}
|
||||
|
||||
// Scroll to active offscreen tab when opened
|
||||
watch(
|
||||
() => workflowStore.activeWorkflow,
|
||||
async () => {
|
||||
if (!selectedWorkflow.value) return
|
||||
|
||||
await nextTick()
|
||||
|
||||
const activeTabElement = document.querySelector('.p-togglebutton-checked')
|
||||
if (!activeTabElement || !scrollPanelRef.value) return
|
||||
|
||||
const container = scrollPanelRef.value.$el.querySelector(
|
||||
'.p-scrollpanel-content'
|
||||
)
|
||||
if (!container) return
|
||||
|
||||
const tabRect = activeTabElement.getBoundingClientRect()
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
|
||||
const offsetLeft = tabRect.left - containerRect.left
|
||||
const offsetRight = tabRect.right - containerRect.right
|
||||
|
||||
if (offsetRight > 0) {
|
||||
container.scrollBy({ left: offsetRight })
|
||||
} else if (offsetLeft < 0) {
|
||||
container.scrollBy({ left: offsetLeft })
|
||||
}
|
||||
() => {
|
||||
void ensureActiveTabVisible()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const scrollContent = computed(
|
||||
() =>
|
||||
scrollPanelRef.value?.$el.querySelector(
|
||||
'.p-scrollpanel-content'
|
||||
) as HTMLElement
|
||||
)
|
||||
let overflowObserver: ReturnType<typeof useOverflowObserver> | null = null
|
||||
let overflowWatch: ReturnType<typeof watch> | null = null
|
||||
watch(scrollContent, (value) => {
|
||||
const scrollState = useScroll(value)
|
||||
let stopArrivedWatch: WatchStopHandle | null = null
|
||||
let stopOverflowWatch: WatchStopHandle | null = null
|
||||
|
||||
watch(scrollState.arrivedState, () => {
|
||||
leftArrowEnabled.value = !scrollState.arrivedState.left
|
||||
rightArrowEnabled.value = !scrollState.arrivedState.right
|
||||
})
|
||||
watch(
|
||||
scrollContent,
|
||||
(el, _prev, onCleanup) => {
|
||||
stopArrivedWatch?.()
|
||||
stopOverflowWatch?.()
|
||||
overflowObserver?.dispose()
|
||||
|
||||
overflowObserver?.dispose()
|
||||
overflowWatch?.stop()
|
||||
overflowObserver = useOverflowObserver(value)
|
||||
overflowWatch = watch(
|
||||
overflowObserver.isOverflowing,
|
||||
(value) => {
|
||||
showOverflowArrows.value = value
|
||||
void nextTick(() => {
|
||||
// Force a new check after arrows are updated
|
||||
scrollState.measure()
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
if (!el) return
|
||||
|
||||
const scrollState = useScroll(el)
|
||||
|
||||
stopArrivedWatch = watch(
|
||||
[
|
||||
() => scrollState.arrivedState.left,
|
||||
() => scrollState.arrivedState.right
|
||||
],
|
||||
([atLeft, atRight]) => {
|
||||
leftArrowEnabled.value = !atLeft
|
||||
rightArrowEnabled.value = !atRight
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
overflowObserver = useOverflowObserver(el)
|
||||
stopOverflowWatch = watch(
|
||||
overflowObserver.isOverflowing,
|
||||
(isOverflow) => {
|
||||
showOverflowArrows.value = isOverflow
|
||||
if (!isOverflow) return
|
||||
void nextTick(() => {
|
||||
// Force a new check after arrows are updated
|
||||
scrollState.measure()
|
||||
void ensureActiveTabVisible({ waitForDom: false })
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
stopArrivedWatch?.()
|
||||
stopOverflowWatch?.()
|
||||
overflowObserver?.dispose()
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onUpdated(() => {
|
||||
if (!overflowObserver?.disposed.value) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
import { AuthErrorCodes } from 'firebase/auth'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
@@ -122,6 +125,47 @@ export const useFirebaseAuthActions = () => {
|
||||
reportError
|
||||
)
|
||||
|
||||
/**
|
||||
* Recovery strategy for Firebase auth/requires-recent-login errors.
|
||||
* Prompts user to reauthenticate and retries the operation after successful login.
|
||||
*/
|
||||
const createReauthenticationRecovery = <
|
||||
TArgs extends unknown[],
|
||||
TReturn
|
||||
>(): ErrorRecoveryStrategy<TArgs, TReturn> => {
|
||||
const dialogService = useDialogService()
|
||||
|
||||
return {
|
||||
shouldHandle: (error: unknown) =>
|
||||
error instanceof FirebaseError &&
|
||||
error.code === AuthErrorCodes.CREDENTIAL_TOO_OLD_LOGIN_AGAIN,
|
||||
|
||||
recover: async (
|
||||
_error: unknown,
|
||||
retry: (...args: TArgs) => Promise<TReturn> | TReturn,
|
||||
args: TArgs
|
||||
) => {
|
||||
const confirmed = await dialogService.confirm({
|
||||
title: t('auth.reauthRequired.title'),
|
||||
message: t('auth.reauthRequired.message'),
|
||||
type: 'default'
|
||||
})
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
await authStore.logout()
|
||||
|
||||
const signedIn = await dialogService.showSignInDialog()
|
||||
|
||||
if (signedIn) {
|
||||
await retry(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatePassword = wrapWithErrorHandlingAsync(
|
||||
async (newPassword: string) => {
|
||||
await authStore.updatePassword(newPassword)
|
||||
@@ -132,18 +176,25 @@ export const useFirebaseAuthActions = () => {
|
||||
life: 5000
|
||||
})
|
||||
},
|
||||
reportError
|
||||
reportError,
|
||||
undefined,
|
||||
[createReauthenticationRecovery<[string], void>()]
|
||||
)
|
||||
|
||||
const deleteAccount = wrapWithErrorHandlingAsync(async () => {
|
||||
await authStore.deleteAccount()
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.deleteAccount.success'),
|
||||
detail: t('auth.deleteAccount.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
}, reportError)
|
||||
const deleteAccount = wrapWithErrorHandlingAsync(
|
||||
async () => {
|
||||
await authStore.deleteAccount()
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.deleteAccount.success'),
|
||||
detail: t('auth.deleteAccount.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
},
|
||||
reportError,
|
||||
undefined,
|
||||
[createReauthenticationRecovery<[], void>()]
|
||||
)
|
||||
|
||||
return {
|
||||
logout,
|
||||
|
||||
@@ -125,7 +125,7 @@ export function useNumberWidgetValue(
|
||||
transform: (value: number | number[]) => {
|
||||
// Handle PrimeVue Slider which can emit number | number[]
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0 ? value[0] ?? 0 : 0
|
||||
return value.length > 0 ? (value[0] ?? 0) : 0
|
||||
}
|
||||
return Number(value) || 0
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ export const useNodeBadge = () => {
|
||||
? `#${node.id}`
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)
|
||||
? nodeDef?.nodeLifeCycleBadgeText ?? ''
|
||||
? (nodeDef?.nodeLifeCycleBadgeText ?? '')
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)
|
||||
? nodeDef?.nodeSource?.badgeText ?? ''
|
||||
? (nodeDef?.nodeSource?.badgeText ?? '')
|
||||
: ''
|
||||
]
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
@@ -1647,7 +1647,7 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
? '720p'
|
||||
: resolutionStr.includes('480')
|
||||
? '480p'
|
||||
: resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? ''
|
||||
: (resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? '')
|
||||
|
||||
const pricePerSecond: Record<string, number> = {
|
||||
'480p': 0.05,
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { promoteRecommendedWidgets } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import {
|
||||
promoteRecommendedWidgets,
|
||||
tryToggleWidgetPromotion
|
||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
@@ -888,7 +892,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ConvertToSubgraph',
|
||||
icon: 'pi pi-sitemap',
|
||||
icon: 'icon-[lucide--shrink]',
|
||||
label: 'Convert Selection to Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
category: 'essentials' as const,
|
||||
@@ -916,10 +920,9 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.UnpackSubgraph',
|
||||
icon: 'pi pi-sitemap',
|
||||
icon: 'icon-[lucide--expand]',
|
||||
label: 'Unpack the selected Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
category: 'essentials' as const,
|
||||
versionAdded: '1.26.3',
|
||||
function: () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
@@ -931,6 +934,20 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.EditSubgraphWidgets',
|
||||
label: 'Edit Subgraph Widgets',
|
||||
icon: 'icon-[lucide--settings-2]',
|
||||
versionAdded: '1.28.5',
|
||||
function: showSubgraphNodeDialog
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ToggleWidgetPromotion',
|
||||
icon: 'icon-[lucide--arrow-left-right]',
|
||||
label: 'Toggle promotion of hovered widget',
|
||||
versionAdded: '1.30.1',
|
||||
function: tryToggleWidgetPromotion
|
||||
},
|
||||
{
|
||||
id: 'Comfy.OpenManagerDialog',
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
|
||||
@@ -1,6 +1,54 @@
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
/**
|
||||
* Strategy for recovering from specific error conditions.
|
||||
* Allows operations to be retried after resolving the error condition.
|
||||
*
|
||||
* @template TArgs - The argument types of the operation to be retried
|
||||
* @template TReturn - The return type of the operation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const networkRecovery: ErrorRecoveryStrategy = {
|
||||
* shouldHandle: (error) => error instanceof NetworkError,
|
||||
* recover: async (error, retry) => {
|
||||
* await waitForNetwork()
|
||||
* await retry()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface ErrorRecoveryStrategy<
|
||||
TArgs extends unknown[] = unknown[],
|
||||
TReturn = unknown
|
||||
> {
|
||||
/**
|
||||
* Determines if this strategy should handle the given error.
|
||||
* @param error - The error to check
|
||||
* @returns true if this strategy can handle the error
|
||||
*/
|
||||
shouldHandle: (error: unknown) => boolean
|
||||
|
||||
/**
|
||||
* Attempts to recover from the error and retry the operation.
|
||||
* This function is responsible for:
|
||||
* 1. Resolving the error condition (e.g., reauthentication, network reconnect)
|
||||
* 2. Calling retry() to re-execute the original operation
|
||||
* 3. Handling the retry result (success or failure)
|
||||
*
|
||||
* @param error - The error that occurred
|
||||
* @param retry - Function to retry the original operation
|
||||
* @param args - Original arguments passed to the operation
|
||||
* @returns Promise that resolves when recovery completes (whether successful or not)
|
||||
*/
|
||||
recover: (
|
||||
error: unknown,
|
||||
retry: (...args: TArgs) => Promise<TReturn> | TReturn,
|
||||
args: TArgs
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export function useErrorHandling() {
|
||||
const toast = useToastStore()
|
||||
const toastErrorHandler = (error: unknown) => {
|
||||
@@ -13,9 +61,9 @@ export function useErrorHandling() {
|
||||
}
|
||||
|
||||
const wrapWithErrorHandling =
|
||||
<TArgs extends any[], TReturn>(
|
||||
<TArgs extends unknown[], TReturn>(
|
||||
action: (...args: TArgs) => TReturn,
|
||||
errorHandler?: (error: any) => void,
|
||||
errorHandler?: (error: unknown) => void,
|
||||
finallyHandler?: () => void
|
||||
) =>
|
||||
(...args: TArgs): TReturn | undefined => {
|
||||
@@ -29,15 +77,27 @@ export function useErrorHandling() {
|
||||
}
|
||||
|
||||
const wrapWithErrorHandlingAsync =
|
||||
<TArgs extends any[], TReturn>(
|
||||
<TArgs extends unknown[], TReturn>(
|
||||
action: (...args: TArgs) => Promise<TReturn> | TReturn,
|
||||
errorHandler?: (error: any) => void,
|
||||
finallyHandler?: () => void
|
||||
errorHandler?: (error: unknown) => void,
|
||||
finallyHandler?: () => void,
|
||||
recoveryStrategies: ErrorRecoveryStrategy<TArgs, TReturn>[] = []
|
||||
) =>
|
||||
async (...args: TArgs): Promise<TReturn | undefined> => {
|
||||
try {
|
||||
return await action(...args)
|
||||
} catch (e) {
|
||||
for (const strategy of recoveryStrategies) {
|
||||
if (strategy.shouldHandle(e)) {
|
||||
try {
|
||||
await strategy.recover(e, action, args)
|
||||
return
|
||||
} catch (recoveryError) {
|
||||
console.error('Recovery strategy failed:', recoveryError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
;(errorHandler ?? toastErrorHandler)(e)
|
||||
} finally {
|
||||
finallyHandler?.()
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'
|
||||
import { t } from '@/i18n'
|
||||
import type {
|
||||
IContextMenuValue,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
|
||||
@@ -63,7 +66,15 @@ function getParentNodes(): SubgraphNode[] {
|
||||
//or by adding a new event for parent listeners to collect from
|
||||
const { navigationStack } = useSubgraphNavigationStore()
|
||||
const subgraph = navigationStack.at(-1)
|
||||
if (!subgraph) throw new Error("Can't promote widget when not in subgraph")
|
||||
if (!subgraph) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('subgraphStore.promoteOutsideSubgraph'),
|
||||
life: 2000
|
||||
})
|
||||
return []
|
||||
}
|
||||
const parentGraph = navigationStack.at(-2) ?? subgraph.rootGraph
|
||||
return parentGraph.nodes.filter(
|
||||
(node): node is SubgraphNode =>
|
||||
@@ -96,6 +107,21 @@ export function addWidgetPromotionOptions(
|
||||
})
|
||||
}
|
||||
}
|
||||
export function tryToggleWidgetPromotion() {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const [x, y] = canvas.graph_mouse
|
||||
const node = canvas.graph?.getNodeOnPos(x, y, canvas.visible_nodes)
|
||||
if (!node) return
|
||||
const widget = node.getWidgetOnPos(x, y, true)
|
||||
const parents = getParentNodes()
|
||||
if (!parents.length || !widget) return
|
||||
const promotableParents = parents.filter(
|
||||
(s) => !getProxyWidgets(s).some(matchesPropertyItem([node, widget]))
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
promoteWidget(node, widget, promotableParents)
|
||||
else demoteWidget(node, widget, parents)
|
||||
}
|
||||
const recommendedNodes = [
|
||||
'CLIPTextEncode',
|
||||
'LoadImage',
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.CloudBadge',
|
||||
// Only show badge when running in cloud environment
|
||||
topbarBadges: isCloud
|
||||
? [
|
||||
{
|
||||
label: t('g.beta'),
|
||||
text: 'Comfy Cloud'
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
topbarBadges: [
|
||||
{
|
||||
label: t('g.beta'),
|
||||
text: 'Comfy Cloud'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -542,7 +542,7 @@ export class GroupNodeConfig {
|
||||
primitiveConfig
|
||||
)
|
||||
primitiveConfig[1] =
|
||||
config?.customConfig ?? inputs[inputName][1]
|
||||
(config?.customConfig ?? inputs[inputName][1])
|
||||
? { ...inputs[inputName][1] }
|
||||
: {}
|
||||
|
||||
|
||||
@@ -553,8 +553,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.element.replaceChildren(outer)
|
||||
this.changeGroup(
|
||||
type
|
||||
? groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === type) ??
|
||||
groupNodes[0]
|
||||
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === type) ??
|
||||
groupNodes[0])
|
||||
: groupNodes[0]
|
||||
)
|
||||
this.element.showModal()
|
||||
|
||||
@@ -370,7 +370,7 @@ class Load3d {
|
||||
await ModelExporter.exportOBJ(model, filename, originalURL)
|
||||
break
|
||||
case 'stl':
|
||||
await ModelExporter.exportSTL(model, filename), originalURL
|
||||
;(await ModelExporter.exportSTL(model, filename), originalURL)
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`)
|
||||
|
||||
@@ -253,7 +253,7 @@ app.registerExtension({
|
||||
audio.setAttribute('name', 'media')
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
|
||||
audioUIWidget.options.canvasOnly = true
|
||||
audioUIWidget.options.canvasOnly = false
|
||||
|
||||
let mediaRecorder: MediaRecorder | null = null
|
||||
let isRecording = false
|
||||
@@ -376,10 +376,12 @@ app.registerExtension({
|
||||
mediaRecorder.stop()
|
||||
}
|
||||
},
|
||||
{ serialize: false, canvasOnly: true }
|
||||
{ serialize: false, canvasOnly: false }
|
||||
)
|
||||
|
||||
recordWidget.label = t('g.startRecording')
|
||||
// Override the type for Vue rendering while keeping 'button' for LiteGraph
|
||||
recordWidget.type = 'audiorecord'
|
||||
|
||||
const originalOnRemoved = node.onRemoved
|
||||
node.onRemoved = function () {
|
||||
|
||||
@@ -4888,9 +4888,9 @@ export class LGraphCanvas
|
||||
/** Get the target snap / highlight point in graph space */
|
||||
#getHighlightPosition(): Readonly<Point> {
|
||||
return LiteGraph.snaps_for_comfy
|
||||
? this.linkConnector.state.snapLinksPos ??
|
||||
? (this.linkConnector.state.snapLinksPos ??
|
||||
this._highlight_pos ??
|
||||
this.graph_mouse
|
||||
this.graph_mouse)
|
||||
: this.graph_mouse
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ interface DrawTitleTextOptions extends DrawTitleOptions {
|
||||
default_title_color: string
|
||||
}
|
||||
|
||||
interface DrawTitleBoxOptions extends DrawTitleOptions {
|
||||
export interface DrawTitleBoxOptions extends DrawTitleOptions {
|
||||
box_size?: number
|
||||
}
|
||||
|
||||
@@ -2467,7 +2467,7 @@ export class LGraphNode
|
||||
}
|
||||
}
|
||||
|
||||
return doNotUseOccupied ? -1 : occupiedSlot ?? -1
|
||||
return doNotUseOccupied ? -1 : (occupiedSlot ?? -1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -312,7 +312,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
const inputNode =
|
||||
this.target_id === -1
|
||||
? undefined
|
||||
: network.getNodeById(this.target_id) ?? undefined
|
||||
: (network.getNodeById(this.target_id) ?? undefined)
|
||||
const input = inputNode?.inputs[this.target_slot]
|
||||
const subgraphInput = this.originIsIoNode
|
||||
? network.inputNode?.slots[this.origin_slot]
|
||||
@@ -324,7 +324,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
const outputNode =
|
||||
this.origin_id === -1
|
||||
? undefined
|
||||
: network.getNodeById(this.origin_id) ?? undefined
|
||||
: (network.getNodeById(this.origin_id) ?? undefined)
|
||||
const output = outputNode?.outputs[this.origin_slot]
|
||||
const subgraphOutput = this.targetIsIoNode
|
||||
? network.outputNode?.slots[this.target_slot]
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { BaseLGraph, LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphButton } from '@/lib/litegraph/src/LGraphButton'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { DrawTitleBoxOptions } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { ResolvedConnection } from '@/lib/litegraph/src/LLink'
|
||||
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
|
||||
@@ -9,6 +10,7 @@ import type {
|
||||
ISubgraphInput,
|
||||
IWidgetLocator
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
ISlotType,
|
||||
@@ -32,6 +34,10 @@ import { ExecutableNodeDTO } from './ExecutableNodeDTO'
|
||||
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
|
||||
import type { SubgraphInput } from './SubgraphInput'
|
||||
|
||||
const workflowSvg = new Image()
|
||||
workflowSvg.src =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='white' stroke-linecap='round' stroke-width='1.3' d='M9.18613 3.09999H6.81377M9.18613 12.9H7.55288c-3.08678 0-5.35171-2.99581-4.60305-6.08843l.3054-1.26158M14.7486 2.1721l-.5931 2.45c-.132.54533-.6065.92789-1.1508.92789h-2.2993c-.77173 0-1.33797-.74895-1.1508-1.5221l.5931-2.45c.132-.54533.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.74896 1.1508 1.52211Zm-8.3033 0-.59309 2.45c-.13201.54533-.60646.92789-1.15076.92789H2.4021c-.7717 0-1.33793-.74895-1.15077-1.5221l.59309-2.45c.13201-.54533.60647-.9279 1.15077-.9279h2.29935c.77169 0 1.33792.74896 1.15076 1.52211Zm8.3033 9.8-.5931 2.45c-.132.5453-.6065.9279-1.1508.9279h-2.2993c-.77173 0-1.33797-.749-1.1508-1.5221l.5931-2.45c.132-.5453.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.7489 1.1508 1.5221Z'/%3E%3C/svg%3E %3C/svg%3E"
|
||||
|
||||
/**
|
||||
* An instance of a {@link Subgraph}, displayed as a node on the containing (parent) graph.
|
||||
*/
|
||||
@@ -555,6 +561,31 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
}
|
||||
}
|
||||
override drawTitleBox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{
|
||||
scale,
|
||||
low_quality = false,
|
||||
title_height = LiteGraph.NODE_TITLE_HEIGHT,
|
||||
box_size = 10
|
||||
}: DrawTitleBoxOptions
|
||||
): void {
|
||||
if (this.onDrawTitleBox) {
|
||||
this.onDrawTitleBox(ctx, title_height, this.renderingSize, scale)
|
||||
return
|
||||
}
|
||||
ctx.save()
|
||||
ctx.fillStyle = '#3b82f6'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(6, -24.5, 22, 20, 5)
|
||||
ctx.fill()
|
||||
if (!low_quality) {
|
||||
ctx.translate(25, 23)
|
||||
ctx.scale(-1.5, 1.5)
|
||||
ctx.drawImage(workflowSvg, 0, -title_height, box_size, box_size)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes widget values from this SubgraphNode instance to the
|
||||
|
||||
@@ -79,7 +79,6 @@ export type IWidget =
|
||||
| ISelectButtonWidget
|
||||
| ITextareaWidget
|
||||
| IAssetWidget
|
||||
| IAudioRecordWidget
|
||||
|
||||
export interface IBooleanWidget extends IBaseWidget<boolean, 'toggle'> {
|
||||
type: 'toggle'
|
||||
@@ -228,11 +227,6 @@ export interface ITextareaWidget extends IBaseWidget<string, 'textarea'> {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface IAudioRecordWidget extends IBaseWidget<string, 'audiorecord'> {
|
||||
type: 'audiorecord'
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface IAssetWidget
|
||||
extends IBaseWidget<string, 'asset', IWidgetOptions<string[]>> {
|
||||
type: 'asset'
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convert Selection to Subgraph"
|
||||
},
|
||||
"Comfy_Graph_EditSubgraphWidgets": {
|
||||
"label": "Edit Subgraph Widgets"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Exit Subgraph"
|
||||
},
|
||||
@@ -137,6 +140,9 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Group Selected Nodes"
|
||||
},
|
||||
"Comfy_Graph_ToggleWidgetPromotion": {
|
||||
"label": "Toggle promotion of hovered widget"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Unpack the selected Subgraph"
|
||||
},
|
||||
|
||||
@@ -1102,6 +1102,7 @@
|
||||
"overwriteBlueprintTitle": "Overwrite existing blueprint?",
|
||||
"overwriteBlueprint": "Saving will overwrite the current blueprint with your changes",
|
||||
"blueprintName": "Subgraph name",
|
||||
"promoteOutsideSubgraph": "Can't promote widget when not in subgraph",
|
||||
"publish": "Publish Subgraph",
|
||||
"publishSuccess": "Saved to Nodes Library",
|
||||
"publishSuccessMessage": "You can find your subgraph blueprint in the nodes library under \"Subgraph Blueprints\"",
|
||||
@@ -1212,9 +1213,11 @@
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Edit Subgraph Widgets": "Edit Subgraph Widgets",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Toggle promotion of hovered widget": "Toggle promotion of hovered widget",
|
||||
"Unpack the selected Subgraph": "Unpack the selected Subgraph",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
"Manage group nodes": "Manage group nodes",
|
||||
@@ -1863,6 +1866,12 @@
|
||||
"success": "Account Deleted",
|
||||
"successDetail": "Your account has been successfully deleted."
|
||||
},
|
||||
"reauthRequired": {
|
||||
"title": "Re-authentication Required",
|
||||
"message": "For security reasons, this action requires you to sign in again. Would you like to proceed?",
|
||||
"confirm": "Sign In Again",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"loginButton": {
|
||||
"tooltipHelp": "Login to be able to use \"API Nodes\"",
|
||||
"tooltipLearnMore": "Learn more..."
|
||||
|
||||
@@ -1532,10 +1532,12 @@
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "positive"
|
||||
"name": "positive",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "negative"
|
||||
"name": "negative",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10373,6 +10375,11 @@
|
||||
"type": {
|
||||
"name": "type"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"SkipLayerGuidanceDiT": {
|
||||
|
||||
@@ -17,4 +17,4 @@ const DISTRIBUTION: Distribution = __DISTRIBUTION__
|
||||
/** Distribution type checks */
|
||||
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
|
||||
export const isCloud = DISTRIBUTION === 'cloud'
|
||||
// export const isLocalhost = !isDesktop && !isCloud
|
||||
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
|
||||
|
||||
@@ -133,7 +133,7 @@ const searchResults = computed<ISettingGroup[]>(() =>
|
||||
)
|
||||
|
||||
const tabValue = computed<string>(() =>
|
||||
inSearch.value ? 'Search Results' : activeCategory.value?.label ?? ''
|
||||
inSearch.value ? 'Search Results' : (activeCategory.value?.label ?? '')
|
||||
)
|
||||
|
||||
// Don't allow null category to be set outside of search.
|
||||
|
||||
@@ -78,7 +78,7 @@ export function useTemplateWorkflows() {
|
||||
const fallback =
|
||||
template.title ?? template.name ?? `${sourceModule} Template`
|
||||
return sourceModule === 'default'
|
||||
? template.localizedTitle ?? fallback
|
||||
? (template.localizedTitle ?? fallback)
|
||||
: fallback
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
ref="minimapRef"
|
||||
class="minimap-main-container absolute right-2 bottom-[66px] z-1000 flex"
|
||||
class="minimap-main-container absolute right-0 bottom-[58px] z-1000 flex"
|
||||
>
|
||||
<MiniMapPanel
|
||||
v-if="showOptionsPanel"
|
||||
|
||||
@@ -2,7 +2,11 @@ import { useThrottleFn } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LGraphTriggerEvent
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -14,6 +18,7 @@ interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
onNodeRemoved?: (node: LGraphNode) => void
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
onTrigger?: (event: LGraphTriggerEvent) => void
|
||||
}
|
||||
|
||||
export function useMinimapGraph(
|
||||
@@ -53,7 +58,8 @@ export function useMinimapGraph(
|
||||
const originalCallbacks: GraphCallbacks = {
|
||||
onNodeAdded: g.onNodeAdded,
|
||||
onNodeRemoved: g.onNodeRemoved,
|
||||
onConnectionChange: g.onConnectionChange
|
||||
onConnectionChange: g.onConnectionChange,
|
||||
onTrigger: g.onTrigger
|
||||
}
|
||||
originalCallbacksMap.set(g.id, originalCallbacks)
|
||||
|
||||
@@ -72,6 +78,22 @@ export function useMinimapGraph(
|
||||
originalCallbacks.onConnectionChange?.call(this, node)
|
||||
void handleGraphChangedThrottled()
|
||||
}
|
||||
|
||||
g.onTrigger = function (event: LGraphTriggerEvent) {
|
||||
originalCallbacks.onTrigger?.call(this, event)
|
||||
|
||||
// Listen for visual property changes that affect minimap rendering
|
||||
if (
|
||||
event.type === 'node:property:changed' &&
|
||||
(event.property === 'mode' ||
|
||||
event.property === 'bgcolor' ||
|
||||
event.property === 'color')
|
||||
) {
|
||||
// Invalidate cache for this node to force redraw
|
||||
nodeStatesCache.delete(String(event.nodeId))
|
||||
void handleGraphChangedThrottled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupEventListeners = (oldGraph?: LGraph) => {
|
||||
@@ -89,6 +111,7 @@ export function useMinimapGraph(
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
g.onTrigger = originalCallbacks.onTrigger
|
||||
|
||||
originalCallbacksMap.delete(g.id)
|
||||
}
|
||||
|
||||
@@ -41,21 +41,21 @@
|
||||
/>
|
||||
|
||||
<!-- Floating Action Buttons (appear on hover) -->
|
||||
<div v-if="isHovered" class="actions absolute top-2 right-2 flex gap-1">
|
||||
<div v-if="isHovered" class="actions absolute top-2 right-2 flex gap-2.5">
|
||||
<!-- Mask/Edit Button -->
|
||||
<button
|
||||
v-if="!hasMultipleImages"
|
||||
class="action-btn cursor-pointer rounded-lg border-0 bg-white p-2 text-black shadow-sm transition-all duration-200 hover:bg-gray-100"
|
||||
:class="actionButtonClass"
|
||||
:title="$t('g.editOrMaskImage')"
|
||||
:aria-label="$t('g.editOrMaskImage')"
|
||||
@click="handleEditMask"
|
||||
>
|
||||
<i class="icon-[lucide--venetian-mask] h-4 w-4" />
|
||||
<i-comfy:mask class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<!-- Download Button -->
|
||||
<button
|
||||
class="action-btn cursor-pointer rounded-lg border-0 bg-white p-2 text-black shadow-sm transition-all duration-200 hover:bg-gray-100"
|
||||
:class="actionButtonClass"
|
||||
:title="$t('g.downloadImage')"
|
||||
:aria-label="$t('g.downloadImage')"
|
||||
@click="handleDownload"
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
class="action-btn cursor-pointer rounded-lg border-0 bg-white p-2 text-black shadow-sm transition-all duration-200 hover:bg-gray-100"
|
||||
:class="actionButtonClass"
|
||||
:title="$t('g.removeImage')"
|
||||
:aria-label="$t('g.removeImage')"
|
||||
@click="handleRemove"
|
||||
@@ -138,6 +138,9 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const actionButtonClass =
|
||||
'flex h-8 min-h-8 items-center justify-center gap-2.5 rounded-lg border-0 bg-button-surface px-2 py-2 text-button-surface-contrast shadow-sm transition-colors duration-200 hover:bg-button-hover-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-button-surface-contrast focus-visible:ring-offset-2 focus-visible:ring-offset-transparent cursor-pointer'
|
||||
|
||||
// Component state
|
||||
const currentIndex = ref(0)
|
||||
const isHovered = ref(false)
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
|
||||
<!-- Node Title -->
|
||||
<div
|
||||
v-tooltip.top="tooltipConfig"
|
||||
|
||||
@@ -309,7 +309,7 @@ export function useSlotLinkInteraction({
|
||||
hoveredSlotKey = elWithSlot?.dataset['slotKey'] ?? null
|
||||
hoveredNodeId = hoveredSlotKey
|
||||
? null
|
||||
: elWithNode?.dataset['nodeId'] ?? null
|
||||
: (elWithNode?.dataset['nodeId'] ?? null)
|
||||
dragContext.lastPointerEventTarget = target
|
||||
dragContext.lastPointerTargetSlotKey = hoveredSlotKey
|
||||
dragContext.lastPointerTargetNodeId = hoveredNodeId
|
||||
@@ -620,8 +620,8 @@ export function useSlotLinkInteraction({
|
||||
}
|
||||
|
||||
const baseDirection = isInputSlot
|
||||
? inputSlot?.dir ?? LinkDirection.LEFT
|
||||
: outputSlot?.dir ?? LinkDirection.RIGHT
|
||||
? (inputSlot?.dir ?? LinkDirection.LEFT)
|
||||
: (outputSlot?.dir ?? LinkDirection.RIGHT)
|
||||
|
||||
const existingAnchor =
|
||||
isInputSlot && !shouldBreakExistingInputLink
|
||||
|
||||
@@ -33,16 +33,20 @@ const selectedItems = computed(() => {
|
||||
})
|
||||
|
||||
const chevronClass = computed(() =>
|
||||
cn('mr-2 size-4 transition-transform duration-200 flex-shrink-0', {
|
||||
'rotate-180': props.isOpen
|
||||
})
|
||||
cn(
|
||||
'mr-2 size-4 transition-transform duration-200 flex-shrink-0 text-button-icon',
|
||||
{
|
||||
'rotate-180': props.isOpen
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const theButtonStyle = computed(() =>
|
||||
cn('bg-transparent border-0 outline-none text-zinc-400', {
|
||||
cn('bg-transparent border-0 outline-none text-text-secondary', {
|
||||
'hover:bg-node-component-widget-input-surface/30 cursor-pointer':
|
||||
!props.disabled,
|
||||
'cursor-not-allowed': props.disabled
|
||||
'cursor-not-allowed': props.disabled,
|
||||
'text-text-primary': selectedItems.value.length > 0
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IAudioRecordWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
AudioRecordInputSpec,
|
||||
InputSpec as InputSpecV2
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useAudioRecordWidget = (): ComfyWidgetConstructorV2 => {
|
||||
return (node: LGraphNode, inputSpec: InputSpecV2): IAudioRecordWidget => {
|
||||
const {
|
||||
name,
|
||||
default: defaultValue = '',
|
||||
options = {}
|
||||
} = inputSpec as AudioRecordInputSpec
|
||||
|
||||
const widget = node.addWidget('audiorecord', name, defaultValue, () => {}, {
|
||||
serialize: true,
|
||||
...options
|
||||
}) as IAudioRecordWidget
|
||||
|
||||
return widget
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
/**
|
||||
* Format time in MM:SS format
|
||||
@@ -23,10 +24,6 @@ export function getAudioUrlFromPath(
|
||||
return api.apiURL(getResourceURL(subfolder, filename, type))
|
||||
}
|
||||
|
||||
function getRandParam() {
|
||||
return '&rand=' + Math.random()
|
||||
}
|
||||
|
||||
export function getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
@@ -36,7 +33,7 @@ export function getResourceURL(
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder,
|
||||
getRandParam().substring(1)
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
|
||||
@@ -152,13 +152,6 @@ const zTextareaInputSpec = zBaseInputOptions.extend({
|
||||
.optional()
|
||||
})
|
||||
|
||||
const zAudioRecordInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('AUDIORECORD'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z.record(z.unknown()).optional()
|
||||
})
|
||||
|
||||
const zCustomInputSpec = zBaseInputOptions.extend({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
@@ -174,7 +167,6 @@ const zInputSpec = z.union([
|
||||
zColorInputSpec,
|
||||
zFileUploadInputSpec,
|
||||
zImageInputSpec,
|
||||
zAudioRecordInputSpec,
|
||||
zImageCompareInputSpec,
|
||||
zMarkdownInputSpec,
|
||||
zTreeSelectInputSpec,
|
||||
@@ -230,7 +222,6 @@ export type GalleriaInputSpec = z.infer<typeof zGalleriaInputSpec>
|
||||
export type SelectButtonInputSpec = z.infer<typeof zSelectButtonInputSpec>
|
||||
export type TextareaInputSpec = z.infer<typeof zTextareaInputSpec>
|
||||
export type CustomInputSpec = z.infer<typeof zCustomInputSpec>
|
||||
export type AudioRecordInputSpec = z.infer<typeof zAudioRecordInputSpec>
|
||||
|
||||
export type InputSpec = z.infer<typeof zInputSpec>
|
||||
export type OutputSpec = z.infer<typeof zOutputSpec>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
@@ -336,6 +337,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
getRandParam() {
|
||||
if (isCloud) return ''
|
||||
return '&rand=' + Math.random()
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
IStringWidget
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useAudioRecordWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useAudioRecordWidget'
|
||||
import { useBooleanWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBooleanWidget'
|
||||
import { useChartWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChartWidget'
|
||||
import { useColorWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useColorWidget'
|
||||
@@ -305,6 +304,5 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
CHART: transformWidgetConstructorV2ToV1(useChartWidget()),
|
||||
GALLERIA: transformWidgetConstructorV2ToV1(useGalleriaWidget()),
|
||||
SELECTBUTTON: transformWidgetConstructorV2ToV1(useSelectButtonWidget()),
|
||||
TEXTAREA: transformWidgetConstructorV2ToV1(useTextareaWidget()),
|
||||
AUDIO_RECORD: transformWidgetConstructorV2ToV1(useAudioRecordWidget())
|
||||
TEXTAREA: transformWidgetConstructorV2ToV1(useTextareaWidget())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { addWidgetPromotionOptions } from '@/core/graph/subgraph/proxyWidgetUtil
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { st, t } from '@/i18n'
|
||||
import {
|
||||
LGraphBadge,
|
||||
LGraphCanvas,
|
||||
LGraphEventMode,
|
||||
LGraphNode,
|
||||
@@ -135,19 +134,6 @@ export const useLitegraphService = () => {
|
||||
this.#setInitialSize()
|
||||
this.serialize_widgets = true
|
||||
void extensionService.invokeExtensionsAsync('nodeCreated', this)
|
||||
this.badges.push(
|
||||
new LGraphBadge({
|
||||
text: '',
|
||||
iconOptions: {
|
||||
unicode: '\ue96e',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: '#ffffff',
|
||||
fontSize: 12
|
||||
},
|
||||
fgColor: '#ffffff',
|
||||
bgColor: '#3b82f6'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -845,7 +831,7 @@ export const useLitegraphService = () => {
|
||||
)
|
||||
}
|
||||
if (this.graph && !this.graph.isRootGraph) {
|
||||
const [x, y] = canvas.canvas_mouse
|
||||
const [x, y] = canvas.graph_mouse
|
||||
const overWidget = this.getWidgetOnPos(x, y, true)
|
||||
if (overWidget) {
|
||||
addWidgetPromotionOptions(options, overWidget, this)
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface TreeExplorerNode<T = any> extends TreeNode {
|
||||
event: MouseEvent
|
||||
) => void | Promise<void>
|
||||
/** Function to handle errors */
|
||||
handleError?: (this: TreeExplorerNode<T>, error: Error) => void
|
||||
handleError?: (this: TreeExplorerNode<T>, error: unknown) => void
|
||||
/** Extra context menu items */
|
||||
contextMenuItems?:
|
||||
| MenuItem[]
|
||||
|
||||
@@ -84,8 +84,8 @@ function isIPv6Loopback(h: string): boolean {
|
||||
|
||||
// Count explicit zero groups on each side of '::' to ensure at least one group is compressed.
|
||||
// (leftCount + rightCount) must be ≤ 6 so that the total expanded groups = 8.
|
||||
const leftCount = m[1] ? m[1].match(/0{1,4}:/gi)?.length ?? 0 : 0
|
||||
const rightCount = m[2] ? m[2].match(/0{1,4}:/gi)?.length ?? 0 : 0
|
||||
const leftCount = m[1] ? (m[1].match(/0{1,4}:/gi)?.length ?? 0) : 0
|
||||
const rightCount = m[2] ? (m[2].match(/0{1,4}:/gi)?.length ?? 0) : 0
|
||||
|
||||
// Require that at least one group was actually compressed: i.e., leftCount + rightCount ≤ 6.
|
||||
return leftCount + rightCount <= 6
|
||||
|
||||
@@ -236,7 +236,7 @@ const handleSubmit = async () => {
|
||||
// Convert 'latest' to actual version number for installation
|
||||
const actualVersion =
|
||||
selectedVersion.value === 'latest'
|
||||
? nodePack.latest_version?.version ?? 'latest'
|
||||
? (nodePack.latest_version?.version ?? 'latest')
|
||||
: selectedVersion.value
|
||||
|
||||
await managerStore.installPack.call({
|
||||
|
||||
@@ -74,8 +74,8 @@ const createPayload = (installItem: NodePack) => {
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: (installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']))
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
@@ -140,7 +140,7 @@ const performInstallation = async (packs: NodePack[]) => {
|
||||
const computedLabel = computed(() =>
|
||||
isInstalling.value
|
||||
? t('g.installing')
|
||||
: label ??
|
||||
(nodePacks.length > 1 ? t('manager.installSelected') : t('g.install'))
|
||||
: (label ??
|
||||
(nodePacks.length > 1 ? t('manager.installSelected') : t('g.install')))
|
||||
)
|
||||
</script>
|
||||
|
||||
353
tests-ui/tests/composables/useErrorHandling.test.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
|
||||
describe('useErrorHandling', () => {
|
||||
let errorHandler: ReturnType<typeof useErrorHandling>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
errorHandler = useErrorHandling()
|
||||
})
|
||||
|
||||
describe('wrapWithErrorHandlingAsync', () => {
|
||||
it('should execute action successfully', async () => {
|
||||
const action = vi.fn(async () => 'success')
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(action)
|
||||
|
||||
const result = await wrapped()
|
||||
|
||||
expect(result).toBe('success')
|
||||
expect(action).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call error handler when action throws', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(async () => {
|
||||
throw testError
|
||||
})
|
||||
const customErrorHandler = vi.fn()
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
customErrorHandler
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(customErrorHandler).toHaveBeenCalledWith(testError)
|
||||
})
|
||||
|
||||
it('should call finally handler after success', async () => {
|
||||
const action = vi.fn(async () => 'success')
|
||||
const finallyHandler = vi.fn()
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
undefined,
|
||||
finallyHandler
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(finallyHandler).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call finally handler after error', async () => {
|
||||
const action = vi.fn(async () => {
|
||||
throw new Error('test error')
|
||||
})
|
||||
const finallyHandler = vi.fn()
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
vi.fn(),
|
||||
finallyHandler
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(finallyHandler).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
describe('error recovery', () => {
|
||||
it('should not use recovery strategy when no error occurs', async () => {
|
||||
const action = vi.fn(async () => 'success')
|
||||
const recoveryStrategy: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn()
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
undefined,
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(recoveryStrategy.shouldHandle).not.toHaveBeenCalled()
|
||||
expect(recoveryStrategy.recover).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use recovery strategy when it matches error', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(async () => {
|
||||
throw testError
|
||||
})
|
||||
const recoveryStrategy: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn((error) => error === testError),
|
||||
recover: vi.fn(async () => {
|
||||
// Recovery succeeds, does nothing
|
||||
})
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
vi.fn(),
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(recoveryStrategy.shouldHandle).toHaveBeenCalledWith(testError)
|
||||
expect(recoveryStrategy.recover).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass action and args to recovery strategy', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(async (_arg1: string, _arg2: number) => {
|
||||
throw testError
|
||||
})
|
||||
const recoveryStrategy: ErrorRecoveryStrategy<[string, number], void> =
|
||||
{
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn()
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
vi.fn(),
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped('test', 123)
|
||||
|
||||
expect(recoveryStrategy.recover).toHaveBeenCalledWith(
|
||||
testError,
|
||||
action,
|
||||
['test', 123]
|
||||
)
|
||||
})
|
||||
|
||||
it('should retry operation when recovery succeeds', async () => {
|
||||
let attemptCount = 0
|
||||
const action = vi.fn(async (value: string) => {
|
||||
attemptCount++
|
||||
if (attemptCount === 1) {
|
||||
throw new Error('first attempt failed')
|
||||
}
|
||||
return `success: ${value}`
|
||||
})
|
||||
|
||||
const recoveryStrategy: ErrorRecoveryStrategy<[string], string> = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn(async (_error, retry, args) => {
|
||||
await retry(...args)
|
||||
})
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
vi.fn(),
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped('test-value')
|
||||
|
||||
expect(action).toHaveBeenCalledTimes(2)
|
||||
expect(recoveryStrategy.recover).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should not call error handler when recovery succeeds', async () => {
|
||||
const action = vi.fn(async () => {
|
||||
throw new Error('test error')
|
||||
})
|
||||
const customErrorHandler = vi.fn()
|
||||
const recoveryStrategy: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn(async () => {
|
||||
// Recovery succeeds
|
||||
})
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
customErrorHandler,
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(customErrorHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call error handler when recovery fails', async () => {
|
||||
const originalError = new Error('original error')
|
||||
const recoveryError = new Error('recovery error')
|
||||
const action = vi.fn(async () => {
|
||||
throw originalError
|
||||
})
|
||||
const customErrorHandler = vi.fn()
|
||||
const recoveryStrategy: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn(async () => {
|
||||
throw recoveryError
|
||||
})
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
customErrorHandler,
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(customErrorHandler).toHaveBeenCalledWith(originalError)
|
||||
})
|
||||
|
||||
it('should try multiple recovery strategies in order', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(async () => {
|
||||
throw testError
|
||||
})
|
||||
|
||||
const strategy1: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => false),
|
||||
recover: vi.fn()
|
||||
}
|
||||
|
||||
const strategy2: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn(async () => {
|
||||
// Recovery succeeds
|
||||
})
|
||||
}
|
||||
|
||||
const strategy3: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn()
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
vi.fn(),
|
||||
undefined,
|
||||
[strategy1, strategy2, strategy3]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(strategy1.shouldHandle).toHaveBeenCalledWith(testError)
|
||||
expect(strategy1.recover).not.toHaveBeenCalled()
|
||||
|
||||
expect(strategy2.shouldHandle).toHaveBeenCalledWith(testError)
|
||||
expect(strategy2.recover).toHaveBeenCalled()
|
||||
|
||||
// Strategy 3 should not be checked because strategy 2 handled it
|
||||
expect(strategy3.shouldHandle).not.toHaveBeenCalled()
|
||||
expect(strategy3.recover).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fall back to error handler when no strategy matches', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(async () => {
|
||||
throw testError
|
||||
})
|
||||
const customErrorHandler = vi.fn()
|
||||
|
||||
const strategy: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => false),
|
||||
recover: vi.fn()
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
customErrorHandler,
|
||||
undefined,
|
||||
[strategy]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(strategy.shouldHandle).toHaveBeenCalledWith(testError)
|
||||
expect(strategy.recover).not.toHaveBeenCalled()
|
||||
expect(customErrorHandler).toHaveBeenCalledWith(testError)
|
||||
})
|
||||
|
||||
it('should work with synchronous actions', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(() => {
|
||||
throw testError
|
||||
})
|
||||
const recoveryStrategy: ErrorRecoveryStrategy = {
|
||||
shouldHandle: vi.fn(() => true),
|
||||
recover: vi.fn(async () => {
|
||||
// Recovery succeeds
|
||||
})
|
||||
}
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
vi.fn(),
|
||||
undefined,
|
||||
[recoveryStrategy]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(recoveryStrategy.recover).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('backward compatibility', () => {
|
||||
it('should work without recovery strategies parameter', async () => {
|
||||
const action = vi.fn(async () => 'success')
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(action)
|
||||
|
||||
const result = await wrapped()
|
||||
|
||||
expect(result).toBe('success')
|
||||
})
|
||||
|
||||
it('should work with empty recovery strategies array', async () => {
|
||||
const testError = new Error('test error')
|
||||
const action = vi.fn(async () => {
|
||||
throw testError
|
||||
})
|
||||
const customErrorHandler = vi.fn()
|
||||
|
||||
const wrapped = errorHandler.wrapWithErrorHandlingAsync(
|
||||
action,
|
||||
customErrorHandler,
|
||||
undefined,
|
||||
[]
|
||||
)
|
||||
|
||||
await wrapped()
|
||||
|
||||
expect(customErrorHandler).toHaveBeenCalledWith(testError)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -113,7 +113,12 @@ describe('ImagePreview', () => {
|
||||
|
||||
// Action buttons should now be visible
|
||||
expect(wrapper.find('.actions').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.action-btn')).toHaveLength(2) // download, remove (no mask for multiple images)
|
||||
// For multiple images: download and remove buttons (no mask button)
|
||||
expect(wrapper.find('[aria-label="Download image"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[aria-label="Remove image"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[aria-label="Edit or mask image"]').exists()).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('hides action buttons when not hovering', async () => {
|
||||
@@ -203,8 +208,9 @@ describe('ImagePreview', () => {
|
||||
await navigationDots[1].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// After clicking, component shows loading state (Skeleton)
|
||||
// After clicking, component shows loading state (Skeleton), not img
|
||||
expect(wrapper.find('skeleton-stub').exists()).toBe(true)
|
||||
expect(wrapper.find('img').exists()).toBe(false)
|
||||
|
||||
// Simulate image load event to clear loading state
|
||||
const component = wrapper.vm as any
|
||||
|
||||