Compare commits
1 Commits
v1.26.0
...
sno-fireba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d25e53861 |
@@ -171,7 +171,53 @@ echo "Last stable release: $LAST_STABLE"
|
||||
|
||||
### Step 7: Analyze Dependency Updates
|
||||
|
||||
1. **Check significant dependency updates:**
|
||||
1. **Check for dependency version changes:**
|
||||
```bash
|
||||
# Compare package.json between versions to detect dependency updates
|
||||
PREV_PACKAGE_JSON=$(git show ${BASE_TAG}:package.json 2>/dev/null || echo '{}')
|
||||
CURRENT_PACKAGE_JSON=$(cat package.json)
|
||||
|
||||
# Extract litegraph versions
|
||||
PREV_LITEGRAPH=$(echo "$PREV_PACKAGE_JSON" | grep -o '"@comfyorg/litegraph": "[^"]*"' | grep -o '[0-9][^"]*' || echo "not found")
|
||||
CURRENT_LITEGRAPH=$(echo "$CURRENT_PACKAGE_JSON" | grep -o '"@comfyorg/litegraph": "[^"]*"' | grep -o '[0-9][^"]*' || echo "not found")
|
||||
|
||||
echo "Litegraph version change: ${PREV_LITEGRAPH} → ${CURRENT_LITEGRAPH}"
|
||||
```
|
||||
|
||||
2. **Generate litegraph changelog if version changed:**
|
||||
```bash
|
||||
if [ "$PREV_LITEGRAPH" != "$CURRENT_LITEGRAPH" ] && [ "$PREV_LITEGRAPH" != "not found" ]; then
|
||||
echo "📦 Fetching litegraph changes between v${PREV_LITEGRAPH} and v${CURRENT_LITEGRAPH}..."
|
||||
|
||||
# Clone or update litegraph repo for changelog analysis
|
||||
if [ ! -d ".temp-litegraph" ]; then
|
||||
git clone https://github.com/comfyanonymous/litegraph.js.git .temp-litegraph
|
||||
else
|
||||
cd .temp-litegraph && git fetch --all && cd ..
|
||||
fi
|
||||
|
||||
# Get litegraph changelog between versions
|
||||
LITEGRAPH_CHANGES=$(cd .temp-litegraph && git log v${PREV_LITEGRAPH}..v${CURRENT_LITEGRAPH} --oneline --no-merges 2>/dev/null || \
|
||||
git log --oneline --no-merges --since="$(git log -1 --format=%ci ${BASE_TAG})" --until="$(git log -1 --format=%ci HEAD)" 2>/dev/null || \
|
||||
echo "Unable to fetch litegraph changes")
|
||||
|
||||
# Categorize litegraph changes
|
||||
LITEGRAPH_FEATURES=$(echo "$LITEGRAPH_CHANGES" | grep -iE "(feat|feature|add)" || echo "")
|
||||
LITEGRAPH_FIXES=$(echo "$LITEGRAPH_CHANGES" | grep -iE "(fix|bug)" || echo "")
|
||||
LITEGRAPH_BREAKING=$(echo "$LITEGRAPH_CHANGES" | grep -iE "(break|breaking)" || echo "")
|
||||
LITEGRAPH_OTHER=$(echo "$LITEGRAPH_CHANGES" | grep -viE "(feat|feature|add|fix|bug|break|breaking)" || echo "")
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf .temp-litegraph
|
||||
|
||||
echo "✅ Litegraph changelog extracted"
|
||||
else
|
||||
echo "ℹ️ No litegraph version change detected"
|
||||
LITEGRAPH_CHANGES=""
|
||||
fi
|
||||
```
|
||||
|
||||
3. **Check other significant dependency updates:**
|
||||
```bash
|
||||
# Extract all dependency changes for major version bumps
|
||||
OTHER_DEP_CHANGES=""
|
||||
@@ -348,14 +394,6 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
sleep 10
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
```
|
||||
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
|
||||
- Create a `core/x.yy` branch for the PREVIOUS minor version
|
||||
- Apply branch protection rules
|
||||
- Document the feature freeze policy
|
||||
```bash
|
||||
# Monitor branch creation (for minor/major releases)
|
||||
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
|
||||
```
|
||||
4. If workflow didn't trigger due to [skip ci]:
|
||||
```bash
|
||||
echo "ERROR: Release workflow didn't trigger!"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
description: Creating unit tests
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Creating unit tests
|
||||
|
||||
- This project uses `vitest` for unit testing
|
||||
- Tests are stored in the `test/` directory
|
||||
- Tests should be cross-platform compatible; able to run on Windows, macOS, and linux
|
||||
- e.g. the use of `path.resolve`, or `path.join` and `path.sep` to ensure that tests work the same on all platforms
|
||||
- Tests should be mocked properly
|
||||
- Mocks should be cleanly written and easy to understand
|
||||
- Mocks should be re-usable where possible
|
||||
|
||||
## Unit test style
|
||||
|
||||
- Prefer the use of `test.extend` over loose variables
|
||||
- To achieve this, import `test as baseTest` from `vitest`
|
||||
- Never use `it`; `test` should be used in place of this
|
||||
165
.github/workflows/backport.yaml
vendored
@@ -1,165 +0,0 @@
|
||||
name: Auto Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Extract version labels
|
||||
id: versions
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
for label in $(echo "$LABELS" | jq -r '.[].name'); do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
|
||||
VERSIONS="${VERSIONS}${label} "
|
||||
else
|
||||
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VERSIONS" ]; then
|
||||
echo "::error::No version labels found (e.g., 1.24, 1.22)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
|
||||
- name: Backport commits
|
||||
id: backport
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
run: |
|
||||
FAILED=""
|
||||
SUCCESS=""
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
|
||||
TARGET_BRANCH="core/${version}"
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
|
||||
|
||||
# Fetch target branch (fail if doesn't exist)
|
||||
if ! git fetch origin "${TARGET_BRANCH}"; then
|
||||
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
|
||||
FAILED="${FAILED}${version}:branch-missing "
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create backport branch
|
||||
git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
|
||||
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
git checkout main
|
||||
else
|
||||
# Get conflict info
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',')
|
||||
git cherry-pick --abort
|
||||
|
||||
echo "::error::Cherry-pick failed due to conflicts"
|
||||
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
|
||||
|
||||
# Clean up the failed branch
|
||||
git checkout main
|
||||
git branch -D "${BACKPORT_BRANCH}"
|
||||
fi
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
|
||||
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -n "${FAILED}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create PR for each successful backport
|
||||
if: steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
|
||||
if PR_URL=$(gh pr create \
|
||||
--base "core/${version}" \
|
||||
--head "${branch}" \
|
||||
--title "[backport ${version}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--label "backport" 2>&1); then
|
||||
|
||||
# Extract PR number from URL
|
||||
PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$')
|
||||
|
||||
if [ -n "${PR_NUM}" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
|
||||
fi
|
||||
else
|
||||
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
|
||||
# Still try to comment on the original PR about the failure
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Comment on failures
|
||||
if: failure() && steps.backport.outputs.failed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
# Convert comma-separated conflicts back to newlines for display
|
||||
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
||||
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
fi
|
||||
done
|
||||
@@ -1,214 +0,0 @@
|
||||
name: Create Release Branch
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'package.json'
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Release')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Check version bump type
|
||||
id: check_version
|
||||
run: |
|
||||
# Get current version from main
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
# Remove 'v' prefix if present (shouldn't be in package.json, but defensive)
|
||||
CURRENT_VERSION=${CURRENT_VERSION#v}
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Validate version format
|
||||
if ! [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "ERROR: Invalid version format: $CURRENT_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract major and minor versions
|
||||
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
|
||||
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
|
||||
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3 | cut -d- -f1)
|
||||
|
||||
echo "major=$MAJOR" >> $GITHUB_OUTPUT
|
||||
echo "minor=$MINOR" >> $GITHUB_OUTPUT
|
||||
echo "patch=$PATCH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get previous version from the commit before the merge
|
||||
git checkout HEAD^1
|
||||
PREV_VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")
|
||||
# Remove 'v' prefix if present
|
||||
PREV_VERSION=${PREV_VERSION#v}
|
||||
|
||||
# Validate previous version format
|
||||
if ! [[ "$PREV_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "WARNING: Invalid previous version format: $PREV_VERSION, using 0.0.0"
|
||||
PREV_VERSION="0.0.0"
|
||||
fi
|
||||
|
||||
PREV_MINOR=$(echo $PREV_VERSION | cut -d. -f2)
|
||||
|
||||
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "prev_minor=$PREV_MINOR" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get previous major version for comparison
|
||||
PREV_MAJOR=$(echo $PREV_VERSION | cut -d. -f1)
|
||||
|
||||
# Check if current version is a pre-release
|
||||
if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
|
||||
IS_PRERELEASE=true
|
||||
else
|
||||
IS_PRERELEASE=false
|
||||
fi
|
||||
|
||||
# Check if this was a minor version bump or major version bump
|
||||
# But skip if it's a pre-release version
|
||||
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||
echo "is_minor_bump=false" >> $GITHUB_OUTPUT
|
||||
echo "reason=prerelease version" >> $GITHUB_OUTPUT
|
||||
elif [[ "$MAJOR" -gt "$PREV_MAJOR" && "$MINOR" == "0" && "$PATCH" == "0" ]]; then
|
||||
# Major version bump (e.g., 1.99.x → 2.0.0)
|
||||
echo "is_minor_bump=true" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME="core/${PREV_MAJOR}.${PREV_MINOR}"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
elif [[ "$MAJOR" == "$PREV_MAJOR" && "$MINOR" -gt "$PREV_MINOR" && "$PATCH" == "0" ]]; then
|
||||
# Minor version bump (e.g., 1.23.x → 1.24.0)
|
||||
echo "is_minor_bump=true" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME="core/${MAJOR}.${PREV_MINOR}"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_minor_bump=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Return to main branch
|
||||
git checkout main
|
||||
|
||||
- name: Create release branch
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
|
||||
# Check if branch already exists
|
||||
if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
|
||||
echo "⚠️ Branch $BRANCH_NAME already exists, skipping creation"
|
||||
echo "branch_exists=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
else
|
||||
echo "branch_exists=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Create branch from the commit BEFORE the version bump
|
||||
# This ensures the release branch has the previous minor version
|
||||
git checkout -b "$BRANCH_NAME" HEAD^1
|
||||
|
||||
# Push the new branch
|
||||
git push origin "$BRANCH_NAME"
|
||||
|
||||
echo "✅ Created release branch: $BRANCH_NAME"
|
||||
echo "This branch is now in feature freeze and will only receive:"
|
||||
echo "- Bug fixes"
|
||||
echo "- Critical security patches"
|
||||
echo "- Documentation updates"
|
||||
|
||||
- name: Create branch protection rules
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true' && env.branch_exists != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
|
||||
# Create branch protection using GitHub API
|
||||
echo "Setting up branch protection for $BRANCH_NAME..."
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/branches/$BRANCH_NAME/protection" \
|
||||
-d '{
|
||||
"required_status_checks": {
|
||||
"strict": true,
|
||||
"contexts": ["build", "test"]
|
||||
},
|
||||
"enforce_admins": false,
|
||||
"required_pull_request_reviews": {
|
||||
"required_approving_review_count": 1,
|
||||
"dismiss_stale_reviews": true
|
||||
},
|
||||
"restrictions": null,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false
|
||||
}')
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [[ "$HTTP_CODE" -eq 200 ]] || [[ "$HTTP_CODE" -eq 201 ]]; then
|
||||
echo "✅ Branch protection successfully applied"
|
||||
else
|
||||
echo "⚠️ Failed to apply branch protection (HTTP $HTTP_CODE)"
|
||||
echo "Response: $BODY"
|
||||
# Don't fail the workflow, just warn
|
||||
fi
|
||||
|
||||
- name: Post summary
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
PREV_VERSION="${{ steps.check_version.outputs.prev_version }}"
|
||||
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
|
||||
|
||||
if [[ "${{ env.branch_exists }}" == "true" ]]; then
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
## 🌿 Release Branch Already Exists
|
||||
|
||||
The release branch for the previous minor version already exists:
|
||||
EOF
|
||||
else
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
## 🌿 Release Branch Created
|
||||
|
||||
A new release branch has been created for the previous minor version:
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
|
||||
- **Branch**: \`$BRANCH_NAME\`
|
||||
- **Version**: \`$PREV_VERSION\` (feature frozen)
|
||||
- **Main branch**: \`$CURRENT_VERSION\` (active development)
|
||||
|
||||
### Branch Policy
|
||||
|
||||
The \`$BRANCH_NAME\` branch is now in **feature freeze** and will only accept:
|
||||
- 🐛 Bug fixes
|
||||
- 🔒 Security patches
|
||||
- 📚 Documentation updates
|
||||
|
||||
All new features should continue to be developed against \`main\`.
|
||||
|
||||
### Backporting Changes
|
||||
|
||||
To backport a fix to this release branch:
|
||||
1. Create your fix on \`main\` first
|
||||
2. Cherry-pick to \`$BRANCH_NAME\`
|
||||
3. Create a PR targeting \`$BRANCH_NAME\`
|
||||
4. Use the \`Release\` label on the PR
|
||||
EOF
|
||||
43
.github/workflows/update-litegraph.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Update Litegraph Dependency
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-litegraph:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Update litegraph
|
||||
run: npm install @comfyorg/litegraph@latest
|
||||
|
||||
- name: Get new version
|
||||
id: get-version
|
||||
run: |
|
||||
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package-lock.json')).packages['node_modules/@comfyorg/litegraph'].version)")
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
title: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
body: |
|
||||
Automated update of litegraph to version ${{ steps.get-version.outputs.NEW_VERSION }}.
|
||||
Ref: https://github.com/Comfy-Org/litegraph.js/releases/tag/v${{ steps.get-version.outputs.NEW_VERSION }}
|
||||
branch: update-litegraph-${{ steps.get-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
labels: |
|
||||
dependencies
|
||||
7
.github/workflows/update-manager-types.yaml
vendored
@@ -3,11 +3,6 @@ name: Update ComfyUI-Manager API Types
|
||||
on:
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_branch:
|
||||
description: 'Target branch for the PR'
|
||||
required: true
|
||||
default: 'main'
|
||||
|
||||
jobs:
|
||||
update-manager-types:
|
||||
@@ -90,7 +85,7 @@ jobs:
|
||||
|
||||
These types are automatically generated using openapi-typescript.
|
||||
branch: update-manager-types-${{ steps.manager-info.outputs.commit }}
|
||||
base: ${{ inputs.target_branch }}
|
||||
base: main
|
||||
labels: Manager
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
|
||||
11
CLAUDE.md
@@ -6,9 +6,6 @@
|
||||
- `npm run typecheck`: Type checking
|
||||
- `npm run lint`: Linting
|
||||
- `npm run format`: Prettier formatting
|
||||
- `npm run test:component`: Run component tests with browser environment
|
||||
- `npm run test:unit`: Run all unit tests
|
||||
- `npm run test:unit -- tests-ui/tests/example.test.ts`: Run single test file
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -51,11 +48,3 @@ When referencing Comfy-Org repos:
|
||||
1. Check for local copy
|
||||
2. Use GitHub API for branches/PRs/metadata
|
||||
3. Curl GitHub website if needed
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- NEVER use `any` type - use proper TypeScript types
|
||||
- NEVER use `as any` type assertions - fix the underlying type issue
|
||||
- NEVER use `--no-verify` flag when committing
|
||||
- NEVER delete or disable tests to make them pass
|
||||
- NEVER circumvent quality checks
|
||||
|
||||
327
CONTRIBUTING.md
@@ -1,327 +0,0 @@
|
||||
# Contributing to ComfyUI Frontend
|
||||
|
||||
We're building this frontend together and would love your help — no matter how you'd like to pitch in! You don't need to write code to make a difference.
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
- **Pull Requests:** Add features, fix bugs, or improve code health. Browse [issues](https://github.com/Comfy-Org/ComfyUI_frontend/issues) for inspiration. Look for the `Good first issue` label if you're new to the project.
|
||||
- **Vote on Features:** Give a 👍 to the feature requests you care about to help us prioritize.
|
||||
- **Verify Bugs:** Try reproducing reported issues and share your results (even if the bug doesn't occur!).
|
||||
- **Community Support:** Hop into our [Discord](https://discord.com/invite/comfyorg) to answer questions or get help.
|
||||
- **Share & Advocate:** Tell your friends, tweet about us, or share tips to support the project.
|
||||
|
||||
Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites & Technology Stack
|
||||
|
||||
- **Required Software**:
|
||||
- Node.js (v16 or later; v20/v22 strongly recommended) and npm
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance
|
||||
|
||||
- **Tech Stack**:
|
||||
- [Vue 3.5 Composition API](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Pinia](https://pinia.vuejs.org/) for state management
|
||||
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
|
||||
- litegraph.js (integrated in src/lib) for node editor
|
||||
- [zod](https://zod.dev/) for schema validation
|
||||
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/Comfy-Org/ComfyUI_frontend.git
|
||||
cd ComfyUI_frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Configure environment (optional):
|
||||
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
|
||||
|
||||
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
|
||||
|
||||
### Dev Server Configuration
|
||||
|
||||
To launch ComfyUI and have it connect to your development server:
|
||||
|
||||
```bash
|
||||
python main.py --port 8188
|
||||
```
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
|
||||
|
||||
### Dev Server
|
||||
|
||||
Note: The dev server will NOT load any extension from the ComfyUI server. Only core extensions will be loaded.
|
||||
|
||||
- Start local ComfyUI backend at `localhost:8188`
|
||||
- Run `npm run dev` to start the dev server
|
||||
- Run `npm run dev:electron` to start the dev server with electron API mocked
|
||||
|
||||
#### Access dev server on touch devices
|
||||
|
||||
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.
|
||||
|
||||
After you start the dev server, you should see following logs:
|
||||
|
||||
```
|
||||
> comfyui-frontend@1.3.42 dev
|
||||
> vite
|
||||
|
||||
|
||||
VITE v5.4.6 ready in 488 ms
|
||||
|
||||
➜ Local: http://localhost:5173/
|
||||
➜ Network: http://172.21.80.1:5173/
|
||||
➜ Network: http://192.168.2.20:5173/
|
||||
➜ press h + enter to show help
|
||||
```
|
||||
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Architecture Decision Records
|
||||
|
||||
We document significant architectural decisions using ADRs (Architecture Decision Records). See [docs/adr/](docs/adr/) for all ADRs and the template for creating new ones.
|
||||
|
||||
### Backporting Changes to Release Branches
|
||||
|
||||
When you fix a bug that affects a version in feature freeze, we use an automated backport process to apply the fix to the release candidate branch.
|
||||
|
||||
#### Real Example
|
||||
|
||||
- Subgraphs feature was released in v1.24
|
||||
- While developing v1.25, we discovered a bug in subgraphs
|
||||
- v1.24 is in feature freeze (only accepting bug fixes, no new features)
|
||||
- The fix needs to be applied to both main (v1.25) and the v1.24 release candidate
|
||||
|
||||
#### How to Backport Your Fix
|
||||
|
||||
1. Create your PR fixing the bug on `main` branch as usual
|
||||
2. Before merging, add these labels to your PR:
|
||||
- `needs-backport` - triggers the automated backport workflow
|
||||
- `1.24` - targets the `core/1.24` release candidate branch
|
||||
|
||||
3. Merge your PR normally
|
||||
4. The automated workflow will:
|
||||
- Create a new branch from `core/1.24`
|
||||
- Apply your changes to that branch
|
||||
- Open a new PR to `core/1.24`
|
||||
- Comment on your original PR with a link to the backport PR
|
||||
|
||||
#### When to Use Backporting
|
||||
|
||||
- Bug fixes for features already released
|
||||
- Security fixes
|
||||
- Critical issues affecting existing functionality
|
||||
- Never for new features (these wait for the next release cycle)
|
||||
|
||||
#### Handling Conflicts
|
||||
|
||||
If the automated cherry-pick fails due to conflicts, the workflow will comment on your PR with:
|
||||
- The list of conflicting files
|
||||
- Instructions to manually cherry-pick to the release candidate branch
|
||||
|
||||
See [PR #4616](https://github.com/Comfy-Org/ComfyUI_frontend/pull/4616) for the actual subgraph bugfix that was backported from v1.25 to v1.24.
|
||||
|
||||
## Code Editor Configuration
|
||||
|
||||
### Recommended Setup
|
||||
|
||||
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
|
||||
|
||||
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
|
||||
### MCP Integrations
|
||||
|
||||
#### Playwright Browser Automation
|
||||
|
||||
The Playwright MCP server enables LLM coding assistants (like Copilot, Claude Code, etc.) to control and inspect web browsers programmatically. This allows assistants to navigate websites, take screenshots, and interact with web pages on your behalf.
|
||||
|
||||
For ComfyUI_frontend development, you can ask coding assistants to use Playwright screenshots to continuously verify that your code changes produce the expected visual results.
|
||||
|
||||
##### Setup for Claude Code
|
||||
|
||||
After installing dependencies with `npm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
|
||||
|
||||
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
|
||||
|
||||
```
|
||||
> navigate to localhost:5173 and take a screenshot.
|
||||
tell me the items in the menubar
|
||||
|
||||
● I'll navigate to localhost:5173 and take a
|
||||
screenshot to see the menubar items.
|
||||
|
||||
● playwright:playwright_navigate (MCP)(url: …
|
||||
"http://local
|
||||
host:5173")
|
||||
⎿ Navigated to http://localhost:5173
|
||||
|
||||
● playwright:playwright_screenshot (MCP)(name: "lo…
|
||||
alhost_5173
|
||||
_menubar")
|
||||
|
||||
⎿ Screenshot saved to: ../../../Downloads/localho
|
||||
st_5173_menubar-2025-05-30T17-23-00-942Z.png
|
||||
⎿ Screenshot also stored in memory with name:
|
||||
'localhost_5173_menubar'
|
||||
|
||||
● Read(../../../Downloads/localhost_5173_menubar-2…
|
||||
25-05-30T17-23-00-942Z.png)
|
||||
⎿ Read image (94.3KB)
|
||||
|
||||
● Based on the screenshot, I can see the menubar at
|
||||
the top of the ComfyUI interface contains the
|
||||
following items:
|
||||
|
||||
Main Menubar Items:
|
||||
- Workflow - Menu for workflow-related actions
|
||||
- Edit - Menu for editing operations
|
||||
- Help - Menu for help and documentation
|
||||
|
||||
Additional UI Elements:
|
||||
- ComfyUI Logo - On the far left
|
||||
- Workflow Tab - Shows "Unsaved Workflow" with a
|
||||
dropdown and close button
|
||||
- Layout Controls - On the far right (grid view
|
||||
and hamburger menu icons)
|
||||
|
||||
The interface shows a typical ComfyUI workflow
|
||||
graph with nodes like "Load Checkpoint", "CLIP
|
||||
Text Encode (Prompt)", "KSampler", and "Empty
|
||||
Latent Image" connected with colored cables.
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `npm i` to install all dependencies
|
||||
- `npm run test:unit` to execute all unit tests
|
||||
|
||||
### Component Tests
|
||||
|
||||
Component tests verify Vue components in `src/components/`.
|
||||
|
||||
- `npm run test:component` to execute all component tests
|
||||
|
||||
### Playwright Tests
|
||||
|
||||
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details.
|
||||
|
||||
### Running All Tests
|
||||
|
||||
Before submitting a PR, ensure all tests pass:
|
||||
|
||||
```bash
|
||||
npm run test:unit
|
||||
npm run test:component
|
||||
npm run test:browser
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run format
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### TypeScript
|
||||
- Use TypeScript for all new code
|
||||
- Avoid `any` types - use proper type definitions
|
||||
- Never use `@ts-expect-error` - fix the underlying type issue
|
||||
|
||||
### Vue 3 Patterns
|
||||
- Use Composition API for all components
|
||||
- Follow Vue 3.5+ patterns (props destructuring is reactive)
|
||||
- Use `<script setup>` syntax
|
||||
|
||||
### Styling
|
||||
- Use Tailwind CSS classes instead of custom CSS
|
||||
- Follow the existing dark theme pattern: `dark-theme:` prefix (not `dark:`)
|
||||
|
||||
### Internationalization
|
||||
- All user-facing strings must use vue-i18n
|
||||
- Add translations to `src/locales/en/main.json`
|
||||
- Use translation keys: `const { t } = useI18n(); t('key.path')`
|
||||
|
||||
## Custom Icons
|
||||
|
||||
The project supports custom SVG icons through the unplugin-icons system. Custom icons are stored in `src/assets/icons/custom/` and can be used as Vue components with the `i-comfy:` prefix.
|
||||
|
||||
For detailed instructions on adding and using custom icons, see [src/assets/icons/README.md](src/assets/icons/README.md).
|
||||
|
||||
## Working with litegraph.js
|
||||
|
||||
Since Aug 5, 2025, litegraph.js is now integrated directly into this repository. It was merged using git subtree to preserve the complete commit history ([PR #4667](https://github.com/Comfy-Org/ComfyUI_frontend/pull/4667), [ADR](docs/adr/0001-merge-litegraph-into-frontend.md)).
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Issue References**: Commits from the original litegraph repository may contain issue/PR numbers (e.g., #4667) that refer to issues/PRs in the original litegraph.js repository, not this one.
|
||||
- **File Paths**: When viewing historical commits, file paths may show the original structure before the subtree merge. In those cases, just consider the paths relative to the new litegraph folder.
|
||||
- **Contributing**: All litegraph modifications should now be made directly in this repository.
|
||||
|
||||
The original litegraph repository (https://github.com/Comfy-Org/litegraph.js) is now archived.
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. Ensure your branch is up to date with main
|
||||
2. Run all tests and ensure they pass
|
||||
3. Create a pull request with a clear title and description
|
||||
4. Use conventional commit format for PR titles:
|
||||
- `[feat]` for new features
|
||||
- `[fix]` for bug fixes
|
||||
- `[docs]` for documentation
|
||||
- `[refactor]` for code refactoring
|
||||
- `[test]` for test additions/changes
|
||||
- `[chore]` for maintenance tasks
|
||||
|
||||
### PR Description Template
|
||||
|
||||
```
|
||||
## Description
|
||||
Brief description of the changes
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation update
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Component tests pass
|
||||
- [ ] Browser tests pass (if applicable)
|
||||
- [ ] Manual testing completed
|
||||
|
||||
## Screenshots (if applicable)
|
||||
Add screenshots for UI changes
|
||||
```
|
||||
|
||||
### Review Process
|
||||
|
||||
1. All PRs require at least one review
|
||||
2. Address review feedback promptly
|
||||
3. Keep PRs focused - one feature/fix per PR
|
||||
4. Large features should be discussed in an issue first
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions about contributing:
|
||||
- Check existing issues and discussions
|
||||
- Ask in our [Discord](https://discord.com/invite/comfyorg)
|
||||
- Open a new issue for clarification
|
||||
|
||||
Thank you for contributing to ComfyUI Frontend!
|
||||
194
README.md
@@ -512,18 +512,196 @@ The selection toolbox will display the command button when items are selected:
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to ComfyUI Frontend! Please see our [Contributing Guide](CONTRIBUTING.md) for:
|
||||
We're building this frontend together and would love your help — no matter how you'd like to pitch in! You don't need to write code to make a difference.
|
||||
|
||||
- Ways to contribute (code, documentation, testing, community support)
|
||||
- Development setup and workflow
|
||||
- Code style guidelines
|
||||
- Testing requirements
|
||||
- How to submit pull requests
|
||||
- Backporting fixes to release branches
|
||||
Here are some ways to get involved:
|
||||
|
||||
- **Pull Requests:** Add features, fix bugs, or improve code health. Browse [issues](https://github.com/Comfy-Org/ComfyUI_frontend/issues) for inspiration.
|
||||
- **Vote on Features:** Give a 👍 to the feature requests you care about to help us prioritize.
|
||||
- **Verify Bugs:** Try reproducing reported issues and share your results (even if the bug doesn't occur!).
|
||||
- **Community Support:** Hop into our [Discord](https://www.comfy.org/discord) to answer questions or get help.
|
||||
- **Share & Advocate:** Tell your friends, tweet about us, or share tips to support the project.
|
||||
|
||||
Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
|
||||
## Development
|
||||
|
||||
For detailed development setup, testing procedures, and technical information, please refer to [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
### Prerequisites & Technology Stack
|
||||
|
||||
- **Required Software**:
|
||||
- Node.js (v16 or later; v20/v22 strongly recommended) and npm
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance
|
||||
|
||||
- **Tech Stack**:
|
||||
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Pinia](https://pinia.vuejs.org/) for state management
|
||||
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
|
||||
- [litegraph.js](https://github.com/Comfy-Org/litegraph.js) for node editor
|
||||
- [zod](https://zod.dev/) for schema validation
|
||||
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/Comfy-Org/ComfyUI_frontend.git
|
||||
cd ComfyUI_frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Configure environment (optional):
|
||||
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
|
||||
|
||||
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
|
||||
|
||||
### Dev Server Configuration
|
||||
|
||||
To launch ComfyUI and have it connect to your development server:
|
||||
|
||||
```bash
|
||||
python main.py --port 8188
|
||||
```
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit
|
||||
hook is used to auto-format code on commit.
|
||||
|
||||
### Dev Server
|
||||
|
||||
Note: The dev server will NOT load any extension from the ComfyUI server. Only
|
||||
core extensions will be loaded.
|
||||
|
||||
- Start local ComfyUI backend at `localhost:8188`
|
||||
- Run `npm run dev` to start the dev server
|
||||
- Run `npm run dev:electron` to start the dev server with electron API mocked
|
||||
|
||||
#### Access dev server on touch devices
|
||||
|
||||
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.
|
||||
|
||||
After you start the dev server, you should see following logs:
|
||||
|
||||
```
|
||||
> comfyui-frontend@1.3.42 dev
|
||||
> vite
|
||||
|
||||
|
||||
VITE v5.4.6 ready in 488 ms
|
||||
|
||||
➜ Local: http://localhost:5173/
|
||||
➜ Network: http://172.21.80.1:5173/
|
||||
➜ Network: http://192.168.2.20:5173/
|
||||
➜ press h + enter to show help
|
||||
```
|
||||
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
### Recommended Code Editor Configuration
|
||||
|
||||
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
|
||||
|
||||
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
|
||||
### Recommended MCP Integrations
|
||||
|
||||
#### Playwright Browser Automation
|
||||
|
||||
The Playwright MCP server enables LLM coding assistants (like Copilot, Claude Code, etc.) to control and inspect web browsers programmatically. This allows assistants to navigate websites, take screenshots, and interact with web pages on your behalf.
|
||||
|
||||
For ComfyUI_frontend development, you can ask coding assistants to use Playwright screenshots to continuously verify that your code changes produce the expected visual results.
|
||||
|
||||
##### Setup for Claude Code
|
||||
|
||||
After installing dependencies with `npm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
|
||||
|
||||
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
|
||||
|
||||
|
||||
```
|
||||
> navigate to localhost:5173 and take a screenshot.
|
||||
tell me the items in the menubar
|
||||
|
||||
● I'll navigate to localhost:5173 and take a
|
||||
screenshot to see the menubar items.
|
||||
|
||||
● playwright:playwright_navigate (MCP)(url: …
|
||||
"http://local
|
||||
host:5173")
|
||||
⎿ Navigated to http://localhost:5173
|
||||
|
||||
● playwright:playwright_screenshot (MCP)(name: "lo…
|
||||
alhost_5173
|
||||
_menubar")
|
||||
|
||||
⎿ Screenshot saved to: ../../../Downloads/localho
|
||||
st_5173_menubar-2025-05-30T17-23-00-942Z.png
|
||||
⎿ Screenshot also stored in memory with name:
|
||||
'localhost_5173_menubar'
|
||||
|
||||
● Read(../../../Downloads/localhost_5173_menubar-2…
|
||||
25-05-30T17-23-00-942Z.png)
|
||||
⎿ Read image (94.3KB)
|
||||
|
||||
● Based on the screenshot, I can see the menubar at
|
||||
the top of the ComfyUI interface contains the
|
||||
following items:
|
||||
|
||||
Main Menubar Items:
|
||||
- Workflow - Menu for workflow-related actions
|
||||
- Edit - Menu for editing operations
|
||||
- Help - Menu for help and documentation
|
||||
|
||||
Additional UI Elements:
|
||||
- ComfyUI Logo - On the far left
|
||||
- Workflow Tab - Shows "Unsaved Workflow" with a
|
||||
dropdown and close button
|
||||
- Layout Controls - On the far right (grid view
|
||||
and hamburger menu icons)
|
||||
|
||||
The interface shows a typical ComfyUI workflow
|
||||
graph with nodes like "Load Checkpoint", "CLIP
|
||||
Text Encode (Prompt)", "KSampler", and "Empty
|
||||
Latent Image" connected with colored cables.
|
||||
```
|
||||
|
||||
### Unit Test
|
||||
|
||||
- `npm i` to install all dependencies
|
||||
- `npm run test:unit` to execute all unit tests.
|
||||
|
||||
### Component Test
|
||||
|
||||
Component test verifies Vue components in `src/components/`.
|
||||
|
||||
- `npm run test:component` to execute all component tests.
|
||||
|
||||
### Playwright Test
|
||||
|
||||
Playwright test verifies the whole app. See <https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/README.md> for details.
|
||||
|
||||
### Custom Icons
|
||||
|
||||
The project supports custom SVG icons through the unplugin-icons system. Custom icons are stored in `src/assets/icons/custom/` and can be used as Vue components with the `i-comfy:` prefix.
|
||||
|
||||
For detailed instructions on adding and using custom icons, see [src/assets/icons/README.md](src/assets/icons/README.md).
|
||||
|
||||
### litegraph.js
|
||||
|
||||
This repo is using litegraph package hosted on <https://github.com/Comfy-Org/litegraph.js>. Any changes to litegraph should be submitted in that repo instead.
|
||||
|
||||
#### Test litegraph.js changes
|
||||
|
||||
- Run `npm link` in the local litegraph repo.
|
||||
- Run `npm link @comfyorg/litegraph` in this repo.
|
||||
|
||||
This will replace the litegraph package in this repo with the local litegraph repo.
|
||||
|
||||
### i18n
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/schemas/comfyWorkflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 78 KiB |
@@ -1,362 +0,0 @@
|
||||
# ComfyUI Feature Flags System
|
||||
|
||||
## Overview
|
||||
|
||||
The ComfyUI feature flags system enables capability negotiation between frontend and backend, allowing both sides to communicate their supported features and adapt behavior accordingly. This ensures backward compatibility while enabling progressive enhancement of features.
|
||||
|
||||
## System Architecture
|
||||
|
||||
### High-Level Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Frontend
|
||||
participant WebSocket
|
||||
participant Backend
|
||||
participant FeatureFlags Module
|
||||
|
||||
Frontend->>WebSocket: Connect
|
||||
WebSocket-->>Frontend: Connection established
|
||||
|
||||
Note over Frontend: First message must be feature flags
|
||||
Frontend->>WebSocket: Send client feature flags
|
||||
WebSocket->>Backend: Receive feature flags
|
||||
Backend->>FeatureFlags Module: Store client capabilities
|
||||
|
||||
Backend->>FeatureFlags Module: Get server features
|
||||
FeatureFlags Module-->>Backend: Return server capabilities
|
||||
Backend->>WebSocket: Send server feature flags
|
||||
WebSocket-->>Frontend: Receive server features
|
||||
|
||||
Note over Frontend,Backend: Both sides now know each other's capabilities
|
||||
|
||||
Frontend->>Frontend: Store server features
|
||||
Frontend->>Frontend: Components use useFeatureFlags()
|
||||
```
|
||||
|
||||
### Component Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Frontend
|
||||
A[clientFeatureFlags.json] --> B[api.ts]
|
||||
B --> C[WebSocket Handler]
|
||||
D[useFeatureFlags composable] --> B
|
||||
E[Vue Components] --> D
|
||||
end
|
||||
|
||||
subgraph Backend
|
||||
F[feature_flags.py] --> G[SERVER_FEATURE_FLAGS]
|
||||
H[server.py WebSocket] --> F
|
||||
I[Feature Consumers] --> F
|
||||
end
|
||||
|
||||
C <--> H
|
||||
|
||||
style A fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style G fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style D fill:#9ff,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
## Feature Flag Structure
|
||||
|
||||
Feature flags are organized as a flat dictionary at the top level, with extensions nested under an `extension` object:
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- **Core features**: Top-level keys (e.g., `"async_execution"`, `"supports_batch_queue"`)
|
||||
- **Client features**: Top-level keys (e.g., `"supports_preview_metadata"`)
|
||||
- **Extensions**: Nested under `"extension"` object (e.g., `extension.manager`)
|
||||
|
||||
### Structure Example
|
||||
|
||||
```json
|
||||
{
|
||||
"async_execution": true,
|
||||
"supports_batch_queue": false,
|
||||
"supports_preview_metadata": true,
|
||||
"supports_websocket_v2": false,
|
||||
"max_upload_size": 104857600,
|
||||
"extension": {
|
||||
"manager": {
|
||||
"supports_v4": true,
|
||||
"supports_ai_search": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Backend Implementation
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class FeatureFlagsModule {
|
||||
+SERVER_FEATURE_FLAGS: Dict
|
||||
+get_server_features() Dict
|
||||
+supports_feature(sockets_metadata, sid, feature_name) bool
|
||||
+get_connection_feature(sockets_metadata, sid, feature_name, default) Any
|
||||
}
|
||||
|
||||
class PromptServer {
|
||||
-sockets_metadata: Dict
|
||||
+websocket_handler()
|
||||
+send()
|
||||
}
|
||||
|
||||
class FeatureConsumer {
|
||||
<<interface>>
|
||||
+check_feature()
|
||||
+use_feature()
|
||||
}
|
||||
|
||||
PromptServer --> FeatureFlagsModule
|
||||
FeatureConsumer --> FeatureFlagsModule
|
||||
```
|
||||
|
||||
### Frontend Implementation
|
||||
|
||||
The `useFeatureFlags` composable provides reactive access to feature flags, meaning components will automatically update when feature flags change (e.g., during WebSocket reconnection).
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ComfyApi {
|
||||
+serverFeatureFlags: Record~string, unknown~
|
||||
+getClientFeatureFlags() Record
|
||||
+serverSupportsFeature(name) boolean
|
||||
+getServerFeature(name, default) T
|
||||
}
|
||||
|
||||
class useFeatureFlags {
|
||||
+serverSupports(name) boolean
|
||||
+getServerFeature(name, default) T
|
||||
+createServerFeatureFlag(name) ComputedRef
|
||||
+extension: ExtensionFlags
|
||||
}
|
||||
|
||||
class VueComponent {
|
||||
<<component>>
|
||||
+setup()
|
||||
}
|
||||
|
||||
ComfyApi <-- useFeatureFlags
|
||||
VueComponent --> useFeatureFlags
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Preview Metadata Support
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Preview Generation] --> B{supports_preview_metadata?}
|
||||
B -->|Yes| C[Send metadata with preview]
|
||||
B -->|No| D[Send preview only]
|
||||
|
||||
C --> E[Enhanced preview with node info]
|
||||
D --> F[Basic preview image]
|
||||
```
|
||||
|
||||
**Backend Usage:**
|
||||
```python
|
||||
# Check if client supports preview metadata
|
||||
if feature_flags.supports_feature(
|
||||
self.server_instance.sockets_metadata,
|
||||
self.server_instance.client_id,
|
||||
"supports_preview_metadata"
|
||||
):
|
||||
# Send enhanced preview with metadata
|
||||
metadata = {
|
||||
"node_id": node_id,
|
||||
"prompt_id": prompt_id,
|
||||
"display_node_id": display_node_id,
|
||||
"parent_node_id": parent_node_id,
|
||||
"real_node_id": real_node_id,
|
||||
}
|
||||
self.server_instance.send_sync(
|
||||
BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA,
|
||||
(image, metadata),
|
||||
self.server_instance.client_id,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Max Upload Size
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Client File Upload] --> B[Check max_upload_size]
|
||||
B --> C{File size OK?}
|
||||
C -->|Yes| D[Upload file]
|
||||
C -->|No| E[Show error]
|
||||
|
||||
F[Backend] --> G[Set from CLI args]
|
||||
G --> H[Convert MB to bytes]
|
||||
H --> I[Include in feature flags]
|
||||
```
|
||||
|
||||
**Backend Configuration:**
|
||||
```python
|
||||
# In feature_flags.py
|
||||
SERVER_FEATURE_FLAGS = {
|
||||
"supports_preview_metadata": True,
|
||||
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend Usage:**
|
||||
```typescript
|
||||
const { getServerFeature } = useFeatureFlags()
|
||||
const maxUploadSize = getServerFeature('max_upload_size', 100 * 1024 * 1024) // Default 100MB
|
||||
```
|
||||
|
||||
## Using Feature Flags
|
||||
|
||||
### Frontend Access Patterns
|
||||
|
||||
1. **Direct API access:**
|
||||
```typescript
|
||||
// Check boolean feature
|
||||
if (api.serverSupportsFeature('supports_preview_metadata')) {
|
||||
// Feature is supported
|
||||
}
|
||||
|
||||
// Get feature value with default
|
||||
const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024)
|
||||
```
|
||||
|
||||
2. **Using the composable (recommended for reactive components):**
|
||||
```typescript
|
||||
const { serverSupports, getServerFeature, extension } = useFeatureFlags()
|
||||
|
||||
// Check feature support
|
||||
if (serverSupports('supports_preview_metadata')) {
|
||||
// Use enhanced previews
|
||||
}
|
||||
|
||||
// Use reactive convenience properties (automatically update if flags change)
|
||||
if (extension.manager.supportsV4.value) {
|
||||
// Use V4 manager API
|
||||
}
|
||||
```
|
||||
|
||||
3. **Reactive usage in templates:**
|
||||
```vue
|
||||
<template>
|
||||
<div v-if="featureFlags.extension.manager.supportsV4">
|
||||
<!-- V4-specific UI -->
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Legacy UI -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
const featureFlags = useFeatureFlags()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Backend Access Patterns
|
||||
|
||||
```python
|
||||
# Check if a specific client supports a feature
|
||||
if feature_flags.supports_feature(
|
||||
sockets_metadata,
|
||||
client_id,
|
||||
"supports_preview_metadata"
|
||||
):
|
||||
# Client supports this feature
|
||||
|
||||
# Get feature value with default
|
||||
max_size = feature_flags.get_connection_feature(
|
||||
sockets_metadata,
|
||||
client_id,
|
||||
"max_upload_size",
|
||||
100 * 1024 * 1024 # Default 100MB
|
||||
)
|
||||
```
|
||||
|
||||
## Adding New Feature Flags
|
||||
|
||||
### Backend
|
||||
|
||||
1. **For server capabilities**, add to `SERVER_FEATURE_FLAGS` in `comfy_api/feature_flags.py`:
|
||||
```python
|
||||
SERVER_FEATURE_FLAGS = {
|
||||
"supports_preview_metadata": True,
|
||||
"max_upload_size": args.max_upload_size * 1024 * 1024,
|
||||
"your_new_feature": True, # Add your flag
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use in your code:**
|
||||
```python
|
||||
if feature_flags.supports_feature(sockets_metadata, sid, "your_new_feature"):
|
||||
# Feature-specific code
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
1. **For client capabilities**, add to `src/config/clientFeatureFlags.json`:
|
||||
```json
|
||||
{
|
||||
"supports_preview_metadata": false,
|
||||
"your_new_feature": true
|
||||
}
|
||||
```
|
||||
|
||||
2. **For extension features**, update the composable to add convenience accessors:
|
||||
```typescript
|
||||
// In useFeatureFlags.ts
|
||||
const extension = {
|
||||
manager: {
|
||||
supportsV4: computed(() => getServerFeature('extension.manager.supports_v4', false))
|
||||
},
|
||||
yourExtension: {
|
||||
supportsNewFeature: computed(() => getServerFeature('extension.yourExtension.supports_new_feature', false))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// ... existing returns
|
||||
extension
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Feature Flags
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Test Scenarios] --> B[Both support feature]
|
||||
A --> C[Only frontend supports]
|
||||
A --> D[Only backend supports]
|
||||
A --> E[Neither supports]
|
||||
|
||||
B --> F[Feature enabled]
|
||||
C --> G[Feature disabled]
|
||||
D --> H[Feature disabled]
|
||||
E --> I[Feature disabled]
|
||||
```
|
||||
|
||||
Test your feature flags with different combinations:
|
||||
- Frontend with flag + Backend with flag = Feature works
|
||||
- Frontend with flag + Backend without = Graceful degradation
|
||||
- Frontend without + Backend with flag = No feature usage
|
||||
- Neither has flag = Default behavior
|
||||
|
||||
### Example Test
|
||||
|
||||
```typescript
|
||||
// In tests-ui/tests/api.featureFlags.test.ts
|
||||
it('should handle preview metadata based on feature flag', () => {
|
||||
// Mock server supports feature
|
||||
api.serverFeatureFlags = { supports_preview_metadata: true }
|
||||
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
|
||||
// Mock server doesn't support feature
|
||||
api.serverFeatureFlags = {}
|
||||
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
|
||||
})
|
||||
@@ -1,65 +0,0 @@
|
||||
# 1. Merge LiteGraph.js into ComfyUI Frontend
|
||||
|
||||
Date: 2025-08-05
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ComfyUI's frontend architecture currently depends on a forked version of litegraph.js maintained as a separate package (@comfyorg/litegraph). This separation has created several architectural and operational challenges:
|
||||
|
||||
**Architectural Issues:**
|
||||
- The current split creates a distributed monolith where both packages handle rendering, user interactions, and data models without clear separation of responsibilities
|
||||
- Both frontend and litegraph manipulate the same data structures, forcing tight coupling across the frontend's data model, views, and business logic
|
||||
- The lack of clear boundaries prevents implementation of modern architectural patterns like MVC or event-sourcing
|
||||
|
||||
**Operational Issues:**
|
||||
- ComfyUI is the only known user of the @comfyorg/litegraph fork
|
||||
- Managing separate repositories significantly slows developer velocity due to coordination overhead
|
||||
- Version mismatches between frontend and litegraph cause recurring issues
|
||||
- No upstream contributions to consider (original litegraph.js is no longer maintained)
|
||||
|
||||
**Future Requirements:**
|
||||
The following planned features are blocked by the current architecture:
|
||||
- Multiplayer collaboration requiring CRDT-based state management
|
||||
- Cloud-based backend support
|
||||
- Alternative rendering backends
|
||||
- Improved undo/redo system
|
||||
- Clear API versioning and compatibility layers
|
||||
|
||||
## Decision
|
||||
|
||||
We will merge litegraph.js directly into the ComfyUI frontend repository using git subtree to preserve the complete commit history.
|
||||
|
||||
The merge will:
|
||||
1. Move litegraph source to `src/lib/litegraph/`
|
||||
2. Update all import paths from `@comfyorg/litegraph` to `@/lib/litegraph`
|
||||
3. Remove the npm dependency on `@comfyorg/litegraph`
|
||||
4. Preserve the full git history using subtree merge
|
||||
|
||||
This integration is the first step toward restructuring the application along clear Model-View-Controller boundaries, with state mutations going through a single CRDT-mediated access point.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Enables architectural refactoring**: Direct integration allows restructuring along proper MVC boundaries
|
||||
- **Unblocks new features**: Multiplayer, cloud features, and improved undo/redo can now be implemented
|
||||
- **Faster development**: Eliminates overhead of coordinating changes across two tightly-coupled packages
|
||||
- **Better developer experience**: No more version mismatch issues or cross-repository debugging
|
||||
- **Simplified maintenance**: One less repository to maintain, release, and version
|
||||
|
||||
### Negative
|
||||
|
||||
- **Larger repository**: The frontend repository will increase in size
|
||||
- **Loss of versioning**: No more semantic versioning for litegraph changes
|
||||
- **Maintenance responsibility**: Must maintain litegraph code directly
|
||||
- **Historical references**: Past commit messages may reference issues from the original litegraph repository
|
||||
|
||||
## Notes
|
||||
|
||||
- Git subtree was chosen over submodules to provide a cleaner developer experience
|
||||
- The original litegraph repository will be archived after the merge
|
||||
- Future litegraph improvements will be made directly in the frontend repository
|
||||
@@ -1,79 +0,0 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
This directory contains Architecture Decision Records (ADRs) for the ComfyUI Frontend project.
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record captures an important architectural decision made along with its context and consequences. ADRs help future developers understand why certain decisions were made and provide a historical record of the project's evolution.
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
1. Copy the template below
|
||||
2. Name it with the next number in sequence: `NNNN-descriptive-title.md`
|
||||
3. Fill in all sections
|
||||
4. Update this index
|
||||
5. Submit as part of your PR
|
||||
|
||||
## ADR Template
|
||||
|
||||
```markdown
|
||||
# N. Title
|
||||
|
||||
Date: YYYY-MM-DD
|
||||
|
||||
## Status
|
||||
|
||||
[Proposed | Accepted | Rejected | Deprecated | Superseded by [ADR-NNNN](NNNN-title.md)]
|
||||
|
||||
## Context
|
||||
|
||||
Describe the issue that motivated this decision and any context that influences or constrains the decision.
|
||||
|
||||
- What is the problem?
|
||||
- Why does it need to be solved?
|
||||
- What forces are at play (technical, business, team)?
|
||||
|
||||
## Decision
|
||||
|
||||
Describe the decision that was made and the key points that led to it.
|
||||
|
||||
- What are we going to do?
|
||||
- How will we do it?
|
||||
- What alternatives were considered?
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- What becomes easier or better?
|
||||
- What opportunities does this create?
|
||||
|
||||
### Negative
|
||||
|
||||
- What becomes harder or worse?
|
||||
- What risks are we accepting?
|
||||
- What technical debt might we incur?
|
||||
|
||||
## Notes
|
||||
|
||||
Optional section for additional information, references, or clarifications.
|
||||
```
|
||||
|
||||
## ADR Status Values
|
||||
|
||||
- **Proposed**: The decision is being discussed
|
||||
- **Accepted**: The decision has been agreed upon
|
||||
- **Rejected**: The decision was not accepted
|
||||
- **Deprecated**: The decision is no longer relevant
|
||||
- **Superseded**: The decision has been replaced by another ADR
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) by Michael Nygard
|
||||
- [Architecture Decision Records](https://adr.github.io/) - Collection of ADR resources
|
||||
11
package-lock.json
generated
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.0",
|
||||
"version": "1.25.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.0",
|
||||
"version": "1.25.4",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.17.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -975,6 +976,12 @@
|
||||
"integrity": "sha512-o6WFbYn9yAkGbkOwvhPF7pbKDvN0occZ21Tfyhya8CIsIqKpTHLft0aOqo4yhSh+kTxN16FYjsfrTH5Olk4WuA==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.17.1.tgz",
|
||||
"integrity": "sha512-SaDDWFvoH1bCfibvZjtX0JoLvFTJw2MUOWzrjyeuWVs00JpxiJ1I5f6oH/AO8lJqKdASWBVPzpC9zPMG45w4IQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.0",
|
||||
"version": "1.25.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -78,6 +78,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.17.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -16,7 +16,9 @@ const typesPackage = {
|
||||
homepage: mainPackage.homepage,
|
||||
description: `TypeScript definitions for ${mainPackage.name}`,
|
||||
license: mainPackage.license,
|
||||
dependencies: {},
|
||||
dependencies: {
|
||||
'@comfyorg/litegraph': mainPackage.dependencies['@comfyorg/litegraph']
|
||||
},
|
||||
peerDependencies: {
|
||||
vue: mainPackage.dependencies.vue,
|
||||
zod: mainPackage.dependencies.zod
|
||||
|
||||
@@ -6,22 +6,9 @@
|
||||
|
||||
- Use `api.apiURL()` for backend endpoints
|
||||
- Use `api.fileURL()` for static files
|
||||
|
||||
#### ✅ Correct Usage
|
||||
```typescript
|
||||
// Backend API call
|
||||
const response = await api.get(api.apiURL('/prompt'))
|
||||
|
||||
// Static file
|
||||
const template = await fetch(api.fileURL('/templates/default.json'))
|
||||
```
|
||||
|
||||
#### ❌ Incorrect Usage
|
||||
```typescript
|
||||
// WRONG - Direct URL construction
|
||||
const response = await fetch('/api/prompt')
|
||||
const template = await fetch('/templates/default.json')
|
||||
```
|
||||
- Examples:
|
||||
- Backend: `api.apiURL('/prompt')`
|
||||
- Static: `api.fileURL('/templates/default.json')`
|
||||
|
||||
### Error Handling
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="relative overflow-hidden w-full h-full flex items-center justify-center"
|
||||
>
|
||||
<Skeleton
|
||||
v-if="!isImageLoaded"
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="absolute inset-0"
|
||||
/>
|
||||
<img
|
||||
v-show="isImageLoaded"
|
||||
ref="imageRef"
|
||||
:src="cachedSrc"
|
||||
:alt="alt"
|
||||
draggable="false"
|
||||
:class="imageClass"
|
||||
:style="imageStyle"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
/>
|
||||
<div
|
||||
v-if="hasError"
|
||||
class="absolute inset-0 flex items-center justify-center bg-surface-50 dark-theme:bg-surface-800 text-muted"
|
||||
>
|
||||
<i class="pi pi-image text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
import { useMediaCache } from '@/services/mediaCacheService'
|
||||
|
||||
const {
|
||||
src,
|
||||
alt = '',
|
||||
imageClass = '',
|
||||
imageStyle,
|
||||
rootMargin = '300px'
|
||||
} = defineProps<{
|
||||
src: string
|
||||
alt?: string
|
||||
imageClass?: string | string[] | Record<string, boolean>
|
||||
imageStyle?: Record<string, any>
|
||||
rootMargin?: string
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const imageRef = ref<HTMLImageElement | null>(null)
|
||||
const isIntersecting = ref(false)
|
||||
const isImageLoaded = ref(false)
|
||||
const hasError = ref(false)
|
||||
const cachedSrc = ref<string | undefined>(undefined)
|
||||
|
||||
const { getCachedMedia, acquireUrl, releaseUrl } = useMediaCache()
|
||||
|
||||
// Use intersection observer to detect when the image container comes into view
|
||||
useIntersectionObserver(
|
||||
containerRef,
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
isIntersecting.value = entry?.isIntersecting ?? false
|
||||
},
|
||||
{
|
||||
rootMargin,
|
||||
threshold: 0.1
|
||||
}
|
||||
)
|
||||
|
||||
// Only start loading the image when it's in view
|
||||
const shouldLoad = computed(() => isIntersecting.value)
|
||||
|
||||
watch(
|
||||
shouldLoad,
|
||||
async (shouldLoad) => {
|
||||
if (shouldLoad && src && !cachedSrc.value && !hasError.value) {
|
||||
try {
|
||||
const cachedMedia = await getCachedMedia(src)
|
||||
if (cachedMedia.error) {
|
||||
hasError.value = true
|
||||
} else if (cachedMedia.objectUrl) {
|
||||
const acquiredUrl = acquireUrl(src)
|
||||
cachedSrc.value = acquiredUrl || cachedMedia.objectUrl
|
||||
} else {
|
||||
cachedSrc.value = src
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load cached media:', error)
|
||||
cachedSrc.value = src
|
||||
}
|
||||
} else if (!shouldLoad) {
|
||||
if (cachedSrc.value?.startsWith('blob:')) {
|
||||
releaseUrl(src)
|
||||
}
|
||||
// Hide image when out of view
|
||||
isImageLoaded.value = false
|
||||
cachedSrc.value = undefined
|
||||
hasError.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const onImageLoad = () => {
|
||||
isImageLoaded.value = true
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const onImageError = () => {
|
||||
hasError.value = true
|
||||
isImageLoaded.value = false
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cachedSrc.value?.startsWith('blob:')) {
|
||||
releaseUrl(src)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -42,11 +42,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Message from 'primevue/message'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { compareVersions } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
@@ -77,7 +78,6 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.left="minimapTooltip"
|
||||
v-tooltip.left="t('graphCanvasMenu.toggleMinimap') + ' (Alt + m)'"
|
||||
severity="secondary"
|
||||
:icon="'pi pi-map'"
|
||||
:aria-label="$t('graphCanvasMenu.toggleMinimap')"
|
||||
@@ -70,33 +70,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const settingStore = useSettingStore()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
|
||||
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
const minimapTooltip = computed(() => {
|
||||
const baseText = t('graphCanvasMenu.toggleMinimap')
|
||||
const keybinding = keybindingStore.getKeybindingByCommandId(
|
||||
'Comfy.Canvas.ToggleMinimap'
|
||||
)
|
||||
return keybinding ? `${baseText} (${keybinding.combo.toString()})` : baseText
|
||||
})
|
||||
const linkHidden = computed(
|
||||
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
||||
)
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
:style="containerStyles"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseUp"
|
||||
@wheel="handleWheel"
|
||||
>
|
||||
<canvas
|
||||
@@ -40,9 +40,9 @@ const {
|
||||
height,
|
||||
init,
|
||||
destroy,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
handleWheel
|
||||
} = minimap
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import {
|
||||
LiteGraph,
|
||||
isOverNodeInput,
|
||||
isOverNodeOutput
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { createBounds } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<DeleteButton />
|
||||
<RefreshSelectionButton />
|
||||
<RefreshButton />
|
||||
<ExtensionCommandButton
|
||||
v-for="command in extensionToolboxCommands"
|
||||
:key="command.id"
|
||||
@@ -39,7 +39,7 @@ import ExtensionCommandButton from '@/components/graph/selectionToolbox/Extensio
|
||||
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
import RefreshButton from '@/components/graph/selectionToolbox/RefreshButton.vue'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
@@ -13,17 +13,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphGroup, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { type CSSProperties, computed, ref, watch } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import {
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
// Mock the litegraph module
|
||||
vi.mock('@/lib/litegraph/src/litegraph', async () => {
|
||||
const actual = await vi.importActual('@/lib/litegraph/src/litegraph')
|
||||
vi.mock('@comfyorg/litegraph', async () => {
|
||||
const actual = await vi.importActual('@comfyorg/litegraph')
|
||||
return {
|
||||
...actual,
|
||||
LGraphCanvas: {
|
||||
|
||||
@@ -44,17 +44,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ColorOption as CanvasColorOption } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph, isColorable } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ColorOption as CanvasColorOption } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LiteGraph,
|
||||
isColorable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NodeId } from '@comfyorg/litegraph'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import * as markdownRendererUtil from '@/utils/markdownRendererUtil'
|
||||
|
||||
import NodePreview from './NodePreview.vue'
|
||||
|
||||
@@ -130,164 +129,4 @@ describe('NodePreview', () => {
|
||||
|
||||
expect(headdot.classes()).toContain('pr-3')
|
||||
})
|
||||
|
||||
describe('Description Rendering', () => {
|
||||
it('renders plain text description as HTML', () => {
|
||||
const plainTextNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: 'This is a plain text description'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(plainTextNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
expect(description.exists()).toBe(true)
|
||||
expect(description.html()).toContain('This is a plain text description')
|
||||
})
|
||||
|
||||
it('renders markdown description with formatting', () => {
|
||||
const markdownNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: '**Bold text** and *italic text* with `code`'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(markdownNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
expect(description.exists()).toBe(true)
|
||||
expect(description.html()).toContain('<strong>Bold text</strong>')
|
||||
expect(description.html()).toContain('<em>italic text</em>')
|
||||
expect(description.html()).toContain('<code>code</code>')
|
||||
})
|
||||
|
||||
it('does not render description element when description is empty', () => {
|
||||
const noDescriptionNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: ''
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(noDescriptionNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
expect(description.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not render description element when description is undefined', () => {
|
||||
const { description, ...nodeDefWithoutDescription } = mockNodeDef
|
||||
const wrapper = mountComponent(
|
||||
nodeDefWithoutDescription as ComfyNodeDefV2
|
||||
)
|
||||
const descriptionElement = wrapper.find('._sb_description')
|
||||
|
||||
expect(descriptionElement.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('calls renderMarkdownToHtml utility function', () => {
|
||||
const spy = vi.spyOn(markdownRendererUtil, 'renderMarkdownToHtml')
|
||||
const testDescription = 'Test **markdown** description'
|
||||
|
||||
const nodeDefWithDescription: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: testDescription
|
||||
}
|
||||
|
||||
mountComponent(nodeDefWithDescription)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(testDescription)
|
||||
spy.mockRestore()
|
||||
})
|
||||
|
||||
it('handles potentially unsafe markdown content safely', () => {
|
||||
const unsafeNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description:
|
||||
'Safe **markdown** content <script>alert("xss")</script> with `code` blocks'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(unsafeNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
// The description should still exist because there's safe content
|
||||
if (description.exists()) {
|
||||
// Should not contain script tags (sanitized by DOMPurify)
|
||||
expect(description.html()).not.toContain('<script>')
|
||||
expect(description.html()).not.toContain('alert("xss")')
|
||||
// Should contain the safe markdown content rendered as HTML
|
||||
expect(description.html()).toContain('<strong>markdown</strong>')
|
||||
expect(description.html()).toContain('<code>code</code>')
|
||||
} else {
|
||||
// If DOMPurify removes everything, that's also acceptable for security
|
||||
expect(description.exists()).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('handles markdown with line breaks', () => {
|
||||
const multilineNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: 'Line 1\n\nLine 3 after empty line'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(multilineNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
expect(description.exists()).toBe(true)
|
||||
// Should contain paragraph tags for proper line break handling
|
||||
expect(description.html()).toContain('<p>')
|
||||
})
|
||||
|
||||
it('handles markdown lists', () => {
|
||||
const listNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: '- Item 1\n- Item 2\n- Item 3'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(listNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
expect(description.exists()).toBe(true)
|
||||
expect(description.html()).toContain('<ul>')
|
||||
expect(description.html()).toContain('<li>')
|
||||
})
|
||||
|
||||
it('applies correct styling classes to description', () => {
|
||||
const wrapper = mountComponent()
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
expect(description.classes()).toContain('_sb_description')
|
||||
})
|
||||
|
||||
it('uses v-html directive for rendered content', () => {
|
||||
const htmlNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description: 'Content with **bold** text'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(htmlNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
// The component should render the HTML, not escape it
|
||||
expect(description.html()).toContain('<strong>bold</strong>')
|
||||
expect(description.html()).not.toContain('<strong>')
|
||||
})
|
||||
|
||||
it('prevents XSS attacks by sanitizing dangerous HTML elements', () => {
|
||||
const maliciousNodeDef: ComfyNodeDefV2 = {
|
||||
...mockNodeDef,
|
||||
description:
|
||||
'Normal text <img src="x" onerror="alert(\'XSS\')" /> and **bold** text'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent(maliciousNodeDef)
|
||||
const description = wrapper.find('._sb_description')
|
||||
|
||||
if (description.exists()) {
|
||||
// Should not contain dangerous event handlers
|
||||
expect(description.html()).not.toContain('onerror')
|
||||
expect(description.html()).not.toContain('alert(')
|
||||
// Should still contain safe markdown content
|
||||
expect(description.html()).toContain('<strong>bold</strong>')
|
||||
// May or may not contain img tag depending on DOMPurify config
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,14 +70,15 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="renderedDescription"
|
||||
v-if="nodeDef.description"
|
||||
class="_sb_description"
|
||||
:style="{
|
||||
color: litegraphColors.WIDGET_SECONDARY_TEXT_COLOR,
|
||||
backgroundColor: litegraphColors.WIDGET_BGCOLOR
|
||||
}"
|
||||
v-html="renderedDescription"
|
||||
/>
|
||||
>
|
||||
{{ nodeDef.description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,9 +89,8 @@ import { computed } from 'vue'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
|
||||
|
||||
const { nodeDef } = defineProps<{
|
||||
const props = defineProps<{
|
||||
nodeDef: ComfyNodeDefV2
|
||||
}>()
|
||||
|
||||
@@ -101,12 +101,7 @@ const litegraphColors = computed(
|
||||
|
||||
const widgetStore = useWidgetStore()
|
||||
|
||||
const { description } = nodeDef
|
||||
const renderedDescription = computed(() => {
|
||||
if (!description) return ''
|
||||
return renderMarkdownToHtml(description)
|
||||
})
|
||||
|
||||
const nodeDef = props.nodeDef
|
||||
const allInputDefs = Object.values(nodeDef.inputs)
|
||||
const allOutputDefs = nodeDef.outputs
|
||||
const slotInputDefs = allInputDefs.filter(
|
||||
|
||||
@@ -33,18 +33,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent
|
||||
} from '@comfyorg/litegraph'
|
||||
import { Point } from '@comfyorg/litegraph/dist/interfaces'
|
||||
import type { CanvasPointerEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, ref, toRaw, watch, watchEffect } from 'vue'
|
||||
|
||||
import { Point } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div class="relative w-full p-4">
|
||||
<div class="h-12 flex items-center gap-4 justify-between">
|
||||
<div class="flex-1 max-w-md">
|
||||
<AutoComplete
|
||||
v-model.lazy="searchQuery"
|
||||
:placeholder="$t('templateWorkflows.searchPlaceholder')"
|
||||
:complete-on-focus="false"
|
||||
:delay="200"
|
||||
class="w-full"
|
||||
:pt="{
|
||||
pcInputText: {
|
||||
root: {
|
||||
class: 'w-full rounded-2xl'
|
||||
}
|
||||
},
|
||||
loader: {
|
||||
style: 'display: none'
|
||||
}
|
||||
}"
|
||||
:show-empty-message="false"
|
||||
@complete="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 mt-2">
|
||||
<small
|
||||
v-if="searchQuery && filteredCount !== null"
|
||||
class="text-color-secondary"
|
||||
>
|
||||
{{ $t('g.resultsCount', { count: filteredCount }) }}
|
||||
</small>
|
||||
<Button
|
||||
v-if="searchQuery"
|
||||
text
|
||||
size="small"
|
||||
icon="pi pi-times"
|
||||
:label="$t('g.clearFilters')"
|
||||
@click="clearFilters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const { filteredCount } = defineProps<{
|
||||
filteredCount?: number | null
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery', { default: '' })
|
||||
|
||||
const emit = defineEmits<{
|
||||
clearFilters: []
|
||||
}>()
|
||||
|
||||
const clearFilters = () => {
|
||||
searchQuery.value = ''
|
||||
emit('clearFilters')
|
||||
}
|
||||
</script>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<Card
|
||||
class="w-64 template-card rounded-2xl overflow-hidden shadow-elevation-2 dark-theme:bg-dark-elevation-1.5 h-full"
|
||||
:pt="{
|
||||
body: { class: 'p-0 h-full flex flex-col' }
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative overflow-hidden rounded-t-lg">
|
||||
<Skeleton width="16rem" height="12rem" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex items-center px-4 py-3">
|
||||
<div class="flex-1 flex flex-col">
|
||||
<Skeleton width="80%" height="1.25rem" class="mb-2" />
|
||||
<Skeleton width="100%" height="0.875rem" class="mb-1" />
|
||||
<Skeleton width="90%" height="0.875rem" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Card from 'primevue/card'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
</script>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
|
||||
import { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||
@@ -54,46 +53,10 @@ vi.mock('@/components/templates/TemplateWorkflowList.vue', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/templates/TemplateSearchBar.vue', () => ({
|
||||
default: {
|
||||
template: '<div class="mock-search-bar"></div>',
|
||||
props: ['searchQuery', 'filteredCount'],
|
||||
emits: ['update:searchQuery', 'clearFilters']
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/templates/TemplateWorkflowCardSkeleton.vue', () => ({
|
||||
default: {
|
||||
template: '<div class="mock-skeleton"></div>'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useLocalStorage: () => 'grid'
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useIntersectionObserver', () => ({
|
||||
useIntersectionObserver: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useLazyPagination', () => ({
|
||||
useLazyPagination: (items: any) => ({
|
||||
paginatedItems: items,
|
||||
isLoading: { value: false },
|
||||
hasMoreItems: { value: false },
|
||||
loadNextPage: vi.fn(),
|
||||
reset: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useTemplateFiltering', () => ({
|
||||
useTemplateFiltering: (templates: any) => ({
|
||||
searchQuery: { value: '' },
|
||||
filteredTemplates: templates,
|
||||
filteredCount: { value: templates.value?.length || 0 }
|
||||
})
|
||||
}))
|
||||
|
||||
describe('TemplateWorkflowView', () => {
|
||||
const createTemplate = (name: string): TemplateInfo => ({
|
||||
name,
|
||||
@@ -104,18 +67,6 @@ describe('TemplateWorkflowView', () => {
|
||||
})
|
||||
|
||||
const mountView = (props = {}) => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
templateWorkflows: {
|
||||
loadingMore: 'Loading more...'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return mount(TemplateWorkflowView, {
|
||||
props: {
|
||||
title: 'Test Templates',
|
||||
@@ -128,9 +79,6 @@ describe('TemplateWorkflowView', () => {
|
||||
],
|
||||
loading: null,
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
<template>
|
||||
<DataView
|
||||
:value="displayTemplates"
|
||||
:value="templates"
|
||||
:layout="layout"
|
||||
data-key="name"
|
||||
:lazy="true"
|
||||
pt:root="h-full grid grid-rows-[auto_1fr_auto]"
|
||||
pt:root="h-full grid grid-rows-[auto_1fr]"
|
||||
pt:content="p-2 overflow-auto"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg">{{ title }}</h2>
|
||||
<SelectButton
|
||||
v-model="layout"
|
||||
:options="['grid', 'list']"
|
||||
:allow-empty="false"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-table']" />
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
<TemplateSearchBar
|
||||
v-model:search-query="searchQuery"
|
||||
:filtered-count="filteredCount"
|
||||
@clear-filters="() => reset()"
|
||||
/>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg">{{ title }}</h2>
|
||||
<SelectButton
|
||||
v-model="layout"
|
||||
:options="['grid', 'list']"
|
||||
:allow-empty="false"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-table']" />
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -40,35 +33,18 @@
|
||||
</template>
|
||||
|
||||
<template #grid="{ items }">
|
||||
<div>
|
||||
<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-x-4 gap-y-8 px-4 justify-items-center"
|
||||
>
|
||||
<TemplateWorkflowCard
|
||||
v-for="template in items"
|
||||
:key="template.name"
|
||||
:source-module="sourceModule"
|
||||
:template="template"
|
||||
:loading="loading === template.name"
|
||||
:category-title="categoryTitle"
|
||||
@load-workflow="onLoadWorkflow"
|
||||
/>
|
||||
<TemplateWorkflowCardSkeleton
|
||||
v-for="n in shouldUsePagination && isLoadingMore
|
||||
? skeletonCount
|
||||
: 0"
|
||||
:key="`skeleton-${n}`"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldUsePagination && hasMoreTemplates"
|
||||
ref="loadTrigger"
|
||||
class="w-full h-4 flex justify-center items-center"
|
||||
>
|
||||
<div v-if="isLoadingMore" class="text-sm text-muted">
|
||||
{{ t('templateWorkflows.loadingMore') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-x-4 gap-y-8 px-4 justify-items-center"
|
||||
>
|
||||
<TemplateWorkflowCard
|
||||
v-for="template in items"
|
||||
:key="template.name"
|
||||
:source-module="sourceModule"
|
||||
:template="template"
|
||||
:loading="loading === template.name"
|
||||
:category-title="categoryTitle"
|
||||
@load-workflow="onLoadWorkflow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</DataView>
|
||||
@@ -78,21 +54,12 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import DataView from 'primevue/dataview'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TemplateSearchBar from '@/components/templates/TemplateSearchBar.vue'
|
||||
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
|
||||
import TemplateWorkflowCardSkeleton from '@/components/templates/TemplateWorkflowCardSkeleton.vue'
|
||||
import TemplateWorkflowList from '@/components/templates/TemplateWorkflowList.vue'
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
import { useLazyPagination } from '@/composables/useLazyPagination'
|
||||
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
|
||||
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { title, sourceModule, categoryTitle, loading, templates } = defineProps<{
|
||||
defineProps<{
|
||||
title: string
|
||||
sourceModule: string
|
||||
categoryTitle: string
|
||||
@@ -105,59 +72,6 @@ const layout = useLocalStorage<'grid' | 'list'>(
|
||||
'grid'
|
||||
)
|
||||
|
||||
const skeletonCount = 6
|
||||
const loadTrigger = ref<HTMLElement | null>(null)
|
||||
|
||||
const templatesRef = computed(() => templates || [])
|
||||
|
||||
const { searchQuery, filteredTemplates, filteredCount } =
|
||||
useTemplateFiltering(templatesRef)
|
||||
|
||||
// When searching, show all results immediately without pagination
|
||||
// When not searching, use lazy pagination
|
||||
const shouldUsePagination = computed(() => !searchQuery.value.trim())
|
||||
|
||||
// Lazy pagination setup using filtered templates
|
||||
const {
|
||||
paginatedItems: paginatedTemplates,
|
||||
isLoading: isLoadingMore,
|
||||
hasMoreItems: hasMoreTemplates,
|
||||
loadNextPage,
|
||||
reset
|
||||
} = useLazyPagination(filteredTemplates, {
|
||||
itemsPerPage: 12
|
||||
})
|
||||
|
||||
// Final templates to display
|
||||
const displayTemplates = computed(() => {
|
||||
return shouldUsePagination.value
|
||||
? paginatedTemplates.value
|
||||
: filteredTemplates.value
|
||||
})
|
||||
// Intersection observer for auto-loading (only when not searching)
|
||||
useIntersectionObserver(
|
||||
loadTrigger,
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
if (
|
||||
entry?.isIntersecting &&
|
||||
shouldUsePagination.value &&
|
||||
hasMoreTemplates.value &&
|
||||
!isLoadingMore.value
|
||||
) {
|
||||
void loadNextPage()
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '200px',
|
||||
threshold: 0.1
|
||||
}
|
||||
)
|
||||
|
||||
watch([() => templates, searchQuery], () => {
|
||||
reset()
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
loadWorkflow: [name: string]
|
||||
}>()
|
||||
|
||||
@@ -12,15 +12,6 @@ vi.mock('@/components/templates/thumbnails/BaseThumbnail.vue', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/common/LazyImage.vue', () => ({
|
||||
default: {
|
||||
name: 'LazyImage',
|
||||
template:
|
||||
'<img :src="src" :alt="alt" :class="imageClass" :style="imageStyle" draggable="false" />',
|
||||
props: ['src', 'alt', 'imageClass', 'imageStyle']
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useMouseInElement: () => ({
|
||||
elementX: ref(50),
|
||||
@@ -44,24 +35,23 @@ describe('CompareSliderThumbnail', () => {
|
||||
|
||||
it('renders both base and overlay images', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const lazyImages = wrapper.findAllComponents({ name: 'LazyImage' })
|
||||
expect(lazyImages.length).toBe(2)
|
||||
expect(lazyImages[0].props('src')).toBe('/base-image.jpg')
|
||||
expect(lazyImages[1].props('src')).toBe('/overlay-image.jpg')
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images.length).toBe(2)
|
||||
expect(images[0].attributes('src')).toBe('/base-image.jpg')
|
||||
expect(images[1].attributes('src')).toBe('/overlay-image.jpg')
|
||||
})
|
||||
|
||||
it('applies correct alt text to both images', () => {
|
||||
const wrapper = mountThumbnail({ alt: 'Custom Alt Text' })
|
||||
const lazyImages = wrapper.findAllComponents({ name: 'LazyImage' })
|
||||
expect(lazyImages[0].props('alt')).toBe('Custom Alt Text')
|
||||
expect(lazyImages[1].props('alt')).toBe('Custom Alt Text')
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images[0].attributes('alt')).toBe('Custom Alt Text')
|
||||
expect(images[1].attributes('alt')).toBe('Custom Alt Text')
|
||||
})
|
||||
|
||||
it('applies clip-path style to overlay image', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const overlayLazyImage = wrapper.findAllComponents({ name: 'LazyImage' })[1]
|
||||
const imageStyle = overlayLazyImage.props('imageStyle')
|
||||
expect(imageStyle.clipPath).toContain('inset')
|
||||
const overlay = wrapper.findAll('img')[1]
|
||||
expect(overlay.attributes('style')).toContain('clip-path')
|
||||
})
|
||||
|
||||
it('renders slider divider', () => {
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<BaseThumbnail :is-hovered="isHovered">
|
||||
<LazyImage
|
||||
<img
|
||||
:src="baseImageSrc"
|
||||
:alt="alt"
|
||||
:image-class="
|
||||
:class="
|
||||
isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
"
|
||||
/>
|
||||
<div ref="containerRef" class="absolute inset-0">
|
||||
<LazyImage
|
||||
<img
|
||||
:src="overlayImageSrc"
|
||||
:alt="alt"
|
||||
:image-class="
|
||||
:class="
|
||||
isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
"
|
||||
:image-style="{
|
||||
:style="{
|
||||
clipPath: `inset(0 ${100 - sliderPosition}% 0 0)`
|
||||
}"
|
||||
/>
|
||||
@@ -36,7 +36,6 @@
|
||||
import { useMouseInElement } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import LazyImage from '@/components/common/LazyImage.vue'
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
|
||||
const SLIDER_START_POSITION = 50
|
||||
|
||||
@@ -11,15 +11,6 @@ vi.mock('@/components/templates/thumbnails/BaseThumbnail.vue', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/common/LazyImage.vue', () => ({
|
||||
default: {
|
||||
name: 'LazyImage',
|
||||
template:
|
||||
'<img :src="src" :alt="alt" :class="imageClass" :style="imageStyle" draggable="false" />',
|
||||
props: ['src', 'alt', 'imageClass', 'imageStyle']
|
||||
}
|
||||
}))
|
||||
|
||||
describe('DefaultThumbnail', () => {
|
||||
const mountThumbnail = (props = {}) => {
|
||||
return mount(DefaultThumbnail, {
|
||||
@@ -34,9 +25,9 @@ describe('DefaultThumbnail', () => {
|
||||
|
||||
it('renders image with correct src and alt', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
expect(lazyImage.props('src')).toBe('/test-image.jpg')
|
||||
expect(lazyImage.props('alt')).toBe('Test Image')
|
||||
const img = wrapper.find('img')
|
||||
expect(img.attributes('src')).toBe('/test-image.jpg')
|
||||
expect(img.attributes('alt')).toBe('Test Image')
|
||||
})
|
||||
|
||||
it('applies scale transform when hovered', () => {
|
||||
@@ -44,43 +35,35 @@ describe('DefaultThumbnail', () => {
|
||||
isHovered: true,
|
||||
hoverZoom: 10
|
||||
})
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
expect(lazyImage.props('imageStyle')).toEqual({ transform: 'scale(1.1)' })
|
||||
const img = wrapper.find('img')
|
||||
expect(img.attributes('style')).toContain('scale(1.1)')
|
||||
})
|
||||
|
||||
it('does not apply scale transform when not hovered', () => {
|
||||
const wrapper = mountThumbnail({
|
||||
isHovered: false
|
||||
})
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
expect(lazyImage.props('imageStyle')).toBeUndefined()
|
||||
const img = wrapper.find('img')
|
||||
expect(img.attributes('style')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('applies video styling for video type', () => {
|
||||
const wrapper = mountThumbnail({
|
||||
isVideo: true
|
||||
})
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
const imageClass = lazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('w-full')
|
||||
expect(classString).toContain('h-full')
|
||||
expect(classString).toContain('object-cover')
|
||||
const img = wrapper.find('img')
|
||||
expect(img.classes()).toContain('w-full')
|
||||
expect(img.classes()).toContain('h-full')
|
||||
expect(img.classes()).toContain('object-cover')
|
||||
})
|
||||
|
||||
it('applies image styling for non-video type', () => {
|
||||
const wrapper = mountThumbnail({
|
||||
isVideo: false
|
||||
})
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
const imageClass = lazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('max-w-full')
|
||||
expect(classString).toContain('object-contain')
|
||||
const img = wrapper.find('img')
|
||||
expect(img.classes()).toContain('max-w-full')
|
||||
expect(img.classes()).toContain('object-contain')
|
||||
})
|
||||
|
||||
it('applies correct styling for webp images', () => {
|
||||
@@ -88,12 +71,8 @@ describe('DefaultThumbnail', () => {
|
||||
src: '/test-video.webp',
|
||||
isVideo: true
|
||||
})
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
const imageClass = lazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('object-cover')
|
||||
const img = wrapper.find('img')
|
||||
expect(img.classes()).toContain('object-cover')
|
||||
})
|
||||
|
||||
it('image is not draggable', () => {
|
||||
@@ -104,15 +83,11 @@ describe('DefaultThumbnail', () => {
|
||||
|
||||
it('applies transition classes', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const lazyImage = wrapper.findComponent({ name: 'LazyImage' })
|
||||
const imageClass = lazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('transform-gpu')
|
||||
expect(classString).toContain('transition-transform')
|
||||
expect(classString).toContain('duration-300')
|
||||
expect(classString).toContain('ease-out')
|
||||
const img = wrapper.find('img')
|
||||
expect(img.classes()).toContain('transform-gpu')
|
||||
expect(img.classes()).toContain('transition-transform')
|
||||
expect(img.classes()).toContain('duration-300')
|
||||
expect(img.classes()).toContain('ease-out')
|
||||
})
|
||||
|
||||
it('passes correct props to BaseThumbnail', () => {
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
<template>
|
||||
<BaseThumbnail :hover-zoom="hoverZoom" :is-hovered="isHovered">
|
||||
<LazyImage
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:image-class="[
|
||||
'transform-gpu transition-transform duration-300 ease-out',
|
||||
isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
]"
|
||||
:image-style="
|
||||
isHovered ? { transform: `scale(${1 + hoverZoom / 100})` } : undefined
|
||||
"
|
||||
/>
|
||||
<div class="overflow-hidden w-full h-full flex items-center justify-center">
|
||||
<img
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
draggable="false"
|
||||
:class="[
|
||||
'transform-gpu transition-transform duration-300 ease-out',
|
||||
isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
]"
|
||||
:style="
|
||||
isHovered ? { transform: `scale(${1 + hoverZoom / 100})` } : undefined
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</BaseThumbnail>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LazyImage from '@/components/common/LazyImage.vue'
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
|
||||
const { src, isVideo } = defineProps<{
|
||||
|
||||
@@ -11,15 +11,6 @@ vi.mock('@/components/templates/thumbnails/BaseThumbnail.vue', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/common/LazyImage.vue', () => ({
|
||||
default: {
|
||||
name: 'LazyImage',
|
||||
template:
|
||||
'<img :src="src" :alt="alt" :class="imageClass" :style="imageStyle" draggable="false" />',
|
||||
props: ['src', 'alt', 'imageClass', 'imageStyle']
|
||||
}
|
||||
}))
|
||||
|
||||
describe('HoverDissolveThumbnail', () => {
|
||||
const mountThumbnail = (props = {}) => {
|
||||
return mount(HoverDissolveThumbnail, {
|
||||
@@ -36,39 +27,31 @@ describe('HoverDissolveThumbnail', () => {
|
||||
|
||||
it('renders both base and overlay images', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const lazyImages = wrapper.findAllComponents({ name: 'LazyImage' })
|
||||
expect(lazyImages.length).toBe(2)
|
||||
expect(lazyImages[0].props('src')).toBe('/base-image.jpg')
|
||||
expect(lazyImages[1].props('src')).toBe('/overlay-image.jpg')
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images.length).toBe(2)
|
||||
expect(images[0].attributes('src')).toBe('/base-image.jpg')
|
||||
expect(images[1].attributes('src')).toBe('/overlay-image.jpg')
|
||||
})
|
||||
|
||||
it('applies correct alt text to both images', () => {
|
||||
const wrapper = mountThumbnail({ alt: 'Custom Alt Text' })
|
||||
const lazyImages = wrapper.findAllComponents({ name: 'LazyImage' })
|
||||
expect(lazyImages[0].props('alt')).toBe('Custom Alt Text')
|
||||
expect(lazyImages[1].props('alt')).toBe('Custom Alt Text')
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images[0].attributes('alt')).toBe('Custom Alt Text')
|
||||
expect(images[1].attributes('alt')).toBe('Custom Alt Text')
|
||||
})
|
||||
|
||||
it('makes overlay image visible when hovered', () => {
|
||||
const wrapper = mountThumbnail({ isHovered: true })
|
||||
const overlayLazyImage = wrapper.findAllComponents({ name: 'LazyImage' })[1]
|
||||
const imageClass = overlayLazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('opacity-100')
|
||||
expect(classString).not.toContain('opacity-0')
|
||||
const overlayImage = wrapper.findAll('img')[1]
|
||||
expect(overlayImage.classes()).toContain('opacity-100')
|
||||
expect(overlayImage.classes()).not.toContain('opacity-0')
|
||||
})
|
||||
|
||||
it('makes overlay image hidden when not hovered', () => {
|
||||
const wrapper = mountThumbnail({ isHovered: false })
|
||||
const overlayLazyImage = wrapper.findAllComponents({ name: 'LazyImage' })[1]
|
||||
const imageClass = overlayLazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('opacity-0')
|
||||
expect(classString).not.toContain('opacity-100')
|
||||
const overlayImage = wrapper.findAll('img')[1]
|
||||
expect(overlayImage.classes()).toContain('opacity-0')
|
||||
expect(overlayImage.classes()).not.toContain('opacity-100')
|
||||
})
|
||||
|
||||
it('passes isHovered prop to BaseThumbnail', () => {
|
||||
@@ -79,33 +62,21 @@ describe('HoverDissolveThumbnail', () => {
|
||||
|
||||
it('applies transition classes to overlay image', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const overlayLazyImage = wrapper.findAllComponents({ name: 'LazyImage' })[1]
|
||||
const imageClass = overlayLazyImage.props('imageClass')
|
||||
const classString = Array.isArray(imageClass)
|
||||
? imageClass.join(' ')
|
||||
: imageClass
|
||||
expect(classString).toContain('transition-opacity')
|
||||
expect(classString).toContain('duration-300')
|
||||
const overlayImage = wrapper.findAll('img')[1]
|
||||
expect(overlayImage.classes()).toContain('transition-opacity')
|
||||
expect(overlayImage.classes()).toContain('duration-300')
|
||||
})
|
||||
|
||||
it('applies correct positioning to both images', () => {
|
||||
const wrapper = mountThumbnail()
|
||||
const lazyImages = wrapper.findAllComponents({ name: 'LazyImage' })
|
||||
const images = wrapper.findAll('img')
|
||||
|
||||
// Check base image
|
||||
const baseImageClass = lazyImages[0].props('imageClass')
|
||||
const baseClassString = Array.isArray(baseImageClass)
|
||||
? baseImageClass.join(' ')
|
||||
: baseImageClass
|
||||
expect(baseClassString).toContain('absolute')
|
||||
expect(baseClassString).toContain('inset-0')
|
||||
expect(images[0].classes()).toContain('absolute')
|
||||
expect(images[0].classes()).toContain('inset-0')
|
||||
|
||||
// Check overlay image
|
||||
const overlayImageClass = lazyImages[1].props('imageClass')
|
||||
const overlayClassString = Array.isArray(overlayImageClass)
|
||||
? overlayImageClass.join(' ')
|
||||
: overlayImageClass
|
||||
expect(overlayClassString).toContain('absolute')
|
||||
expect(overlayClassString).toContain('inset-0')
|
||||
expect(images[1].classes()).toContain('absolute')
|
||||
expect(images[1].classes()).toContain('inset-0')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
<template>
|
||||
<BaseThumbnail :is-hovered="isHovered">
|
||||
<div class="relative w-full h-full">
|
||||
<LazyImage :src="baseImageSrc" :alt="alt" :image-class="baseImageClass" />
|
||||
<LazyImage
|
||||
<img
|
||||
:src="baseImageSrc"
|
||||
:alt="alt"
|
||||
draggable="false"
|
||||
class="absolute inset-0"
|
||||
:class="
|
||||
isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
"
|
||||
/>
|
||||
<img
|
||||
:src="overlayImageSrc"
|
||||
:alt="alt"
|
||||
:image-class="overlayImageClass"
|
||||
draggable="false"
|
||||
class="absolute inset-0 transition-opacity duration-300"
|
||||
:class="[
|
||||
isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain',
|
||||
{ 'opacity-100': isHovered, 'opacity-0': !isHovered }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</BaseThumbnail>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import LazyImage from '@/components/common/LazyImage.vue'
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
|
||||
const { baseImageSrc, overlayImageSrc, isVideo, isHovered } = defineProps<{
|
||||
const { baseImageSrc, overlayImageSrc, isVideo } = defineProps<{
|
||||
baseImageSrc: string
|
||||
overlayImageSrc: string
|
||||
alt: string
|
||||
@@ -30,17 +44,4 @@ const isVideoType =
|
||||
baseImageSrc?.toLowerCase().endsWith('.webp') ||
|
||||
overlayImageSrc?.toLowerCase().endsWith('.webp') ||
|
||||
false
|
||||
|
||||
const baseImageClass = computed(() => {
|
||||
return `absolute inset-0 ${isVideoType ? 'w-full h-full object-cover' : 'max-w-full max-h-64 object-contain'}`
|
||||
})
|
||||
|
||||
const overlayImageClass = computed(() => {
|
||||
const baseClasses = 'absolute inset-0 transition-opacity duration-300'
|
||||
const sizeClasses = isVideoType
|
||||
? 'w-full h-full object-cover'
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
const opacityClasses = isHovered ? 'opacity-100' : 'opacity-0'
|
||||
return `${baseClasses} ${sizeClasses} ${opacityClasses}`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface CanvasTransformSyncOptions {
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
LGraphNode,
|
||||
Positionable,
|
||||
Reroute
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Size, Vector2 } from '@comfyorg/litegraph'
|
||||
import { CSSProperties, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import type { Size, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LGraphCanvas, Vector2 } from '@comfyorg/litegraph'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
|
||||
import type { LGraphCanvas, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Convert between canvas and client positions
|
||||
* @param canvasElement - The canvas element
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { ANIM_PREVIEW_WIDGET } from '@/scripts/app'
|
||||
import { createImageHost } from '@/scripts/ui/imagePreview'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
import { useComputedWithWidgetWatch } from '@/composables/node/useWatchWidget'
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useImagePreviewWidget } from '@/composables/widgets/useImagePreviewWidget'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import { useChatHistoryWidget } from '@/composables/widgets/useChatHistoryWidget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const CHAT_HISTORY_WIDGET_NAME = '$$node-chat-history'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
type DragHandler = (e: DragEvent) => boolean
|
||||
type DropHandler<T> = (files: File[]) => Promise<T[]>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
interface FileInputOptions {
|
||||
accept?: string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
type PasteHandler<T> = (files: File[]) => Promise<T>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
/**
|
||||
* Function that calculates dynamic pricing based on node widget values
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computedWithControl } from '@vueuse/core'
|
||||
import { type ComputedRef, ref } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
export interface UseComputedWithWidgetWatchOptions {
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { NodeProperty } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
import { groupBy } from 'lodash'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { Ref } from 'vue'
|
||||
|
||||
import { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { st, te } from '@/i18n'
|
||||
import type {
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue,
|
||||
INodeInputSlot,
|
||||
IWidget
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { st, te } from '@/i18n'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import { Point } from '@comfyorg/litegraph'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import {
|
||||
@@ -5,13 +13,6 @@ import {
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* Assign all properties of LiteGraph to window to make it backward compatible.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type Ref, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
export interface UseIntersectionObserverOptions
|
||||
extends IntersectionObserverInit {
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export function useIntersectionObserver(
|
||||
target: Ref<Element | null>,
|
||||
callback: IntersectionObserverCallback,
|
||||
options: UseIntersectionObserverOptions = {}
|
||||
) {
|
||||
const { immediate = true, ...observerOptions } = options
|
||||
|
||||
const isSupported =
|
||||
typeof window !== 'undefined' && 'IntersectionObserver' in window
|
||||
const isIntersecting = ref(false)
|
||||
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
const cleanup = () => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
}
|
||||
|
||||
const observe = () => {
|
||||
cleanup()
|
||||
|
||||
if (!isSupported || !target.value) return
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
isIntersecting.value = entries.some((entry) => entry.isIntersecting)
|
||||
callback(entries, observer!)
|
||||
}, observerOptions)
|
||||
|
||||
observer.observe(target.value)
|
||||
}
|
||||
|
||||
const unobserve = () => {
|
||||
if (observer && target.value) {
|
||||
observer.unobserve(target.value)
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
watch(target, observe, { immediate: true, flush: 'post' })
|
||||
}
|
||||
|
||||
onBeforeUnmount(cleanup)
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
isIntersecting,
|
||||
observe,
|
||||
unobserve,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { type Ref, computed, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
export interface LazyPaginationOptions {
|
||||
itemsPerPage?: number
|
||||
initialPage?: number
|
||||
}
|
||||
|
||||
export function useLazyPagination<T>(
|
||||
items: Ref<T[]> | T[],
|
||||
options: LazyPaginationOptions = {}
|
||||
) {
|
||||
const { itemsPerPage = 12, initialPage = 1 } = options
|
||||
|
||||
const currentPage = ref(initialPage)
|
||||
const isLoading = ref(false)
|
||||
const loadedPages = shallowRef(new Set<number>([]))
|
||||
|
||||
// Get reactive items array
|
||||
const itemsArray = computed(() => {
|
||||
const itemData = 'value' in items ? items.value : items
|
||||
return Array.isArray(itemData) ? itemData : []
|
||||
})
|
||||
|
||||
// Simulate pagination by slicing the items
|
||||
const paginatedItems = computed(() => {
|
||||
const itemData = itemsArray.value
|
||||
if (itemData.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const loadedPageNumbers = Array.from(loadedPages.value).sort(
|
||||
(a, b) => a - b
|
||||
)
|
||||
const maxLoadedPage = Math.max(...loadedPageNumbers, 0)
|
||||
const endIndex = maxLoadedPage * itemsPerPage
|
||||
return itemData.slice(0, endIndex)
|
||||
})
|
||||
|
||||
const hasMoreItems = computed(() => {
|
||||
const itemData = itemsArray.value
|
||||
if (itemData.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const loadedPagesArray = Array.from(loadedPages.value)
|
||||
const maxLoadedPage = Math.max(...loadedPagesArray, 0)
|
||||
return maxLoadedPage * itemsPerPage < itemData.length
|
||||
})
|
||||
|
||||
const totalPages = computed(() => {
|
||||
const itemData = itemsArray.value
|
||||
if (itemData.length === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.ceil(itemData.length / itemsPerPage)
|
||||
})
|
||||
|
||||
const loadNextPage = async () => {
|
||||
if (isLoading.value || !hasMoreItems.value) return
|
||||
|
||||
isLoading.value = true
|
||||
const loadedPagesArray = Array.from(loadedPages.value)
|
||||
const nextPage = Math.max(...loadedPagesArray, 0) + 1
|
||||
|
||||
// Simulate network delay
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
|
||||
const newLoadedPages = new Set(loadedPages.value)
|
||||
newLoadedPages.add(nextPage)
|
||||
loadedPages.value = newLoadedPages
|
||||
currentPage.value = nextPage
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// Initialize with first page
|
||||
watch(
|
||||
() => itemsArray.value.length,
|
||||
(length) => {
|
||||
if (length > 0 && loadedPages.value.size === 0) {
|
||||
loadedPages.value = new Set([1])
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const reset = () => {
|
||||
currentPage.value = initialPage
|
||||
loadedPages.value = new Set([])
|
||||
isLoading.value = false
|
||||
|
||||
// Immediately load first page if we have items
|
||||
const itemData = itemsArray.value
|
||||
if (itemData.length > 0) {
|
||||
loadedPages.value = new Set([1])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paginatedItems,
|
||||
isLoading,
|
||||
hasMoreItems,
|
||||
currentPage,
|
||||
totalPages,
|
||||
loadNextPage,
|
||||
reset
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { CanvasPointer, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
import {
|
||||
CanvasPointer,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -434,14 +434,13 @@ export function useMinimap() {
|
||||
const { startSync: startViewportSync, stopSync: stopViewportSync } =
|
||||
useCanvasTransformSync(updateViewport, { autoStart: false })
|
||||
|
||||
// Pointer event handlers for touch screen support
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
isDragging.value = true
|
||||
updateContainerRect()
|
||||
handlePointerMove(e)
|
||||
handleMouseMove(e)
|
||||
}
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value || !canvasRef.value || !canvas.value) return
|
||||
|
||||
const x = e.clientX - containerRect.value.left
|
||||
@@ -456,7 +455,7 @@ export function useMinimap() {
|
||||
centerViewOn(worldX, worldY)
|
||||
}
|
||||
|
||||
const handlePointerUp = () => {
|
||||
const handleMouseUp = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
@@ -696,9 +695,9 @@ export function useMinimap() {
|
||||
init,
|
||||
destroy,
|
||||
toggle,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
handleWheel,
|
||||
setMinimapRef
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { type Ref, computed, ref } from 'vue'
|
||||
|
||||
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||
|
||||
export interface TemplateFilterOptions {
|
||||
searchQuery?: string
|
||||
}
|
||||
|
||||
export function useTemplateFiltering(
|
||||
templates: Ref<TemplateInfo[]> | TemplateInfo[]
|
||||
) {
|
||||
const searchQuery = ref('')
|
||||
|
||||
const templatesArray = computed(() => {
|
||||
const templateData = 'value' in templates ? templates.value : templates
|
||||
return Array.isArray(templateData) ? templateData : []
|
||||
})
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
const templateData = templatesArray.value
|
||||
if (templateData.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!searchQuery.value.trim()) {
|
||||
return templateData
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
return templateData.filter((template) => {
|
||||
const searchableText = [
|
||||
template.name,
|
||||
template.description,
|
||||
template.sourceModule
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
|
||||
return searchableText.includes(query)
|
||||
})
|
||||
})
|
||||
|
||||
const resetFilters = () => {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
const filteredCount = computed(() => filteredTemplates.value.length)
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
filteredTemplates,
|
||||
filteredCount,
|
||||
resetFilters
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { ISerialisedGraph } from '@comfyorg/litegraph/dist/types/serialisation'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { validateComfyWorkflow } from '@/schemas/comfyWorkflowSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import {
|
||||
type InputSpec,
|
||||
isBooleanInputSpec
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
ComboInputSpec,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import {
|
||||
type InputSpec,
|
||||
isFloatInputSpec
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
type CanvasPointer,
|
||||
type LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
import { useNodeImageUpload } from '@/composables/node/useNodeImageUpload'
|
||||
import { useValueTransform } from '@/composables/useValueTransform'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ResultItem, ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
type InputSpec,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { Editor as TiptapEditor } from '@tiptap/core'
|
||||
import TiptapLink from '@tiptap/extension-link'
|
||||
import TiptapTable from '@tiptap/extension-table'
|
||||
@@ -7,7 +8,6 @@ import TiptapTableRow from '@tiptap/extension-table-row'
|
||||
import TiptapStarterKit from '@tiptap/starter-kit'
|
||||
import { Markdown as TiptapMarkdown } from 'tiptap-markdown'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { type InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import axios from 'axios'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { IWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import {
|
||||
type InputSpec,
|
||||
isStringInputSpec
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { LinkMarkerShape, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
|
||||
import type { Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LiteGraph,
|
||||
isComboWidget
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph, isComboWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { t } from '@/i18n'
|
||||
import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
type ExecutionId,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
import { type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
ComfyLink,
|
||||
ComfyNode,
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
type LGraphNode,
|
||||
type LGraphNodeConstructor,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
import { type ComfyApp, app } from '../../scripts/app'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphGroup } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
@@ -6,7 +7,6 @@ import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { t } from '@/i18n'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
|
||||