Compare commits
59 Commits
drjkl/remo
...
cloud/trac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54f94fd8c0 | ||
|
|
d84ded8d83 | ||
|
|
8417acd75c | ||
|
|
5c0eef8d3f | ||
|
|
43db891c1a | ||
|
|
1b1cb956e6 | ||
|
|
ff0c15b119 | ||
|
|
1c0f151d02 | ||
|
|
2702ac64fe | ||
|
|
8ca541e850 | ||
|
|
d3a5d9e995 | ||
|
|
168e885d50 | ||
|
|
504aabd097 | ||
|
|
c7bbab53a6 | ||
|
|
33b6df55a8 | ||
|
|
16ebe33488 | ||
|
|
108ad22d82 | ||
|
|
4a10017bd2 | ||
|
|
646d7a68be | ||
|
|
c13371ef47 | ||
|
|
775c856bf7 | ||
|
|
e035f895a3 | ||
|
|
98e543ec31 | ||
|
|
992efc4486 | ||
|
|
88130a9cae | ||
|
|
ffd2b0efab | ||
|
|
7c9b8bb7a6 | ||
|
|
18b3b11b9a | ||
|
|
80b1c2aaf7 | ||
|
|
a13eeaea7e | ||
|
|
59a1380f39 | ||
|
|
f1fbab6e1f | ||
|
|
9bd3d5cbe6 | ||
|
|
bd48649604 | ||
|
|
c6c9487c0d | ||
|
|
799795cf56 | ||
|
|
4899c9d25b | ||
|
|
0bd3c1271d | ||
|
|
6eb91e4aed | ||
|
|
3b3071c975 | ||
|
|
68f0275a83 | ||
|
|
a0d66bb0d7 | ||
|
|
1292ae0f14 | ||
|
|
8da2b304ef | ||
|
|
0950da0b43 | ||
|
|
86e2b1fc61 | ||
|
|
4a612b09ed | ||
|
|
4a3c3d9c97 | ||
|
|
c3c59988f4 | ||
|
|
e6d3e94a34 | ||
|
|
1c0c501105 | ||
|
|
980b727ff8 | ||
|
|
40c47a8e67 | ||
|
|
f0f4313afa | ||
|
|
cb5894a100 | ||
|
|
7649feb47f | ||
|
|
c27edb7e94 | ||
|
|
23e881e220 | ||
|
|
c5c06b6ba8 |
2
.gitattributes
vendored
@@ -12,5 +12,5 @@
|
|||||||
*.yaml text eol=lf
|
*.yaml text eol=lf
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true
|
src/types/comfyRegistryTypes.ts linguist-generated=true
|
||||||
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true
|
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true
|
||||||
|
|||||||
67
.github/actions/setup-frontend/action.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: Setup Frontend
|
|
||||||
description: 'Setup ComfyUI frontend development environment'
|
|
||||||
inputs:
|
|
||||||
extra_server_params:
|
|
||||||
description: 'Additional parameters to pass to ComfyUI server'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
- name: Checkout ComfyUI
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: 'comfyanonymous/ComfyUI'
|
|
||||||
path: 'ComfyUI'
|
|
||||||
|
|
||||||
- name: Checkout ComfyUI_frontend
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
|
||||||
path: 'ComfyUI_frontend'
|
|
||||||
|
|
||||||
- name: Copy ComfyUI_devtools from frontend repo
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
|
|
||||||
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 'lts/*'
|
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.10'
|
|
||||||
|
|
||||||
- name: Install Python requirements
|
|
||||||
shell: bash
|
|
||||||
working-directory: ComfyUI
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install wait-for-it
|
|
||||||
|
|
||||||
- name: Build & Install ComfyUI_frontend
|
|
||||||
shell: bash
|
|
||||||
working-directory: ComfyUI_frontend
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
- name: Start ComfyUI server
|
|
||||||
shell: bash
|
|
||||||
working-directory: ComfyUI
|
|
||||||
run: |
|
|
||||||
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist ${{ inputs.extra_server_params }} &
|
|
||||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
|
||||||
2
.github/workflows/backport.yaml
vendored
@@ -60,7 +60,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
184
.github/workflows/chromatic.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Storybook and Chromatic CI
|
name: 'Chromatic'
|
||||||
|
|
||||||
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
|
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
|
||||||
|
|
||||||
@@ -8,100 +8,13 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Post starting comment for non-forked PRs
|
|
||||||
comment-on-pr-start:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Post starting comment
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
|
|
||||||
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
|
|
||||||
"${{ github.event.pull_request.number }}" \
|
|
||||||
"${{ github.head_ref }}" \
|
|
||||||
"starting" \
|
|
||||||
"$(date -u '+%m/%d/%Y, %I:%M:%S %p')"
|
|
||||||
|
|
||||||
# Build Storybook for all PRs (free Cloudflare deployment)
|
|
||||||
storybook-build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
outputs:
|
|
||||||
conclusion: ${{ steps.job-status.outputs.conclusion }}
|
|
||||||
workflow-url: ${{ steps.workflow-url.outputs.url }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Cache tool outputs
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.cache
|
|
||||||
storybook-static
|
|
||||||
tsconfig.tsbuildinfo
|
|
||||||
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
|
|
||||||
restore-keys: |
|
|
||||||
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
|
||||||
storybook-cache-${{ runner.os }}-
|
|
||||||
storybook-tools-cache-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build Storybook
|
|
||||||
run: pnpm build-storybook
|
|
||||||
|
|
||||||
- name: Set job status
|
|
||||||
id: job-status
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get workflow URL
|
|
||||||
id: workflow-url
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload Storybook build
|
|
||||||
if: success() && github.event.pull_request.head.repo.fork == false
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: storybook-static
|
|
||||||
path: storybook-static/
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# Chromatic deployment only for version-bump-* branches or manual triggers
|
|
||||||
chromatic-deployment:
|
chromatic-deployment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'version-bump-'))
|
# Only run for PRs from version-bump-* branches or manual triggers
|
||||||
outputs:
|
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
|
||||||
conclusion: ${{ steps.job-status.outputs.conclusion }}
|
|
||||||
workflow-url: ${{ steps.workflow-url.outputs.url }}
|
|
||||||
chromatic-build-url: ${{ steps.chromatic.outputs.buildUrl }}
|
|
||||||
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Required for Chromatic baseline
|
fetch-depth: 0 # Required for Chromatic baseline
|
||||||
|
|
||||||
@@ -116,6 +29,7 @@ jobs:
|
|||||||
node-version: '20'
|
node-version: '20'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
|
||||||
- name: Cache tool outputs
|
- name: Cache tool outputs
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -140,92 +54,4 @@ jobs:
|
|||||||
buildScriptName: build-storybook
|
buildScriptName: build-storybook
|
||||||
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
||||||
exitOnceUploaded: true # Don't wait for UI tests to complete
|
exitOnceUploaded: true # Don't wait for UI tests to complete
|
||||||
onlyChanged: true # Only capture changed stories
|
|
||||||
|
|
||||||
- name: Set job status
|
|
||||||
id: job-status
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get workflow URL
|
|
||||||
id: workflow-url
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Deploy and comment for non-forked PRs only
|
|
||||||
deploy-and-comment:
|
|
||||||
needs: [storybook-build]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && always()
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Download Storybook build
|
|
||||||
if: needs.storybook-build.outputs.conclusion == 'success'
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: storybook-static
|
|
||||||
path: storybook-static
|
|
||||||
|
|
||||||
- name: Make deployment script executable
|
|
||||||
run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
|
|
||||||
|
|
||||||
- name: Deploy Storybook and comment on PR
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
WORKFLOW_CONCLUSION: ${{ needs.storybook-build.outputs.conclusion }}
|
|
||||||
WORKFLOW_URL: ${{ needs.storybook-build.outputs.workflow-url }}
|
|
||||||
run: |
|
|
||||||
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
|
|
||||||
"${{ github.event.pull_request.number }}" \
|
|
||||||
"${{ github.head_ref }}" \
|
|
||||||
"completed"
|
|
||||||
|
|
||||||
# Update comment with Chromatic URLs for version-bump branches
|
|
||||||
update-comment-with-chromatic:
|
|
||||||
needs: [chromatic-deployment, deploy-and-comment]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && startsWith(github.head_ref, 'version-bump-') && needs.chromatic-deployment.outputs.chromatic-build-url != ''
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Update comment with Chromatic URLs
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
|
|
||||||
const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
|
|
||||||
|
|
||||||
// Find the existing Storybook comment
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: ${{ github.event.pull_request.number }}
|
|
||||||
});
|
|
||||||
|
|
||||||
const storybookComment = comments.find(comment =>
|
|
||||||
comment.body.includes('<!-- STORYBOOK_BUILD_STATUS -->')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (storybookComment && buildUrl && storybookUrl) {
|
|
||||||
// Append Chromatic info to existing comment
|
|
||||||
const updatedBody = storybookComment.body.replace(
|
|
||||||
/---\n(.*)$/s,
|
|
||||||
`---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
|
|
||||||
);
|
|
||||||
|
|
||||||
await github.rest.issues.updateComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
comment_id: storybookComment.id,
|
|
||||||
body: updatedBody
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
2
.github/workflows/claude-pr-review.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/dev-release.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
version: ${{ steps.current_version.outputs.version }}
|
version: ${{ steps.current_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- name: Download dist artifact
|
- name: Download dist artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
26
.github/workflows/devtools-python.yaml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: Devtools Python Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'tools/devtools/**'
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
paths:
|
|
||||||
- 'tools/devtools/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
syntax:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Validate Python syntax
|
|
||||||
run: python3 -m compileall -q tools/devtools
|
|
||||||
6
.github/workflows/i18n-custom-nodes.yaml
vendored
@@ -22,13 +22,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ComfyUI
|
- name: Checkout ComfyUI
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: comfyanonymous/ComfyUI
|
repository: comfyanonymous/ComfyUI
|
||||||
path: ComfyUI
|
path: ComfyUI
|
||||||
ref: master
|
ref: master
|
||||||
- name: Checkout ComfyUI_frontend
|
- name: Checkout ComfyUI_frontend
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: Comfy-Org/ComfyUI_frontend
|
repository: Comfy-Org/ComfyUI_frontend
|
||||||
path: ComfyUI_frontend
|
path: ComfyUI_frontend
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
|
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
|
||||||
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
|
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||||
- name: Checkout custom node repository
|
- name: Checkout custom node repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
||||||
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
||||||
|
|||||||
3
.github/workflows/i18n-node-defs.yaml
vendored
@@ -13,8 +13,7 @@ jobs:
|
|||||||
update-locales:
|
update-locales:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Frontend
|
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||||
uses: ./.github/actions/setup-frontend
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: pnpm exec playwright install chromium --with-deps
|
run: pnpm exec playwright install chromium --with-deps
|
||||||
working-directory: ComfyUI_frontend
|
working-directory: ComfyUI_frontend
|
||||||
|
|||||||
3
.github/workflows/i18n.yaml
vendored
@@ -14,8 +14,7 @@ jobs:
|
|||||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
|
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Frontend
|
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||||
uses: ./.github/actions/setup-frontend
|
|
||||||
|
|
||||||
- name: Cache tool outputs
|
- name: Cache tool outputs
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
15
.github/workflows/json-validate.yaml
vendored
@@ -1,15 +0,0 @@
|
|||||||
name: Validate JSON
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
json-lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Validate JSON syntax
|
|
||||||
run: ./scripts/cicd/check-json.sh
|
|
||||||
2
.github/workflows/lint-and-format.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
|||||||
2
.github/workflows/pr-playwright-deploy.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
|
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get PR Number
|
- name: Get PR Number
|
||||||
id: pr
|
id: pr
|
||||||
|
|||||||
126
.github/workflows/pr-storybook-comment.yaml
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
name: PR Storybook Comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ['Chromatic']
|
||||||
|
types: [requested, completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment-storybook:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||||
|
&& github.event.workflow_run.event == 'pull_request'
|
||||||
|
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: Get PR number
|
||||||
|
id: pr
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: pullRequests } = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pullRequests.length === 0) {
|
||||||
|
console.log('No open PR found for this branch');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pullRequests[0].number;
|
||||||
|
|
||||||
|
- name: Log when no PR found
|
||||||
|
if: steps.pr.outputs.result == 'null'
|
||||||
|
run: |
|
||||||
|
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
|
||||||
|
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
|
||||||
|
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
|
||||||
|
echo "Event: ${{ github.event.workflow_run.event }}"
|
||||||
|
|
||||||
|
- name: Get workflow run details
|
||||||
|
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||||
|
id: workflow-run
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const run = await github.rest.actions.getWorkflowRun({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.payload.workflow_run.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
conclusion: run.data.conclusion,
|
||||||
|
html_url: run.data.html_url
|
||||||
|
};
|
||||||
|
|
||||||
|
- name: Get completion time
|
||||||
|
id: completion-time
|
||||||
|
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Comment PR - Storybook Started
|
||||||
|
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
|
||||||
|
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ steps.pr.outputs.result }}
|
||||||
|
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!-- STORYBOOK_BUILD_STATUS -->
|
||||||
|
## 🎨 Storybook Build Status
|
||||||
|
|
||||||
|
<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Build is starting...**
|
||||||
|
|
||||||
|
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
|
||||||
|
|
||||||
|
### 🚀 Building Storybook
|
||||||
|
- 📦 Installing dependencies...
|
||||||
|
- 🔧 Building Storybook components...
|
||||||
|
- 🎨 Running Chromatic visual tests...
|
||||||
|
|
||||||
|
---
|
||||||
|
⏱️ Please wait while the Storybook build is in progress...
|
||||||
|
|
||||||
|
- name: Comment PR - Storybook Complete
|
||||||
|
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||||
|
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ steps.pr.outputs.result }}
|
||||||
|
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!-- STORYBOOK_BUILD_STATUS -->
|
||||||
|
## 🎨 Storybook Build Status
|
||||||
|
|
||||||
|
${{
|
||||||
|
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|
||||||
|
|| '❌'
|
||||||
|
}} **${{
|
||||||
|
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|
||||||
|
|| 'Build failed!'
|
||||||
|
}}**
|
||||||
|
|
||||||
|
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
|
||||||
|
|
||||||
|
### 🔗 Links
|
||||||
|
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
|
||||||
|
|
||||||
|
---
|
||||||
|
${{
|
||||||
|
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'ℹ️ Chromatic was skipped for this PR.'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'ℹ️ The Chromatic run was cancelled.'
|
||||||
|
|| '⚠️ Please check the workflow logs for error details.'
|
||||||
|
}}
|
||||||
90
.github/workflows/pr-storybook-deploy.yaml
vendored
@@ -1,90 +0,0 @@
|
|||||||
name: PR Storybook Deploy (Forks)
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ['Storybook and Chromatic CI']
|
|
||||||
types: [requested, completed]
|
|
||||||
|
|
||||||
env:
|
|
||||||
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-and-comment-forked-pr:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
|
||||||
github.event.workflow_run.event == 'pull_request' &&
|
|
||||||
github.event.workflow_run.head_repository != null &&
|
|
||||||
github.event.workflow_run.repository != null &&
|
|
||||||
github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
- name: Log workflow trigger info
|
|
||||||
run: |
|
|
||||||
echo "Repository: ${{ github.repository }}"
|
|
||||||
echo "Event: ${{ github.event.workflow_run.event }}"
|
|
||||||
echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}"
|
|
||||||
echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}"
|
|
||||||
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
|
|
||||||
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Get PR Number
|
|
||||||
id: pr
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { data: prs } = await github.rest.pulls.list({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
|
|
||||||
|
|
||||||
if (!pr) {
|
|
||||||
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
|
|
||||||
return pr.number;
|
|
||||||
|
|
||||||
- name: Handle Storybook Start
|
|
||||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
|
|
||||||
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
|
|
||||||
"${{ steps.pr.outputs.result }}" \
|
|
||||||
"${{ github.event.workflow_run.head_branch }}" \
|
|
||||||
"starting" \
|
|
||||||
"$(date -u '${{ env.DATE_FORMAT }}')"
|
|
||||||
|
|
||||||
- name: Download and Deploy Storybook
|
|
||||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
name: storybook-static
|
|
||||||
path: storybook-static
|
|
||||||
|
|
||||||
- name: Handle Storybook Completion
|
|
||||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
|
|
||||||
WORKFLOW_URL: ${{ github.event.workflow_run.html_url }}
|
|
||||||
run: |
|
|
||||||
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
|
|
||||||
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
|
|
||||||
"${{ steps.pr.outputs.result }}" \
|
|
||||||
"${{ github.event.workflow_run.head_branch }}" \
|
|
||||||
"completed"
|
|
||||||
5
.github/workflows/test-browser-exp.yaml
vendored
@@ -10,10 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.label.name == 'New Browser Test Expectations'
|
if: github.event.label.name == 'New Browser Test Expectations'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout workflow repo
|
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||||
uses: actions/checkout@v5
|
|
||||||
- name: Setup Frontend
|
|
||||||
uses: ./.github/actions/setup-frontend
|
|
||||||
- name: Cache Playwright browsers
|
- name: Cache Playwright browsers
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/test-ui.yaml
vendored
@@ -15,14 +15,14 @@ jobs:
|
|||||||
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
|
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ComfyUI
|
- name: Checkout ComfyUI
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'comfyanonymous/ComfyUI'
|
repository: 'comfyanonymous/ComfyUI'
|
||||||
path: 'ComfyUI'
|
path: 'ComfyUI'
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Checkout ComfyUI_frontend
|
- name: Checkout ComfyUI_frontend
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
repository: 'Comfy-Org/ComfyUI_frontend'
|
||||||
path: 'ComfyUI_frontend'
|
path: 'ComfyUI_frontend'
|
||||||
@@ -250,7 +250,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ComfyUI_frontend
|
- name: Checkout ComfyUI_frontend
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
repository: 'Comfy-Org/ComfyUI_frontend'
|
||||||
path: 'ComfyUI_frontend'
|
path: 'ComfyUI_frontend'
|
||||||
@@ -306,7 +306,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get start time
|
- name: Get start time
|
||||||
id: start-time
|
id: start-time
|
||||||
@@ -333,7 +333,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download all playwright reports
|
- name: Download all playwright reports
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
|
|||||||
2
.github/workflows/update-electron-types.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|||||||
4
.github/workflows/update-manager-types.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
comfyui-manager-repo-${{ runner.os }}-
|
comfyui-manager-repo-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Checkout ComfyUI-Manager repository
|
- name: Checkout ComfyUI-Manager repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: Comfy-Org/ComfyUI-Manager
|
repository: Comfy-Org/ComfyUI-Manager
|
||||||
path: ComfyUI-Manager
|
path: ComfyUI-Manager
|
||||||
|
|||||||
17
.github/workflows/update-registry-types.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
comfy-api-repo-${{ runner.os }}-
|
comfy-api-repo-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Checkout comfy-api repository
|
- name: Checkout comfy-api repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: Comfy-Org/comfy-api
|
repository: Comfy-Org/comfy-api
|
||||||
path: comfy-api
|
path: comfy-api
|
||||||
@@ -68,18 +68,17 @@ jobs:
|
|||||||
- name: Generate API types
|
- name: Generate API types
|
||||||
run: |
|
run: |
|
||||||
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
|
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
|
||||||
mkdir -p ./packages/registry-types/src
|
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
|
||||||
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./packages/registry-types/src/comfyRegistryTypes.ts
|
|
||||||
|
|
||||||
- name: Validate generated types
|
- name: Validate generated types
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
|
if [ ! -f ./src/types/comfyRegistryTypes.ts ]; then
|
||||||
echo "Error: Types file was not generated."
|
echo "Error: Types file was not generated."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if file is not empty
|
# Check if file is not empty
|
||||||
if [ ! -s ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
|
if [ ! -s ./src/types/comfyRegistryTypes.ts ]; then
|
||||||
echo "Error: Generated types file is empty."
|
echo "Error: Generated types file is empty."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -87,12 +86,12 @@ jobs:
|
|||||||
- name: Lint generated types
|
- name: Lint generated types
|
||||||
run: |
|
run: |
|
||||||
echo "Linting generated Comfy Registry API types..."
|
echo "Linting generated Comfy Registry API types..."
|
||||||
pnpm lint:fix:no-cache -- ./packages/registry-types/src/comfyRegistryTypes.ts
|
pnpm lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
id: check-changes
|
id: check-changes
|
||||||
run: |
|
run: |
|
||||||
if [[ -z $(git status --porcelain ./packages/registry-types/src/comfyRegistryTypes.ts) ]]; then
|
if [[ -z $(git status --porcelain ./src/types/comfyRegistryTypes.ts) ]]; then
|
||||||
echo "No changes to Comfy Registry API types detected."
|
echo "No changes to Comfy Registry API types detected."
|
||||||
echo "changed=false" >> $GITHUB_OUTPUT
|
echo "changed=false" >> $GITHUB_OUTPUT
|
||||||
exit 0
|
exit 0
|
||||||
@@ -122,4 +121,4 @@ jobs:
|
|||||||
labels: CNR
|
labels: CNR
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
add-paths: |
|
add-paths: |
|
||||||
packages/registry-types/src/comfyRegistryTypes.ts
|
src/types/comfyRegistryTypes.ts
|
||||||
|
|||||||
2
.github/workflows/version-bump.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|||||||
2
.github/workflows/vitest.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
packages/registry-types/src/comfyRegistryTypes.ts
|
src/types/comfyRegistryTypes.ts
|
||||||
src/types/generatedManagerTypes.ts
|
src/types/generatedManagerTypes.ts
|
||||||
@@ -31,9 +31,10 @@
|
|||||||
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
|
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
- Commits: Use `[skip ci]` for locale-only updates when appropriate.
|
- Commits: Prefer Conventional Commits (e.g., `feat(ui): add sidebar`), `refactor(litegraph): …`. Use `[skip ci]` for locale-only updates when appropriate.
|
||||||
- PRs: Include clear description, linked issues (`- Fixes #123`), and screenshots/GIFs for UI changes.
|
- PRs: Include clear description, linked issues (`Fixes #123`), and screenshots/GIFs for UI changes. Add/adjust tests and i18n strings when applicable.
|
||||||
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
|
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
|
||||||
|
|
||||||
## Security & Configuration Tips
|
## Security & Configuration Tips
|
||||||
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
|
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
|
||||||
|
- Backend: Dev server expects ComfyUI backend at `localhost:8188` by default; configure via `.env`.
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ UI mode features:
|
|||||||
- **Console/Network Tabs**: View logs and API calls at each step
|
- **Console/Network Tabs**: View logs and API calls at each step
|
||||||
- **Attachments Tab**: View all snapshots with expected and actual images
|
- **Attachments Tab**: View all snapshots with expected and actual images
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
For CI or headless testing:
|
For CI or headless testing:
|
||||||
|
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "e74f5af9-b886-4a21-abbf-ed535d12e2fb",
|
|
||||||
"revision": 0,
|
|
||||||
"last_node_id": 8,
|
|
||||||
"last_link_id": 0,
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"type": "LoadAudio",
|
|
||||||
"pos": [
|
|
||||||
41.52964782714844,
|
|
||||||
16.930862426757812
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
444,
|
|
||||||
125
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 0,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "AUDIO",
|
|
||||||
"type": "AUDIO",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "LoadAudio"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"type": "LoadVideo",
|
|
||||||
"pos": [
|
|
||||||
502.28570556640625,
|
|
||||||
16.857147216796875
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
444,
|
|
||||||
525
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 1,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "VIDEO",
|
|
||||||
"type": "VIDEO",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "LoadVideo"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
null,
|
|
||||||
"image"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"type": "DevToolsLoadAnimatedImageTest",
|
|
||||||
"pos": [
|
|
||||||
41.71427917480469,
|
|
||||||
188.0000457763672
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
444,
|
|
||||||
553
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 2,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "IMAGE",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MASK",
|
|
||||||
"type": "MASK",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
null,
|
|
||||||
"image"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"type": "LoadImage",
|
|
||||||
"pos": [
|
|
||||||
958.285888671875,
|
|
||||||
16.57145118713379
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
444,
|
|
||||||
553
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 3,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "IMAGE",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MASK",
|
|
||||||
"type": "MASK",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "LoadImage"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
null,
|
|
||||||
"image"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"type": "LoadImageMask",
|
|
||||||
"pos": [
|
|
||||||
503.4285888671875,
|
|
||||||
588
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
444,
|
|
||||||
563
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 4,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "MASK",
|
|
||||||
"type": "MASK",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "LoadImageMask"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
null,
|
|
||||||
"alpha",
|
|
||||||
"image"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 7,
|
|
||||||
"type": "LoadImageOutput",
|
|
||||||
"pos": [
|
|
||||||
965.1429443359375,
|
|
||||||
612
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
444,
|
|
||||||
553
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 5,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "IMAGE",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MASK",
|
|
||||||
"type": "MASK",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "LoadImageOutput"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
"refresh",
|
|
||||||
"image"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": [],
|
|
||||||
"groups": [],
|
|
||||||
"config": {},
|
|
||||||
"extra": {
|
|
||||||
"ds": {
|
|
||||||
"scale": 1,
|
|
||||||
"offset": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"frontendVersion": "1.28.3"
|
|
||||||
},
|
|
||||||
"version": 0.4
|
|
||||||
}
|
|
||||||
@@ -34,17 +34,23 @@ const getContentType = (filename: string, fileType: OutputFileType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setQueueIndex = (task: TaskItem) => {
|
const setQueueIndex = (task: TaskItem) => {
|
||||||
task.prompt[0] = TaskHistory.queueIndex++
|
task.prompt.priority = TaskHistory.queueIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPromptId = (task: TaskItem) => {
|
const setPromptId = (task: TaskItem) => {
|
||||||
task.prompt[1] = uuidv4()
|
if (!task.prompt.prompt_id || task.prompt.prompt_id === 'prompt-id') {
|
||||||
|
task.prompt.prompt_id = uuidv4()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TaskHistory {
|
export default class TaskHistory {
|
||||||
static queueIndex = 0
|
static queueIndex = 0
|
||||||
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
||||||
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
|
prompt: {
|
||||||
|
priority: 0,
|
||||||
|
prompt_id: 'prompt-id',
|
||||||
|
extra_data: { client_id: uuidv4() }
|
||||||
|
},
|
||||||
outputs: {},
|
outputs: {},
|
||||||
status: {
|
status: {
|
||||||
status_str: 'success',
|
status_str: 'success',
|
||||||
@@ -66,10 +72,37 @@ export default class TaskHistory {
|
|||||||
)
|
)
|
||||||
|
|
||||||
private async handleGetHistory(route: Route) {
|
private async handleGetHistory(route: Route) {
|
||||||
|
const url = route.request().url()
|
||||||
|
|
||||||
|
// Handle history_v2/:prompt_id endpoint
|
||||||
|
const promptIdMatch = url.match(/history_v2\/([^?]+)/)
|
||||||
|
if (promptIdMatch) {
|
||||||
|
const promptId = promptIdMatch[1]
|
||||||
|
const task = this.tasks.find((t) => t.prompt.prompt_id === promptId)
|
||||||
|
const response: Record<string, any> = {}
|
||||||
|
if (task) {
|
||||||
|
response[promptId] = task
|
||||||
|
}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle history_v2 list endpoint
|
||||||
|
// Convert HistoryTaskItem to RawHistoryItem format expected by API
|
||||||
|
const rawHistoryItems = this.tasks.map((task) => ({
|
||||||
|
prompt_id: task.prompt.prompt_id,
|
||||||
|
prompt: task.prompt,
|
||||||
|
status: task.status,
|
||||||
|
outputs: task.outputs,
|
||||||
|
...(task.meta && { meta: task.meta })
|
||||||
|
}))
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: JSON.stringify(this.tasks)
|
body: JSON.stringify({ history: rawHistoryItems })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +126,7 @@ export default class TaskHistory {
|
|||||||
|
|
||||||
async setupRoutes() {
|
async setupRoutes() {
|
||||||
return this.comfyPage.page.route(
|
return this.comfyPage.page.route(
|
||||||
/.*\/api\/(view|history)(\?.*)?$/,
|
/.*\/api\/(view|history_v2)(\/[^?]*)?(\?.*)?$/,
|
||||||
async (route) => {
|
async (route) => {
|
||||||
const request = route.request()
|
const request = route.request()
|
||||||
const method = request.method()
|
const method = request.method()
|
||||||
|
|||||||
131
browser_tests/tests/historyApi.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
comfyExpect as expect,
|
||||||
|
comfyPageFixture as test
|
||||||
|
} from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('History API v2', () => {
|
||||||
|
const TEST_PROMPT_ID = 'test-prompt-id'
|
||||||
|
const TEST_CLIENT_ID = 'test-client'
|
||||||
|
|
||||||
|
test('Can fetch history with new v2 format', async ({ comfyPage }) => {
|
||||||
|
// Set up mocked history with tasks
|
||||||
|
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||||
|
|
||||||
|
// Verify history_v2 API response format
|
||||||
|
const result = await comfyPage.page.evaluate(async () => {
|
||||||
|
try {
|
||||||
|
const response = await window['app'].api.getHistory()
|
||||||
|
return { success: true, data: response }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch history:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.data).toHaveProperty('History')
|
||||||
|
expect(Array.isArray(result.data.History)).toBe(true)
|
||||||
|
expect(result.data.History.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const historyItem = result.data.History[0]
|
||||||
|
|
||||||
|
// Verify the new prompt structure (object instead of array)
|
||||||
|
expect(historyItem.prompt).toHaveProperty('priority')
|
||||||
|
expect(historyItem.prompt).toHaveProperty('prompt_id')
|
||||||
|
expect(historyItem.prompt).toHaveProperty('extra_data')
|
||||||
|
expect(typeof historyItem.prompt.priority).toBe('number')
|
||||||
|
expect(typeof historyItem.prompt.prompt_id).toBe('string')
|
||||||
|
expect(historyItem.prompt.extra_data).toHaveProperty('client_id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can load workflow from history using history_v2 endpoint', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Simple mock workflow for testing
|
||||||
|
const mockWorkflow = {
|
||||||
|
version: 0.4,
|
||||||
|
nodes: [{ id: 1, type: 'TestNode', pos: [100, 100], size: [200, 100] }],
|
||||||
|
links: [],
|
||||||
|
groups: [],
|
||||||
|
config: {},
|
||||||
|
extra: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up history with workflow data
|
||||||
|
await comfyPage
|
||||||
|
.setupHistory()
|
||||||
|
.withTask(['example.webp'], 'images', {
|
||||||
|
prompt: {
|
||||||
|
priority: 0,
|
||||||
|
prompt_id: TEST_PROMPT_ID,
|
||||||
|
extra_data: {
|
||||||
|
client_id: TEST_CLIENT_ID,
|
||||||
|
extra_pnginfo: { workflow: mockWorkflow }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setupRoutes()
|
||||||
|
|
||||||
|
// Load initial workflow to clear canvas
|
||||||
|
await comfyPage.loadWorkflow('simple_slider')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
// Load workflow from history
|
||||||
|
const loadResult = await comfyPage.page.evaluate(async (promptId) => {
|
||||||
|
try {
|
||||||
|
const workflow =
|
||||||
|
await window['app'].api.getWorkflowFromHistory(promptId)
|
||||||
|
if (workflow) {
|
||||||
|
await window['app'].loadGraphData(workflow)
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
return { success: false, error: 'No workflow found' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load workflow from history:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}, TEST_PROMPT_ID)
|
||||||
|
|
||||||
|
expect(loadResult.success).toBe(true)
|
||||||
|
|
||||||
|
// Verify workflow loaded correctly
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
const nodeInfo = await comfyPage.page.evaluate(() => {
|
||||||
|
try {
|
||||||
|
const graph = window['app'].graph
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
nodeCount: graph.nodes?.length || 0,
|
||||||
|
firstNodeType: graph.nodes?.[0]?.type || null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(nodeInfo.success).toBe(true)
|
||||||
|
expect(nodeInfo.nodeCount).toBe(1)
|
||||||
|
expect(nodeInfo.firstNodeType).toBe('TestNode')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Handles missing workflow data gracefully', async ({ comfyPage }) => {
|
||||||
|
// Set up empty history routes
|
||||||
|
await comfyPage.setupHistory().setupRoutes()
|
||||||
|
|
||||||
|
// Test loading from history with invalid prompt_id
|
||||||
|
const result = await comfyPage.page.evaluate(async () => {
|
||||||
|
try {
|
||||||
|
const workflow =
|
||||||
|
await window['app'].api.getWorkflowFromHistory('invalid-id')
|
||||||
|
return { success: true, workflow }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Expected error for invalid prompt_id:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Should handle gracefully without throwing
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.workflow).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -187,6 +187,7 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow5.json'
|
'workflow5.json'
|
||||||
])
|
])
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,12 +1,10 @@
|
|||||||
import type { Locator, Page } from '@playwright/test'
|
import type { Locator } from '@playwright/test'
|
||||||
|
|
||||||
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
|
||||||
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||||
import {
|
import {
|
||||||
comfyExpect as expect,
|
comfyExpect as expect,
|
||||||
comfyPageFixture as test
|
comfyPageFixture as test
|
||||||
} from '../../../../fixtures/ComfyPage'
|
} from '../../../../fixtures/ComfyPage'
|
||||||
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
|
|
||||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||||
|
|
||||||
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
||||||
@@ -18,87 +16,6 @@ async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInputLinkDetails(
|
|
||||||
page: Page,
|
|
||||||
nodeId: NodeId,
|
|
||||||
slotIndex: number
|
|
||||||
) {
|
|
||||||
return await page.evaluate(
|
|
||||||
([targetNodeId, targetSlot]) => {
|
|
||||||
const app = window['app']
|
|
||||||
const graph = app?.canvas?.graph ?? app?.graph
|
|
||||||
if (!graph) return null
|
|
||||||
|
|
||||||
const node = graph.getNodeById(targetNodeId)
|
|
||||||
if (!node) return null
|
|
||||||
|
|
||||||
const input = node.inputs?.[targetSlot]
|
|
||||||
if (!input) return null
|
|
||||||
|
|
||||||
const linkId = input.link
|
|
||||||
if (linkId == null) return null
|
|
||||||
|
|
||||||
const link = graph.getLink?.(linkId)
|
|
||||||
if (!link) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: link.id,
|
|
||||||
originId: link.origin_id,
|
|
||||||
originSlot:
|
|
||||||
typeof link.origin_slot === 'string'
|
|
||||||
? Number.parseInt(link.origin_slot, 10)
|
|
||||||
: link.origin_slot,
|
|
||||||
targetId: link.target_id,
|
|
||||||
targetSlot:
|
|
||||||
typeof link.target_slot === 'string'
|
|
||||||
? Number.parseInt(link.target_slot, 10)
|
|
||||||
: link.target_slot,
|
|
||||||
parentId: link.parentId ?? null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[nodeId, slotIndex] as const
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test helpers to reduce repetition across cases
|
|
||||||
function slotLocator(
|
|
||||||
page: Page,
|
|
||||||
nodeId: NodeId,
|
|
||||||
slotIndex: number,
|
|
||||||
isInput: boolean
|
|
||||||
) {
|
|
||||||
const key = getSlotKey(String(nodeId), slotIndex, isInput)
|
|
||||||
return page.locator(`[data-slot-key="${key}"]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectVisibleAll(...locators: Locator[]) {
|
|
||||||
await Promise.all(locators.map((l) => expect(l).toBeVisible()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSlotCenter(
|
|
||||||
page: Page,
|
|
||||||
nodeId: NodeId,
|
|
||||||
slotIndex: number,
|
|
||||||
isInput: boolean
|
|
||||||
) {
|
|
||||||
const locator = slotLocator(page, nodeId, slotIndex, isInput)
|
|
||||||
await expect(locator).toBeVisible()
|
|
||||||
return await getCenter(locator)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectSlots(
|
|
||||||
page: Page,
|
|
||||||
from: { nodeId: NodeId; index: number },
|
|
||||||
to: { nodeId: NodeId; index: number },
|
|
||||||
nextFrame: () => Promise<void>
|
|
||||||
) {
|
|
||||||
const fromLoc = slotLocator(page, from.nodeId, from.index, false)
|
|
||||||
const toLoc = slotLocator(page, to.nodeId, to.index, true)
|
|
||||||
await expectVisibleAll(fromLoc, toLoc)
|
|
||||||
await fromLoc.dragTo(toLoc)
|
|
||||||
await nextFrame()
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Vue Node Link Interaction', () => {
|
test.describe('Vue Node Link Interaction', () => {
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||||
@@ -113,13 +30,21 @@ test.describe('Vue Node Link Interaction', () => {
|
|||||||
comfyPage,
|
comfyPage,
|
||||||
comfyMouse
|
comfyMouse
|
||||||
}) => {
|
}) => {
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
expect(samplerNode).toBeTruthy()
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
const samplerNode = samplerNodes[0]
|
||||||
await expect(slot).toBeVisible()
|
const outputSlot = await samplerNode.getOutput(0)
|
||||||
|
await outputSlot.removeLinks()
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
const start = await getCenter(slot)
|
const slotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||||
|
const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`)
|
||||||
|
await expect(slotLocator).toBeVisible()
|
||||||
|
|
||||||
|
const start = await getCenter(slotLocator)
|
||||||
|
const canvasBox = await comfyPage.canvas.boundingBox()
|
||||||
|
if (!canvasBox) throw new Error('Canvas bounding box not available')
|
||||||
|
|
||||||
// Arbitrary value
|
// Arbitrary value
|
||||||
const dragTarget = {
|
const dragTarget = {
|
||||||
@@ -143,24 +68,58 @@ test.describe('Vue Node Link Interaction', () => {
|
|||||||
test('should create a link when dropping on a compatible slot', async ({
|
test('should create a link when dropping on a compatible slot', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||||
expect(samplerNode && vaeNode).toBeTruthy()
|
const samplerNode = samplerNodes[0]
|
||||||
|
|
||||||
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
||||||
|
expect(vaeNodes.length).toBeGreaterThan(0)
|
||||||
|
const vaeNode = vaeNodes[0]
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
const samplerOutput = await samplerNode.getOutput(0)
|
||||||
const vaeInput = await vaeNode.getInput(0)
|
const vaeInput = await vaeNode.getInput(0)
|
||||||
|
|
||||||
await connectSlots(
|
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||||
comfyPage.page,
|
const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true)
|
||||||
{ nodeId: samplerNode.id, index: 0 },
|
|
||||||
{ nodeId: vaeNode.id, index: 0 },
|
const outputSlot = comfyPage.page.locator(
|
||||||
() => comfyPage.nextFrame()
|
`[data-slot-key="${outputSlotKey}"]`
|
||||||
)
|
)
|
||||||
|
const inputSlot = comfyPage.page.locator(
|
||||||
|
`[data-slot-key="${inputSlotKey}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(outputSlot).toBeVisible()
|
||||||
|
await expect(inputSlot).toBeVisible()
|
||||||
|
|
||||||
|
await outputSlot.dragTo(inputSlot)
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
expect(await samplerOutput.getLinkCount()).toBe(1)
|
||||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
expect(await vaeInput.getLinkCount()).toBe(1)
|
||||||
|
|
||||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
const linkDetails = await comfyPage.page.evaluate((sourceId) => {
|
||||||
|
const app = window['app']
|
||||||
|
const graph = app?.canvas?.graph
|
||||||
|
if (!graph) return null
|
||||||
|
|
||||||
|
const source = graph.getNodeById(sourceId)
|
||||||
|
if (!source) return null
|
||||||
|
|
||||||
|
const linkId = source.outputs[0]?.links?.[0]
|
||||||
|
if (linkId == null) return null
|
||||||
|
|
||||||
|
const link = graph.links[linkId]
|
||||||
|
if (!link) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
originId: link.origin_id,
|
||||||
|
originSlot: link.origin_slot,
|
||||||
|
targetId: link.target_id,
|
||||||
|
targetSlot: link.target_slot
|
||||||
|
}
|
||||||
|
}, samplerNode.id)
|
||||||
|
|
||||||
expect(linkDetails).not.toBeNull()
|
expect(linkDetails).not.toBeNull()
|
||||||
expect(linkDetails).toMatchObject({
|
expect(linkDetails).toMatchObject({
|
||||||
originId: samplerNode.id,
|
originId: samplerNode.id,
|
||||||
@@ -173,16 +132,29 @@ test.describe('Vue Node Link Interaction', () => {
|
|||||||
test('should not create a link when slot types are incompatible', async ({
|
test('should not create a link when slot types are incompatible', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||||
expect(samplerNode && clipNode).toBeTruthy()
|
const samplerNode = samplerNodes[0]
|
||||||
|
|
||||||
|
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||||
|
expect(clipNodes.length).toBeGreaterThan(0)
|
||||||
|
const clipNode = clipNodes[0]
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
const samplerOutput = await samplerNode.getOutput(0)
|
||||||
const clipInput = await clipNode.getInput(0)
|
const clipInput = await clipNode.getInput(0)
|
||||||
|
|
||||||
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||||
const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true)
|
const inputSlotKey = getSlotKey(String(clipNode.id), 0, true)
|
||||||
await expectVisibleAll(outputSlot, inputSlot)
|
|
||||||
|
const outputSlot = comfyPage.page.locator(
|
||||||
|
`[data-slot-key="${outputSlotKey}"]`
|
||||||
|
)
|
||||||
|
const inputSlot = comfyPage.page.locator(
|
||||||
|
`[data-slot-key="${inputSlotKey}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(outputSlot).toBeVisible()
|
||||||
|
await expect(inputSlot).toBeVisible()
|
||||||
|
|
||||||
await outputSlot.dragTo(inputSlot)
|
await outputSlot.dragTo(inputSlot)
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
@@ -190,507 +162,60 @@ test.describe('Vue Node Link Interaction', () => {
|
|||||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||||
expect(await clipInput.getLinkCount()).toBe(0)
|
expect(await clipInput.getLinkCount()).toBe(0)
|
||||||
|
|
||||||
const graphLinkDetails = await getInputLinkDetails(
|
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
|
||||||
comfyPage.page,
|
const app = window['app']
|
||||||
clipNode.id,
|
const graph = app?.canvas?.graph
|
||||||
0
|
if (!graph) return 0
|
||||||
)
|
|
||||||
expect(graphLinkDetails).toBeNull()
|
const source = graph.getNodeById(sourceId)
|
||||||
|
if (!source) return 0
|
||||||
|
|
||||||
|
return source.outputs[0]?.links?.length ?? 0
|
||||||
|
}, samplerNode.id)
|
||||||
|
|
||||||
|
expect(graphLinkCount).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not create a link when dropping onto a slot on the same node', async ({
|
test('should not create a link when dropping onto a slot on the same node', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
expect(samplerNode).toBeTruthy()
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||||
|
const samplerNode = samplerNodes[0]
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
const samplerOutput = await samplerNode.getOutput(0)
|
||||||
const samplerInput = await samplerNode.getInput(3)
|
const samplerInput = await samplerNode.getInput(3)
|
||||||
|
|
||||||
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||||
const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true)
|
const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true)
|
||||||
await expectVisibleAll(outputSlot, inputSlot)
|
|
||||||
|
const outputSlot = comfyPage.page.locator(
|
||||||
|
`[data-slot-key="${outputSlotKey}"]`
|
||||||
|
)
|
||||||
|
const inputSlot = comfyPage.page.locator(
|
||||||
|
`[data-slot-key="${inputSlotKey}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(outputSlot).toBeVisible()
|
||||||
|
await expect(inputSlot).toBeVisible()
|
||||||
|
|
||||||
await outputSlot.dragTo(inputSlot)
|
await outputSlot.dragTo(inputSlot)
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||||
expect(await samplerInput.getLinkCount()).toBe(0)
|
expect(await samplerInput.getLinkCount()).toBe(0)
|
||||||
})
|
|
||||||
|
|
||||||
test('should reuse the existing origin when dragging an input link', async ({
|
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
|
||||||
comfyPage,
|
const app = window['app']
|
||||||
comfyMouse
|
const graph = app?.canvas?.graph
|
||||||
}) => {
|
if (!graph) return 0
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
|
||||||
expect(samplerNode && vaeNode).toBeTruthy()
|
|
||||||
const samplerOutputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
const vaeInputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
vaeNode.id,
|
|
||||||
0,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
await comfyMouse.move(samplerOutputCenter)
|
const source = graph.getNodeById(sourceId)
|
||||||
await comfyMouse.drag(vaeInputCenter)
|
if (!source) return 0
|
||||||
await comfyMouse.drop()
|
|
||||||
|
|
||||||
const dragTarget = {
|
return source.outputs[0]?.links?.length ?? 0
|
||||||
x: vaeInputCenter.x + 160,
|
}, samplerNode.id)
|
||||||
y: vaeInputCenter.y - 100
|
|
||||||
}
|
|
||||||
|
|
||||||
await comfyMouse.move(vaeInputCenter)
|
expect(graphLinkCount).toBe(0)
|
||||||
await comfyMouse.drag(dragTarget)
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'vue-node-input-drag-reuses-origin.png'
|
|
||||||
)
|
|
||||||
await comfyMouse.drop()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('ctrl+alt drag from an input starts a fresh link', async ({
|
|
||||||
comfyPage,
|
|
||||||
comfyMouse
|
|
||||||
}) => {
|
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
|
||||||
expect(samplerNode && vaeNode).toBeTruthy()
|
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
|
||||||
const vaeInput = await vaeNode.getInput(0)
|
|
||||||
|
|
||||||
const samplerOutputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
const vaeInputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
vaeNode.id,
|
|
||||||
0,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
await comfyMouse.move(samplerOutputCenter)
|
|
||||||
await comfyMouse.drag(vaeInputCenter)
|
|
||||||
await comfyMouse.drop()
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const dragTarget = {
|
|
||||||
x: vaeInputCenter.x + 140,
|
|
||||||
y: vaeInputCenter.y - 110
|
|
||||||
}
|
|
||||||
|
|
||||||
await comfyMouse.move(vaeInputCenter)
|
|
||||||
await comfyPage.page.keyboard.down('Control')
|
|
||||||
await comfyPage.page.keyboard.down('Alt')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await comfyMouse.drag(dragTarget)
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'vue-node-input-drag-ctrl-alt.png'
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await comfyMouse.drop().catch(() => {})
|
|
||||||
await comfyPage.page.keyboard.up('Alt').catch(() => {})
|
|
||||||
await comfyPage.page.keyboard.up('Control').catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
// Tcehnically intended to disconnect existing as well
|
|
||||||
expect(await vaeInput.getLinkCount()).toBe(0)
|
|
||||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dropping an input link back on its slot restores the original connection', async ({
|
|
||||||
comfyPage,
|
|
||||||
comfyMouse
|
|
||||||
}) => {
|
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
|
||||||
expect(samplerNode && vaeNode).toBeTruthy()
|
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
|
||||||
const vaeInput = await vaeNode.getInput(0)
|
|
||||||
|
|
||||||
const samplerOutputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
const vaeInputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
vaeNode.id,
|
|
||||||
0,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
await comfyMouse.move(samplerOutputCenter)
|
|
||||||
try {
|
|
||||||
await comfyMouse.drag(vaeInputCenter)
|
|
||||||
} finally {
|
|
||||||
await comfyMouse.drop()
|
|
||||||
}
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const originalLink = await getInputLinkDetails(
|
|
||||||
comfyPage.page,
|
|
||||||
vaeNode.id,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
expect(originalLink).not.toBeNull()
|
|
||||||
|
|
||||||
const dragTarget = {
|
|
||||||
x: vaeInputCenter.x + 150,
|
|
||||||
y: vaeInputCenter.y - 100
|
|
||||||
}
|
|
||||||
|
|
||||||
// To prevent needing a screenshot expectation for whether the link's off
|
|
||||||
const vaeInputLocator = slotLocator(comfyPage.page, vaeNode.id, 0, true)
|
|
||||||
const inputBox = await vaeInputLocator.boundingBox()
|
|
||||||
if (!inputBox) throw new Error('Input slot bounding box not available')
|
|
||||||
const isOutsideX =
|
|
||||||
dragTarget.x < inputBox.x || dragTarget.x > inputBox.x + inputBox.width
|
|
||||||
const isOutsideY =
|
|
||||||
dragTarget.y < inputBox.y || dragTarget.y > inputBox.y + inputBox.height
|
|
||||||
expect(isOutsideX || isOutsideY).toBe(true)
|
|
||||||
|
|
||||||
await comfyMouse.move(vaeInputCenter)
|
|
||||||
await comfyMouse.drag(dragTarget)
|
|
||||||
await comfyMouse.move(vaeInputCenter)
|
|
||||||
await comfyMouse.drop()
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const restoredLink = await getInputLinkDetails(
|
|
||||||
comfyPage.page,
|
|
||||||
vaeNode.id,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(restoredLink).not.toBeNull()
|
|
||||||
if (!restoredLink || !originalLink) {
|
|
||||||
throw new Error('Expected both original and restored links to exist')
|
|
||||||
}
|
|
||||||
expect(restoredLink).toMatchObject({
|
|
||||||
originId: originalLink.originId,
|
|
||||||
originSlot: originalLink.originSlot,
|
|
||||||
targetId: originalLink.targetId,
|
|
||||||
targetSlot: originalLink.targetSlot,
|
|
||||||
parentId: originalLink.parentId
|
|
||||||
})
|
|
||||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
|
||||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rerouted input drag preview remains anchored to reroute', async ({
|
|
||||||
comfyPage,
|
|
||||||
comfyMouse
|
|
||||||
}) => {
|
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
|
||||||
const vaeInput = await vaeNode.getInput(0)
|
|
||||||
|
|
||||||
await connectSlots(
|
|
||||||
comfyPage.page,
|
|
||||||
{ nodeId: samplerNode.id, index: 0 },
|
|
||||||
{ nodeId: vaeNode.id, index: 0 },
|
|
||||||
() => comfyPage.nextFrame()
|
|
||||||
)
|
|
||||||
|
|
||||||
const outputPosition = await samplerOutput.getPosition()
|
|
||||||
const inputPosition = await vaeInput.getPosition()
|
|
||||||
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
|
|
||||||
|
|
||||||
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
|
|
||||||
// This avoids relying on an exact path hit-test position.
|
|
||||||
await comfyPage.page.evaluate(
|
|
||||||
([targetNodeId, targetSlot, clientPoint]) => {
|
|
||||||
const app = (window as any)['app']
|
|
||||||
const graph = app?.canvas?.graph ?? app?.graph
|
|
||||||
if (!graph) throw new Error('Graph not available')
|
|
||||||
const node = graph.getNodeById(targetNodeId)
|
|
||||||
if (!node) throw new Error('Target node not found')
|
|
||||||
const input = node.inputs?.[targetSlot]
|
|
||||||
if (!input) throw new Error('Target input slot not found')
|
|
||||||
|
|
||||||
const linkId = input.link
|
|
||||||
if (linkId == null) throw new Error('Expected existing link on input')
|
|
||||||
const link = graph.getLink(linkId)
|
|
||||||
if (!link) throw new Error('Link not found')
|
|
||||||
|
|
||||||
// Convert the client/canvas pixel coordinates to graph space
|
|
||||||
const pos = app.canvas.ds.convertCanvasToOffset([
|
|
||||||
clientPoint.x,
|
|
||||||
clientPoint.y
|
|
||||||
])
|
|
||||||
graph.createReroute(pos, link)
|
|
||||||
},
|
|
||||||
[vaeNode.id, 0, reroutePoint] as const
|
|
||||||
)
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const vaeInputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
vaeNode.id,
|
|
||||||
0,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
const dragTarget = {
|
|
||||||
x: vaeInputCenter.x + 160,
|
|
||||||
y: vaeInputCenter.y - 120
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropped = false
|
|
||||||
try {
|
|
||||||
await comfyMouse.move(vaeInputCenter)
|
|
||||||
await comfyMouse.drag(dragTarget)
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'vue-node-reroute-input-drag.png'
|
|
||||||
)
|
|
||||||
await comfyMouse.move(vaeInputCenter)
|
|
||||||
await comfyMouse.drop()
|
|
||||||
dropped = true
|
|
||||||
} finally {
|
|
||||||
if (!dropped) {
|
|
||||||
await comfyMouse.drop().catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
|
||||||
expect(linkDetails).not.toBeNull()
|
|
||||||
expect(linkDetails?.originId).toBe(samplerNode.id)
|
|
||||||
expect(linkDetails?.parentId).not.toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rerouted output shift-drag preview remains anchored to reroute', async ({
|
|
||||||
comfyPage,
|
|
||||||
comfyMouse
|
|
||||||
}) => {
|
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
|
||||||
expect(samplerNode && vaeNode).toBeTruthy()
|
|
||||||
|
|
||||||
const samplerOutput = await samplerNode.getOutput(0)
|
|
||||||
const vaeInput = await vaeNode.getInput(0)
|
|
||||||
|
|
||||||
await connectSlots(
|
|
||||||
comfyPage.page,
|
|
||||||
{ nodeId: samplerNode.id, index: 0 },
|
|
||||||
{ nodeId: vaeNode.id, index: 0 },
|
|
||||||
() => comfyPage.nextFrame()
|
|
||||||
)
|
|
||||||
|
|
||||||
const outputPosition = await samplerOutput.getPosition()
|
|
||||||
const inputPosition = await vaeInput.getPosition()
|
|
||||||
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
|
|
||||||
|
|
||||||
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
|
|
||||||
// This avoids relying on an exact path hit-test position.
|
|
||||||
await comfyPage.page.evaluate(
|
|
||||||
([targetNodeId, targetSlot, clientPoint]) => {
|
|
||||||
const app = (window as any)['app']
|
|
||||||
const graph = app?.canvas?.graph ?? app?.graph
|
|
||||||
if (!graph) throw new Error('Graph not available')
|
|
||||||
const node = graph.getNodeById(targetNodeId)
|
|
||||||
if (!node) throw new Error('Target node not found')
|
|
||||||
const input = node.inputs?.[targetSlot]
|
|
||||||
if (!input) throw new Error('Target input slot not found')
|
|
||||||
|
|
||||||
const linkId = input.link
|
|
||||||
if (linkId == null) throw new Error('Expected existing link on input')
|
|
||||||
const link = graph.getLink(linkId)
|
|
||||||
if (!link) throw new Error('Link not found')
|
|
||||||
|
|
||||||
// Convert the client/canvas pixel coordinates to graph space
|
|
||||||
const pos = app.canvas.ds.convertCanvasToOffset([
|
|
||||||
clientPoint.x,
|
|
||||||
clientPoint.y
|
|
||||||
])
|
|
||||||
graph.createReroute(pos, link)
|
|
||||||
},
|
|
||||||
[vaeNode.id, 0, reroutePoint] as const
|
|
||||||
)
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const outputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
const dragTarget = {
|
|
||||||
x: outputCenter.x + 150,
|
|
||||||
y: outputCenter.y - 140
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropPending = false
|
|
||||||
let shiftHeld = false
|
|
||||||
try {
|
|
||||||
await comfyMouse.move(outputCenter)
|
|
||||||
await comfyPage.page.keyboard.down('Shift')
|
|
||||||
shiftHeld = true
|
|
||||||
dropPending = true
|
|
||||||
await comfyMouse.drag(dragTarget)
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'vue-node-reroute-output-shift-drag.png'
|
|
||||||
)
|
|
||||||
await comfyMouse.move(outputCenter)
|
|
||||||
await comfyMouse.drop()
|
|
||||||
dropPending = false
|
|
||||||
} finally {
|
|
||||||
if (dropPending) await comfyMouse.drop().catch(() => {})
|
|
||||||
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
|
||||||
expect(linkDetails).not.toBeNull()
|
|
||||||
expect(linkDetails?.originId).toBe(samplerNode.id)
|
|
||||||
expect(linkDetails?.parentId).not.toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dragging input to input drags existing link', async ({
|
|
||||||
comfyPage,
|
|
||||||
comfyMouse
|
|
||||||
}) => {
|
|
||||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
expect(clipNode && samplerNode).toBeTruthy()
|
|
||||||
|
|
||||||
// Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1)
|
|
||||||
await connectSlots(
|
|
||||||
comfyPage.page,
|
|
||||||
{ nodeId: clipNode.id, index: 0 },
|
|
||||||
{ nodeId: samplerNode.id, index: 1 },
|
|
||||||
() => comfyPage.nextFrame()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify initial link exists between CLIP -> KSampler input[1]
|
|
||||||
const initialLink = await getInputLinkDetails(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
expect(initialLink).not.toBeNull()
|
|
||||||
expect(initialLink).toMatchObject({
|
|
||||||
originId: clipNode.id,
|
|
||||||
targetId: samplerNode.id,
|
|
||||||
targetSlot: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 2: Drag from KSampler's second input to its third input (index 2)
|
|
||||||
const input2Center = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
1,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
const input3Center = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
2,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
await comfyMouse.move(input2Center)
|
|
||||||
await comfyMouse.drag(input3Center)
|
|
||||||
await comfyMouse.drop()
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
// Expect old link removed from input[1]
|
|
||||||
const afterSecondInput = await getInputLinkDetails(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
expect(afterSecondInput).toBeNull()
|
|
||||||
|
|
||||||
// Expect new link exists at input[2] from CLIP
|
|
||||||
const afterThirdInput = await getInputLinkDetails(
|
|
||||||
comfyPage.page,
|
|
||||||
samplerNode.id,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
expect(afterThirdInput).not.toBeNull()
|
|
||||||
expect(afterThirdInput).toMatchObject({
|
|
||||||
originId: clipNode.id,
|
|
||||||
targetId: samplerNode.id,
|
|
||||||
targetSlot: 2
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift-dragging an output with multiple links should drag all links', async ({
|
|
||||||
comfyPage,
|
|
||||||
comfyMouse
|
|
||||||
}) => {
|
|
||||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
|
||||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
|
||||||
expect(clipNode && samplerNode).toBeTruthy()
|
|
||||||
|
|
||||||
const clipOutput = await clipNode.getOutput(0)
|
|
||||||
|
|
||||||
// Connect output[0] -> inputs[1] and [2]
|
|
||||||
await connectSlots(
|
|
||||||
comfyPage.page,
|
|
||||||
{ nodeId: clipNode.id, index: 0 },
|
|
||||||
{ nodeId: samplerNode.id, index: 1 },
|
|
||||||
() => comfyPage.nextFrame()
|
|
||||||
)
|
|
||||||
await connectSlots(
|
|
||||||
comfyPage.page,
|
|
||||||
{ nodeId: clipNode.id, index: 0 },
|
|
||||||
{ nodeId: samplerNode.id, index: 2 },
|
|
||||||
() => comfyPage.nextFrame()
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(await clipOutput.getLinkCount()).toBe(2)
|
|
||||||
|
|
||||||
const outputCenter = await getSlotCenter(
|
|
||||||
comfyPage.page,
|
|
||||||
clipNode.id,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
const dragTarget = {
|
|
||||||
x: outputCenter.x + 40,
|
|
||||||
y: outputCenter.y - 140
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropPending = false
|
|
||||||
let shiftHeld = false
|
|
||||||
try {
|
|
||||||
await comfyMouse.move(outputCenter)
|
|
||||||
await comfyPage.page.keyboard.down('Shift')
|
|
||||||
shiftHeld = true
|
|
||||||
await comfyMouse.drag(dragTarget)
|
|
||||||
dropPending = true
|
|
||||||
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'vue-node-shift-output-multi-link.png'
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
if (dropPending) await comfyMouse.drop().catch(() => {})
|
|
||||||
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
@@ -1,21 +0,0 @@
|
|||||||
import {
|
|
||||||
comfyExpect as expect,
|
|
||||||
comfyPageFixture as test
|
|
||||||
} from '../../../../fixtures/ComfyPage'
|
|
||||||
|
|
||||||
test.describe('Vue Upload Widgets', () => {
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
|
||||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
|
||||||
await comfyPage.vueNodes.waitForNodes()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
|
|
||||||
await comfyPage.setup()
|
|
||||||
await comfyPage.loadWorkflow('widgets/all_load_widgets')
|
|
||||||
await comfyPage.vueNodes.waitForNodes()
|
|
||||||
|
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
||||||
'vue-nodes-upload-widgets.png'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
Before Width: | Height: | Size: 67 KiB |
@@ -18,12 +18,28 @@ export default defineConfig([
|
|||||||
'src/scripts/*',
|
'src/scripts/*',
|
||||||
'src/extensions/core/*',
|
'src/extensions/core/*',
|
||||||
'src/types/vue-shim.d.ts',
|
'src/types/vue-shim.d.ts',
|
||||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
'src/types/comfyRegistryTypes.ts',
|
||||||
'src/types/generatedManagerTypes.ts',
|
'src/types/generatedManagerTypes.ts',
|
||||||
'**/vite.config.*.timestamp*',
|
'**/vite.config.*.timestamp*',
|
||||||
'**/vitest.config.*.timestamp*'
|
'**/vitest.config.*.timestamp*',
|
||||||
|
'lint-staged.config.js',
|
||||||
|
'vitest.litegraph.config.ts'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['./**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||||
|
},
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-floating-promises': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['./**/*.{ts,mts}'],
|
files: ['./**/*.{ts,mts}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
@@ -36,8 +52,8 @@ export default defineConfig([
|
|||||||
projectService: {
|
projectService: {
|
||||||
allowDefaultProject: [
|
allowDefaultProject: [
|
||||||
'vite.config.mts',
|
'vite.config.mts',
|
||||||
'vite.electron.config.mts',
|
'vite.types.config.mts',
|
||||||
'vite.types.config.mts'
|
'vitest.litegraph.config.ts'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
tsConfigRootDir: import.meta.dirname,
|
tsConfigRootDir: import.meta.dirname,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>ComfyUI</title>
|
<title>ComfyUI</title>
|
||||||
|
<!-- All assets should be loaded from the root no matter the initial path -->
|
||||||
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||||
@@ -12,10 +14,10 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<!-- Status bar style (eg. black or transparent) -->
|
<!-- Status bar style (eg. black or transparent) -->
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="litegraph grid">
|
<body class="litegraph grid">
|
||||||
<div id="vue-app"></div>
|
<div id="vue-app"></div>
|
||||||
<script type="module" src="src/main.ts"></script>
|
<script type="module" src="src/main.ts"></script>
|
||||||
|
|||||||
@@ -18,13 +18,8 @@ const config: KnipConfig = {
|
|||||||
'packages/design-system': {
|
'packages/design-system': {
|
||||||
entry: ['src/**/*.ts'],
|
entry: ['src/**/*.ts'],
|
||||||
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
|
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
|
||||||
},
|
|
||||||
'packages/registry-types': {
|
|
||||||
entry: ['src/comfyRegistryTypes.ts'],
|
|
||||||
project: ['src/**/*.{js,ts}']
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ignoreBinaries: ['python3'],
|
|
||||||
ignoreDependencies: [
|
ignoreDependencies: [
|
||||||
// Weird importmap things
|
// Weird importmap things
|
||||||
'@iconify/json',
|
'@iconify/json',
|
||||||
@@ -38,7 +33,7 @@ const config: KnipConfig = {
|
|||||||
ignore: [
|
ignore: [
|
||||||
// Auto generated manager types
|
// Auto generated manager types
|
||||||
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
|
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
|
||||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
'src/types/comfyRegistryTypes.ts',
|
||||||
// Used by a custom node (that should move off of this)
|
// Used by a custom node (that should move off of this)
|
||||||
'src/scripts/ui/components/splitButton.ts'
|
'src/scripts/ui/components/splitButton.ts'
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
|
'./**/*.js': 'pnpm exec eslint --cache --fix',
|
||||||
|
|
||||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
'./**/*.{ts,tsx,vue,mts}': [
|
||||||
...formatAndEslint(stagedFiles),
|
'pnpm exec eslint --cache --fix',
|
||||||
'pnpm typecheck'
|
'pnpm exec prettier --cache --write'
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatAndEslint(fileNames) {
|
|
||||||
return [
|
|
||||||
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
|
|
||||||
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
15
lint-staged.config.mjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
|
||||||
|
|
||||||
|
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
||||||
|
...formatAndEslint(stagedFiles),
|
||||||
|
'pnpm typecheck'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAndEslint(fileNames) {
|
||||||
|
return [
|
||||||
|
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
|
||||||
|
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -38,8 +38,7 @@
|
|||||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||||
"storybook": "nx storybook -p 6006",
|
"storybook": "nx storybook -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build"
|
||||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.35.0",
|
"@eslint/js": "^9.35.0",
|
||||||
@@ -107,7 +106,6 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||||
"@comfyorg/design-system": "workspace:*",
|
"@comfyorg/design-system": "workspace:*",
|
||||||
"@comfyorg/registry-types": "workspace:*",
|
|
||||||
"@comfyorg/tailwind-utils": "workspace:*",
|
"@comfyorg/tailwind-utils": "workspace:*",
|
||||||
"@iconify/json": "^2.2.380",
|
"@iconify/json": "^2.2.380",
|
||||||
"@primeuix/forms": "0.0.2",
|
"@primeuix/forms": "0.0.2",
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@comfyorg/registry-types",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Comfy Registry API TypeScript types",
|
|
||||||
"packageManager": "pnpm@10.17.1",
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/comfyRegistryTypes.ts"
|
|
||||||
},
|
|
||||||
"nx": {
|
|
||||||
"tags": [
|
|
||||||
"scope:shared",
|
|
||||||
"type:types"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@comfyorg/shared-frontend-utils",
|
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Shared frontend utils for ComfyUI Frontend",
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"packageManager": "pnpm@10.17.1",
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
"./formatUtil": "./src/formatUtil.ts",
|
|
||||||
"./networkUtil": "./src/networkUtil.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.11.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
23
pnpm-lock.yaml
generated
@@ -20,9 +20,6 @@ importers:
|
|||||||
'@comfyorg/design-system':
|
'@comfyorg/design-system':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/design-system
|
version: link:packages/design-system
|
||||||
'@comfyorg/registry-types':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:packages/registry-types
|
|
||||||
'@comfyorg/tailwind-utils':
|
'@comfyorg/tailwind-utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/tailwind-utils
|
version: link:packages/tailwind-utils
|
||||||
@@ -371,18 +368,6 @@ importers:
|
|||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
|
|
||||||
packages/registry-types: {}
|
|
||||||
|
|
||||||
packages/shared-frontend-utils:
|
|
||||||
dependencies:
|
|
||||||
axios:
|
|
||||||
specifier: ^1.11.0
|
|
||||||
version: 1.11.0
|
|
||||||
devDependencies:
|
|
||||||
typescript:
|
|
||||||
specifier: ^5.9.2
|
|
||||||
version: 5.9.2
|
|
||||||
|
|
||||||
packages/tailwind-utils:
|
packages/tailwind-utils:
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx:
|
clsx:
|
||||||
@@ -6522,8 +6507,8 @@ packages:
|
|||||||
vue-component-type-helpers@3.0.7:
|
vue-component-type-helpers@3.0.7:
|
||||||
resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==}
|
resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==}
|
||||||
|
|
||||||
vue-component-type-helpers@3.1.0:
|
vue-component-type-helpers@3.0.8:
|
||||||
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
|
resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==}
|
||||||
|
|
||||||
vue-demi@0.14.10:
|
vue-demi@0.14.10:
|
||||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||||
@@ -9081,7 +9066,7 @@ snapshots:
|
|||||||
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))
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
vue: 3.5.13(typescript@5.9.2)
|
vue: 3.5.13(typescript@5.9.2)
|
||||||
vue-component-type-helpers: 3.1.0
|
vue-component-type-helpers: 3.0.8
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13879,7 +13864,7 @@ snapshots:
|
|||||||
|
|
||||||
vue-component-type-helpers@3.0.7: {}
|
vue-component-type-helpers@3.0.7: {}
|
||||||
|
|
||||||
vue-component-type-helpers@3.1.0: {}
|
vue-component-type-helpers@3.0.8: {}
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
4
public/assets/images/comfy-cloud-logo.svg
Normal file
|
After Width: | Height: | Size: 18 KiB |
@@ -1,77 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 [--debug]" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
debug=0
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--debug)
|
|
||||||
debug=1
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1" >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# Validate JSON syntax in tracked files using jq
|
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
|
||||||
echo "Error: jq is required but not installed" >&2
|
|
||||||
exit 127
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXCLUDE_PATTERNS=(
|
|
||||||
'**/tsconfig*.json'
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then
|
|
||||||
# shellcheck disable=SC2206
|
|
||||||
EXCLUDE_PATTERNS+=( ${JSON_LINT_EXCLUDES} )
|
|
||||||
fi
|
|
||||||
|
|
||||||
pathspecs=(-- '*.json')
|
|
||||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
|
||||||
if [[ ${pattern:0:1} == ':' ]]; then
|
|
||||||
pathspecs+=("$pattern")
|
|
||||||
else
|
|
||||||
pathspecs+=(":(glob,exclude)${pattern}")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
mapfile -t json_files < <(git ls-files "${pathspecs[@]}")
|
|
||||||
|
|
||||||
if [ "${#json_files[@]}" -eq 0 ]; then
|
|
||||||
echo 'No JSON files found.'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$debug" -eq 1 ]; then
|
|
||||||
echo 'JSON files to validate:'
|
|
||||||
printf ' %s\n' "${json_files[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
failed=0
|
|
||||||
for file in "${json_files[@]}"; do
|
|
||||||
if ! jq -e . "$file" >/dev/null; then
|
|
||||||
echo "Invalid JSON syntax: $file" >&2
|
|
||||||
failed=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$failed" -ne 0 ]; then
|
|
||||||
echo 'JSON validation failed.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo 'All JSON files are valid.'
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Deploy Storybook to Cloudflare Pages and comment on PR
|
|
||||||
# Usage: ./pr-storybook-deploy-and-comment.sh <pr_number> <branch_name> <status> [start_time]
|
|
||||||
|
|
||||||
# Input validation
|
|
||||||
# Validate PR number is numeric
|
|
||||||
case "$1" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
echo "Error: PR_NUMBER must be numeric" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
PR_NUMBER="$1"
|
|
||||||
|
|
||||||
# Sanitize and validate branch name (allow alphanumeric, dots, dashes, underscores, slashes)
|
|
||||||
BRANCH_NAME=$(echo "$2" | sed 's/[^a-zA-Z0-9._/-]//g')
|
|
||||||
if [ -z "$BRANCH_NAME" ]; then
|
|
||||||
echo "Error: Invalid or empty branch name" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate status parameter
|
|
||||||
STATUS="${3:-completed}"
|
|
||||||
case "$STATUS" in
|
|
||||||
starting|completed) ;;
|
|
||||||
*)
|
|
||||||
echo "Error: STATUS must be 'starting' or 'completed'" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
START_TIME="${4:-$(date -u '+%m/%d/%Y, %I:%M:%S %p')}"
|
|
||||||
|
|
||||||
# Required environment variables
|
|
||||||
: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
|
|
||||||
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
|
||||||
|
|
||||||
# Cloudflare variables only required for deployment
|
|
||||||
if [ "$STATUS" = "completed" ]; then
|
|
||||||
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN is required for deployment}"
|
|
||||||
: "${CLOUDFLARE_ACCOUNT_ID:?CLOUDFLARE_ACCOUNT_ID is required for deployment}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
COMMENT_MARKER="<!-- STORYBOOK_BUILD_STATUS -->"
|
|
||||||
|
|
||||||
# Install wrangler if not available (output to stderr for debugging)
|
|
||||||
if ! command -v wrangler > /dev/null 2>&1; then
|
|
||||||
echo "Installing wrangler v4..." >&2
|
|
||||||
npm install -g wrangler@^4.0.0 >&2 || {
|
|
||||||
echo "Failed to install wrangler" >&2
|
|
||||||
echo "failed"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Deploy Storybook report, WARN: ensure inputs are sanitized before calling this function
|
|
||||||
deploy_storybook() {
|
|
||||||
dir="$1"
|
|
||||||
branch="$2"
|
|
||||||
|
|
||||||
[ ! -d "$dir" ] && echo "failed" && return
|
|
||||||
|
|
||||||
project="comfy-storybook"
|
|
||||||
|
|
||||||
echo "Deploying Storybook to project $project on branch $branch..." >&2
|
|
||||||
|
|
||||||
# Try deployment up to 3 times
|
|
||||||
i=1
|
|
||||||
while [ $i -le 3 ]; do
|
|
||||||
echo "Deployment attempt $i of 3..." >&2
|
|
||||||
# Branch is already sanitized, use it directly
|
|
||||||
if output=$(wrangler pages deploy "$dir" \
|
|
||||||
--project-name="$project" \
|
|
||||||
--branch="$branch" 2>&1); then
|
|
||||||
|
|
||||||
# Extract URL from output (improved regex for valid URL characters)
|
|
||||||
url=$(echo "$output" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev\S*' | head -1)
|
|
||||||
result="${url:-https://${branch}.${project}.pages.dev}"
|
|
||||||
echo "Success! URL: $result" >&2
|
|
||||||
echo "$result" # Only this goes to stdout for capture
|
|
||||||
return
|
|
||||||
else
|
|
||||||
echo "Deployment failed on attempt $i: $output" >&2
|
|
||||||
fi
|
|
||||||
[ $i -lt 3 ] && sleep 10
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Post or update GitHub comment
|
|
||||||
post_comment() {
|
|
||||||
body="$1"
|
|
||||||
temp_file=$(mktemp)
|
|
||||||
echo "$body" > "$temp_file"
|
|
||||||
|
|
||||||
if command -v gh > /dev/null 2>&1; then
|
|
||||||
# Find existing comment ID
|
|
||||||
existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
|
|
||||||
--jq ".[] | select(.body | contains(\"$COMMENT_MARKER\")) | .id" | head -1)
|
|
||||||
|
|
||||||
if [ -n "$existing" ]; then
|
|
||||||
# Update specific comment by ID
|
|
||||||
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing" \
|
|
||||||
--field body="$(cat "$temp_file")"
|
|
||||||
else
|
|
||||||
gh pr comment "$PR_NUMBER" --body-file "$temp_file"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "GitHub CLI not available, outputting comment:"
|
|
||||||
cat "$temp_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$temp_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
if [ "$STATUS" = "starting" ]; then
|
|
||||||
# Check if this is a version-bump branch
|
|
||||||
IS_VERSION_BUMP="false"
|
|
||||||
if echo "$BRANCH_NAME" | grep -q "^version-bump-"; then
|
|
||||||
IS_VERSION_BUMP="true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Post starting comment with appropriate message
|
|
||||||
if [ "$IS_VERSION_BUMP" = "true" ]; then
|
|
||||||
comment=$(cat <<EOF
|
|
||||||
$COMMENT_MARKER
|
|
||||||
## 🎨 Storybook Build Status
|
|
||||||
|
|
||||||
<img alt='loading' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px'/> **Build is starting...**
|
|
||||||
|
|
||||||
⏰ Started at: $START_TIME UTC
|
|
||||||
|
|
||||||
### 🚀 Building Storybook
|
|
||||||
- 📦 Installing dependencies...
|
|
||||||
- 🔧 Building Storybook components...
|
|
||||||
- 🎨 Running Chromatic visual tests...
|
|
||||||
|
|
||||||
---
|
|
||||||
⏱️ Please wait while the Storybook build is in progress...
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
else
|
|
||||||
comment=$(cat <<EOF
|
|
||||||
$COMMENT_MARKER
|
|
||||||
## 🎨 Storybook Build Status
|
|
||||||
|
|
||||||
<img alt='loading' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px'/> **Build is starting...**
|
|
||||||
|
|
||||||
⏰ Started at: $START_TIME UTC
|
|
||||||
|
|
||||||
### 🚀 Building Storybook
|
|
||||||
- 📦 Installing dependencies...
|
|
||||||
- 🔧 Building Storybook components...
|
|
||||||
- 🌐 Preparing deployment to Cloudflare Pages...
|
|
||||||
|
|
||||||
---
|
|
||||||
⏱️ Please wait while the Storybook build is in progress...
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
post_comment "$comment"
|
|
||||||
|
|
||||||
elif [ "$STATUS" = "completed" ]; then
|
|
||||||
# Deploy and post completion comment
|
|
||||||
# Convert branch name to Cloudflare-compatible format (lowercase, only alphanumeric and dashes)
|
|
||||||
cloudflare_branch=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | \
|
|
||||||
sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
|
||||||
|
|
||||||
echo "Looking for Storybook build in: $(pwd)/storybook-static"
|
|
||||||
|
|
||||||
# Deploy Storybook if build exists
|
|
||||||
deployment_url="Not deployed"
|
|
||||||
if [ -d "storybook-static" ]; then
|
|
||||||
echo "Found Storybook build, deploying..."
|
|
||||||
url=$(deploy_storybook "storybook-static" "$cloudflare_branch")
|
|
||||||
if [ "$url" != "failed" ] && [ -n "$url" ]; then
|
|
||||||
deployment_url="[View Storybook]($url)"
|
|
||||||
else
|
|
||||||
deployment_url="Deployment failed"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Storybook build not found at storybook-static"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get workflow conclusion from environment or default to success
|
|
||||||
WORKFLOW_CONCLUSION="${WORKFLOW_CONCLUSION:-success}"
|
|
||||||
WORKFLOW_URL="${WORKFLOW_URL:-}"
|
|
||||||
|
|
||||||
# Generate completion comment based on conclusion
|
|
||||||
if [ "$WORKFLOW_CONCLUSION" = "success" ]; then
|
|
||||||
status_icon="✅"
|
|
||||||
status_text="Build completed successfully!"
|
|
||||||
footer_text="🎉 Your Storybook is ready for review!"
|
|
||||||
elif [ "$WORKFLOW_CONCLUSION" = "skipped" ]; then
|
|
||||||
status_icon="⏭️"
|
|
||||||
status_text="Build skipped."
|
|
||||||
footer_text="ℹ️ Chromatic was skipped for this PR."
|
|
||||||
elif [ "$WORKFLOW_CONCLUSION" = "cancelled" ]; then
|
|
||||||
status_icon="🚫"
|
|
||||||
status_text="Build cancelled."
|
|
||||||
footer_text="ℹ️ The Chromatic run was cancelled."
|
|
||||||
else
|
|
||||||
status_icon="❌"
|
|
||||||
status_text="Build failed!"
|
|
||||||
footer_text="⚠️ Please check the workflow logs for error details."
|
|
||||||
fi
|
|
||||||
|
|
||||||
comment="$COMMENT_MARKER
|
|
||||||
## 🎨 Storybook Build Status
|
|
||||||
|
|
||||||
$status_icon **$status_text**
|
|
||||||
|
|
||||||
⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC
|
|
||||||
|
|
||||||
### 🔗 Links
|
|
||||||
- [📊 View Workflow Run]($WORKFLOW_URL)"
|
|
||||||
|
|
||||||
# Add deployment status
|
|
||||||
if [ "$deployment_url" != "Not deployed" ]; then
|
|
||||||
if [ "$deployment_url" = "Deployment failed" ]; then
|
|
||||||
comment="$comment
|
|
||||||
- ❌ Storybook deployment failed"
|
|
||||||
elif [ "$WORKFLOW_CONCLUSION" = "success" ]; then
|
|
||||||
comment="$comment
|
|
||||||
- 🎨 $deployment_url"
|
|
||||||
else
|
|
||||||
comment="$comment
|
|
||||||
- ⚠️ Build failed - $deployment_url"
|
|
||||||
fi
|
|
||||||
elif [ "$WORKFLOW_CONCLUSION" != "success" ]; then
|
|
||||||
comment="$comment
|
|
||||||
- ⏭️ Storybook deployment skipped (build did not succeed)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
comment="$comment
|
|
||||||
|
|
||||||
---
|
|
||||||
$footer_text"
|
|
||||||
|
|
||||||
post_comment "$comment"
|
|
||||||
fi
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||||
import {
|
|
||||||
formatCamelCase,
|
|
||||||
normalizeI18nKey
|
|
||||||
} from '../packages/shared-frontend-utils/src/formatUtil'
|
|
||||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||||
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
|
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
|
||||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||||
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
||||||
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
||||||
|
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
|
||||||
|
|
||||||
const localePath = './src/locales/en/main.json'
|
const localePath = './src/locales/en/main.json'
|
||||||
const commandsPath = './src/locales/en/commands.json'
|
const commandsPath = './src/locales/en/commands.json'
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import * as fs from 'fs'
|
|||||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||||
|
|
||||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||||
import { normalizeI18nKey } from '../packages/shared-frontend-utils/src/formatUtil'
|
|
||||||
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||||
|
import { normalizeI18nKey } from '../src/utils/formatUtil'
|
||||||
|
|
||||||
const localePath = './src/locales/en/main.json'
|
const localePath = './src/locales/en/main.json'
|
||||||
const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
||||||
|
|||||||
365
src/api/auth.ts
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import * as Sentry from '@sentry/vue'
|
||||||
|
import { isEmpty } from 'es-toolkit/compat'
|
||||||
|
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
|
||||||
|
interface UserCloudStatus {
|
||||||
|
status: 'active' | 'waitlisted'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONBOARDING_SURVEY_KEY = 'onboarding_survey'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to capture API errors with Sentry
|
||||||
|
*/
|
||||||
|
function captureApiError(
|
||||||
|
error: Error,
|
||||||
|
endpoint: string,
|
||||||
|
errorType: 'http_error' | 'network_error',
|
||||||
|
httpStatus?: number,
|
||||||
|
operation?: string,
|
||||||
|
extraContext?: Record<string, any>
|
||||||
|
) {
|
||||||
|
const tags: Record<string, any> = {
|
||||||
|
api_endpoint: endpoint,
|
||||||
|
error_type: errorType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpStatus !== undefined) {
|
||||||
|
tags.http_status = httpStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation) {
|
||||||
|
tags.operation = operation
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentryOptions: any = {
|
||||||
|
tags,
|
||||||
|
extra: extraContext ? { ...extraContext } : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.captureException(error, sentryOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to check if error is already handled HTTP error
|
||||||
|
*/
|
||||||
|
function isHttpError(error: unknown, errorMessagePrefix: string): boolean {
|
||||||
|
return error instanceof Error && error.message.startsWith(errorMessagePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi('/user', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(`Failed to get user: ${response.statusText}`)
|
||||||
|
captureApiError(
|
||||||
|
error,
|
||||||
|
'/user',
|
||||||
|
'http_error',
|
||||||
|
response.status,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
api: {
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: '/user',
|
||||||
|
status_code: response.status,
|
||||||
|
status_text: response.statusText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
} catch (error) {
|
||||||
|
// Only capture network errors (not HTTP errors we already captured)
|
||||||
|
if (!isHttpError(error, 'Failed to get user:')) {
|
||||||
|
captureApiError(error as Error, '/user', 'network_error')
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInviteCodeStatus(
|
||||||
|
inviteCode: string
|
||||||
|
): Promise<{ claimed: boolean; expired: boolean }> {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi(
|
||||||
|
`/invite_code/${encodeURIComponent(inviteCode)}/status`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(
|
||||||
|
`Failed to get invite code status: ${response.statusText}`
|
||||||
|
)
|
||||||
|
captureApiError(
|
||||||
|
error,
|
||||||
|
'/invite_code/{code}/status',
|
||||||
|
'http_error',
|
||||||
|
response.status,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
api: {
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: `/invite_code/${inviteCode}/status`,
|
||||||
|
status_code: response.status,
|
||||||
|
status_text: response.statusText
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
invite_code_length: inviteCode.length
|
||||||
|
},
|
||||||
|
route_template: '/invite_code/{code}/status',
|
||||||
|
route_actual: `/invite_code/${inviteCode}/status`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
} catch (error) {
|
||||||
|
// Only capture network errors (not HTTP errors we already captured)
|
||||||
|
if (!isHttpError(error, 'Failed to get invite code status:')) {
|
||||||
|
captureApiError(
|
||||||
|
error as Error,
|
||||||
|
'/invite_code/{code}/status',
|
||||||
|
'network_error',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
route_template: '/invite_code/{code}/status',
|
||||||
|
route_actual: `/invite_code/${inviteCode}/status`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSurveyCompletedStatus(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
// Not an error case - survey not completed is a valid state
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
category: 'auth',
|
||||||
|
message: 'Survey status check returned non-ok response',
|
||||||
|
level: 'info',
|
||||||
|
data: {
|
||||||
|
status: response.status,
|
||||||
|
endpoint: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
// Check if data exists and is not empty
|
||||||
|
return !isEmpty(data.value)
|
||||||
|
} catch (error) {
|
||||||
|
// Network error - still capture it as it's not thrown from above
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: {
|
||||||
|
api_endpoint: '/settings/{key}',
|
||||||
|
error_type: 'network_error'
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
route_template: '/settings/{key}',
|
||||||
|
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||||
|
},
|
||||||
|
level: 'warning'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - Unused function kept for future use
|
||||||
|
async function postSurveyStatus(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: undefined })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(
|
||||||
|
`Failed to post survey status: ${response.statusText}`
|
||||||
|
)
|
||||||
|
captureApiError(
|
||||||
|
error,
|
||||||
|
'/settings/{key}',
|
||||||
|
'http_error',
|
||||||
|
response.status,
|
||||||
|
'post_survey_status',
|
||||||
|
{
|
||||||
|
route_template: '/settings/{key}',
|
||||||
|
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Only capture network errors (not HTTP errors we already captured)
|
||||||
|
if (!isHttpError(error, 'Failed to post survey status:')) {
|
||||||
|
captureApiError(
|
||||||
|
error as Error,
|
||||||
|
'/settings/{key}',
|
||||||
|
'network_error',
|
||||||
|
undefined,
|
||||||
|
'post_survey_status',
|
||||||
|
{
|
||||||
|
route_template: '/settings/{key}',
|
||||||
|
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitSurvey(
|
||||||
|
survey: Record<string, unknown>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
category: 'auth',
|
||||||
|
message: 'Submitting survey',
|
||||||
|
level: 'info',
|
||||||
|
data: {
|
||||||
|
survey_fields: Object.keys(survey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await api.fetchApi('/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: survey })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(`Failed to submit survey: ${response.statusText}`)
|
||||||
|
captureApiError(
|
||||||
|
error,
|
||||||
|
'/settings',
|
||||||
|
'http_error',
|
||||||
|
response.status,
|
||||||
|
'submit_survey',
|
||||||
|
{
|
||||||
|
survey: {
|
||||||
|
field_count: Object.keys(survey).length,
|
||||||
|
field_names: Object.keys(survey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful survey submission
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
category: 'auth',
|
||||||
|
message: 'Survey submitted successfully',
|
||||||
|
level: 'info'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// Only capture network errors (not HTTP errors we already captured)
|
||||||
|
if (!isHttpError(error, 'Failed to submit survey:')) {
|
||||||
|
captureApiError(
|
||||||
|
error as Error,
|
||||||
|
'/settings',
|
||||||
|
'network_error',
|
||||||
|
undefined,
|
||||||
|
'submit_survey'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function claimInvite(
|
||||||
|
code: string
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
category: 'auth',
|
||||||
|
message: 'Attempting to claim invite',
|
||||||
|
level: 'info',
|
||||||
|
data: {
|
||||||
|
code_length: code.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await api.fetchApi(
|
||||||
|
`/invite_code/${encodeURIComponent(code)}/claim`,
|
||||||
|
{
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = new Error(
|
||||||
|
`Failed to claim invite: ${res.status} ${res.statusText}`
|
||||||
|
)
|
||||||
|
captureApiError(
|
||||||
|
error,
|
||||||
|
'/invite_code/{code}/claim',
|
||||||
|
'http_error',
|
||||||
|
res.status,
|
||||||
|
'claim_invite',
|
||||||
|
{
|
||||||
|
invite: {
|
||||||
|
code_length: code.length,
|
||||||
|
status_code: res.status,
|
||||||
|
status_text: res.statusText
|
||||||
|
},
|
||||||
|
route_template: '/invite_code/{code}/claim',
|
||||||
|
route_actual: `/invite_code/${encodeURIComponent(code)}/claim`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful invite claim
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
category: 'auth',
|
||||||
|
message: 'Invite claimed successfully',
|
||||||
|
level: 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
} catch (error) {
|
||||||
|
// Only capture network errors (not HTTP errors we already captured)
|
||||||
|
if (!isHttpError(error, 'Failed to claim invite:')) {
|
||||||
|
captureApiError(
|
||||||
|
error as Error,
|
||||||
|
'/invite_code/{code}/claim',
|
||||||
|
'network_error',
|
||||||
|
undefined,
|
||||||
|
'claim_invite',
|
||||||
|
{
|
||||||
|
route_template: '/invite_code/{code}/claim',
|
||||||
|
route_actual: `/invite_code/${encodeURIComponent(code)}/claim`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,12 +32,16 @@
|
|||||||
</Message>
|
</Message>
|
||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
<SignInForm
|
||||||
|
v-if="isSignIn"
|
||||||
|
:auth-error="authError"
|
||||||
|
@submit="signInWithEmail"
|
||||||
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
||||||
{{ t('auth.signup.regionRestrictionChina') }}
|
{{ t('auth.signup.regionRestrictionChina') }}
|
||||||
</Message>
|
</Message>
|
||||||
<SignUpForm v-else @submit="signUpWithEmail" />
|
<SignUpForm v-else :auth-error="authError" @submit="signUpWithEmail" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
@@ -149,6 +153,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||||
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
|
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||||
|
import { translateAuthError } from '@/utils/authErrorTranslation'
|
||||||
import { isInChina } from '@/utils/networkUtil'
|
import { isInChina } from '@/utils/networkUtil'
|
||||||
|
|
||||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||||
@@ -164,32 +169,58 @@ const authActions = useFirebaseAuthActions()
|
|||||||
const isSecureContext = window.isSecureContext
|
const isSecureContext = window.isSecureContext
|
||||||
const isSignIn = ref(true)
|
const isSignIn = ref(true)
|
||||||
const showApiKeyForm = ref(false)
|
const showApiKeyForm = ref(false)
|
||||||
|
const authError = ref('')
|
||||||
|
|
||||||
const toggleState = () => {
|
const toggleState = () => {
|
||||||
isSignIn.value = !isSignIn.value
|
isSignIn.value = !isSignIn.value
|
||||||
showApiKeyForm.value = false
|
showApiKeyForm.value = false
|
||||||
|
authError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error handler for inline display
|
||||||
|
const inlineErrorHandler = (error: unknown) => {
|
||||||
|
// Set inline error with auth error translation
|
||||||
|
authError.value = translateAuthError(error)
|
||||||
|
// Also show toast (original behavior)
|
||||||
|
authActions.reportError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const signInWithGoogle = async () => {
|
const signInWithGoogle = async () => {
|
||||||
if (await authActions.signInWithGoogle()) {
|
authError.value = ''
|
||||||
|
if (await authActions.signInWithGoogle(inlineErrorHandler)()) {
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signInWithGithub = async () => {
|
const signInWithGithub = async () => {
|
||||||
if (await authActions.signInWithGithub()) {
|
authError.value = ''
|
||||||
|
if (await authActions.signInWithGithub(inlineErrorHandler)()) {
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signInWithEmail = async (values: SignInData) => {
|
const signInWithEmail = async (values: SignInData) => {
|
||||||
if (await authActions.signInWithEmail(values.email, values.password)) {
|
authError.value = ''
|
||||||
|
if (
|
||||||
|
await authActions.signInWithEmail(
|
||||||
|
values.email,
|
||||||
|
values.password,
|
||||||
|
inlineErrorHandler
|
||||||
|
)()
|
||||||
|
) {
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signUpWithEmail = async (values: SignUpData) => {
|
const signUpWithEmail = async (values: SignUpData) => {
|
||||||
if (await authActions.signUpWithEmail(values.email, values.password)) {
|
authError.value = ''
|
||||||
|
if (
|
||||||
|
await authActions.signUpWithEmail(
|
||||||
|
values.email,
|
||||||
|
values.password,
|
||||||
|
inlineErrorHandler
|
||||||
|
)()
|
||||||
|
) {
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,11 @@
|
|||||||
}}</small>
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth Error Message -->
|
||||||
|
<Message v-if="authError" severity="error">
|
||||||
|
{{ authError }}
|
||||||
|
</Message>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||||
<Button
|
<Button
|
||||||
@@ -76,6 +81,7 @@ import { Form } from '@primevue/forms'
|
|||||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Message from 'primevue/message'
|
||||||
import Password from 'primevue/password'
|
import Password from 'primevue/password'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
@@ -93,6 +99,10 @@ const toast = useToast()
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
authError?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [values: SignInData]
|
submit: [values: SignInData]
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -44,16 +44,21 @@
|
|||||||
>
|
>
|
||||||
{{ t('auth.signup.personalDataConsentLabel') }}
|
{{ t('auth.signup.personalDataConsentLabel') }}
|
||||||
</label>
|
</label>
|
||||||
<small v-if="$field.error" class="text-red-500 -mt-4">{{
|
<small v-if="$field.error" class="text-red-500 mt-4">{{
|
||||||
$field.error.message
|
$field.error.message
|
||||||
}}</small>
|
}}</small>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<!-- Auth Error Message -->
|
||||||
|
<Message v-if="authError" severity="error">
|
||||||
|
{{ authError }}
|
||||||
|
</Message>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
:label="t('auth.signup.signUpButton')"
|
:label="t('auth.signup.signUpButton')"
|
||||||
class="h-10 font-medium mt-4"
|
class="h-10 font-medium mt-4 text-white"
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
@@ -65,6 +70,7 @@ import { zodResolver } from '@primevue/forms/resolvers/zod'
|
|||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Checkbox from 'primevue/checkbox'
|
import Checkbox from 'primevue/checkbox'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Message from 'primevue/message'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
|
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
|
||||||
@@ -73,6 +79,10 @@ import PasswordFields from './PasswordFields.vue'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
authError?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [values: SignUpData]
|
submit: [values: SignUpData]
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
|
|
||||||
// Global variable from vite build defined in global.d.ts
|
import { isProductionEnvironment } from '@/config/environment'
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const isStaging = !__USE_PROD_CONFIG__
|
const isStaging = !isProductionEnvironment()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -125,43 +125,50 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
useEventListener(document, 'mousedown', (event) => {
|
|
||||||
if (!isDOMWidget(widget) || !widgetState.visible || !widget.element.blur) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!widget.element.contains(event.target as HTMLElement)) {
|
|
||||||
widget.element.blur()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
// Set up event listeners only after the widget is mounted and visible
|
||||||
if (!isDOMWidget(widget)) {
|
const setupDOMEventListeners = () => {
|
||||||
return
|
if (!isDOMWidget(widget) || !widgetState.visible) return
|
||||||
|
|
||||||
|
if (widget.element.blur) {
|
||||||
|
useEventListener(document, 'mousedown', (event) => {
|
||||||
|
if (!widget.element.contains(event.target as HTMLElement)) {
|
||||||
|
widget.element.blur()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
useEventListener(
|
|
||||||
widget.element,
|
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
|
||||||
widget.options.selectOn ?? ['focus', 'click'],
|
useEventListener(widget.element, evt, () => {
|
||||||
() => {
|
|
||||||
const lgCanvas = canvasStore.canvas
|
const lgCanvas = canvasStore.canvas
|
||||||
lgCanvas?.selectNode(widget.node)
|
lgCanvas?.selectNode(widget.node)
|
||||||
lgCanvas?.bringToFront(widget.node)
|
lgCanvas?.bringToFront(widget.node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event listeners when widget becomes visible
|
||||||
|
watch(
|
||||||
|
() => widgetState.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
setupDOMEventListeners()
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
})
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
const inputSpec = widget.node.constructor.nodeData
|
const inputSpec = widget.node.constructor.nodeData
|
||||||
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
||||||
|
|
||||||
// Mount DOM element when widget is or becomes visible
|
// Mount DOM element when widget is or becomes visible
|
||||||
const mountElementIfVisible = () => {
|
const mountElementIfVisible = () => {
|
||||||
if (!(widgetState.visible && isDOMWidget(widget) && widgetElement.value)) {
|
if (widgetState.visible && isDOMWidget(widget) && widgetElement.value) {
|
||||||
return
|
// Only append if not already a child
|
||||||
|
if (!widgetElement.value.contains(widget.element)) {
|
||||||
|
widgetElement.value.appendChild(widget.element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Only append if not already a child
|
|
||||||
if (widgetElement.value.contains(widget.element)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
widgetElement.value.appendChild(widget.element)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check on mount - but only after next tick to ensure visibility is calculated
|
// Check on mount - but only after next tick to ensure visibility is calculated
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ import { computed, onMounted, ref, watch } from 'vue'
|
|||||||
import UrlInput from '@/components/common/UrlInput.vue'
|
import UrlInput from '@/components/common/UrlInput.vue'
|
||||||
import type { UVMirror } from '@/constants/uvMirrors'
|
import type { UVMirror } from '@/constants/uvMirrors'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
import { checkMirrorReachable } from '@/utils/electronMirrorCheck'
|
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
|
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||||
import { ValidationState } from '@/utils/validationUtil'
|
import { ValidationState } from '@/utils/validationUtil'
|
||||||
|
|
||||||
const FILE_URL_SCHEME = 'file://'
|
const FILE_URL_SCHEME = 'file://'
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ const canvasStore = useCanvasStore()
|
|||||||
|
|
||||||
function addNode(nodeDef: ComfyNodeDefImpl) {
|
function addNode(nodeDef: ComfyNodeDefImpl) {
|
||||||
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
||||||
pos: getNewNodeLocation()
|
pos: getNewNodeLocation(),
|
||||||
|
telemetrySource: 'search-popover'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (disconnectOnReset && triggerEvent) {
|
if (disconnectOnReset && triggerEvent) {
|
||||||
|
|||||||
@@ -265,7 +265,9 @@ const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
|||||||
handleClick(e: MouseEvent) {
|
handleClick(e: MouseEvent) {
|
||||||
if (this.leaf) {
|
if (this.leaf) {
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
useLitegraphService().addNodeOnGraph(this.data)
|
useLitegraphService().addNodeOnGraph(this.data, {
|
||||||
|
telemetrySource: 'sidebar-click'
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
toggleNodeOnEvent(e, this)
|
toggleNodeOnEvent(e, this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||||
@@ -123,6 +123,7 @@ const toast = useToast()
|
|||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
|
const workflowService = useWorkflowService()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Expanded view: show all outputs in a flat list.
|
// Expanded view: show all outputs in a flat list.
|
||||||
@@ -196,6 +197,30 @@ const menuTargetTask = ref<TaskItemImpl | null>(null)
|
|||||||
const menuTargetNode = ref<ComfyNode | null>(null)
|
const menuTargetNode = ref<ComfyNode | null>(null)
|
||||||
const menuItems = computed<MenuItem[]>(() => {
|
const menuItems = computed<MenuItem[]>(() => {
|
||||||
const items: MenuItem[] = [
|
const items: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: t('g.copyJobId'),
|
||||||
|
icon: 'pi pi-copy',
|
||||||
|
command: async () => {
|
||||||
|
if (menuTargetTask.value) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(menuTargetTask.value.promptId)
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('g.copied'),
|
||||||
|
detail: t('g.jobIdCopied'),
|
||||||
|
life: 2000
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('g.error'),
|
||||||
|
detail: t('g.failedToCopyJobId'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('g.delete'),
|
label: t('g.delete'),
|
||||||
icon: 'pi pi-trash',
|
icon: 'pi pi-trash',
|
||||||
@@ -205,8 +230,16 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
{
|
{
|
||||||
label: t('g.loadWorkflow'),
|
label: t('g.loadWorkflow'),
|
||||||
icon: 'pi pi-file-export',
|
icon: 'pi pi-file-export',
|
||||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
command: () => {
|
||||||
disabled: !menuTargetTask.value?.workflow
|
if (menuTargetTask.value) {
|
||||||
|
void workflowService.loadTaskWorkflow(menuTargetTask.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: !(
|
||||||
|
menuTargetTask.value?.workflow ||
|
||||||
|
(menuTargetTask.value?.isHistory &&
|
||||||
|
menuTargetTask.value?.prompt.prompt_id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('g.goToNode'),
|
label: t('g.goToNode'),
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ import {
|
|||||||
useWorkflowStore
|
useWorkflowStore
|
||||||
} from '@/platform/workflow/management/stores/workflowStore'
|
} from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
import { TelemetryEvents, trackTypedEvent } from '@/services/telemetryService'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||||
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||||
@@ -234,6 +235,13 @@ const renderTreeNode = (
|
|||||||
e: MouseEvent
|
e: MouseEvent
|
||||||
) {
|
) {
|
||||||
if (this.leaf) {
|
if (this.leaf) {
|
||||||
|
// Track workflow opening from sidebar
|
||||||
|
trackTypedEvent(TelemetryEvents.WORKFLOW_OPENED_FROM_SIDEBAR, {
|
||||||
|
workflow_path: workflow.path,
|
||||||
|
workflow_type: type,
|
||||||
|
is_bookmarked: type === WorkflowTreeType.Bookmarks,
|
||||||
|
is_open: type === WorkflowTreeType.Open
|
||||||
|
})
|
||||||
await workflowService.openWorkflow(workflow)
|
await workflowService.openWorkflow(workflow)
|
||||||
} else {
|
} else {
|
||||||
toggleNodeOnEvent(e, this)
|
toggleNodeOnEvent(e, this)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FirebaseError } from 'firebase/app'
|
import { FirebaseError } from 'firebase/app'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
@@ -15,6 +16,7 @@ import { usdToMicros } from '@/utils/formatUtil'
|
|||||||
export const useFirebaseAuthActions = () => {
|
export const useFirebaseAuthActions = () => {
|
||||||
const authStore = useFirebaseAuthStore()
|
const authStore = useFirebaseAuthStore()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
|
const router = useRouter()
|
||||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||||
|
|
||||||
const accessError = ref(false)
|
const accessError = ref(false)
|
||||||
@@ -51,6 +53,12 @@ export const useFirebaseAuthActions = () => {
|
|||||||
detail: t('auth.signOut.successDetail'),
|
detail: t('auth.signOut.successDetail'),
|
||||||
life: 5000
|
life: 5000
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Redirect to login page if we're on cloud domain
|
||||||
|
const hostname = window.location.hostname
|
||||||
|
if (hostname.includes('cloud.comfy.org')) {
|
||||||
|
await router.push({ name: 'cloud-login' })
|
||||||
|
}
|
||||||
}, reportError)
|
}, reportError)
|
||||||
|
|
||||||
const sendPasswordReset = wrapWithErrorHandlingAsync(
|
const sendPasswordReset = wrapWithErrorHandlingAsync(
|
||||||
@@ -100,27 +108,33 @@ export const useFirebaseAuthActions = () => {
|
|||||||
return await authStore.fetchBalance()
|
return await authStore.fetchBalance()
|
||||||
}, reportError)
|
}, reportError)
|
||||||
|
|
||||||
const signInWithGoogle = wrapWithErrorHandlingAsync(async () => {
|
const signInWithGoogle = (errorHandler = reportError) =>
|
||||||
return await authStore.loginWithGoogle()
|
wrapWithErrorHandlingAsync(async () => {
|
||||||
}, reportError)
|
return await authStore.loginWithGoogle()
|
||||||
|
}, errorHandler)
|
||||||
|
|
||||||
const signInWithGithub = wrapWithErrorHandlingAsync(async () => {
|
const signInWithGithub = (errorHandler = reportError) =>
|
||||||
return await authStore.loginWithGithub()
|
wrapWithErrorHandlingAsync(async () => {
|
||||||
}, reportError)
|
return await authStore.loginWithGithub()
|
||||||
|
}, errorHandler)
|
||||||
|
|
||||||
const signInWithEmail = wrapWithErrorHandlingAsync(
|
const signInWithEmail = (
|
||||||
async (email: string, password: string) => {
|
email: string,
|
||||||
|
password: string,
|
||||||
|
errorHandler = reportError
|
||||||
|
) =>
|
||||||
|
wrapWithErrorHandlingAsync(async () => {
|
||||||
return await authStore.login(email, password)
|
return await authStore.login(email, password)
|
||||||
},
|
}, errorHandler)
|
||||||
reportError
|
|
||||||
)
|
|
||||||
|
|
||||||
const signUpWithEmail = wrapWithErrorHandlingAsync(
|
const signUpWithEmail = (
|
||||||
async (email: string, password: string) => {
|
email: string,
|
||||||
|
password: string,
|
||||||
|
errorHandler = reportError
|
||||||
|
) =>
|
||||||
|
wrapWithErrorHandlingAsync(async () => {
|
||||||
return await authStore.register(email, password)
|
return await authStore.register(email, password)
|
||||||
},
|
}, errorHandler)
|
||||||
reportError
|
|
||||||
)
|
|
||||||
|
|
||||||
const updatePassword = wrapWithErrorHandlingAsync(
|
const updatePassword = wrapWithErrorHandlingAsync(
|
||||||
async (newPassword: string) => {
|
async (newPassword: string) => {
|
||||||
@@ -156,7 +170,8 @@ export const useFirebaseAuthActions = () => {
|
|||||||
signInWithEmail,
|
signInWithEmail,
|
||||||
signUpWithEmail,
|
signUpWithEmail,
|
||||||
updatePassword,
|
updatePassword,
|
||||||
deleteAccount,
|
accessError,
|
||||||
accessError
|
reportError,
|
||||||
|
deleteAccount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Ref } from 'vue'
|
|||||||
|
|
||||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import type { Rect } from '@/lib/litegraph/src/interfaces'
|
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
@@ -71,7 +71,7 @@ export function useSelectionToolboxPosition(
|
|||||||
visible.value = true
|
visible.value = true
|
||||||
|
|
||||||
// Get bounds for all selected items
|
// Get bounds for all selected items
|
||||||
const allBounds: Rect[] = []
|
const allBounds: ReadOnlyRect[] = []
|
||||||
for (const item of selectableItems) {
|
for (const item of selectableItems) {
|
||||||
// Skip items without valid IDs
|
// Skip items without valid IDs
|
||||||
if (item.id == null) continue
|
if (item.id == null) continue
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up widget callbacks for a node
|
* Sets up widget callbacks for a node - now with reduced nesting
|
||||||
*/
|
*/
|
||||||
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
|
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
|
||||||
if (!node.widgets) return
|
if (!node.widgets) return
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { api } from '@/scripts/api'
|
|||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
|
import { TelemetryEvents, trackTypedEvent } from '@/services/telemetryService'
|
||||||
import type { ComfyCommand } from '@/stores/commandStore'
|
import type { ComfyCommand } from '@/stores/commandStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||||
@@ -550,6 +551,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
versionAdded: '1.3.11',
|
versionAdded: '1.3.11',
|
||||||
category: 'essentials' as const,
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
|
const selectedNodes = getSelectedNodes()
|
||||||
|
trackTypedEvent(TelemetryEvents.NODE_MUTED, {
|
||||||
|
node_count: selectedNodes.length,
|
||||||
|
action_type: 'keyboard_shortcut'
|
||||||
|
})
|
||||||
toggleSelectedNodesMode(LGraphEventMode.NEVER)
|
toggleSelectedNodesMode(LGraphEventMode.NEVER)
|
||||||
app.canvas.setDirty(true, true)
|
app.canvas.setDirty(true, true)
|
||||||
}
|
}
|
||||||
@@ -561,6 +567,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
versionAdded: '1.3.11',
|
versionAdded: '1.3.11',
|
||||||
category: 'essentials' as const,
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
|
const selectedNodes = getSelectedNodes()
|
||||||
|
trackTypedEvent(TelemetryEvents.NODE_BYPASSED, {
|
||||||
|
node_count: selectedNodes.length,
|
||||||
|
action_type: 'keyboard_shortcut'
|
||||||
|
})
|
||||||
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
||||||
app.canvas.setDirty(true, true)
|
app.canvas.setDirty(true, true)
|
||||||
}
|
}
|
||||||
@@ -896,6 +907,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
const graph = canvas.subgraph ?? canvas.graph
|
const graph = canvas.subgraph ?? canvas.graph
|
||||||
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
||||||
|
|
||||||
|
const selectedCount = canvas.selectedItems.size
|
||||||
const res = graph.convertToSubgraph(canvas.selectedItems)
|
const res = graph.convertToSubgraph(canvas.selectedItems)
|
||||||
if (!res) {
|
if (!res) {
|
||||||
toastStore.add({
|
toastStore.add({
|
||||||
@@ -907,6 +919,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track subgraph creation
|
||||||
|
trackTypedEvent(TelemetryEvents.SUBGRAPH_CREATED, {
|
||||||
|
selected_item_count: selectedCount,
|
||||||
|
action_type: 'keyboard_shortcut'
|
||||||
|
})
|
||||||
|
|
||||||
const { node } = res
|
const { node } = res
|
||||||
canvas.select(node)
|
canvas.select(node)
|
||||||
canvasStore.updateSelectedItems()
|
canvasStore.updateSelectedItems()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
|
import { isProductionEnvironment } from './environment'
|
||||||
|
|
||||||
|
export const COMFY_API_BASE_URL = isProductionEnvironment()
|
||||||
? 'https://api.comfy.org'
|
? 'https://api.comfy.org'
|
||||||
: 'https://stagingapi.comfy.org'
|
: 'https://stagingapi.comfy.org'
|
||||||
|
|
||||||
export const COMFY_PLATFORM_BASE_URL = __USE_PROD_CONFIG__
|
export const COMFY_PLATFORM_BASE_URL = isProductionEnvironment()
|
||||||
? 'https://platform.comfy.org'
|
? 'https://platform.comfy.org'
|
||||||
: 'https://stagingplatform.comfy.org'
|
: 'https://stagingplatform.comfy.org'
|
||||||
|
|||||||
18
src/config/environment.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Runtime environment configuration that determines if we're in production or staging
|
||||||
|
* based on the hostname. Replaces the build-time __USE_PROD_CONFIG__ constant.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the application is running in production environment
|
||||||
|
* @returns true if hostname is cloud.comfy.org (production), false otherwise (staging)
|
||||||
|
*/
|
||||||
|
export function isProductionEnvironment(): boolean {
|
||||||
|
// In SSR/Node.js environments or during build, use the environment variable
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return process.env.USE_PROD_CONFIG === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
// In browser, check the hostname
|
||||||
|
return window.location.hostname === 'cloud.comfy.org'
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { FirebaseOptions } from 'firebase/app'
|
import type { FirebaseOptions } from 'firebase/app'
|
||||||
|
|
||||||
|
import { isProductionEnvironment } from './environment'
|
||||||
|
|
||||||
const DEV_CONFIG: FirebaseOptions = {
|
const DEV_CONFIG: FirebaseOptions = {
|
||||||
apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
|
apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
|
||||||
authDomain: 'dreamboothy-dev.firebaseapp.com',
|
authDomain: 'dreamboothy-dev.firebaseapp.com',
|
||||||
@@ -23,6 +25,6 @@ const PROD_CONFIG: FirebaseOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To test with prod config while using dev server, set USE_PROD_CONFIG=true in .env
|
// To test with prod config while using dev server, set USE_PROD_CONFIG=true in .env
|
||||||
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_CONFIG__
|
export const FIREBASE_CONFIG: FirebaseOptions = isProductionEnvironment()
|
||||||
? PROD_CONFIG
|
? PROD_CONFIG
|
||||||
: DEV_CONFIG
|
: DEV_CONFIG
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
|
|||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { checkMirrorReachable } from '@/utils/electronMirrorCheck'
|
|
||||||
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||||
|
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||||
|
|
||||||
// Desktop documentation URLs
|
// Desktop documentation URLs
|
||||||
const DESKTOP_DOCS = {
|
const DESKTOP_DOCS = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { ResultItemType } from '@/schemas/apiSchema'
|
|||||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||||
import type { DOMWidget } from '@/scripts/domWidget'
|
import type { DOMWidget } from '@/scripts/domWidget'
|
||||||
import { useAudioService } from '@/services/audioService'
|
import { useAudioService } from '@/services/audioService'
|
||||||
|
import { fileNameMappingService } from '@/services/fileNameMappingService'
|
||||||
import { type NodeLocatorId } from '@/types'
|
import { type NodeLocatorId } from '@/types'
|
||||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||||
|
|
||||||
@@ -66,10 +67,19 @@ async function uploadFile(
|
|||||||
|
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
// Add the file to the dropdown list and update the widget value
|
// Build the file path
|
||||||
let path = data.name
|
let path = data.name
|
||||||
if (data.subfolder) path = data.subfolder + '/' + path
|
if (data.subfolder) path = data.subfolder + '/' + path
|
||||||
|
|
||||||
|
// CRITICAL: Refresh mappings FIRST before updating dropdown
|
||||||
|
// This ensures new hash→human mappings are available when dropdown renders
|
||||||
|
try {
|
||||||
|
await fileNameMappingService.refreshMapping('input')
|
||||||
|
} catch (error) {
|
||||||
|
// Continue anyway - will show hash values as fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now add the file to the dropdown list - any filename proxy will use fresh mappings
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
if (!audioWidget.options.values.includes(path)) {
|
if (!audioWidget.options.values.includes(path)) {
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
@@ -241,7 +251,7 @@ app.registerExtension({
|
|||||||
inputName,
|
inputName,
|
||||||
'',
|
'',
|
||||||
openFileSelection,
|
openFileSelection,
|
||||||
{ serialize: false, canvasOnly: true }
|
{ serialize: false }
|
||||||
)
|
)
|
||||||
uploadWidget.label = t('g.choose_file_to_upload')
|
uploadWidget.label = t('g.choose_file_to_upload')
|
||||||
|
|
||||||
@@ -398,7 +408,7 @@ app.registerExtension({
|
|||||||
mediaRecorder.stop()
|
mediaRecorder.stop()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ serialize: false, canvasOnly: true }
|
{ serialize: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
recordWidget.label = t('g.startRecording')
|
recordWidget.label = t('g.startRecording')
|
||||||
|
|||||||
@@ -106,8 +106,7 @@ app.registerExtension({
|
|||||||
'button',
|
'button',
|
||||||
'waiting for camera...',
|
'waiting for camera...',
|
||||||
'capture',
|
'capture',
|
||||||
capture,
|
capture
|
||||||
{ canvasOnly: true }
|
|
||||||
)
|
)
|
||||||
btn.disabled = true
|
btn.disabled = true
|
||||||
btn.serializeValue = () => undefined
|
btn.serializeValue = () => undefined
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Point, Rect } from './interfaces'
|
import type { Point, ReadOnlyRect, Rect } from './interfaces'
|
||||||
import { EaseFunction, Rectangle } from './litegraph'
|
import { EaseFunction, Rectangle } from './litegraph'
|
||||||
|
|
||||||
export interface DragAndScaleState {
|
export interface DragAndScaleState {
|
||||||
@@ -188,7 +188,10 @@ export class DragAndScale {
|
|||||||
* Fits the view to the specified bounds.
|
* Fits the view to the specified bounds.
|
||||||
* @param bounds The bounds to fit the view to, defined by a rectangle.
|
* @param bounds The bounds to fit the view to, defined by a rectangle.
|
||||||
*/
|
*/
|
||||||
fitToBounds(bounds: Rect, { zoom = 0.75 }: { zoom?: number } = {}): void {
|
fitToBounds(
|
||||||
|
bounds: ReadOnlyRect,
|
||||||
|
{ zoom = 0.75 }: { zoom?: number } = {}
|
||||||
|
): void {
|
||||||
const cw = this.element.width / window.devicePixelRatio
|
const cw = this.element.width / window.devicePixelRatio
|
||||||
const ch = this.element.height / window.devicePixelRatio
|
const ch = this.element.height / window.devicePixelRatio
|
||||||
let targetScale = this.scale
|
let targetScale = this.scale
|
||||||
@@ -220,7 +223,7 @@ export class DragAndScale {
|
|||||||
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
||||||
*/
|
*/
|
||||||
animateToBounds(
|
animateToBounds(
|
||||||
bounds: Readonly<Rect | Rectangle>,
|
bounds: ReadOnlyRect,
|
||||||
setDirty: () => void,
|
setDirty: () => void,
|
||||||
{
|
{
|
||||||
duration = 350,
|
duration = 350,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
SUBGRAPH_INPUT_ID,
|
SUBGRAPH_INPUT_ID,
|
||||||
SUBGRAPH_OUTPUT_ID
|
SUBGRAPH_OUTPUT_ID
|
||||||
} from '@/lib/litegraph/src/constants'
|
} from '@/lib/litegraph/src/constants'
|
||||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
|
||||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||||
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
|
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
@@ -1708,12 +1707,7 @@ export class LGraph
|
|||||||
...subgraphNode.subgraph.groups
|
...subgraphNode.subgraph.groups
|
||||||
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
|
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
|
||||||
return {
|
return {
|
||||||
boundingRect: new Rectangle(
|
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0]
|
||||||
p.pos[0],
|
|
||||||
p.pos[1],
|
|
||||||
p.size?.[0] ?? 0,
|
|
||||||
p.size?.[1] ?? 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]
|
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import type {
|
|||||||
NullableProperties,
|
NullableProperties,
|
||||||
Point,
|
Point,
|
||||||
Positionable,
|
Positionable,
|
||||||
|
ReadOnlyPoint,
|
||||||
|
ReadOnlyRect,
|
||||||
Rect,
|
Rect,
|
||||||
Size
|
Size
|
||||||
} from './interfaces'
|
} from './interfaces'
|
||||||
@@ -234,11 +236,11 @@ export class LGraphCanvas
|
|||||||
implements CustomEventDispatcher<LGraphCanvasEventMap>
|
implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||||
{
|
{
|
||||||
// Optimised buffers used during rendering
|
// Optimised buffers used during rendering
|
||||||
static #temp = [0, 0, 0, 0] satisfies Rect
|
static #temp = new Float32Array(4)
|
||||||
static #temp_vec2 = [0, 0] satisfies Point
|
static #temp_vec2 = new Float32Array(2)
|
||||||
static #tmp_area = [0, 0, 0, 0] satisfies Rect
|
static #tmp_area = new Float32Array(4)
|
||||||
static #margin_area = [0, 0, 0, 0] satisfies Rect
|
static #margin_area = new Float32Array(4)
|
||||||
static #link_bounding = [0, 0, 0, 0] satisfies Rect
|
static #link_bounding = new Float32Array(4)
|
||||||
|
|
||||||
static DEFAULT_BACKGROUND_IMAGE =
|
static DEFAULT_BACKGROUND_IMAGE =
|
||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
|
||||||
@@ -626,7 +628,7 @@ export class LGraphCanvas
|
|||||||
dirty_area?: Rect | null
|
dirty_area?: Rect | null
|
||||||
/** @deprecated Unused */
|
/** @deprecated Unused */
|
||||||
node_in_panel?: LGraphNode | null
|
node_in_panel?: LGraphNode | null
|
||||||
last_mouse: Point = [0, 0]
|
last_mouse: ReadOnlyPoint = [0, 0]
|
||||||
last_mouseclick: number = 0
|
last_mouseclick: number = 0
|
||||||
graph: LGraph | Subgraph | null
|
graph: LGraph | Subgraph | null
|
||||||
get _graph(): LGraph | Subgraph {
|
get _graph(): LGraph | Subgraph {
|
||||||
@@ -2632,7 +2634,7 @@ export class LGraphCanvas
|
|||||||
pointer: CanvasPointer,
|
pointer: CanvasPointer,
|
||||||
node?: LGraphNode | undefined
|
node?: LGraphNode | undefined
|
||||||
): void {
|
): void {
|
||||||
const dragRect: [number, number, number, number] = [0, 0, 0, 0]
|
const dragRect = new Float32Array(4)
|
||||||
|
|
||||||
dragRect[0] = e.canvasX
|
dragRect[0] = e.canvasX
|
||||||
dragRect[1] = e.canvasY
|
dragRect[1] = e.canvasY
|
||||||
@@ -3172,7 +3174,7 @@ export class LGraphCanvas
|
|||||||
|
|
||||||
LGraphCanvas.active_canvas = this
|
LGraphCanvas.active_canvas = this
|
||||||
this.adjustMouseEvent(e)
|
this.adjustMouseEvent(e)
|
||||||
const mouse: Point = [e.clientX, e.clientY]
|
const mouse: ReadOnlyPoint = [e.clientX, e.clientY]
|
||||||
this.mouse[0] = mouse[0]
|
this.mouse[0] = mouse[0]
|
||||||
this.mouse[1] = mouse[1]
|
this.mouse[1] = mouse[1]
|
||||||
const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]
|
const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]
|
||||||
@@ -4075,10 +4077,7 @@ export class LGraphCanvas
|
|||||||
this.setDirty(true)
|
this.setDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleMultiSelect(
|
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Float32Array) {
|
||||||
e: CanvasPointerEvent,
|
|
||||||
dragRect: [number, number, number, number]
|
|
||||||
) {
|
|
||||||
// Process drag
|
// Process drag
|
||||||
// Convert Point pair (pos, offset) to Rect
|
// Convert Point pair (pos, offset) to Rect
|
||||||
const { graph, selectedItems, subgraph } = this
|
const { graph, selectedItems, subgraph } = this
|
||||||
@@ -4733,47 +4732,32 @@ export class LGraphCanvas
|
|||||||
for (const renderLink of renderLinks) {
|
for (const renderLink of renderLinks) {
|
||||||
const {
|
const {
|
||||||
fromSlot,
|
fromSlot,
|
||||||
fromPos: pos
|
fromPos: pos,
|
||||||
// fromDirection,
|
fromDirection,
|
||||||
// dragDirection
|
dragDirection
|
||||||
} = renderLink
|
} = renderLink
|
||||||
const connShape = fromSlot.shape
|
const connShape = fromSlot.shape
|
||||||
const connType = fromSlot.type
|
const connType = fromSlot.type
|
||||||
|
|
||||||
const color = resolveConnectingLinkColor(connType)
|
const colour = resolveConnectingLinkColor(connType)
|
||||||
|
|
||||||
// the connection being dragged by the mouse
|
// the connection being dragged by the mouse
|
||||||
if (
|
if (this.linkRenderer) {
|
||||||
this.linkRenderer &&
|
this.linkRenderer.renderDraggingLink(
|
||||||
renderLink.fromSlotIndex !== undefined &&
|
ctx,
|
||||||
renderLink.node !== undefined
|
pos,
|
||||||
) {
|
highlightPos,
|
||||||
const { fromSlotIndex, node } = renderLink
|
colour,
|
||||||
if (
|
fromDirection,
|
||||||
node instanceof LGraphNode &&
|
dragDirection,
|
||||||
('link' in fromSlot || 'links' in fromSlot)
|
{
|
||||||
) {
|
...this.buildLinkRenderContext(),
|
||||||
this.linkRenderer.renderDraggingLink(
|
linkMarkerShape: LinkMarkerShape.None
|
||||||
ctx,
|
}
|
||||||
node,
|
)
|
||||||
fromSlot,
|
|
||||||
fromSlotIndex,
|
|
||||||
highlightPos,
|
|
||||||
this.buildLinkRenderContext(),
|
|
||||||
{ fromInput: 'link' in fromSlot, color }
|
|
||||||
// pos,
|
|
||||||
// colour,
|
|
||||||
// fromDirection,
|
|
||||||
// dragDirection,
|
|
||||||
// {
|
|
||||||
// ...this.buildLinkRenderContext(),
|
|
||||||
// linkMarkerShape: LinkMarkerShape.None
|
|
||||||
// }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = colour
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
|
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
|
||||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
||||||
@@ -4864,7 +4848,7 @@ export class LGraphCanvas
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Get the target snap / highlight point in graph space */
|
/** Get the target snap / highlight point in graph space */
|
||||||
#getHighlightPosition(): Point {
|
#getHighlightPosition(): ReadOnlyPoint {
|
||||||
return LiteGraph.snaps_for_comfy
|
return LiteGraph.snaps_for_comfy
|
||||||
? this.linkConnector.state.snapLinksPos ??
|
? this.linkConnector.state.snapLinksPos ??
|
||||||
this._highlight_pos ??
|
this._highlight_pos ??
|
||||||
@@ -4879,7 +4863,7 @@ export class LGraphCanvas
|
|||||||
*/
|
*/
|
||||||
#renderSnapHighlight(
|
#renderSnapHighlight(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
highlightPos: Point
|
highlightPos: ReadOnlyPoint
|
||||||
): void {
|
): void {
|
||||||
const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos
|
const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos
|
||||||
if (!this._highlight_pos && !linkConnectorSnap) return
|
if (!this._highlight_pos && !linkConnectorSnap) return
|
||||||
@@ -5221,8 +5205,7 @@ export class LGraphCanvas
|
|||||||
// clip if required (mask)
|
// clip if required (mask)
|
||||||
const shape = node._shape || RenderShape.BOX
|
const shape = node._shape || RenderShape.BOX
|
||||||
const size = LGraphCanvas.#temp_vec2
|
const size = LGraphCanvas.#temp_vec2
|
||||||
size[0] = node.renderingSize[0]
|
size.set(node.renderingSize)
|
||||||
size[1] = node.renderingSize[1]
|
|
||||||
|
|
||||||
if (node.collapsed) {
|
if (node.collapsed) {
|
||||||
ctx.font = this.inner_text_font
|
ctx.font = this.inner_text_font
|
||||||
@@ -5417,10 +5400,7 @@ export class LGraphCanvas
|
|||||||
|
|
||||||
// Normalised node dimensions
|
// Normalised node dimensions
|
||||||
const area = LGraphCanvas.#tmp_area
|
const area = LGraphCanvas.#tmp_area
|
||||||
area[0] = node.boundingRect[0]
|
area.set(node.boundingRect)
|
||||||
area[1] = node.boundingRect[1]
|
|
||||||
area[2] = node.boundingRect[2]
|
|
||||||
area[3] = node.boundingRect[3]
|
|
||||||
area[0] -= node.pos[0]
|
area[0] -= node.pos[0]
|
||||||
area[1] -= node.pos[1]
|
area[1] -= node.pos[1]
|
||||||
|
|
||||||
@@ -5522,10 +5502,7 @@ export class LGraphCanvas
|
|||||||
shape = RenderShape.ROUND
|
shape = RenderShape.ROUND
|
||||||
) {
|
) {
|
||||||
const snapGuide = LGraphCanvas.#temp
|
const snapGuide = LGraphCanvas.#temp
|
||||||
snapGuide[0] = item.boundingRect[0]
|
snapGuide.set(item.boundingRect)
|
||||||
snapGuide[1] = item.boundingRect[1]
|
|
||||||
snapGuide[2] = item.boundingRect[2]
|
|
||||||
snapGuide[3] = item.boundingRect[3]
|
|
||||||
|
|
||||||
// Not all items have pos equal to top-left of bounds
|
// Not all items have pos equal to top-left of bounds
|
||||||
const { pos } = item
|
const { pos } = item
|
||||||
@@ -5965,8 +5942,8 @@ export class LGraphCanvas
|
|||||||
*/
|
*/
|
||||||
renderLink(
|
renderLink(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
a: Point,
|
a: ReadOnlyPoint,
|
||||||
b: Point,
|
b: ReadOnlyPoint,
|
||||||
link: LLink | null,
|
link: LLink | null,
|
||||||
skip_border: boolean,
|
skip_border: boolean,
|
||||||
flow: number | null,
|
flow: number | null,
|
||||||
@@ -5983,9 +5960,9 @@ export class LGraphCanvas
|
|||||||
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
|
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
|
||||||
reroute?: Reroute
|
reroute?: Reroute
|
||||||
/** Offset of the bezier curve control point from {@link a point a} (output side) */
|
/** Offset of the bezier curve control point from {@link a point a} (output side) */
|
||||||
startControl?: Point
|
startControl?: ReadOnlyPoint
|
||||||
/** Offset of the bezier curve control point from {@link b point b} (input side) */
|
/** Offset of the bezier curve control point from {@link b point b} (input side) */
|
||||||
endControl?: Point
|
endControl?: ReadOnlyPoint
|
||||||
/** Number of sublines (useful to represent vec3 or rgb) @todo If implemented, refactor calculations out of the loop */
|
/** Number of sublines (useful to represent vec3 or rgb) @todo If implemented, refactor calculations out of the loop */
|
||||||
num_sublines?: number
|
num_sublines?: number
|
||||||
/** Whether this is a floating link segment */
|
/** Whether this is a floating link segment */
|
||||||
@@ -8456,7 +8433,7 @@ export class LGraphCanvas
|
|||||||
* Starts an animation to fit the view around the specified selection of nodes.
|
* Starts an animation to fit the view around the specified selection of nodes.
|
||||||
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
||||||
*/
|
*/
|
||||||
animateToBounds(bounds: Rect | Rectangle, options: AnimationOptions = {}) {
|
animateToBounds(bounds: ReadOnlyRect, options: AnimationOptions = {}) {
|
||||||
const setDirty = () => this.setDirty(true, true)
|
const setDirty = () => this.setDirty(true, true)
|
||||||
this.ds.animateToBounds(bounds, setDirty, options)
|
this.ds.animateToBounds(bounds, setDirty, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
|
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
|
||||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
|
||||||
|
|
||||||
import type { LGraph } from './LGraph'
|
import type { LGraph } from './LGraph'
|
||||||
import { LGraphCanvas } from './LGraphCanvas'
|
import { LGraphCanvas } from './LGraphCanvas'
|
||||||
@@ -41,15 +40,15 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
title: string
|
title: string
|
||||||
font?: string
|
font?: string
|
||||||
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
||||||
_bounding: [number, number, number, number] = [
|
_bounding: Float32Array = new Float32Array([
|
||||||
10,
|
10,
|
||||||
10,
|
10,
|
||||||
LGraphGroup.minWidth,
|
LGraphGroup.minWidth,
|
||||||
LGraphGroup.minHeight
|
LGraphGroup.minHeight
|
||||||
]
|
])
|
||||||
|
|
||||||
_pos: Point = [10, 10]
|
_pos: Point = this._bounding.subarray(0, 2)
|
||||||
_size: Size = [LGraphGroup.minWidth, LGraphGroup.minHeight]
|
_size: Size = this._bounding.subarray(2, 4)
|
||||||
/** @deprecated See {@link _children} */
|
/** @deprecated See {@link _children} */
|
||||||
_nodes: LGraphNode[] = []
|
_nodes: LGraphNode[] = []
|
||||||
_children: Set<Positionable> = new Set()
|
_children: Set<Positionable> = new Set()
|
||||||
@@ -108,13 +107,8 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
|
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
get boundingRect(): Rectangle {
|
get boundingRect() {
|
||||||
return Rectangle.from([
|
return this._bounding
|
||||||
this._pos[0],
|
|
||||||
this._pos[1],
|
|
||||||
this._size[0],
|
|
||||||
this._size[1]
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get nodes() {
|
get nodes() {
|
||||||
@@ -151,17 +145,14 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
configure(o: ISerialisedGroup): void {
|
configure(o: ISerialisedGroup): void {
|
||||||
this.id = o.id
|
this.id = o.id
|
||||||
this.title = o.title
|
this.title = o.title
|
||||||
this._pos[0] = o.bounding[0]
|
this._bounding.set(o.bounding)
|
||||||
this._pos[1] = o.bounding[1]
|
|
||||||
this._size[0] = o.bounding[2]
|
|
||||||
this._size[1] = o.bounding[3]
|
|
||||||
this.color = o.color
|
this.color = o.color
|
||||||
this.flags = o.flags || this.flags
|
this.flags = o.flags || this.flags
|
||||||
if (o.font_size) this.font_size = o.font_size
|
if (o.font_size) this.font_size = o.font_size
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): ISerialisedGroup {
|
serialize(): ISerialisedGroup {
|
||||||
const b = [this._pos[0], this._pos[1], this._size[0], this._size[1]]
|
const b = this._bounding
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
@@ -219,7 +210,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (LiteGraph.highlight_selected_group && this.selected) {
|
if (LiteGraph.highlight_selected_group && this.selected) {
|
||||||
strokeShape(ctx, this.boundingRect, {
|
strokeShape(ctx, this._bounding, {
|
||||||
title_height: this.titleHeight,
|
title_height: this.titleHeight,
|
||||||
padding
|
padding
|
||||||
})
|
})
|
||||||
@@ -260,7 +251,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
|
|
||||||
// Move nodes we overlap the centre point of
|
// Move nodes we overlap the centre point of
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (containsCentre(this.boundingRect, node.boundingRect)) {
|
if (containsCentre(this._bounding, node.boundingRect)) {
|
||||||
this._nodes.push(node)
|
this._nodes.push(node)
|
||||||
children.add(node)
|
children.add(node)
|
||||||
}
|
}
|
||||||
@@ -268,13 +259,12 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
|||||||
|
|
||||||
// Move reroutes we overlap the centre point of
|
// Move reroutes we overlap the centre point of
|
||||||
for (const reroute of reroutes.values()) {
|
for (const reroute of reroutes.values()) {
|
||||||
if (isPointInRect(reroute.pos, this.boundingRect)) children.add(reroute)
|
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move groups we wholly contain
|
// Move groups we wholly contain
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (containsRect(this.boundingRect, group.boundingRect))
|
if (containsRect(this._bounding, group._bounding)) children.add(group)
|
||||||
children.add(group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.sort((a, b) => {
|
groups.sort((a, b) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type { Reroute, RerouteId } from './Reroute'
|
|||||||
import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots'
|
import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots'
|
||||||
import type { IDrawBoundingOptions } from './draw'
|
import type { IDrawBoundingOptions } from './draw'
|
||||||
import { NullGraphError } from './infrastructure/NullGraphError'
|
import { NullGraphError } from './infrastructure/NullGraphError'
|
||||||
|
import type { ReadOnlyRectangle } from './infrastructure/Rectangle'
|
||||||
import { Rectangle } from './infrastructure/Rectangle'
|
import { Rectangle } from './infrastructure/Rectangle'
|
||||||
import type {
|
import type {
|
||||||
ColorOption,
|
ColorOption,
|
||||||
@@ -36,6 +37,8 @@ import type {
|
|||||||
ISlotType,
|
ISlotType,
|
||||||
Point,
|
Point,
|
||||||
Positionable,
|
Positionable,
|
||||||
|
ReadOnlyPoint,
|
||||||
|
ReadOnlyRect,
|
||||||
Rect,
|
Rect,
|
||||||
Size
|
Size
|
||||||
} from './interfaces'
|
} from './interfaces'
|
||||||
@@ -384,7 +387,7 @@ export class LGraphNode
|
|||||||
* Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}.
|
* Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}.
|
||||||
* WARNING: Making changes to boundingRect via onBounding is poorly supported, and will likely result in strange behaviour.
|
* WARNING: Making changes to boundingRect via onBounding is poorly supported, and will likely result in strange behaviour.
|
||||||
*/
|
*/
|
||||||
onBounding?(this: LGraphNode, out: Rectangle): void
|
onBounding?(this: LGraphNode, out: Rect): void
|
||||||
console?: string[]
|
console?: string[]
|
||||||
_level?: number
|
_level?: number
|
||||||
_shape?: RenderShape
|
_shape?: RenderShape
|
||||||
@@ -410,12 +413,12 @@ export class LGraphNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc {@link renderArea} */
|
/** @inheritdoc {@link renderArea} */
|
||||||
#renderArea: [number, number, number, number] = [0, 0, 0, 0]
|
#renderArea: Float32Array = new Float32Array(4)
|
||||||
/**
|
/**
|
||||||
* Rect describing the node area, including shadows and any protrusions.
|
* Rect describing the node area, including shadows and any protrusions.
|
||||||
* Determines if the node is visible. Calculated once at the start of every frame.
|
* Determines if the node is visible. Calculated once at the start of every frame.
|
||||||
*/
|
*/
|
||||||
get renderArea(): Rect {
|
get renderArea(): ReadOnlyRect {
|
||||||
return this.#renderArea
|
return this.#renderArea
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,12 +429,12 @@ export class LGraphNode
|
|||||||
*
|
*
|
||||||
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
|
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
|
||||||
*/
|
*/
|
||||||
get boundingRect(): Rectangle {
|
get boundingRect(): ReadOnlyRectangle {
|
||||||
return this.#boundingRect
|
return this.#boundingRect
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
|
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
|
||||||
get boundingOffset(): Point {
|
get boundingOffset(): ReadOnlyPoint {
|
||||||
const {
|
const {
|
||||||
pos: [posX, posY],
|
pos: [posX, posY],
|
||||||
boundingRect: [bX, bY]
|
boundingRect: [bX, bY]
|
||||||
@@ -440,9 +443,9 @@ export class LGraphNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** {@link pos} and {@link size} values are backed by this {@link Rect}. */
|
/** {@link pos} and {@link size} values are backed by this {@link Rect}. */
|
||||||
_posSize: [number, number, number, number] = [0, 0, 0, 0]
|
_posSize: Float32Array = new Float32Array(4)
|
||||||
_pos: Point = [0, 0]
|
_pos: Point = this._posSize.subarray(0, 2)
|
||||||
_size: Size = [0, 0]
|
_size: Size = this._posSize.subarray(2, 4)
|
||||||
|
|
||||||
public get pos() {
|
public get pos() {
|
||||||
return this._pos
|
return this._pos
|
||||||
@@ -1650,7 +1653,7 @@ export class LGraphNode
|
|||||||
inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1,
|
inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1,
|
||||||
outputs ? outputs.length : 1
|
outputs ? outputs.length : 1
|
||||||
)
|
)
|
||||||
const size = out || [0, 0]
|
const size = out || new Float32Array([0, 0])
|
||||||
rows = Math.max(rows, 1)
|
rows = Math.max(rows, 1)
|
||||||
// although it should be graphcanvas.inner_text_font size
|
// although it should be graphcanvas.inner_text_font size
|
||||||
const font_size = LiteGraph.NODE_TEXT_SIZE
|
const font_size = LiteGraph.NODE_TEXT_SIZE
|
||||||
@@ -1975,7 +1978,7 @@ export class LGraphNode
|
|||||||
* @param out `x, y, width, height` are written to this array.
|
* @param out `x, y, width, height` are written to this array.
|
||||||
* @param ctx The canvas context to use for measuring text.
|
* @param ctx The canvas context to use for measuring text.
|
||||||
*/
|
*/
|
||||||
measure(out: Rectangle, ctx: CanvasRenderingContext2D): void {
|
measure(out: Rect, ctx: CanvasRenderingContext2D): void {
|
||||||
const titleMode = this.title_mode
|
const titleMode = this.title_mode
|
||||||
const renderTitle =
|
const renderTitle =
|
||||||
titleMode != TitleMode.TRANSPARENT_TITLE &&
|
titleMode != TitleMode.TRANSPARENT_TITLE &&
|
||||||
@@ -2001,13 +2004,13 @@ export class LGraphNode
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* returns the bounding of the object, used for rendering purposes
|
* returns the bounding of the object, used for rendering purposes
|
||||||
* @param out {Rect?} [optional] a place to store the output, to free garbage
|
* @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
|
||||||
* @param includeExternal {boolean?} [optional] set to true to
|
* @param includeExternal {boolean?} [optional] set to true to
|
||||||
* include the shadow and connection points in the bounding calculation
|
* include the shadow and connection points in the bounding calculation
|
||||||
* @returns the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
|
* @returns the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
|
||||||
*/
|
*/
|
||||||
getBounding(out?: Rect, includeExternal?: boolean): Rect {
|
getBounding(out?: Rect, includeExternal?: boolean): Rect {
|
||||||
out ||= [0, 0, 0, 0]
|
out ||= new Float32Array(4)
|
||||||
|
|
||||||
const rect = includeExternal ? this.renderArea : this.boundingRect
|
const rect = includeExternal ? this.renderArea : this.boundingRect
|
||||||
out[0] = rect[0]
|
out[0] = rect[0]
|
||||||
@@ -2028,10 +2031,7 @@ export class LGraphNode
|
|||||||
this.onBounding?.(bounds)
|
this.onBounding?.(bounds)
|
||||||
|
|
||||||
const renderArea = this.#renderArea
|
const renderArea = this.#renderArea
|
||||||
renderArea[0] = bounds[0]
|
renderArea.set(bounds)
|
||||||
renderArea[1] = bounds[1]
|
|
||||||
renderArea[2] = bounds[2]
|
|
||||||
renderArea[3] = bounds[3]
|
|
||||||
// 4 offset for collapsed node connection points
|
// 4 offset for collapsed node connection points
|
||||||
renderArea[0] -= 4
|
renderArea[0] -= 4
|
||||||
renderArea[1] -= 4
|
renderArea[1] -= 4
|
||||||
@@ -3174,7 +3174,7 @@ export class LGraphNode
|
|||||||
* @returns the position
|
* @returns the position
|
||||||
*/
|
*/
|
||||||
getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point {
|
getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point {
|
||||||
out ||= [0, 0]
|
out ||= new Float32Array(2)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pos: [nodeX, nodeY],
|
pos: [nodeX, nodeY],
|
||||||
@@ -3839,7 +3839,7 @@ export class LGraphNode
|
|||||||
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
|
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
#measureSlots(): Rect | null {
|
#measureSlots(): ReadOnlyRect | null {
|
||||||
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
|
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
|
||||||
|
|
||||||
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
|
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
|||||||
data?: number | string | boolean | { toToolTip?(): string }
|
data?: number | string | boolean | { toToolTip?(): string }
|
||||||
_data?: unknown
|
_data?: unknown
|
||||||
/** Centre point of the link, calculated during render only - can be inaccurate */
|
/** Centre point of the link, calculated during render only - can be inaccurate */
|
||||||
_pos: [number, number]
|
_pos: Float32Array
|
||||||
/** @todo Clean up - never implemented in comfy. */
|
/** @todo Clean up - never implemented in comfy. */
|
||||||
_last_time?: number
|
_last_time?: number
|
||||||
/** The last canvas 2D path that was used to render this link */
|
/** The last canvas 2D path that was used to render this link */
|
||||||
@@ -171,7 +171,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
|||||||
|
|
||||||
this._data = null
|
this._data = null
|
||||||
// center
|
// center
|
||||||
this._pos = [0, 0]
|
this._pos = new Float32Array(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use {@link LLink.create} */
|
/** @deprecated Use {@link LLink.create} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ import type {
|
|||||||
LinkSegment,
|
LinkSegment,
|
||||||
Point,
|
Point,
|
||||||
Positionable,
|
Positionable,
|
||||||
ReadonlyLinkNetwork,
|
ReadOnlyRect,
|
||||||
Rect
|
ReadonlyLinkNetwork
|
||||||
} from './interfaces'
|
} from './interfaces'
|
||||||
import { distance, isPointInRect } from './measure'
|
import { distance, isPointInRect } from './measure'
|
||||||
import type { Serialisable, SerialisableReroute } from './types/serialisation'
|
import type { Serialisable, SerialisableReroute } from './types/serialisation'
|
||||||
@@ -50,6 +49,8 @@ export class Reroute
|
|||||||
return Reroute.radius + gap + Reroute.slotRadius
|
return Reroute.radius + gap + Reroute.slotRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#malloc = new Float32Array(8)
|
||||||
|
|
||||||
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
||||||
#network: WeakRef<LinkNetwork>
|
#network: WeakRef<LinkNetwork>
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ export class Reroute
|
|||||||
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
|
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
|
||||||
floating?: FloatingRerouteSlot
|
floating?: FloatingRerouteSlot
|
||||||
|
|
||||||
#pos: [number, number] = [0, 0]
|
#pos = this.#malloc.subarray(0, 2)
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
get pos(): Point {
|
get pos(): Point {
|
||||||
return this.#pos
|
return this.#pos
|
||||||
@@ -88,17 +89,17 @@ export class Reroute
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
get boundingRect(): Rectangle {
|
get boundingRect(): ReadOnlyRect {
|
||||||
const { radius } = Reroute
|
const { radius } = Reroute
|
||||||
const [x, y] = this.#pos
|
const [x, y] = this.#pos
|
||||||
return Rectangle.from([x - radius, y - radius, 2 * radius, 2 * radius])
|
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
|
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
|
||||||
* Eliminates most hover positions using an extremely cheap check.
|
* Eliminates most hover positions using an extremely cheap check.
|
||||||
*/
|
*/
|
||||||
get #hoverArea(): Rect {
|
get #hoverArea(): ReadOnlyRect {
|
||||||
const xOffset = 2 * Reroute.slotOffset
|
const xOffset = 2 * Reroute.slotOffset
|
||||||
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
|
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
|
||||||
|
|
||||||
@@ -125,14 +126,14 @@ export class Reroute
|
|||||||
sin: number = 0
|
sin: number = 0
|
||||||
|
|
||||||
/** Bezier curve control point for the "target" (input) side of the link */
|
/** Bezier curve control point for the "target" (input) side of the link */
|
||||||
controlPoint: [number, number] = [0, 0]
|
controlPoint: Point = this.#malloc.subarray(4, 6)
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
path?: Path2D
|
path?: Path2D
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_centreAngle?: number
|
_centreAngle?: number
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_pos: [number, number] = [0, 0]
|
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_dragging?: boolean
|
_dragging?: boolean
|
||||||
|
|||||||