Compare commits
34 Commits
test-fix
...
luke-mino-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a87e4c601 | ||
|
|
3fc1d1663b | ||
|
|
5ece3d6f2e | ||
|
|
17ceb75dce | ||
|
|
bd6825a274 | ||
|
|
f490b81be5 | ||
|
|
ddbd26c062 | ||
|
|
adfd2e514e | ||
|
|
f0f554392d | ||
|
|
e639577685 | ||
|
|
596add9f63 | ||
|
|
5e4965d131 | ||
|
|
63ca4a3779 | ||
|
|
60647fd5b9 | ||
|
|
bbfada561e | ||
|
|
c4477fc7ab | ||
|
|
ef46f0cbf4 | ||
|
|
02d303c039 | ||
|
|
23b0d2eb7f | ||
|
|
cfbd5361d3 | ||
|
|
1e71eae177 | ||
|
|
2c3c97d4b5 | ||
|
|
f97cf77e75 | ||
|
|
2cf3739236 | ||
|
|
30fc784ae4 | ||
|
|
d14c416cc4 | ||
|
|
1f78b59afc | ||
|
|
cbe7e8967c | ||
|
|
2542449d45 | ||
|
|
3c550e953a | ||
|
|
6541f5cda5 | ||
|
|
812f1797d5 | ||
|
|
0be1da2041 | ||
|
|
879cb8f1a8 |
6
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -36,9 +36,9 @@ body:
|
||||
3. Click Queue Prompt
|
||||
4. See error
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ runs:
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
if [[ -z "$VERSION_FILE" ]]; then
|
||||
echo '::error::version_file input is required' >&2
|
||||
echo "::error::version_file input is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -55,11 +55,13 @@ runs:
|
||||
|
||||
case "$VERSION_FILE" in
|
||||
package.json)
|
||||
LINKS_VALUE=$'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/\n''npm types|https://npm.im/@comfyorg/comfyui-frontend-types@{{version}}'
|
||||
LINKS_VALUE=$(printf '%s\n%s' \
|
||||
'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/' \
|
||||
'npm types|https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/{{version}}')
|
||||
;;
|
||||
apps/desktop-ui/package.json)
|
||||
MARKER='desktop-release-summary'
|
||||
LINKS_VALUE='npm desktop UI|https://npm.im/@comfyorg/desktop-ui@{{version}}'
|
||||
LINKS_VALUE='npm desktop UI|https://www.npmjs.com/package/@comfyorg/desktop-ui/v/{{version}}'
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -69,12 +71,13 @@ runs:
|
||||
COMMENT_FILE=$(mktemp)
|
||||
|
||||
{
|
||||
printf '<!--%s:%s%s-->\n' "$MARKER" "$DIFF_PREFIX" "$NEW_VERSION"
|
||||
printf '%s\n\n' "$MESSAGE"
|
||||
printf -- '- %s: [%s%s...%s%s](%s)\n' "$DIFF_LABEL" "$DIFF_PREFIX" "$PREV_VERSION" "$DIFF_PREFIX" "$NEW_VERSION" "$DIFF_URL"
|
||||
echo "<!--$MARKER:$DIFF_PREFIX$NEW_VERSION-->"
|
||||
echo "$MESSAGE"
|
||||
echo ""
|
||||
echo "- $DIFF_LABEL: [\`$DIFF_PREFIX$PREV_VERSION...$DIFF_PREFIX$NEW_VERSION\`]($DIFF_URL)"
|
||||
|
||||
while IFS= read -r RAW_LINE; do
|
||||
LINE=$(printf '%s' "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
LINE=$(echo "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -z "$LINE" ]] && continue
|
||||
if [[ "$LINE" != *"|"* ]]; then
|
||||
echo "::warning::Skipping malformed link entry: $LINE" >&2
|
||||
@@ -84,16 +87,16 @@ runs:
|
||||
URL_TEMPLATE=${LINE#*|}
|
||||
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
|
||||
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
|
||||
printf -- '- %s: %s\n' "$LABEL" "$URL"
|
||||
echo "- $LABEL: [\`$NEW_VERSION\`]($URL)"
|
||||
done <<< "$LINKS_VALUE"
|
||||
|
||||
printf '\n'
|
||||
echo ""
|
||||
} > "$COMMENT_FILE"
|
||||
|
||||
{
|
||||
echo "body<<'EOF'"
|
||||
echo "body<<COMMENT_BODY_END_MARKER"
|
||||
cat "$COMMENT_FILE"
|
||||
echo 'EOF'
|
||||
echo "COMMENT_BODY_END_MARKER"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -105,4 +105,4 @@ jobs:
|
||||
labels: Manager
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
src/types/generatedManagerTypes.ts
|
||||
src/types/generatedManagerTypes.ts
|
||||
|
||||
2
.github/workflows/ci-json-validation.yaml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.json'
|
||||
|
||||
jobs:
|
||||
json-lint:
|
||||
|
||||
2
.github/workflows/ci-lint-format.yaml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit changes
|
||||
|
||||
2
.github/workflows/ci-python-validation.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
paths:
|
||||
- 'tools/devtools/**'
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tools/devtools/**'
|
||||
|
||||
|
||||
14
.github/workflows/ci-tests-e2e-forks.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
deploy-and-comment-forked-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.head_repository != null &&
|
||||
github.event.workflow_run.repository != null &&
|
||||
@@ -43,14 +43,14 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
});
|
||||
|
||||
|
||||
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
|
||||
|
||||
|
||||
if (!pr) {
|
||||
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
|
||||
return pr.number;
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: playwright-report-*
|
||||
path: reports
|
||||
|
||||
|
||||
- name: Handle Test Completion
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
env:
|
||||
@@ -85,9 +85,9 @@ jobs:
|
||||
# Rename merged report if exists
|
||||
[ -d "reports/playwright-report-chromium-merged" ] && \
|
||||
mv reports/playwright-report-chromium-merged reports/playwright-report-chromium
|
||||
|
||||
|
||||
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
|
||||
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
|
||||
"${{ steps.pr.outputs.result }}" \
|
||||
"${{ github.event.workflow_run.head_branch }}" \
|
||||
"completed"
|
||||
"completed"
|
||||
|
||||
2
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
|
||||
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
|
||||
|
||||
# Save the entire workspace as cache for later test jobs to restore
|
||||
- name: Generate cache key
|
||||
|
||||
12
.github/workflows/ci-tests-storybook-forks.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
deploy-and-comment-forked-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.head_repository != null &&
|
||||
github.event.workflow_run.repository != null &&
|
||||
@@ -43,14 +43,14 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
});
|
||||
|
||||
|
||||
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
|
||||
|
||||
|
||||
if (!pr) {
|
||||
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
|
||||
return pr.number;
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: storybook-static
|
||||
path: storybook-static
|
||||
|
||||
|
||||
- name: Handle Storybook Completion
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
env:
|
||||
@@ -88,4 +88,4 @@ jobs:
|
||||
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
|
||||
"${{ steps.pr.outputs.result }}" \
|
||||
"${{ github.event.workflow_run.head_branch }}" \
|
||||
"completed"
|
||||
"completed"
|
||||
|
||||
28
.github/workflows/ci-tests-storybook.yaml
vendored
@@ -2,7 +2,7 @@ name: "CI: Tests Storybook"
|
||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
|
||||
- name: Post starting comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # Required for Chromatic baseline
|
||||
fetch-depth: 0 # Required for Chromatic baseline
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -111,9 +111,9 @@ jobs:
|
||||
with:
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
buildScriptName: build-storybook
|
||||
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
||||
exitOnceUploaded: true # Don't wait for UI tests to complete
|
||||
onlyChanged: true # Only capture changed stories
|
||||
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
||||
exitOnceUploaded: true # Don't wait for UI tests to complete
|
||||
onlyChanged: true # Only capture changed stories
|
||||
|
||||
- name: Set job status
|
||||
id: job-status
|
||||
@@ -138,17 +138,17 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
|
||||
- name: Download Storybook build
|
||||
if: needs.storybook-build.outputs.conclusion == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static
|
||||
|
||||
|
||||
- name: Make deployment script executable
|
||||
run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
|
||||
|
||||
|
||||
- name: Deploy Storybook and comment on PR
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -176,25 +176,25 @@ jobs:
|
||||
script: |
|
||||
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
|
||||
const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
|
||||
|
||||
|
||||
// Find the existing Storybook comment
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ github.event.pull_request.number }}
|
||||
});
|
||||
|
||||
const storybookComment = comments.find(comment =>
|
||||
|
||||
const storybookComment = comments.find(comment =>
|
||||
comment.body.includes('<!-- STORYBOOK_BUILD_STATUS -->')
|
||||
);
|
||||
|
||||
|
||||
if (storybookComment && buildUrl && storybookUrl) {
|
||||
// Append Chromatic info to existing comment
|
||||
const updatedBody = storybookComment.body.replace(
|
||||
/---\n(.*)$/s,
|
||||
`---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
|
||||
);
|
||||
|
||||
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
|
||||
33
.github/workflows/ci-yaml-validation.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "CI: YAML Validation"
|
||||
description: "Validates YAML syntax and style using yamllint with relaxed rules"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
|
||||
jobs:
|
||||
yaml-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install yamllint
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install yamllint
|
||||
|
||||
- name: Validate YAML syntax and style
|
||||
run: ./scripts/cicd/check-yaml.sh
|
||||
82
.github/workflows/i18n-update-core.yaml
vendored
@@ -2,11 +2,11 @@ name: "i18n: Update Core"
|
||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
||||
|
||||
on:
|
||||
# Manual dispatch for urgent translation updates
|
||||
# Manual dispatch for urgent translation updates
|
||||
workflow_dispatch:
|
||||
# Only trigger on PRs to main/master - additional branch filtering in job condition
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
@@ -15,45 +15,45 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Setup playwright environment
|
||||
- name: Setup ComfyUI Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup ComfyUI Server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
# Setup playwright environment
|
||||
- name: Setup ComfyUI Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup ComfyUI Server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: pnpm dev:electron &
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: pnpm dev:electron &
|
||||
|
||||
# Update locales, collect new strings and update translations using OpenAI, then commit changes
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
- name: Update translations
|
||||
run: pnpm locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
- name: Commit updated locales
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git fetch origin ${{ github.head_ref }}
|
||||
# Stash any local changes before checkout
|
||||
git stash -u
|
||||
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
||||
# Apply the stashed changes if any
|
||||
git stash pop || true
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
# Update locales, collect new strings and update translations using OpenAI, then commit changes
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
- name: Update translations
|
||||
run: pnpm locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
- name: Commit updated locales
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git fetch origin ${{ github.head_ref }}
|
||||
# Stash any local changes before checkout
|
||||
git stash -u
|
||||
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
||||
# Apply the stashed changes if any
|
||||
git stash pop || true
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
|
||||
204
.github/workflows/i18n-update-custom-nodes.yaml
vendored
@@ -21,116 +21,116 @@ jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Setup playwright environment with custom node repository
|
||||
- name: Setup ComfyUI Server (without launching)
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: 'true'
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
# Setup playwright environment with custom node repository
|
||||
- name: Setup ComfyUI Server (without launching)
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: 'true'
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
# Install the custom node repository
|
||||
- name: Checkout custom node repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
||||
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
||||
- name: Install custom node Python requirements
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
# Install the custom node repository
|
||||
- name: Checkout custom node repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
||||
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
||||
- name: Install custom node Python requirements
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
|
||||
# Start ComfyUI Server
|
||||
- name: Start ComfyUI Server
|
||||
shell: bash
|
||||
working-directory: ComfyUI
|
||||
run: |
|
||||
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
|
||||
wait-for-it --service
|
||||
# Start ComfyUI Server
|
||||
- name: Start ComfyUI Server
|
||||
shell: bash
|
||||
working-directory: ComfyUI
|
||||
run: |
|
||||
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
|
||||
wait-for-it --service
|
||||
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: pnpm dev:electron &
|
||||
|
||||
- name: Capture base i18n
|
||||
run: pnpm exec tsx scripts/diff-i18n capture
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
- name: Update translations
|
||||
run: pnpm locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
- name: Diff base vs updated i18n
|
||||
run: pnpm exec tsx scripts/diff-i18n diff
|
||||
- name: Update i18n in custom node repository
|
||||
run: |
|
||||
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
|
||||
install -d "$LOCALE_DIR"
|
||||
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
|
||||
|
||||
# Git ops for pushing changes and creating PR
|
||||
- name: Check and create fork of custom node repository
|
||||
run: |
|
||||
# Try to fork the repository
|
||||
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
|
||||
echo "Fork failed - repository might already be forked"
|
||||
# Exit 0 to prevent the workflow from failing
|
||||
exit 0
|
||||
}
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: pnpm dev:electron &
|
||||
|
||||
# Enable workflows on the forked repository
|
||||
gh api \
|
||||
--method PUT \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
|
||||
-F can_approve_pull_request_reviews=true \
|
||||
-F default_workflow_permissions="write" \
|
||||
-F enabled=true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
- name: Capture base i18n
|
||||
run: pnpm exec tsx scripts/diff-i18n capture
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
- name: Update translations
|
||||
run: pnpm locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
- name: Diff base vs updated i18n
|
||||
run: pnpm exec tsx scripts/diff-i18n diff
|
||||
- name: Update i18n in custom node repository
|
||||
run: |
|
||||
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
|
||||
install -d "$LOCALE_DIR"
|
||||
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
|
||||
|
||||
- name: Commit changes
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
# Git ops for pushing changes and creating PR
|
||||
- name: Check and create fork of custom node repository
|
||||
run: |
|
||||
# Try to fork the repository
|
||||
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
|
||||
echo "Fork failed - repository might already be forked"
|
||||
# Exit 0 to prevent the workflow from failing
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Create and switch to new branch
|
||||
git checkout -b update-locales
|
||||
# Enable workflows on the forked repository
|
||||
gh api \
|
||||
--method PUT \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
|
||||
-F can_approve_pull_request_reviews=true \
|
||||
-F default_workflow_permissions="write" \
|
||||
-F enabled=true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
# Stage and commit changes
|
||||
git add -A
|
||||
git commit -m "Update locales"
|
||||
- name: Commit changes
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
- name: Install SSH key For PUSH
|
||||
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
|
||||
with:
|
||||
# PR private key from action server
|
||||
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
|
||||
# github public key to confirm it's github server
|
||||
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
||||
# Create and switch to new branch
|
||||
git checkout -b update-locales
|
||||
|
||||
- name: Push changes
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
# Force push to create the branch
|
||||
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
|
||||
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
|
||||
# Stage and commit changes
|
||||
git add -A
|
||||
git commit -m "Update locales"
|
||||
|
||||
- name: Create PR
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
# Create PR using gh cli
|
||||
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
- name: Install SSH key For PUSH
|
||||
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
|
||||
with:
|
||||
# PR private key from action server
|
||||
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
|
||||
# github public key to confirm it's github server
|
||||
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
||||
|
||||
- name: Push changes
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
# Force push to create the branch
|
||||
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
|
||||
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
|
||||
|
||||
- name: Create PR
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
run: |
|
||||
# Create PR using gh cli
|
||||
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
74
.github/workflows/i18n-update-nodes.yaml
vendored
@@ -13,42 +13,42 @@ jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
# Setup playwright environment
|
||||
- name: Setup ComfyUI Server (and start)
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
# Setup playwright environment
|
||||
- name: Setup ComfyUI Server (and start)
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: pnpm dev:electron &
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
- name: Update translations
|
||||
run: pnpm locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: "Update locales for node definitions"
|
||||
title: "Update locales for node definitions"
|
||||
body: |
|
||||
Automated PR to update locales for node definitions
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: pnpm dev:electron &
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
- name: Update translations
|
||||
run: pnpm locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: "Update locales for node definitions"
|
||||
title: "Update locales for node definitions"
|
||||
body: |
|
||||
Automated PR to update locales for node definitions
|
||||
|
||||
This PR was created automatically by the frontend update workflow.
|
||||
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
|
||||
base: main
|
||||
labels: dependencies
|
||||
This PR was created automatically by the frontend update workflow.
|
||||
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
|
||||
base: main
|
||||
labels: dependencies
|
||||
|
||||
85
.github/workflows/pr-backport.yaml
vendored
@@ -19,8 +19,8 @@ on:
|
||||
jobs:
|
||||
backport:
|
||||
if: >
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -38,19 +38,19 @@ jobs:
|
||||
echo "::error::Invalid PR number format. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Validate PR exists and is merged
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
|
||||
if [ "$MERGED" != "true" ]; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Validate PR has needs-backport label
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
|
||||
@@ -164,6 +164,7 @@ jobs:
|
||||
|
||||
PENDING=()
|
||||
SKIPPED=()
|
||||
REUSED=()
|
||||
|
||||
for target in $REQUESTED_TARGETS; do
|
||||
SAFE_TARGET=$(echo "$target" | tr '/' '-')
|
||||
@@ -176,10 +177,22 @@ jobs:
|
||||
|
||||
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
|
||||
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
|
||||
SKIPPED+=("$target")
|
||||
else
|
||||
PENDING+=("$target")
|
||||
OPEN_PR=$(
|
||||
gh pr list \
|
||||
--state open \
|
||||
--head "${BACKPORT_BRANCH}" \
|
||||
--json number \
|
||||
--jq 'if length > 0 then .[0].number else "" end'
|
||||
)
|
||||
if [ -n "$OPEN_PR" ]; then
|
||||
SKIPPED+=("${target} (PR #${OPEN_PR})")
|
||||
continue
|
||||
fi
|
||||
|
||||
REUSED+=("$BACKPORT_BRANCH")
|
||||
fi
|
||||
|
||||
PENDING+=("$target")
|
||||
done
|
||||
|
||||
SKIPPED_JOINED="${SKIPPED[*]:-}"
|
||||
@@ -187,16 +200,20 @@ jobs:
|
||||
|
||||
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
|
||||
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
|
||||
echo "reused-branches=${REUSED[*]:-}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -z "$PENDING_JOINED" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
if [ -n "$SKIPPED_JOINED" ]; then
|
||||
echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}"
|
||||
echo "::warning::Backport branches exist: ${SKIPPED_JOINED}"
|
||||
fi
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
if [ -n "$SKIPPED_JOINED" ]; then
|
||||
echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}"
|
||||
echo "::notice::Skipping backport targets: ${SKIPPED_JOINED}"
|
||||
fi
|
||||
if [ "${#REUSED[@]}" -gt 0 ]; then
|
||||
echo "::notice::Reusing backport branches: ${REUSED[*]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -208,7 +225,12 @@ jobs:
|
||||
run: |
|
||||
FAILED=""
|
||||
SUCCESS=""
|
||||
|
||||
|
||||
CREATED_BRANCHES_FILE="$(
|
||||
mktemp "$RUNNER_TEMP/backport-branches-XXXXXX"
|
||||
)"
|
||||
echo "CREATED_BRANCHES_FILE=$CREATED_BRANCHES_FILE" >> "$GITHUB_ENV"
|
||||
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
|
||||
@@ -223,6 +245,12 @@ jobs:
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
REMOTE_BACKPORT_EXISTS=false
|
||||
|
||||
if git ls-remote --exit-code origin "${BACKPORT_BRANCH}" >/dev/null 2>&1; then
|
||||
REMOTE_BACKPORT_EXISTS=true
|
||||
echo "::notice::Updating existing branch ${BACKPORT_BRANCH}"
|
||||
fi
|
||||
|
||||
echo "::group::Backporting to ${TARGET_BRANCH}"
|
||||
|
||||
@@ -247,7 +275,12 @@ jobs:
|
||||
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then
|
||||
git push --force-with-lease origin "${BACKPORT_BRANCH}"
|
||||
else
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
fi
|
||||
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
@@ -271,6 +304,13 @@ jobs:
|
||||
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
|
||||
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -s "$CREATED_BRANCHES_FILE" ]; then
|
||||
CREATED_LIST=$(paste -sd' ' "$CREATED_BRANCHES_FILE")
|
||||
echo "created-branches=${CREATED_LIST}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "created-branches=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ -n "${FAILED}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -290,7 +330,7 @@ jobs:
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
fi
|
||||
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r target branch <<< "${backport}"
|
||||
|
||||
@@ -348,6 +388,25 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Cleanup stranded backport branches
|
||||
if: steps.filter-targets.outputs.skip != 'true' && failure()
|
||||
run: |
|
||||
FILE="${CREATED_BRANCHES_FILE:-}"
|
||||
|
||||
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
|
||||
echo "No backport branches recorded for cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while IFS= read -r branch; do
|
||||
[ -z "$branch" ] && continue
|
||||
printf 'Deleting branch %s\n' "${branch}"
|
||||
if ! git push origin --delete "$branch"; then
|
||||
echo "::warning::Failed to delete ${branch}"
|
||||
fi
|
||||
done < "$FILE"
|
||||
|
||||
|
||||
- name: Remove needs-backport label
|
||||
if: steps.filter-targets.outputs.skip != 'true' && success()
|
||||
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"
|
||||
|
||||
@@ -127,26 +127,26 @@ jobs:
|
||||
echo "=========================================="
|
||||
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
|
||||
echo "=========================================="
|
||||
|
||||
|
||||
# Get list of changed snapshot files
|
||||
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
|
||||
|
||||
|
||||
if [ -z "$changed_files" ]; then
|
||||
echo "No snapshot changes in this shard"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
echo "✓ Found changed files:"
|
||||
echo "$changed_files"
|
||||
file_count=$(echo "$changed_files" | wc -l)
|
||||
echo "Count: $file_count"
|
||||
echo "has-changes=true" >> $GITHUB_OUTPUT
|
||||
echo ""
|
||||
|
||||
|
||||
# Create staging directory
|
||||
mkdir -p /tmp/changed_snapshots_shard
|
||||
|
||||
|
||||
# Copy only changed files, preserving directory structure
|
||||
# Strip 'browser_tests/' prefix to avoid double nesting
|
||||
echo "Copying changed files to staging directory..."
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix"
|
||||
echo " → $file_without_prefix"
|
||||
done <<< "$changed_files"
|
||||
|
||||
|
||||
echo ""
|
||||
echo "Staged files for upload:"
|
||||
find /tmp/changed_snapshots_shard -type f
|
||||
@@ -233,18 +233,18 @@ jobs:
|
||||
|
||||
shard_name=$(basename "$shard_dir")
|
||||
file_count=$(find "$shard_dir" -type f | wc -l)
|
||||
|
||||
|
||||
if [ "$file_count" -eq 0 ]; then
|
||||
echo " $shard_name: no files"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Processing $shard_name ($file_count file(s))..."
|
||||
|
||||
|
||||
# Copy files directly, preserving directory structure
|
||||
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
|
||||
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
|
||||
|
||||
|
||||
merged_count=$((merged_count + 1))
|
||||
echo " ✓ Merged"
|
||||
echo ""
|
||||
@@ -272,25 +272,25 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
|
||||
if git diff --quiet browser_tests/; then
|
||||
echo "No changes to commit"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
echo "=========================================="
|
||||
echo "COMMITTING CHANGES"
|
||||
echo "=========================================="
|
||||
|
||||
|
||||
echo "has-changes=true" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
git add browser_tests/
|
||||
git commit -m "[automated] Update test expectations"
|
||||
|
||||
|
||||
echo "Pushing to ${{ needs.setup.outputs.branch }}..."
|
||||
git push origin ${{ needs.setup.outputs.branch }}
|
||||
|
||||
|
||||
echo "✓ Commit and push successful"
|
||||
|
||||
- name: Add Done Reaction
|
||||
@@ -306,4 +306,4 @@ jobs:
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
run: gh pr edit ${{ needs.setup.outputs.pr-number }} --remove-label "New Browser Test Expectations"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
72
.github/workflows/release-branch-create.yaml
vendored
@@ -153,8 +153,78 @@ jobs:
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post summary
|
||||
- name: Ensure release labels
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH_BASE="${{ steps.check_version.outputs.branch_base }}"
|
||||
|
||||
if [[ -z "$BRANCH_BASE" ]]; then
|
||||
echo "::error::Branch base not set; unable to manage labels"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -A COLORS=(
|
||||
[core]="4361ee"
|
||||
[cloud]="4f6ef5"
|
||||
)
|
||||
|
||||
for PREFIX in core cloud; do
|
||||
LABEL="${PREFIX}/${BRANCH_BASE}"
|
||||
COLOR="${COLORS[$PREFIX]}"
|
||||
DESCRIPTION="Backport PRs for ${PREFIX} ${BRANCH_BASE}"
|
||||
|
||||
if gh label view "$LABEL" >/dev/null 2>&1; then
|
||||
gh label edit "$LABEL" \
|
||||
--color "$COLOR" \
|
||||
--description "$DESCRIPTION"
|
||||
echo "🔄 Updated label $LABEL"
|
||||
else
|
||||
gh label create "$LABEL" \
|
||||
--color "$COLOR" \
|
||||
--description "$DESCRIPTION"
|
||||
echo "✨ Created label $LABEL"
|
||||
fi
|
||||
done
|
||||
|
||||
MIN_LABELS_TO_KEEP=3
|
||||
MAX_LABELS_TO_FETCH=200
|
||||
|
||||
for PREFIX in core cloud; do
|
||||
mapfile -t LABELS < <(
|
||||
gh label list \
|
||||
--json name \
|
||||
--limit "$MAX_LABELS_TO_FETCH" \
|
||||
--jq '.[].name' |
|
||||
grep -E "^${PREFIX}/[0-9]+\.[0-9]+$" |
|
||||
sort -t/ -k2,2V
|
||||
)
|
||||
|
||||
TOTAL=${#LABELS[@]}
|
||||
|
||||
if (( TOTAL <= MIN_LABELS_TO_KEEP )); then
|
||||
echo "ℹ️ Nothing to prune for $PREFIX labels"
|
||||
continue
|
||||
fi
|
||||
|
||||
REMOVE_COUNT=$((TOTAL - MIN_LABELS_TO_KEEP))
|
||||
|
||||
if (( REMOVE_COUNT > 1 )); then
|
||||
REMOVE_COUNT=1
|
||||
fi
|
||||
|
||||
for ((i=0; i<REMOVE_COUNT; i++)); do
|
||||
OLD_LABEL="${LABELS[$i]}"
|
||||
gh label delete "$OLD_LABEL" --yes
|
||||
echo "🗑️ Removed old label $OLD_LABEL"
|
||||
done
|
||||
done
|
||||
|
||||
- name: Post summary
|
||||
if: always() && steps.check_version.outputs.is_minor_bump == 'true'
|
||||
run: |
|
||||
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
|
||||
RESULTS="${{ steps.create_branches.outputs.results }}"
|
||||
|
||||
1
.github/workflows/release-version-bump.yaml
vendored
@@ -59,6 +59,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Bump version
|
||||
id: bump-version
|
||||
|
||||
@@ -92,4 +92,3 @@ jobs:
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
43
.oxlintrc.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".i18nrc.cjs",
|
||||
"components.d.ts",
|
||||
"lint-staged.config.js",
|
||||
"vitest.setup.ts",
|
||||
"**/vite.config.*.timestamp*",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"src/extensions/core/*",
|
||||
"src/scripts/*",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"src/types/vue-shim.d.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "off",
|
||||
"no-self-assign": "allow",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-unused-vars": "off",
|
||||
"unicorn/no-empty-file": "off",
|
||||
"unicorn/no-new-array": "off",
|
||||
"unicorn/no-single-promise-in-promise-methods": "off",
|
||||
"unicorn/no-useless-fallback-in-spread": "off",
|
||||
"unicorn/no-useless-spread": "off",
|
||||
"typescript/await-thenable": "off",
|
||||
"typescript/no-base-to-string": "off",
|
||||
"typescript/no-duplicate-type-constituents": "off",
|
||||
"typescript/no-for-in-array": "off",
|
||||
"typescript/no-meaningless-void-operator": "off",
|
||||
"typescript/no-redundant-type-constituents": "off",
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error"
|
||||
}
|
||||
}
|
||||
10
.yamllint
Normal file
@@ -0,0 +1,10 @@
|
||||
extends: default
|
||||
|
||||
ignore: |
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
rules:
|
||||
line-length: disable
|
||||
document-start: disable
|
||||
truthy: disable
|
||||
@@ -17,6 +17,7 @@ This bootstraps the monorepo with dependencies, builds, tests, and dev server ve
|
||||
- `pnpm typecheck`: Type checking
|
||||
- `pnpm build`: Build for production (via nx)
|
||||
- `pnpm lint`: Linting (via nx)
|
||||
- `pnpm oxlint`: Fast Rust-based linting with Oxc
|
||||
- `pnpm format`: Prettier formatting
|
||||
- `pnpm test:unit`: Run all unit tests
|
||||
- `pnpm test:browser`: Run E2E tests via Playwright
|
||||
|
||||
@@ -71,8 +71,8 @@ const updateConsent = async () => {
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('install.errorUpdatingConsent'),
|
||||
detail: t('install.errorUpdatingConsentDetail'),
|
||||
summary: t('install.settings.errorUpdatingConsent'),
|
||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@/*": ["./src/*"],
|
||||
"@frontend-locales/*": ["../../src/locales/*"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -88,6 +88,10 @@ test.describe('Missing models warning', () => {
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
|
||||
// Check that the copy URL button is also visible for Desktop environment
|
||||
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
|
||||
await expect(copyUrlButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should display a warning when missing models are found in node properties', async ({
|
||||
@@ -101,6 +105,10 @@ test.describe('Missing models warning', () => {
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
|
||||
// Check that the copy URL button is also visible for Desktop environment
|
||||
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
|
||||
await expect(copyUrlButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should not display a warning when no missing models are found', async ({
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 82 KiB |
@@ -3,6 +3,7 @@ import pluginJs from '@eslint/js'
|
||||
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
@@ -33,7 +34,18 @@ const settings = {
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
]
|
||||
],
|
||||
'vue-i18n': {
|
||||
localeDir: [
|
||||
{
|
||||
pattern: './src/locales/**/*.json',
|
||||
localeKey: 'path',
|
||||
localePattern:
|
||||
/^\.?\/?src\/locales\/(?<locale>[A-Za-z0-9-]+)\/.+\.json$/
|
||||
}
|
||||
],
|
||||
messageSyntaxVersion: '^9.0.0'
|
||||
}
|
||||
} as const
|
||||
|
||||
const commonParserOptions = {
|
||||
@@ -94,19 +106,23 @@ export default defineConfig([
|
||||
// @ts-ignore Bad types in the plugin
|
||||
pluginVue.configs['flat/recommended'],
|
||||
eslintPluginPrettierRecommended,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
storybook.configs['flat/recommended'],
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
importX.flatConfigs.recommended,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
|
||||
importX.flatConfigs.typescript,
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Type incompatibility in i18n plugin
|
||||
'@intlify/vue-i18n': pluginI18n
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
@@ -259,5 +275,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Turn off ESLint rules that are already handled by oxlint
|
||||
...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json')
|
||||
])
|
||||
|
||||
15
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.32.4",
|
||||
"version": "1.32.5",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -28,13 +28,14 @@
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "oxlint src --type-aware --fix && eslint src --fix",
|
||||
"lint:fix": "oxlint src --type-aware --fix && eslint src --cache --fix",
|
||||
"lint:no-cache": "oxlint src --type-aware && eslint src",
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint": "eslint src --cache",
|
||||
"lint": "oxlint src --type-aware && eslint src --cache",
|
||||
"locale": "lobe-i18n locale",
|
||||
"oxlint": "oxlint src --type-aware",
|
||||
"preinstall": "pnpm dlx only-allow pnpm",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
@@ -42,6 +43,7 @@
|
||||
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 pnpm test:browser",
|
||||
"test:unit": "nx run test",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
@@ -78,6 +80,7 @@
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-typescript": "catalog:",
|
||||
"eslint-plugin-import-x": "catalog:",
|
||||
"eslint-plugin-oxlint": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
@@ -93,6 +96,8 @@
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"oxlint-tsgolint": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import type { PlaywrightTestConfig } from '@playwright/test'
|
||||
|
||||
const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
|
||||
? {
|
||||
// VERY HELPFUL: Skip screenshot tests locally
|
||||
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
|
||||
timeout: 30_000, // Longer timeout for breakpoints
|
||||
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
|
||||
workers: 1, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
|
||||
|
||||
use: {
|
||||
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
|
||||
video: 'on' // Always record video (CI uses 'retain-on-failure')
|
||||
}
|
||||
}
|
||||
: {
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
use: {
|
||||
trace: 'on-first-retry'
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './browser_tests',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
reporter: 'html',
|
||||
// /* // Toggle for [LOCAL] testing.
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
use: {
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
/*/ // [LOCAL]
|
||||
// VERY HELPFUL: Skip screenshot tests locally
|
||||
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
|
||||
timeout: 30_000, // Longer timeout for breakpoints
|
||||
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
|
||||
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
|
||||
|
||||
use: {
|
||||
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
|
||||
video: 'on' // Always record video (CI uses 'retain-on-failure')
|
||||
},
|
||||
//*/
|
||||
...maybeLocalOptions,
|
||||
|
||||
globalSetup: './browser_tests/globalSetup.ts',
|
||||
globalTeardown: './browser_tests/globalTeardown.ts',
|
||||
|
||||
191
pnpm-lock.yaml
generated
@@ -12,9 +12,15 @@ catalogs:
|
||||
'@eslint/js':
|
||||
specifier: ^9.35.0
|
||||
version: 9.35.0
|
||||
'@iconify-json/lucide':
|
||||
specifier: ^1.1.178
|
||||
version: 1.2.66
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.380
|
||||
version: 2.2.380
|
||||
'@iconify/tailwind':
|
||||
specifier: ^1.1.3
|
||||
version: 1.2.0
|
||||
'@intlify/eslint-plugin-vue-i18n':
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@@ -141,6 +147,9 @@ catalogs:
|
||||
eslint-plugin-import-x:
|
||||
specifier: ^4.16.1
|
||||
version: 4.16.1
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 1.25.0
|
||||
version: 1.25.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4
|
||||
@@ -186,6 +195,12 @@ catalogs:
|
||||
nx:
|
||||
specifier: 21.4.1
|
||||
version: 21.4.1
|
||||
oxlint:
|
||||
specifier: ^1.25.0
|
||||
version: 1.28.0
|
||||
oxlint-tsgolint:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -555,6 +570,9 @@ importers:
|
||||
eslint-plugin-import-x:
|
||||
specifier: 'catalog:'
|
||||
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.25.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
|
||||
@@ -600,6 +618,12 @@ importers:
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.28.0(oxlint-tsgolint@0.4.0)
|
||||
oxlint-tsgolint:
|
||||
specifier: 'catalog:'
|
||||
version: 0.4.0
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
@@ -2529,6 +2553,76 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.4.0':
|
||||
resolution: {integrity: sha512-2jNvhxs6JJy93Z4SQ/VErODBzZtFKxQ+sybcKYcw5/K41tXOiBbJSwBMZ2PPvivCVkVcyOkJvfs5UXWW7o79uw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.4.0':
|
||||
resolution: {integrity: sha512-6A+YBecdZhk2NJ8Dh3kRkR6htNekDmAopFkdyrtNvsHJs5qNNuwUv5RZlVMYiaQTh/Y/tZ0YWE4+cVdqPIEyxQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.4.0':
|
||||
resolution: {integrity: sha512-JaX8JfQnY3UwX7l6BXIjhEaJAVeKVASELLFCdoo5+DOHgPuiiSKcxCVgTl92WPAuS0TYFXOgqOg31WXkvdi8bQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.4.0':
|
||||
resolution: {integrity: sha512-iu106lxV1O64O4vK2eRoIuY2iHuil/hyDNKLRNVaTg1un+yoxN6/C5uxrJix/EJ+1O27P9c+sXmMplcmbXujtg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.4.0':
|
||||
resolution: {integrity: sha512-KTp9EzkTCGAh4/sL3l5a9otX63TvTs5riBcrcqu0jYS3P762rZSezzMMDc0Ld51x+I37125p9+bue2vmlH/KbQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.4.0':
|
||||
resolution: {integrity: sha512-ioyBLHx0HA+hn5of8mhnA8W8DWQyJEHc7SBvwku0EW9bWt7zvBtWRJfx1YilvM+KVBdLVX731qeofdJT1fbJiQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/darwin-arm64@1.28.0':
|
||||
resolution: {integrity: sha512-H7J41/iKbgm7tTpdSnA/AtjEAhxyzNzCMKWtKU5wDuP2v39jrc3fasQEJruk6hj1YXPbJY4N+1nK/jE27GMGDQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/darwin-x64@1.28.0':
|
||||
resolution: {integrity: sha512-bGsSDEwpyYzNc6FIwhTmbhSK7piREUjMlmWBt7eoR3ract0+RfhZYYG4se1Ngs+4WOFC0B3gbv23fyF+cnbGGQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/linux-arm64-gnu@1.28.0':
|
||||
resolution: {integrity: sha512-eNH/evMpV3xAA4jIS8dMLcGkM/LK0WEHM0RO9bxrHPAwfS72jhyPJtd0R7nZhvhG6U1bhn5jhoXbk1dn27XIAQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.28.0':
|
||||
resolution: {integrity: sha512-ickvpcekNeRLND3llndiZOtJBb6LDZqNnZICIDkovURkOIWPGJGmAxsHUOI6yW6iny9gLmIEIGl/c1b5nFk6Ag==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.28.0':
|
||||
resolution: {integrity: sha512-DkgAh4LQ8NR3DwTT7/LGMhaMau0RtZkih91Ez5Usk7H7SOxo1GDi84beE7it2Q+22cAzgY4hbw3c6svonQTjxg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/linux-x64-musl@1.28.0':
|
||||
resolution: {integrity: sha512-VBnMi3AJ2w5p/kgeyrjcGOKNY8RzZWWvlGHjCJwzqPgob4MXu6T+5Yrdi7EVJyIlouL8E3LYPYjmzB9NBi9gZw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/win32-arm64@1.28.0':
|
||||
resolution: {integrity: sha512-tomhIks+4dKs8axB+s4GXHy+ZWXhUgptf1XnG5cZg8CzRfX4JFX9k8l2fPUgFwytWnyyvZaaXLRPWGzoZ6yoHQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/win32-x64@1.28.0':
|
||||
resolution: {integrity: sha512-4+VO5P/UJ2nq9sj6kQToJxFy5cKs7dGIN2DiUSQ7cqyUi7EKYNQKe+98HFcDOjtm33jQOQnc4kw8Igya5KPozg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@phenomnomnominal/tsquery@5.0.1':
|
||||
resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==}
|
||||
peerDependencies:
|
||||
@@ -4732,6 +4826,9 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
|
||||
eslint-plugin-oxlint@1.25.0:
|
||||
resolution: {integrity: sha512-grS4KdR9FAxoQC+wMkepeQHL4osMhoYfUI11Pot6Gitqr4wWi+JZrX0Shr8Bs9fjdWhEjtaZIV6cr4mbfytmyw==}
|
||||
|
||||
eslint-plugin-prettier@5.5.4:
|
||||
resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -5692,6 +5789,9 @@ packages:
|
||||
jsonc-parser@3.2.0:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
|
||||
jsonc-parser@3.3.1:
|
||||
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -6386,6 +6486,20 @@ packages:
|
||||
oxc-resolver@11.6.1:
|
||||
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
|
||||
|
||||
oxlint-tsgolint@0.4.0:
|
||||
resolution: {integrity: sha512-RpvLxPvSt0Xzr3frTiw5rP6HUW0djZ2uNdzHc8Pv556sbTnFWXmLdK8FRqqy7vVXZTkoVSdY3PsvOsVAqGjc+Q==}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.28.0:
|
||||
resolution: {integrity: sha512-gE97d0BcIlTTSJrim395B49mIbQ9VO8ZVoHdWai7Svl+lEeUAyCLTN4d7piw1kcB8VfgTp1JFVlAvMPD9GewMA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
oxlint-tsgolint: '>=0.4.0'
|
||||
peerDependenciesMeta:
|
||||
oxlint-tsgolint:
|
||||
optional: true
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7698,8 +7812,8 @@ packages:
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
vue-component-type-helpers@3.1.2:
|
||||
resolution: {integrity: sha512-ch3/SKBtxdZq18vsEntiGCdSszCRNfhX5QaTxjSacCAXLlNQRXfXo+ANjoQEYJMsJOJy1/vHF6Tkc4s85MS+zw==}
|
||||
vue-component-type-helpers@3.1.3:
|
||||
resolution: {integrity: sha512-V1dOD8XYfstOKCnXbWyEJIrhTBMwSyNjv271L1Jlx9ExpNlCSuqOs3OdWrGJ0V544zXufKbcYabi/o+gK8lyfQ==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
@@ -10092,6 +10206,48 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.6.1':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.4.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/darwin-arm64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/darwin-x64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-arm64-gnu@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/linux-x64-musl@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/win32-arm64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/win32-x64@1.28.0':
|
||||
optional: true
|
||||
|
||||
'@phenomnomnominal/tsquery@5.0.1(typescript@5.9.2)':
|
||||
dependencies:
|
||||
esquery: 1.6.0
|
||||
@@ -10458,7 +10614,7 @@ snapshots:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.2
|
||||
vue-component-type-helpers: 3.1.3
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
@@ -12516,6 +12672,10 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-oxlint@1.25.0:
|
||||
dependencies:
|
||||
jsonc-parser: 3.3.1
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
@@ -13581,6 +13741,8 @@ snapshots:
|
||||
|
||||
jsonc-parser@3.2.0: {}
|
||||
|
||||
jsonc-parser@3.3.1: {}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
dependencies:
|
||||
'@types/diff-match-patch': 1.0.36
|
||||
@@ -14548,6 +14710,27 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-ia32-msvc': 11.6.1
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.6.1
|
||||
|
||||
oxlint-tsgolint@0.4.0:
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.4.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.4.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.4.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.4.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.4.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.4.0
|
||||
|
||||
oxlint@1.28.0(oxlint-tsgolint@0.4.0):
|
||||
optionalDependencies:
|
||||
'@oxlint/darwin-arm64': 1.28.0
|
||||
'@oxlint/darwin-x64': 1.28.0
|
||||
'@oxlint/linux-arm64-gnu': 1.28.0
|
||||
'@oxlint/linux-arm64-musl': 1.28.0
|
||||
'@oxlint/linux-x64-gnu': 1.28.0
|
||||
'@oxlint/linux-x64-musl': 1.28.0
|
||||
'@oxlint/win32-arm64': 1.28.0
|
||||
'@oxlint/win32-x64': 1.28.0
|
||||
oxlint-tsgolint: 0.4.0
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
@@ -16157,7 +16340,7 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-component-type-helpers@3.1.2: {}
|
||||
vue-component-type-helpers@3.1.3: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
|
||||
@@ -50,6 +50,7 @@ catalog:
|
||||
eslint-config-prettier: ^10.1.8
|
||||
eslint-import-resolver-typescript: ^4.4.4
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-oxlint: 1.25.0
|
||||
eslint-plugin-prettier: ^5.5.4
|
||||
eslint-plugin-storybook: ^9.1.6
|
||||
eslint-plugin-unused-imports: ^4.2.0
|
||||
@@ -65,6 +66,8 @@ catalog:
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 21.4.1
|
||||
oxlint: ^1.25.0
|
||||
oxlint-tsgolint: ^0.4.0
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
|
||||
14
scripts/cicd/check-yaml.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
mapfile -t yaml_files < <(git ls-files '*.yml' '*.yaml')
|
||||
|
||||
if [[ ${#yaml_files[@]} -eq 0 ]]; then
|
||||
echo "No YAML files found to lint"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
yamllint --config-file .yamllint "${yaml_files[@]}"
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-action">
|
||||
<div class="file-action flex flex-row items-center gap-2">
|
||||
<Button
|
||||
v-if="status === null || status === 'error'"
|
||||
class="file-action-button"
|
||||
@@ -23,6 +23,13 @@
|
||||
icon="pi pi-download"
|
||||
@click="triggerDownload"
|
||||
/>
|
||||
<Button
|
||||
v-if="(status === null || status === 'error') && !!props.url"
|
||||
:label="$t('g.copyURL')"
|
||||
size="small"
|
||||
outlined
|
||||
@click="copyURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -80,6 +87,7 @@ import ProgressBar from 'primevue/progressbar'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useDownload } from '@/composables/useDownload'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
@@ -100,6 +108,7 @@ const status = ref<string | null>(null)
|
||||
const fileSize = computed(() =>
|
||||
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
|
||||
)
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const electronDownloadStore = useElectronDownloadStore()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const [savePath, filename] = props.label.split('/')
|
||||
@@ -126,4 +135,8 @@ const triggerDownload = async () => {
|
||||
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
|
||||
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
|
||||
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
|
||||
|
||||
const copyURL = async () => {
|
||||
await copyToClipboard(props.url)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { ValidationState } from '@/utils/validationUtil'
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
validateUrlFn?: (url: string) => Promise<boolean>
|
||||
disableValidation?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -101,6 +102,8 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
|
||||
}
|
||||
|
||||
const validateUrl = async (value: string) => {
|
||||
if (props.disableValidation) return
|
||||
|
||||
if (validationState.value === ValidationState.LOADING) return
|
||||
|
||||
const url = cleanInput(value)
|
||||
|
||||
69
src/components/dialog/content/CloudMissingNodesContent.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-[490px] flex-col border-t-1 border-b-1 border-border-default"
|
||||
>
|
||||
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<p class="m-0 text-sm leading-4 text-muted-foreground">
|
||||
{{ $t('cloud.missingNodes.description') }}
|
||||
<br /><br />
|
||||
{{ $t('cloud.missingNodes.priorityMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Missing Nodes List Wrapper -->
|
||||
<div
|
||||
class="flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-secondary-background"
|
||||
>
|
||||
<div
|
||||
v-for="(node, i) in uniqueNodes"
|
||||
:key="i"
|
||||
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
|
||||
>
|
||||
<span class="text-xs">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom instruction -->
|
||||
<div>
|
||||
<p class="m-0 text-sm leading-4 text-muted-foreground">
|
||||
{{ $t('cloud.missingNodes.replacementInstruction') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
return props.missingNodeTypes
|
||||
.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
if (seenTypes.has(type)) return false
|
||||
seenTypes.add(type)
|
||||
return true
|
||||
})
|
||||
.map((node) => {
|
||||
if (typeof node === 'object') {
|
||||
return {
|
||||
label: node.type,
|
||||
hint: node.hint,
|
||||
action: node.action
|
||||
}
|
||||
}
|
||||
return { label: node }
|
||||
})
|
||||
})
|
||||
</script>
|
||||
37
src/components/dialog/content/CloudMissingNodesFooter.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between gap-2 py-2 px-4">
|
||||
<IconTextButton
|
||||
:label="$t('cloud.missingNodes.learnMore')"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
icon-position="left"
|
||||
@click="handleLearnMoreClick"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<TextButton
|
||||
:label="$t('cloud.missingNodes.gotIt')"
|
||||
type="secondary"
|
||||
size="md"
|
||||
@click="handleGotItClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
window.open('https://www.comfy.org/cloud', '_blank')
|
||||
}
|
||||
|
||||
const handleGotItClick = () => {
|
||||
dialogStore.closeDialog({ key: 'global-cloud-missing-nodes' })
|
||||
}
|
||||
</script>
|
||||
10
src/components/dialog/content/CloudMissingNodesHeader.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="icon-[lucide--triangle-alert] text-gold-600"></i>
|
||||
<p class="m-0 text-sm">
|
||||
{{ $t('cloud.missingNodes.title') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -452,10 +452,13 @@ onMounted(async () => {
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
// Restore workflow and workflow tabs state from storage
|
||||
await workflowPersistence.restorePreviousWorkflow()
|
||||
// Restore saved workflow and workflow tabs state
|
||||
await workflowPersistence.initializeWorkflow()
|
||||
workflowPersistence.restoreWorkflowTabsState()
|
||||
|
||||
// Load template from URL if present
|
||||
await workflowPersistence.loadTemplateFromUrlIfPresent()
|
||||
|
||||
// Initialize release store to fetch releases from comfy-api (fire-and-forget)
|
||||
const { useReleaseStore } = await import(
|
||||
'@/platform/updates/common/releaseStore'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
value: $t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -18,11 +18,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const toggleBypass = async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: $t('Edit Subgraph Widgets'),
|
||||
value: $t('commands.Comfy_Graph_EditSubgraphWidgets.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-if="isUnpackVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
value: $t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -17,7 +17,7 @@
|
||||
<Button
|
||||
v-else-if="isConvertVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
value: $t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -34,12 +34,10 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { isSingleSubgraph, hasAnySelection } = useSelectionState()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-show="isDeletable"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
value: $t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -17,13 +17,11 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { selectedItems } = useSelectionState()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
value: $t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -15,7 +15,6 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-show="isSingleImageNode"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_MaskEditor_OpenMaskEditor.label'),
|
||||
value: $t('commands.Comfy_MaskEditor_OpenMaskEditor.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -17,7 +17,6 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
v-show="isVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_PublishSubgraph.label'),
|
||||
value: $t('commands.Comfy_PublishSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
@@ -18,13 +18,11 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ if (isComponentWidget(props.widget)) {
|
||||
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
|
||||
|
||||
const {
|
||||
// config
|
||||
// configs
|
||||
sceneConfig,
|
||||
modelConfig,
|
||||
cameraConfig,
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
v-model:show-grid="sceneConfig!.showGrid"
|
||||
v-model:background-color="sceneConfig!.backgroundColor"
|
||||
v-model:background-image="sceneConfig!.backgroundImage"
|
||||
v-model:background-render-mode="sceneConfig!.backgroundRenderMode"
|
||||
v-model:fov="cameraConfig!.fov"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
/>
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
<SceneControls
|
||||
v-model:background-color="viewer.backgroundColor.value"
|
||||
v-model:show-grid="viewer.showGrid.value"
|
||||
v-model:background-render-mode="viewer.backgroundRenderMode.value"
|
||||
v-model:fov="viewer.fov.value"
|
||||
:has-background-image="viewer.hasBackgroundImage.value"
|
||||
@update-background-image="viewer.handleBackgroundImageUpdate"
|
||||
/>
|
||||
|
||||
@@ -6,65 +6,30 @@
|
||||
value: $t('load3d.switchCamera'),
|
||||
showDelay: 300
|
||||
}"
|
||||
:class="['pi', getCameraIcon, 'text-lg text-white']"
|
||||
:class="['pi', 'pi-camera', 'text-lg text-white']"
|
||||
/>
|
||||
</Button>
|
||||
<div v-if="showFOVButton" class="show-fov relative">
|
||||
<Button class="p-button-rounded p-button-text" @click="toggleFOV">
|
||||
<i
|
||||
v-tooltip.right="{ value: $t('load3d.fov'), showDelay: 300 }"
|
||||
class="pi pi-expand text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-show="showFOV"
|
||||
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
|
||||
style="width: 150px"
|
||||
>
|
||||
<Slider v-model="fov" class="w-full" :min="10" :max="150" :step="1" />
|
||||
</div>
|
||||
</div>
|
||||
<PopupSlider
|
||||
v-if="showFOVButton"
|
||||
v-model="fov"
|
||||
:tooltip-text="$t('load3d.fov')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
|
||||
import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const showFOV = ref(false)
|
||||
|
||||
const cameraType = defineModel<CameraType>('cameraType')
|
||||
const fov = defineModel<number>('fov')
|
||||
const showFOVButton = computed(() => cameraType.value === 'perspective')
|
||||
const getCameraIcon = computed(() => {
|
||||
return cameraType.value === 'perspective' ? 'pi-camera' : 'pi-camera'
|
||||
})
|
||||
|
||||
const toggleFOV = () => {
|
||||
showFOV.value = !showFOV.value
|
||||
}
|
||||
|
||||
const switchCamera = () => {
|
||||
cameraType.value =
|
||||
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
|
||||
}
|
||||
|
||||
const closeCameraSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (!target.closest('.show-fov')) {
|
||||
showFOV.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeCameraSlider)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeCameraSlider)
|
||||
})
|
||||
</script>
|
||||
|
||||
64
src/components/load3d/controls/PopupSlider.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="relative show-slider">
|
||||
<Button class="p-button-rounded p-button-text" @click="toggleSlider">
|
||||
<i
|
||||
v-tooltip.right="{ value: tooltipText, showDelay: 300 }"
|
||||
:class="['pi', icon, 'text-lg text-white']"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-show="showSlider"
|
||||
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg w-[150px]"
|
||||
>
|
||||
<Slider
|
||||
v-model="value"
|
||||
class="w-full"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const {
|
||||
icon = 'pi-expand',
|
||||
min = 10,
|
||||
max = 150,
|
||||
step = 1
|
||||
} = defineProps<{
|
||||
icon?: string
|
||||
tooltipText: string
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}>()
|
||||
|
||||
const value = defineModel<number>()
|
||||
const showSlider = ref(false)
|
||||
|
||||
const toggleSlider = () => {
|
||||
showSlider.value = !showSlider.value
|
||||
}
|
||||
|
||||
const closeSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (!target.closest('.show-slider')) {
|
||||
showSlider.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeSlider)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeSlider)
|
||||
})
|
||||
</script>
|
||||
@@ -51,6 +51,28 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="hasBackgroundImage">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
:class="{ 'p-button-outlined': backgroundRenderMode === 'panorama' }"
|
||||
@click="toggleBackgroundRenderMode"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.panoramaMode'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-globe text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PopupSlider
|
||||
v-if="hasBackgroundImage && backgroundRenderMode === 'panorama'"
|
||||
v-model="fov"
|
||||
:tooltip-text="$t('load3d.fov')"
|
||||
/>
|
||||
|
||||
<div v-if="hasBackgroundImage">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@@ -72,6 +94,9 @@
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
|
||||
import type { BackgroundRenderModeType } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateBackgroundImage', file: File | null): void
|
||||
}>()
|
||||
@@ -79,6 +104,11 @@ const emit = defineEmits<{
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const backgroundImage = defineModel<string>('backgroundImage')
|
||||
const backgroundRenderMode = defineModel<BackgroundRenderModeType>(
|
||||
'backgroundRenderMode',
|
||||
{ default: 'tiled' }
|
||||
)
|
||||
const fov = defineModel<number>('fov')
|
||||
const hasBackgroundImage = computed(
|
||||
() => backgroundImage.value && backgroundImage.value !== ''
|
||||
)
|
||||
@@ -113,4 +143,9 @@ const uploadBackgroundImage = (event: Event) => {
|
||||
const removeBackgroundImage = () => {
|
||||
emit('updateBackgroundImage', null)
|
||||
}
|
||||
|
||||
const toggleBackgroundRenderMode = () => {
|
||||
backgroundRenderMode.value =
|
||||
backgroundRenderMode.value === 'panorama' ? 'tiled' : 'panorama'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<label>
|
||||
{{ t('load3d.viewer.cameraType') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="cameraType"
|
||||
:options="cameras"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<label>
|
||||
{{ t('load3d.viewer.cameraType') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="cameraType"
|
||||
:options="cameras"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="showFOVButton" class="space-y-4">
|
||||
<label>{{ t('load3d.fov') }}</label>
|
||||
<Slider
|
||||
v-model="fov"
|
||||
:min="10"
|
||||
:max="150"
|
||||
:step="1"
|
||||
:aria-label="t('load3d.fov')"
|
||||
/>
|
||||
<div v-if="showFOVButton" class="space-y-4">
|
||||
<label>{{ t('load3d.fov') }}</label>
|
||||
<Slider
|
||||
v-model="fov"
|
||||
:min="10"
|
||||
:max="150"
|
||||
:step="1"
|
||||
:aria-label="t('load3d.fov')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<template>
|
||||
<Select
|
||||
v-model="exportFormat"
|
||||
:options="exportFormats"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="exportFormat"
|
||||
:options="exportFormats"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
|
||||
<Button severity="secondary" text rounded @click="exportModel(exportFormat)">
|
||||
{{ $t('load3d.export') }}
|
||||
</Button>
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
@click="exportModel(exportFormat)"
|
||||
>
|
||||
{{ $t('load3d.export') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<label>{{ $t('load3d.lightIntensity') }}</label>
|
||||
<div class="space-y-4">
|
||||
<label>{{ $t('load3d.lightIntensity') }}</label>
|
||||
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
class="w-full"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
/>
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
class="w-full"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -32,6 +32,24 @@
|
||||
</div>
|
||||
|
||||
<div v-if="hasBackgroundImage" class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
:severity="backgroundRenderMode === 'tiled' ? 'primary' : 'secondary'"
|
||||
:label="$t('load3d.tiledMode')"
|
||||
icon="pi pi-th-large"
|
||||
class="flex-1"
|
||||
@click="setBackgroundRenderMode('tiled')"
|
||||
/>
|
||||
<Button
|
||||
:severity="
|
||||
backgroundRenderMode === 'panorama' ? 'primary' : 'secondary'
|
||||
"
|
||||
:label="$t('load3d.panoramaMode')"
|
||||
icon="pi pi-globe"
|
||||
class="flex-1"
|
||||
@click="setBackgroundRenderMode('panorama')"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('load3d.removeBackgroundImage')"
|
||||
@@ -50,6 +68,9 @@ import { ref } from 'vue'
|
||||
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
const backgroundRenderMode = defineModel<'tiled' | 'panorama'>(
|
||||
'backgroundRenderMode'
|
||||
)
|
||||
|
||||
defineProps<{
|
||||
hasBackgroundImage?: boolean
|
||||
@@ -77,4 +98,8 @@ const handleImageUpload = (event: Event) => {
|
||||
const removeBackgroundImage = () => {
|
||||
emit('updateBackgroundImage', null)
|
||||
}
|
||||
|
||||
const setBackgroundRenderMode = (mode: 'tiled' | 'panorama') => {
|
||||
backgroundRenderMode.value = mode
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- h-0 to force scrollpanel to grow -->
|
||||
<ScrollPanel class="h-0 grow">
|
||||
<!-- min-h-0 to force scrollpanel to grow -->
|
||||
<ScrollPanel class="min-h-0 grow">
|
||||
<slot name="body" />
|
||||
</ScrollPanel>
|
||||
<div v-if="slots.footer">
|
||||
|
||||
@@ -41,9 +41,27 @@
|
||||
</TabList>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="displayAssets.length" class="relative size-full">
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading">
|
||||
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="!displayAssets.length">
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
$t(
|
||||
activeTab === 'input'
|
||||
? 'sideToolbar.noImportedFiles'
|
||||
: 'sideToolbar.noGeneratedFiles'
|
||||
)
|
||||
"
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div v-else class="relative size-full">
|
||||
<VirtualGrid
|
||||
v-if="displayAssets.length"
|
||||
:items="mediaAssetsWithKey"
|
||||
:grid-style="{
|
||||
display: 'grid',
|
||||
@@ -51,6 +69,7 @@
|
||||
padding: '0.5rem',
|
||||
gap: '0.5rem'
|
||||
}"
|
||||
@approach-end="handleApproachEnd"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MediaAssetCard
|
||||
@@ -66,24 +85,6 @@
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
<div v-else-if="loading">
|
||||
<ProgressSpinner
|
||||
class="absolute left-1/2 w-[50px] -translate-x-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
$t(
|
||||
activeTab === 'input'
|
||||
? 'sideToolbar.noImportedFiles'
|
||||
: 'sideToolbar.noGeneratedFiles'
|
||||
)
|
||||
"
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
@@ -147,6 +148,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -291,6 +293,7 @@ watch(
|
||||
activeTab,
|
||||
() => {
|
||||
clearSelection()
|
||||
// Reset pagination state when tab changes
|
||||
void refreshAssets()
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -395,4 +398,15 @@ const handleDeleteSelected = async () => {
|
||||
await deleteMultipleAssets(selectedAssets)
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
const handleApproachEnd = useDebounceFn(async () => {
|
||||
if (
|
||||
activeTab.value === 'output' &&
|
||||
!isInFolderView.value &&
|
||||
outputAssets.hasMore.value &&
|
||||
!outputAssets.isLoadingMore.value
|
||||
) {
|
||||
await outputAssets.loadMore()
|
||||
}
|
||||
}, 300)
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<BaseModalLayout :content-title="$t('Checkpoints')">
|
||||
<BaseModalLayout :content-title="$t('assetBrowser.checkpoints')">
|
||||
<template #leftPanel>
|
||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
|
||||
<template #header-icon>
|
||||
<i class="text-neutral icon-[lucide--puzzle]" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">{{ t('g.title') }}</span>
|
||||
<span class="text-neutral text-base">{{ $t('g.title') }}</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</template>
|
||||
@@ -132,7 +132,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
@@ -189,8 +188,6 @@ const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
|
||||
}
|
||||
])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { onClose } = defineProps<{
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
@@ -125,7 +125,7 @@ export function useMoreOptionsMenu() {
|
||||
|
||||
const menuOptions = computed((): MenuOption[] => {
|
||||
// Reference selection flags to ensure re-computation when they change
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
|
||||
optionsVersion.value
|
||||
const states = computeSelectionFlags()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import { createSharedComposable, whenever } from '@vueuse/core'
|
||||
import { shallowRef, watch } from 'vue'
|
||||
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
@@ -82,8 +82,9 @@ function useVueNodeLifecycleIndividual() {
|
||||
(enabled, wasEnabled) => {
|
||||
if (enabled) {
|
||||
initializeNodeManager()
|
||||
ensureCorrectLayoutScale()
|
||||
|
||||
ensureCorrectLayoutScale(
|
||||
comfyApp.canvas?.graph?.extra.workflowRendererVersion
|
||||
)
|
||||
if (!wasEnabled && !isVueNodeToastDismissed.value) {
|
||||
useToastStore().add({
|
||||
group: 'vue-nodes-migration',
|
||||
@@ -91,14 +92,22 @@ function useVueNodeLifecycleIndividual() {
|
||||
life: 0
|
||||
})
|
||||
}
|
||||
} else {
|
||||
comfyApp.canvas?.setDirty(true, true)
|
||||
disposeNodeManagerAndSyncs()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => !shouldRenderVueNodes.value,
|
||||
() => {
|
||||
ensureCorrectLayoutScale(
|
||||
comfyApp.canvas?.graph?.extra.workflowRendererVersion
|
||||
)
|
||||
disposeNodeManagerAndSyncs()
|
||||
comfyApp.canvas?.setDirty(true, true)
|
||||
}
|
||||
)
|
||||
|
||||
// Consolidated watch for slot layout sync management
|
||||
watch(
|
||||
() => shouldRenderVueNodes.value,
|
||||
|
||||
@@ -8,7 +8,8 @@ import { api } from '@/scripts/api'
|
||||
export enum ServerFeatureFlag {
|
||||
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size',
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4'
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
|
||||
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,12 @@ export function useFeatureFlags() {
|
||||
},
|
||||
get supportsManagerV4() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4)
|
||||
},
|
||||
get modelUploadButtonEnabled() {
|
||||
return api.getServerFeature(
|
||||
ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED,
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
const sceneConfig = ref<SceneConfig>({
|
||||
showGrid: true,
|
||||
backgroundColor: '#000000',
|
||||
backgroundImage: ''
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled'
|
||||
})
|
||||
|
||||
const modelConfig = ref<ModelConfig>({
|
||||
@@ -131,7 +132,11 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
// Restore configs - watchers will handle applying them to the Three.js scene
|
||||
const savedSceneConfig = node.properties['Scene Config'] as SceneConfig
|
||||
if (savedSceneConfig) {
|
||||
sceneConfig.value = savedSceneConfig
|
||||
sceneConfig.value = {
|
||||
...sceneConfig.value,
|
||||
...savedSceneConfig,
|
||||
backgroundRenderMode: savedSceneConfig.backgroundRenderMode || 'tiled'
|
||||
}
|
||||
}
|
||||
|
||||
const savedModelConfig = node.properties['Model Config'] as ModelConfig
|
||||
@@ -221,12 +226,17 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
|
||||
watch(
|
||||
sceneConfig,
|
||||
(newValue) => {
|
||||
async (newValue) => {
|
||||
if (load3d && nodeRef.value) {
|
||||
nodeRef.value.properties['Scene Config'] = newValue
|
||||
load3d.toggleGrid(newValue.showGrid)
|
||||
load3d.setBackgroundColor(newValue.backgroundColor)
|
||||
void load3d.setBackgroundImage(newValue.backgroundImage || '')
|
||||
|
||||
await load3d.setBackgroundImage(newValue.backgroundImage || '')
|
||||
|
||||
if (newValue.backgroundRenderMode) {
|
||||
load3d.setBackgroundRenderMode(newValue.backgroundRenderMode)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
@@ -424,6 +434,9 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
backgroundColorChange: (value: string) => {
|
||||
sceneConfig.value.backgroundColor = value
|
||||
},
|
||||
backgroundRenderModeChange: (value: string) => {
|
||||
sceneConfig.value.backgroundRenderMode = value as 'tiled' | 'panorama'
|
||||
},
|
||||
lightIntensityChange: (value: number) => {
|
||||
lightConfig.value.intensity = value
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, toRaw, watch } from 'vue'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
BackgroundRenderModeType,
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
@@ -21,6 +22,7 @@ interface Load3dViewerState {
|
||||
lightIntensity: number
|
||||
cameraState: any
|
||||
backgroundImage: string
|
||||
backgroundRenderMode: BackgroundRenderModeType
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
}
|
||||
@@ -33,6 +35,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
const lightIntensity = ref(1)
|
||||
const backgroundImage = ref('')
|
||||
const hasBackgroundImage = ref(false)
|
||||
const backgroundRenderMode = ref<BackgroundRenderModeType>('tiled')
|
||||
const upDirection = ref<UpDirection>('original')
|
||||
const materialMode = ref<MaterialMode>('original')
|
||||
const needApplyChanges = ref(true)
|
||||
@@ -49,6 +52,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
lightIntensity: 1,
|
||||
cameraState: null,
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled',
|
||||
upDirection: 'original',
|
||||
materialMode: 'original'
|
||||
})
|
||||
@@ -124,6 +128,20 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(backgroundRenderMode, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setBackgroundRenderMode(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error updating background render mode:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateBackgroundRenderMode', {
|
||||
mode: newValue
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(upDirection, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
@@ -180,6 +198,10 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
source.sceneManager.currentBackgroundColor
|
||||
showGrid.value =
|
||||
sceneConfig.showGrid ?? source.sceneManager.gridHelper.visible
|
||||
backgroundRenderMode.value =
|
||||
sceneConfig.backgroundRenderMode ||
|
||||
source.sceneManager.backgroundRenderMode ||
|
||||
'tiled'
|
||||
|
||||
const backgroundInfo = source.sceneManager.getCurrentBackgroundInfo()
|
||||
if (backgroundInfo.type === 'image' && sceneConfig.backgroundImage) {
|
||||
@@ -219,6 +241,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
lightIntensity: lightIntensity.value,
|
||||
cameraState: sourceCameraState,
|
||||
backgroundImage: backgroundImage.value,
|
||||
backgroundRenderMode: backgroundRenderMode.value,
|
||||
upDirection: upDirection.value,
|
||||
materialMode: materialMode.value
|
||||
}
|
||||
@@ -274,7 +297,8 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
nodeValue.properties['Scene Config'] = {
|
||||
showGrid: initialState.value.showGrid,
|
||||
backgroundColor: initialState.value.backgroundColor,
|
||||
backgroundImage: initialState.value.backgroundImage
|
||||
backgroundImage: initialState.value.backgroundImage,
|
||||
backgroundRenderMode: initialState.value.backgroundRenderMode
|
||||
}
|
||||
|
||||
nodeValue.properties['Camera Config'] = {
|
||||
@@ -309,7 +333,8 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
nodeValue.properties['Scene Config'] = {
|
||||
showGrid: showGrid.value,
|
||||
backgroundColor: backgroundColor.value,
|
||||
backgroundImage: backgroundImage.value
|
||||
backgroundImage: backgroundImage.value,
|
||||
backgroundRenderMode: backgroundRenderMode.value
|
||||
}
|
||||
|
||||
nodeValue.properties['Camera Config'] = {
|
||||
@@ -331,6 +356,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
await useLoad3dService().copyLoad3dState(load3d, sourceLoad3d)
|
||||
|
||||
await sourceLoad3d.setBackgroundImage(backgroundImage.value)
|
||||
sourceLoad3d.setBackgroundRenderMode(backgroundRenderMode.value)
|
||||
|
||||
sourceLoad3d.forceRender()
|
||||
|
||||
@@ -429,6 +455,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
lightIntensity,
|
||||
backgroundImage,
|
||||
hasBackgroundImage,
|
||||
backgroundRenderMode,
|
||||
upDirection,
|
||||
materialMode,
|
||||
needApplyChanges,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { refDebounced, watchDebounced } from '@vueuse/core'
|
||||
import Fuse from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
@@ -10,17 +11,25 @@ import { debounce } from 'es-toolkit/compat'
|
||||
export function useTemplateFiltering(
|
||||
templates: Ref<TemplateInfo[]> | TemplateInfo[]
|
||||
) {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedModels = ref<string[]>([])
|
||||
const selectedUseCases = ref<string[]>([])
|
||||
const selectedRunsOn = ref<string[]>([])
|
||||
const selectedModels = ref<string[]>(
|
||||
settingStore.get('Comfy.Templates.SelectedModels')
|
||||
)
|
||||
const selectedUseCases = ref<string[]>(
|
||||
settingStore.get('Comfy.Templates.SelectedUseCases')
|
||||
)
|
||||
const selectedRunsOn = ref<string[]>(
|
||||
settingStore.get('Comfy.Templates.SelectedRunsOn')
|
||||
)
|
||||
const sortBy = ref<
|
||||
| 'default'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
| 'model-size-low-to-high'
|
||||
>('newest')
|
||||
>(settingStore.get('Comfy.Templates.SortBy'))
|
||||
|
||||
const templatesArray = computed(() => {
|
||||
const templateData = 'value' in templates ? templates.value : templates
|
||||
@@ -197,7 +206,7 @@ export function useTemplateFiltering(
|
||||
selectedModels.value = []
|
||||
selectedUseCases.value = []
|
||||
selectedRunsOn.value = []
|
||||
sortBy.value = 'default'
|
||||
sortBy.value = 'newest'
|
||||
}
|
||||
|
||||
const removeModelFilter = (model: string) => {
|
||||
@@ -247,6 +256,39 @@ export function useTemplateFiltering(
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Persist filter changes to settings (debounced to avoid excessive saves)
|
||||
watchDebounced(
|
||||
selectedModels,
|
||||
(newValue) => {
|
||||
void settingStore.set('Comfy.Templates.SelectedModels', newValue)
|
||||
},
|
||||
{ debounce: 500, deep: true }
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
selectedUseCases,
|
||||
(newValue) => {
|
||||
void settingStore.set('Comfy.Templates.SelectedUseCases', newValue)
|
||||
},
|
||||
{ debounce: 500, deep: true }
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
selectedRunsOn,
|
||||
(newValue) => {
|
||||
void settingStore.set('Comfy.Templates.SelectedRunsOn', newValue)
|
||||
},
|
||||
{ debounce: 500, deep: true }
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
sortBy,
|
||||
(newValue) => {
|
||||
void settingStore.set('Comfy.Templates.SortBy', newValue)
|
||||
},
|
||||
{ debounce: 500 }
|
||||
)
|
||||
|
||||
return {
|
||||
// State
|
||||
searchQuery,
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
/**
|
||||
* Default colors for node slot types
|
||||
* Mirrors LiteGraph's slot_default_color_by_type
|
||||
*/
|
||||
const SLOT_TYPE_COLORS: Record<string, string> = {
|
||||
number: '#AAD',
|
||||
string: '#DCA',
|
||||
boolean: '#DAA',
|
||||
vec2: '#ADA',
|
||||
vec3: '#ADA',
|
||||
vec4: '#ADA',
|
||||
color: '#DDA',
|
||||
image: '#353',
|
||||
latent: '#858',
|
||||
conditioning: '#FFA',
|
||||
control_net: '#F8F',
|
||||
clip: '#FFD',
|
||||
vae: '#F82',
|
||||
model: '#B98',
|
||||
'*': '#AAA' // Default color
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color for a slot type
|
||||
*/
|
||||
export function getSlotColor(type?: string | number | null): string {
|
||||
if (!type) return SLOT_TYPE_COLORS['*']
|
||||
const typeStr = String(type).toLowerCase()
|
||||
return SLOT_TYPE_COLORS[typeStr] || SLOT_TYPE_COLORS['*']
|
||||
if (!type) return '#AAA'
|
||||
const typeStr = String(type).toUpperCase()
|
||||
return `var(--color-datatype-${typeStr})`
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './load3d'
|
||||
import './maskeditor'
|
||||
import './matchType'
|
||||
import './nodeTemplates'
|
||||
import './noteNode'
|
||||
import './previewAny'
|
||||
|
||||
@@ -162,7 +162,11 @@ class Load3DConfiguration {
|
||||
return
|
||||
}
|
||||
|
||||
this.load3d.setBackgroundImage(config.backgroundImage)
|
||||
void this.load3d.setBackgroundImage(config.backgroundImage)
|
||||
|
||||
if (config.backgroundRenderMode) {
|
||||
this.load3d.setBackgroundRenderMode(config.backgroundRenderMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -510,6 +510,11 @@ class Load3d {
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
setBackgroundRenderMode(mode: 'tiled' | 'panorama'): void {
|
||||
this.sceneManager.setBackgroundRenderMode(mode)
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
toggleCamera(cameraType?: 'perspective' | 'orthographic'): void {
|
||||
this.cameraManager.toggleCamera(cameraType)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import Load3dUtils from './Load3dUtils'
|
||||
import {
|
||||
type BackgroundRenderModeType,
|
||||
type EventManagerInterface,
|
||||
type SceneManagerInterface
|
||||
} from './interfaces'
|
||||
@@ -16,6 +17,8 @@ export class SceneManager implements SceneManagerInterface {
|
||||
backgroundMesh: THREE.Mesh | null = null
|
||||
backgroundTexture: THREE.Texture | null = null
|
||||
|
||||
backgroundRenderMode: 'tiled' | 'panorama' = 'tiled'
|
||||
|
||||
backgroundColorMaterial: THREE.MeshBasicMaterial | null = null
|
||||
currentBackgroundType: 'color' | 'image' = 'color'
|
||||
currentBackgroundColor: string = '#282828'
|
||||
@@ -89,6 +92,10 @@ export class SceneManager implements SceneManagerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scene.background) {
|
||||
this.scene.background = null
|
||||
}
|
||||
|
||||
this.scene.clear()
|
||||
}
|
||||
|
||||
@@ -104,6 +111,15 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.currentBackgroundColor = color
|
||||
this.currentBackgroundType = 'color'
|
||||
|
||||
if (this.scene.background instanceof THREE.Texture) {
|
||||
this.scene.background = null
|
||||
}
|
||||
|
||||
if (this.backgroundRenderMode === 'panorama') {
|
||||
this.backgroundRenderMode = 'tiled'
|
||||
this.eventManager.emitEvent('backgroundRenderModeChange', 'tiled')
|
||||
}
|
||||
|
||||
if (!this.backgroundMesh || !this.backgroundColorMaterial) {
|
||||
this.initBackgroundScene()
|
||||
}
|
||||
@@ -168,36 +184,41 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.backgroundTexture = texture
|
||||
this.currentBackgroundType = 'image'
|
||||
|
||||
if (!this.backgroundMesh) {
|
||||
this.initBackgroundScene()
|
||||
}
|
||||
|
||||
const imageMaterial = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
if (this.backgroundMesh) {
|
||||
if (
|
||||
this.backgroundMesh.material !== this.backgroundColorMaterial &&
|
||||
this.backgroundMesh.material instanceof THREE.Material
|
||||
) {
|
||||
this.backgroundMesh.material.dispose()
|
||||
if (this.backgroundRenderMode === 'panorama') {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping
|
||||
this.scene.background = texture
|
||||
} else {
|
||||
if (!this.backgroundMesh) {
|
||||
this.initBackgroundScene()
|
||||
}
|
||||
|
||||
this.backgroundMesh.material = imageMaterial
|
||||
this.backgroundMesh.position.set(0, 0, 0)
|
||||
}
|
||||
const imageMaterial = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
this.renderer.domElement.clientWidth,
|
||||
this.renderer.domElement.clientHeight
|
||||
)
|
||||
if (this.backgroundMesh) {
|
||||
if (
|
||||
this.backgroundMesh.material !== this.backgroundColorMaterial &&
|
||||
this.backgroundMesh.material instanceof THREE.Material
|
||||
) {
|
||||
this.backgroundMesh.material.dispose()
|
||||
}
|
||||
|
||||
this.backgroundMesh.material = imageMaterial
|
||||
this.backgroundMesh.position.set(0, 0, 0)
|
||||
}
|
||||
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
this.renderer.domElement.clientWidth,
|
||||
this.renderer.domElement.clientHeight
|
||||
)
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('backgroundImageChange', uploadPath)
|
||||
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
|
||||
@@ -213,6 +234,35 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
|
||||
}
|
||||
|
||||
setBackgroundRenderMode(mode: BackgroundRenderModeType): void {
|
||||
if (this.backgroundRenderMode === mode) return
|
||||
|
||||
this.backgroundRenderMode = mode
|
||||
|
||||
if (this.currentBackgroundType === 'image' && this.backgroundTexture) {
|
||||
try {
|
||||
if (mode === 'panorama') {
|
||||
this.backgroundTexture.mapping =
|
||||
THREE.EquirectangularReflectionMapping
|
||||
this.scene.background = this.backgroundTexture
|
||||
} else {
|
||||
this.scene.background = null
|
||||
if (
|
||||
this.backgroundMesh &&
|
||||
this.backgroundMesh.material instanceof THREE.MeshBasicMaterial
|
||||
) {
|
||||
this.backgroundMesh.material.map = this.backgroundTexture
|
||||
this.backgroundMesh.material.needsUpdate = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error set background render mode:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('backgroundRenderModeChange', mode)
|
||||
}
|
||||
|
||||
updateBackgroundSize(
|
||||
backgroundTexture: THREE.Texture | null,
|
||||
backgroundMesh: THREE.Mesh | null,
|
||||
@@ -254,7 +304,11 @@ export class SceneManager implements SceneManagerInterface {
|
||||
}
|
||||
|
||||
renderBackground(): void {
|
||||
if (this.backgroundMesh) {
|
||||
if (
|
||||
(this.backgroundRenderMode === 'tiled' ||
|
||||
this.currentBackgroundType === 'color') &&
|
||||
this.backgroundMesh
|
||||
) {
|
||||
const currentToneMapping = this.renderer.toneMapping
|
||||
const currentExposure = this.renderer.toneMappingExposure
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
|
||||
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
export type CameraType = 'perspective' | 'orthographic'
|
||||
export type BackgroundRenderModeType = 'tiled' | 'panorama'
|
||||
|
||||
export interface CameraState {
|
||||
position: THREE.Vector3
|
||||
@@ -25,6 +26,7 @@ export interface SceneConfig {
|
||||
showGrid: boolean
|
||||
backgroundColor: string
|
||||
backgroundImage?: string
|
||||
backgroundRenderMode?: BackgroundRenderModeType
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
@@ -77,6 +79,7 @@ export interface SceneManagerInterface extends BaseManager {
|
||||
setBackgroundColor(color: string): void
|
||||
setBackgroundImage(uploadPath: string): Promise<void>
|
||||
removeBackgroundImage(): void
|
||||
setBackgroundRenderMode(mode: BackgroundRenderModeType): void
|
||||
handleResize(width: number, height: number): void
|
||||
captureScene(width: number, height: number): Promise<CaptureResult>
|
||||
}
|
||||
|
||||
155
src/extensions/core/matchType.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { without } from 'es-toolkit'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const MATCH_TYPE = 'COMFY_MATCHTYPE_V3'
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.MatchType',
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
const inputs = {
|
||||
...nodeData.input?.required,
|
||||
...nodeData.input?.optional
|
||||
}
|
||||
if (!Object.values(inputs).some((w) => w[0] === MATCH_TYPE)) return
|
||||
nodeType.prototype.onNodeCreated = useChainCallback(
|
||||
nodeType.prototype.onNodeCreated,
|
||||
function (this: LGraphNode) {
|
||||
const inputGroups: Record<string, [string, ISlotType][]> = {}
|
||||
const outputGroups: Record<string, number[]> = {}
|
||||
for (const input of this.inputs) {
|
||||
if (input.type !== MATCH_TYPE) continue
|
||||
const template = inputs[input.name][1]?.template
|
||||
if (!template) continue
|
||||
input.type = template.allowed_types ?? '*'
|
||||
inputGroups[template.template_id] ??= []
|
||||
inputGroups[template.template_id].push([input.name, input.type])
|
||||
}
|
||||
this.outputs.forEach((output, i) => {
|
||||
if (output.type !== MATCH_TYPE) return
|
||||
const id = nodeData.output_matchtypes?.[i]
|
||||
if (id == undefined) return
|
||||
outputGroups[id] ??= []
|
||||
outputGroups[id].push(i)
|
||||
})
|
||||
for (const groupId in inputGroups) {
|
||||
addConnectionGroup(this, inputGroups[groupId], outputGroups[groupId])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
function addConnectionGroup(
|
||||
node: LGraphNode,
|
||||
inputPairs: [string, ISlotType][],
|
||||
outputs?: number[]
|
||||
) {
|
||||
const connectedTypes: ISlotType[] = new Array(inputPairs.length).fill('*')
|
||||
node.onConnectionsChange = useChainCallback(
|
||||
node.onConnectionsChange,
|
||||
function (
|
||||
this: LGraphNode,
|
||||
contype: ISlotType,
|
||||
slot: number,
|
||||
iscon: boolean,
|
||||
linf: LLink | null | undefined
|
||||
) {
|
||||
const input = this.inputs[slot]
|
||||
if (contype !== LiteGraph.INPUT || !this.graph || !input) return
|
||||
const pairIndex = inputPairs.findIndex(([name]) => name === input.name)
|
||||
if (pairIndex == -1) return
|
||||
connectedTypes[pairIndex] = inputPairs[pairIndex][1]
|
||||
if (iscon && linf) {
|
||||
const { output, subgraphInput } = linf.resolve(this.graph)
|
||||
const connectingType = (output ?? subgraphInput)?.type
|
||||
if (connectingType)
|
||||
linf.type = connectedTypes[pairIndex] = connectingType
|
||||
}
|
||||
//An input slot can accept a connection that is
|
||||
// - Compatible with original type
|
||||
// - Compatible with all other input types
|
||||
//An output slot can output
|
||||
// - Only what every input can output
|
||||
for (let i = 0; i < inputPairs.length; i++) {
|
||||
//NOTE: This isn't great. Originally, I kept direct references to each
|
||||
//input, but these were becoming orphaned
|
||||
const input = this.inputs.find((inp) => inp.name === inputPairs[i][0])
|
||||
if (!input) continue
|
||||
const otherConnected = [...connectedTypes]
|
||||
otherConnected.splice(i, 1)
|
||||
const validType = combineTypes(...otherConnected, inputPairs[i][1])
|
||||
if (!validType) throw new Error('invalid connection')
|
||||
input.type = validType
|
||||
}
|
||||
if (outputs) {
|
||||
const outputType = combineTypes(...connectedTypes)
|
||||
if (!outputType) throw new Error('invalid connection')
|
||||
changeOutputType(this, outputType, outputs)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function changeOutputType(
|
||||
node: LGraphNode,
|
||||
combinedType: ISlotType,
|
||||
outputs: number[]
|
||||
) {
|
||||
if (!node.graph) return
|
||||
for (const index of outputs) {
|
||||
if (node.outputs[index].type === combinedType) continue
|
||||
node.outputs[index].type = combinedType
|
||||
|
||||
//check and potentially remove links
|
||||
for (let link_id of node.outputs[index].links ?? []) {
|
||||
let link = node.graph.links[link_id]
|
||||
if (!link) continue
|
||||
const { input, inputNode, subgraphOutput } = link.resolve(node.graph)
|
||||
const inputType = (input ?? subgraphOutput)?.type
|
||||
if (!inputType) continue
|
||||
const keep = LiteGraph.isValidConnection(combinedType, inputType)
|
||||
if (!keep && subgraphOutput) subgraphOutput.disconnect()
|
||||
else if (!keep && inputNode) inputNode.disconnectInput(link.target_slot)
|
||||
if (input && inputNode?.onConnectionsChange)
|
||||
inputNode.onConnectionsChange(
|
||||
LiteGraph.INPUT,
|
||||
link.target_slot,
|
||||
keep,
|
||||
link,
|
||||
input
|
||||
)
|
||||
}
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
}
|
||||
function isStrings(types: ISlotType[]): types is string[] {
|
||||
return !types.some((t) => typeof t !== 'string')
|
||||
}
|
||||
|
||||
function combineTypes(...types: ISlotType[]): ISlotType | undefined {
|
||||
if (!isStrings(types)) return undefined
|
||||
|
||||
const withoutWildcards = without(types, '*')
|
||||
if (withoutWildcards.length === 0) return '*'
|
||||
|
||||
const typeLists: string[][] = withoutWildcards.map((type) => type.split(','))
|
||||
|
||||
const combinedTypes = intersection(...typeLists)
|
||||
if (combinedTypes.length === 0) return undefined
|
||||
|
||||
return combinedTypes.join(',')
|
||||
}
|
||||
function intersection(...sets: string[][]): string[] {
|
||||
const itemCounts: Record<string, number> = {}
|
||||
for (const set of sets)
|
||||
for (const item of new Set(set))
|
||||
itemCounts[item] = (itemCounts[item] ?? 0) + 1
|
||||
return Object.entries(itemCounts)
|
||||
.filter(([, count]) => count == sets.length)
|
||||
.map(([key]) => key)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
@@ -14,7 +15,7 @@ app.registerExtension({
|
||||
name: 'Comfy.RerouteNode',
|
||||
registerCustomNodes(app) {
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string
|
||||
__outputType?: string | number
|
||||
}
|
||||
|
||||
class RerouteNode extends LGraphNode {
|
||||
@@ -22,8 +23,7 @@ app.registerExtension({
|
||||
static defaultVisibility = false
|
||||
|
||||
constructor(title?: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
super(title)
|
||||
super(title ?? '')
|
||||
if (!this.properties) {
|
||||
this.properties = {}
|
||||
}
|
||||
@@ -33,225 +33,198 @@ app.registerExtension({
|
||||
this.addInput('', '*')
|
||||
this.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
|
||||
this.onAfterGraphConfigured = function () {
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.onConnectionsChange(LiteGraph.INPUT, null, true, null)
|
||||
})
|
||||
}
|
||||
|
||||
this.onConnectionsChange = (type, _index, connected) => {
|
||||
if (app.configuringGraph) return
|
||||
|
||||
// Prevent multiple connections to different types when we have no input
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
// Ignore wildcard nodes as these will be updated to real types
|
||||
const types = new Set(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.outputs[0].links
|
||||
.map((l) => app.graph.links[l].type)
|
||||
.filter((t) => t !== '*')
|
||||
)
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = []
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const linkId = this.outputs[0].links[i]
|
||||
const link = app.graph.links[linkId]
|
||||
linksToDisconnect.push(link)
|
||||
}
|
||||
for (const link of linksToDisconnect) {
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.disconnectInput(link.target_slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find root input
|
||||
let currentNode: LGraphNode | null = this
|
||||
let updateNodes = []
|
||||
let inputType = null
|
||||
let inputNode = null
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode)
|
||||
const linkId = currentNode.inputs[0].link
|
||||
if (linkId !== null) {
|
||||
const link = app.graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = app.graph.getNodeById(link.origin_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = node.constructor.type
|
||||
if (type === 'Reroute') {
|
||||
if (node === this) {
|
||||
// We've found a circle
|
||||
currentNode.disconnectInput(link.target_slot)
|
||||
currentNode = null
|
||||
} else {
|
||||
// Move the previous node
|
||||
currentNode = node
|
||||
}
|
||||
} else {
|
||||
// We've found the end
|
||||
inputNode = currentNode
|
||||
// @ts-expect-error fixme ts strict error
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// This path has no input node
|
||||
currentNode = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find all outputs
|
||||
const nodes: LGraphNode[] = [this]
|
||||
let outputType = null
|
||||
while (nodes.length) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
currentNode = nodes.pop()
|
||||
const outputs =
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(currentNode.outputs ? currentNode.outputs[0].links : []) || []
|
||||
if (outputs.length) {
|
||||
for (const linkId of outputs) {
|
||||
const link = app.graph.links[linkId]
|
||||
|
||||
// When disconnecting sometimes the link is still registered
|
||||
if (!link) continue
|
||||
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = node.constructor.type
|
||||
|
||||
if (type === 'Reroute') {
|
||||
// Follow reroute nodes
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodes.push(node)
|
||||
updateNodes.push(node)
|
||||
} else {
|
||||
// We've found an output
|
||||
const nodeOutType =
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs[link?.target_slot] &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs[link.target_slot].type
|
||||
? // @ts-expect-error fixme ts strict error
|
||||
node.inputs[link.target_slot].type
|
||||
: null
|
||||
if (
|
||||
inputType &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
!LiteGraph.isValidConnection(inputType, nodeOutType)
|
||||
) {
|
||||
// The output doesnt match our input so disconnect it
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.disconnectInput(link.target_slot)
|
||||
} else {
|
||||
outputType = nodeOutType
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No more outputs for this path
|
||||
}
|
||||
}
|
||||
|
||||
const displayType = inputType || outputType || '*'
|
||||
const color = LGraphCanvas.link_type_colors[displayType]
|
||||
|
||||
let widgetConfig
|
||||
let widgetType
|
||||
// Update the types of each node
|
||||
for (const node of updateNodes) {
|
||||
// If we dont have an input type we are always wildcard but we'll show the output type
|
||||
// This lets you change the output link to a different type and all nodes will update
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.outputs[0].type = inputType || '*'
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.__outputType = displayType
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.outputs[0].name = node.properties.showOutputText
|
||||
? displayType
|
||||
: ''
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.setSize(node.computeSize())
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l]
|
||||
if (link) {
|
||||
link.color = color
|
||||
|
||||
if (app.configuringGraph) continue
|
||||
const targetNode = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const config = getWidgetConfig(targetInput)
|
||||
if (!widgetConfig) {
|
||||
widgetConfig = config[1] ?? {}
|
||||
widgetType = config[0]
|
||||
}
|
||||
|
||||
const merged = mergeIfValid(targetInput, [
|
||||
config[0],
|
||||
widgetConfig
|
||||
])
|
||||
if (merged.customConfig) {
|
||||
widgetConfig = merged.customConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of updateNodes) {
|
||||
if (widgetConfig && outputType) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs[0].widget = { name: 'value' }
|
||||
// @ts-expect-error fixme ts strict error
|
||||
setWidgetConfig(node.inputs[0], [
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widgetType ?? displayType,
|
||||
widgetConfig
|
||||
])
|
||||
} else {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
setWidgetConfig(node.inputs[0], null)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const link = app.graph.links[inputNode.inputs[0].link]
|
||||
if (link) {
|
||||
link.color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.clone = function () {
|
||||
const cloned = RerouteNode.prototype.clone.apply(this)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
cloned.removeOutput(0)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
cloned.setSize(cloned.computeSize())
|
||||
return cloned
|
||||
}
|
||||
|
||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
override onAfterGraphConfigured() {
|
||||
requestAnimationFrame(() => {
|
||||
this.onConnectionsChange(LiteGraph.INPUT, undefined, true)
|
||||
})
|
||||
}
|
||||
override clone(): LGraphNode | null {
|
||||
const cloned = super.clone()
|
||||
if (!cloned) return cloned
|
||||
cloned.removeOutput(0)
|
||||
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
cloned.setSize(cloned.computeSize())
|
||||
return cloned
|
||||
}
|
||||
override onConnectionsChange(
|
||||
type: ISlotType,
|
||||
_index: number | undefined,
|
||||
connected: boolean
|
||||
) {
|
||||
const { graph } = this
|
||||
if (!graph) return
|
||||
if (app.configuringGraph) return
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getExtraMenuOptions(_, options): IContextMenuValue[] {
|
||||
// Prevent multiple connections to different types when we have no input
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
// Ignore wildcard nodes as these will be updated to real types
|
||||
const types = new Set(
|
||||
this.outputs[0].links
|
||||
?.map((l) => graph.links[l]?.type)
|
||||
?.filter((t) => t && t !== '*') ?? []
|
||||
)
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = []
|
||||
for (const linkId of this.outputs[0].links ?? []) {
|
||||
const link = graph.links[linkId]
|
||||
linksToDisconnect.push(link)
|
||||
}
|
||||
linksToDisconnect.pop()
|
||||
for (const link of linksToDisconnect) {
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
node?.disconnectInput(link.target_slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find root input
|
||||
let currentNode: RerouteNode | null = this
|
||||
let updateNodes: RerouteNode[] = []
|
||||
let inputType = null
|
||||
let inputNode = null
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode)
|
||||
const linkId = currentNode.inputs[0].link
|
||||
if (linkId !== null) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = graph.getNodeById(link.origin_id)
|
||||
if (!node) return
|
||||
if (node instanceof RerouteNode) {
|
||||
if (node === this) {
|
||||
// We've found a circle
|
||||
currentNode.disconnectInput(link.target_slot)
|
||||
currentNode = null
|
||||
} else {
|
||||
// Move the previous node
|
||||
currentNode = node
|
||||
}
|
||||
} else {
|
||||
// We've found the end
|
||||
inputNode = currentNode
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// This path has no input node
|
||||
currentNode = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find all outputs
|
||||
const nodes: RerouteNode[] = [this]
|
||||
let outputType = null
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop()!
|
||||
const outputs = currentNode.outputs?.[0]?.links ?? []
|
||||
for (const linkId of outputs) {
|
||||
const link = graph.links[linkId]
|
||||
|
||||
// When disconnecting sometimes the link is still registered
|
||||
if (!link) continue
|
||||
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
if (!node) continue
|
||||
if (node instanceof RerouteNode) {
|
||||
// Follow reroute nodes
|
||||
nodes.push(node)
|
||||
updateNodes.push(node)
|
||||
} else {
|
||||
// We've found an output
|
||||
const nodeInput = node.inputs[link.target_slot]
|
||||
const nodeOutType = nodeInput.type
|
||||
const keep =
|
||||
!inputType ||
|
||||
!nodeOutType ||
|
||||
LiteGraph.isValidConnection(inputType, nodeOutType)
|
||||
if (!keep) {
|
||||
// The output doesnt match our input so disconnect it
|
||||
node.disconnectInput(link.target_slot)
|
||||
continue
|
||||
}
|
||||
node.onConnectionsChange?.(
|
||||
LiteGraph.INPUT,
|
||||
link.target_slot,
|
||||
keep,
|
||||
link,
|
||||
nodeInput
|
||||
)
|
||||
outputType = node.inputs[link.target_slot].type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const displayType = inputType || outputType || '*'
|
||||
const color = LGraphCanvas.link_type_colors[displayType]
|
||||
|
||||
let widgetConfig
|
||||
let widgetType
|
||||
// Update the types of each node
|
||||
for (const node of updateNodes) {
|
||||
// If we dont have an input type we are always wildcard but we'll show the output type
|
||||
// This lets you change the output link to a different type and all nodes will update
|
||||
node.outputs[0].type = inputType || '*'
|
||||
node.__outputType = displayType
|
||||
node.outputs[0].name = node.properties.showOutputText
|
||||
? `${displayType}`
|
||||
: ''
|
||||
node.setSize(node.computeSize())
|
||||
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = graph.links[l]
|
||||
if (!link) continue
|
||||
link.color = color
|
||||
|
||||
if (app.configuringGraph) continue
|
||||
const targetNode = graph.getNodeById(link.target_id)
|
||||
if (!targetNode) continue
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const config = getWidgetConfig(targetInput)
|
||||
if (!widgetConfig) {
|
||||
widgetConfig = config[1] ?? {}
|
||||
widgetType = config[0]
|
||||
}
|
||||
|
||||
const merged = mergeIfValid(targetInput, [
|
||||
config[0],
|
||||
widgetConfig
|
||||
])
|
||||
if (merged.customConfig) {
|
||||
widgetConfig = merged.customConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of updateNodes) {
|
||||
if (widgetConfig && outputType) {
|
||||
node.inputs[0].widget = { name: 'value' }
|
||||
setWidgetConfig(node.inputs[0], [
|
||||
widgetType ?? `${displayType}`,
|
||||
widgetConfig
|
||||
])
|
||||
} else {
|
||||
setWidgetConfig(node.inputs[0], undefined)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode?.inputs?.[0]?.link) {
|
||||
const link = graph.links[inputNode.inputs[0].link]
|
||||
if (link) {
|
||||
link.color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override getExtraMenuOptions(
|
||||
_: unknown,
|
||||
options: (IContextMenuValue | null)[]
|
||||
): IContextMenuValue[] {
|
||||
options.unshift(
|
||||
{
|
||||
content:
|
||||
@@ -259,13 +232,12 @@ app.registerExtension({
|
||||
callback: () => {
|
||||
this.properties.showOutputText = !this.properties.showOutputText
|
||||
if (this.properties.showOutputText) {
|
||||
this.outputs[0].name =
|
||||
this.__outputType || (this.outputs[0].type as string)
|
||||
this.outputs[0].name = `${this.__outputType || this.outputs[0].type}`
|
||||
} else {
|
||||
this.outputs[0].name = ''
|
||||
}
|
||||
this.setSize(this.computeSize())
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -294,8 +266,7 @@ app.registerExtension({
|
||||
]
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
static setDefaultTextVisibility(visible) {
|
||||
static setDefaultTextVisibility(visible: boolean) {
|
||||
RerouteNode.defaultVisibility = visible
|
||||
if (visible) {
|
||||
localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true'
|
||||
|
||||
@@ -168,16 +168,14 @@ export class PrimitiveNode extends LGraphNode {
|
||||
|
||||
#onFirstConnection(recreating?: boolean) {
|
||||
// First connection can fire before the graph is ready on initial load so random things can be missing
|
||||
if (!this.outputs[0].links) {
|
||||
if (!this.outputs[0].links || !this.graph) {
|
||||
this.onLastDisconnect()
|
||||
return
|
||||
}
|
||||
const linkId = this.outputs[0].links[0]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const link = this.graph.links[linkId]
|
||||
if (!link) return
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const theirNode = this.graph.getNodeById(link.target_id)
|
||||
if (!theirNode || !theirNode.inputs) return
|
||||
|
||||
@@ -445,7 +443,7 @@ function getWidgetType(config: InputSpec) {
|
||||
|
||||
export function setWidgetConfig(
|
||||
slot: INodeInputSlot | INodeOutputSlot,
|
||||
config: InputSpec
|
||||
config?: InputSpec
|
||||
) {
|
||||
if (!slot.widget) return
|
||||
if (config) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ContextMenu<TValue = unknown> {
|
||||
/**
|
||||
* ContextMenu from LiteGUI
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
|
||||
export class ContextMenu<TValue = unknown> {
|
||||
options: IContextMenuOptions<TValue>
|
||||
parentMenu?: ContextMenu<TValue>
|
||||
@@ -274,7 +274,7 @@ export class ContextMenu<TValue = unknown> {
|
||||
}
|
||||
|
||||
// menu option clicked
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
|
||||
const that = this
|
||||
function inner_onclick(this: ContextMenuDivElement<TValue>, e: MouseEvent) {
|
||||
const value = this.value
|
||||
|
||||
@@ -79,6 +79,8 @@ export type {
|
||||
LGraphTriggerParam
|
||||
} from './types/graphTriggers'
|
||||
|
||||
export type rendererType = 'LG' | 'Vue'
|
||||
|
||||
export interface LGraphState {
|
||||
lastGroupId: number
|
||||
lastNodeId: number
|
||||
@@ -104,6 +106,7 @@ export interface LGraphExtra extends Dictionary<unknown> {
|
||||
reroutes?: SerialisableReroute[]
|
||||
linkExtensions?: { id: number; parentId: number | undefined }[]
|
||||
ds?: DragAndScaleState
|
||||
workflowRendererVersion?: rendererType
|
||||
}
|
||||
|
||||
export interface BaseLGraph {
|
||||
@@ -1566,6 +1569,8 @@ export class LGraph
|
||||
|
||||
const subgraph = this.createSubgraph(data)
|
||||
subgraph.configure(data)
|
||||
for (const node of subgraph.nodes) node.onGraphConfigured?.()
|
||||
for (const node of subgraph.nodes) node.onAfterGraphConfigured?.()
|
||||
|
||||
// Position the subgraph input nodes
|
||||
subgraph.inputNode.arrange()
|
||||
|
||||
@@ -2672,7 +2672,7 @@ export class LGraphCanvas
|
||||
): boolean {
|
||||
const outputLinks = [
|
||||
...(output.links ?? []),
|
||||
...(output._floatingLinks ?? new Set())
|
||||
...[...(output._floatingLinks ?? new Set())]
|
||||
]
|
||||
return outputLinks.some(
|
||||
(linkId) =>
|
||||
@@ -6443,7 +6443,7 @@ export class LGraphCanvas
|
||||
optPass || {}
|
||||
)
|
||||
const dirty = () => this.#dirty()
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
|
||||
const that = this
|
||||
const { graph } = this
|
||||
const { afterRerouteId } = opts
|
||||
@@ -6645,7 +6645,6 @@ export class LGraphCanvas
|
||||
event: CanvasPointerEvent,
|
||||
multiline?: boolean
|
||||
): HTMLDivElement {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const that = this
|
||||
title = title || ''
|
||||
|
||||
@@ -6816,7 +6815,7 @@ export class LGraphCanvas
|
||||
Object.assign(options, searchOptions)
|
||||
|
||||
// console.log(options);
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
|
||||
const that = this
|
||||
const graphcanvas = LGraphCanvas.active_canvas
|
||||
const { canvas } = graphcanvas
|
||||
@@ -7629,6 +7628,7 @@ export class LGraphCanvas
|
||||
root.content = root.querySelector('.dialog-content')
|
||||
root.alt_content = root.querySelector('.dialog-alt-content')
|
||||
root.footer = root.querySelector('.dialog-footer')
|
||||
root.footer.style.marginTop = '-96px'
|
||||
|
||||
root.close = function () {
|
||||
if (typeof root.onClose == 'function') root.onClose()
|
||||
@@ -7831,6 +7831,9 @@ export class LGraphCanvas
|
||||
panel.id = 'node-panel'
|
||||
panel.node = node
|
||||
panel.classList.add('settings')
|
||||
panel.style.position = 'absolute'
|
||||
panel.style.top = '96px'
|
||||
panel.style.left = '65px'
|
||||
|
||||
const inner_refresh = () => {
|
||||
// clear
|
||||
|
||||
@@ -206,7 +206,6 @@ supported callbacks:
|
||||
+ getExtraMenuOptions: to add option to context menu
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export interface LGraphNode {
|
||||
constructor: LGraphNodeConstructor
|
||||
}
|
||||
@@ -218,7 +217,7 @@ export interface LGraphNode {
|
||||
* @param title a name for the node
|
||||
* @param type a type for the node
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
|
||||
export class LGraphNode
|
||||
implements NodeLike, Positionable, IPinnable, IColorable
|
||||
{
|
||||
|
||||