Compare commits
53 Commits
vue-node/f
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a71450b85 | ||
|
|
6031e6ccf2 | ||
|
|
4417b0d907 | ||
|
|
ff988cd6f1 | ||
|
|
38cc93b8f6 | ||
|
|
23d8ccd394 | ||
|
|
23d01f0b34 | ||
|
|
64a7955eca | ||
|
|
a5bd37ef56 | ||
|
|
ca539a6fd0 | ||
|
|
585e47f2df | ||
|
|
aa13e4c385 | ||
|
|
57ed0afde6 | ||
|
|
3a18d27b9d | ||
|
|
f9e8dfdbe7 | ||
|
|
ed68b085cc | ||
|
|
bb9475fed6 | ||
|
|
ba43494d32 | ||
|
|
df82698f1e | ||
|
|
d4b993b16d | ||
|
|
04ab767649 | ||
|
|
0b7d0f1d35 | ||
|
|
0dfe36f1f8 | ||
|
|
2c437acff6 | ||
|
|
b97b21add0 | ||
|
|
dfe098897a | ||
|
|
d76c59cb14 | ||
|
|
c298d8a870 | ||
|
|
b50b34ac95 | ||
|
|
7a6cc39c39 | ||
|
|
2b751200be | ||
|
|
5b03d3fcbc | ||
|
|
1caf3fdb0c | ||
|
|
f39faf6b8e | ||
|
|
3ee921119d | ||
|
|
f61bfe666e | ||
|
|
d8795ec997 | ||
|
|
d3aa8dfc88 | ||
|
|
2cce0fe611 | ||
|
|
b9cb335255 | ||
|
|
2f89eb070c | ||
|
|
d94e0720f3 | ||
|
|
fe47a487bb | ||
|
|
63f68543e4 | ||
|
|
9806ba807a | ||
|
|
7433f470fc | ||
|
|
490bb22bd3 | ||
|
|
895775c319 | ||
|
|
e248ecfa4e | ||
|
|
923695ffde | ||
|
|
df653d6ce1 | ||
|
|
28dc9314d2 | ||
|
|
3acf9aae0a |
292
.github/workflows/release-weekly-comfyui.yaml
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
# Automated weekly workflow to bump ComfyUI frontend RC releases
|
||||
name: "Release: Weekly ComfyUI"
|
||||
|
||||
on:
|
||||
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
|
||||
schedule:
|
||||
- cron: '0 20 * * 1'
|
||||
|
||||
# Allow manual triggering
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
comfyui_fork:
|
||||
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
|
||||
required: false
|
||||
default: 'Comfy-Org/ComfyUI'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
resolve-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
current_version: ${{ steps.resolve.outputs.current_version }}
|
||||
target_version: ${{ steps.resolve.outputs.target_version }}
|
||||
target_minor: ${{ steps.resolve.outputs.target_minor }}
|
||||
target_branch: ${{ steps.resolve.outputs.target_branch }}
|
||||
needs_release: ${{ steps.resolve.outputs.needs_release }}
|
||||
diff_url: ${{ steps.resolve.outputs.diff_url }}
|
||||
latest_patch_tag: ${{ steps.resolve.outputs.latest_patch_tag }}
|
||||
|
||||
steps:
|
||||
- name: Checkout ComfyUI_frontend
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: frontend
|
||||
|
||||
- name: Checkout ComfyUI (sparse)
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: comfyanonymous/ComfyUI
|
||||
sparse-checkout: |
|
||||
requirements.txt
|
||||
path: comfyui
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Resolve release information
|
||||
id: resolve
|
||||
working-directory: frontend
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Run the resolver script
|
||||
if ! RESULT=$(tsx scripts/cicd/resolve-comfyui-release.ts ../comfyui .); then
|
||||
echo "Failed to resolve release information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Resolver output:"
|
||||
echo "$RESULT"
|
||||
|
||||
# Validate JSON output
|
||||
if ! echo "$RESULT" | jq empty 2>/dev/null; then
|
||||
echo "Invalid JSON output from resolver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse JSON output and set outputs
|
||||
echo "current_version=$(echo "$RESULT" | jq -r '.current_version')" >> $GITHUB_OUTPUT
|
||||
echo "target_version=$(echo "$RESULT" | jq -r '.target_version')" >> $GITHUB_OUTPUT
|
||||
echo "target_minor=$(echo "$RESULT" | jq -r '.target_minor')" >> $GITHUB_OUTPUT
|
||||
echo "target_branch=$(echo "$RESULT" | jq -r '.target_branch')" >> $GITHUB_OUTPUT
|
||||
echo "needs_release=$(echo "$RESULT" | jq -r '.needs_release')" >> $GITHUB_OUTPUT
|
||||
echo "diff_url=$(echo "$RESULT" | jq -r '.diff_url')" >> $GITHUB_OUTPUT
|
||||
echo "latest_patch_tag=$(echo "$RESULT" | jq -r '.latest_patch_tag')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Release Information" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Current version: ${{ steps.resolve.outputs.current_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target version: ${{ steps.resolve.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target branch: ${{ steps.resolve.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Needs release: ${{ steps.resolve.outputs.needs_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Diff: [${{ steps.resolve.outputs.current_version }}...${{ steps.resolve.outputs.target_version }}](${{ steps.resolve.outputs.diff_url }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
trigger-release-if-needed:
|
||||
needs: resolve-version
|
||||
if: needs.resolve-version.outputs.needs_release == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Trigger release workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Triggering release workflow for branch ${{ needs.resolve-version.outputs.target_branch }}"
|
||||
|
||||
# Trigger the release-version-bump workflow
|
||||
if ! gh workflow run release-version-bump.yaml \
|
||||
--repo Comfy-Org/ComfyUI_frontend \
|
||||
--ref main \
|
||||
--field version_type=patch \
|
||||
--field branch=${{ needs.resolve-version.outputs.target_branch }}; then
|
||||
echo "Failed to trigger release workflow"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release workflow triggered successfully for ${{ needs.resolve-version.outputs.target_branch }}"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Release Workflow Triggered" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Branch: ${{ needs.resolve-version.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target version: ${{ needs.resolve-version.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
create-comfyui-pr:
|
||||
needs: [resolve-version, trigger-release-if-needed]
|
||||
if: always() && needs.resolve-version.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout ComfyUI fork
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
path: comfyui
|
||||
|
||||
- name: Sync with upstream
|
||||
working-directory: comfyui
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Fetch latest upstream to base our branch on fresh code
|
||||
# Note: This only affects the local checkout, NOT the fork's master branch
|
||||
# We only push the automation branch, leaving the fork's master untouched
|
||||
echo "Fetching upstream master..."
|
||||
if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then
|
||||
echo "Failed to fetch upstream master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking out upstream master..."
|
||||
if ! git checkout FETCH_HEAD; then
|
||||
echo "Failed to checkout FETCH_HEAD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully synced with upstream master"
|
||||
|
||||
- name: Update requirements.txt
|
||||
working-directory: comfyui
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}"
|
||||
echo "Updating comfyui-frontend-package to ${TARGET_VERSION}"
|
||||
|
||||
# Update the comfyui-frontend-package version (POSIX-compatible)
|
||||
sed -i.bak "s/comfyui-frontend-package==[0-9.][0-9.]*/comfyui-frontend-package==${TARGET_VERSION}/" requirements.txt
|
||||
rm requirements.txt.bak
|
||||
|
||||
# Verify the change was made
|
||||
if ! grep -q "comfyui-frontend-package==${TARGET_VERSION}" requirements.txt; then
|
||||
echo "Failed to update requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updated requirements.txt:"
|
||||
grep comfyui-frontend-package requirements.txt
|
||||
|
||||
- name: Build PR description
|
||||
id: pr-body
|
||||
run: |
|
||||
BODY=$(cat <<'EOF'
|
||||
Bumps frontend to ${{ needs.resolve-version.outputs.target_version }}
|
||||
|
||||
Test quickly:
|
||||
|
||||
```bash
|
||||
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${{ needs.resolve-version.outputs.target_version }}
|
||||
```
|
||||
|
||||
- Diff: [v${{ needs.resolve-version.outputs.current_version }}...v${{ needs.resolve-version.outputs.target_version }}](${{ needs.resolve-version.outputs.diff_url }})
|
||||
- PyPI: https://pypi.org/project/comfyui-frontend-package/${{ needs.resolve-version.outputs.target_version }}/
|
||||
- npm: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${{ needs.resolve-version.outputs.target_version }}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Add release PR note if release was triggered
|
||||
if [ "${{ needs.resolve-version.outputs.needs_release }}" = "true" ]; then
|
||||
RELEASE_NOTE="⚠️ **Release PR must be merged first** - check [release workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)"
|
||||
BODY=$''"${RELEASE_NOTE}"$'\n\n'"${BODY}"
|
||||
fi
|
||||
|
||||
# Save to file for later use
|
||||
printf '%s\n' "$BODY" > pr-body.txt
|
||||
cat pr-body.txt
|
||||
|
||||
- name: Create PR to ComfyUI
|
||||
working-directory: comfyui
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
COMFYUI_FORK: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract fork owner from repository name
|
||||
FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1)
|
||||
|
||||
echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI"
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create/update branch (reuse same branch name each week)
|
||||
BRANCH="automation/comfyui-frontend-bump"
|
||||
git checkout -B "$BRANCH"
|
||||
git add requirements.txt
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}"
|
||||
else
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Force push to fork (overwrites previous week's branch)
|
||||
# Note: This intentionally destroys branch history to maintain a single PR
|
||||
# Any review comments or manual commits will need to be re-applied
|
||||
if ! git push -f origin "$BRANCH"; then
|
||||
echo "Failed to push branch to fork"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create draft PR from fork to upstream
|
||||
PR_BODY=$(cat ../pr-body.txt)
|
||||
|
||||
# Try to create PR, ignore error if it already exists
|
||||
if ! gh pr create \
|
||||
--repo comfyanonymous/ComfyUI \
|
||||
--head "${FORK_OWNER}:${BRANCH}" \
|
||||
--base master \
|
||||
--title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \
|
||||
--body "$PR_BODY" \
|
||||
--draft 2>&1; then
|
||||
|
||||
# Check if PR already exists
|
||||
set +e
|
||||
EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
|
||||
PR_LIST_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ $PR_LIST_EXIT -ne 0 ]; then
|
||||
echo "Failed to check for existing PR: $EXISTING_PR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
|
||||
echo "PR already exists (#${EXISTING_PR}), updating branch will update the PR"
|
||||
else
|
||||
echo "Failed to create PR and no existing PR found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
|
||||
cat pr-body.txt >> $GITHUB_STEP_SUMMARY
|
||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"typescript/no-redundant-type-constituents": "off",
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error"
|
||||
"typescript/no-floating-promises": "error",
|
||||
"vue/no-import-compiler-macros": "error"
|
||||
}
|
||||
}
|
||||
@@ -54,10 +54,11 @@
|
||||
|
||||
# Translations
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
|
||||
/src/locales/pt-BR/ @JonatanAtila @Yorha4D @KarryCharon @shinshin86
|
||||
|
||||
# LLM Instructions (blank on purpose)
|
||||
.claude/
|
||||
.cursor/
|
||||
.cursorrules
|
||||
**/AGENTS.md
|
||||
**/CLAUDE.md
|
||||
**/CLAUDE.md
|
||||
|
||||
@@ -59,7 +59,8 @@ const LOCALES = [
|
||||
['fr', 'Français'],
|
||||
['es', 'Español'],
|
||||
['ar', 'عربي'],
|
||||
['tr', 'Türkçe']
|
||||
['tr', 'Türkçe'],
|
||||
['pt-BR', 'Português (BR)']
|
||||
] as const satisfies ReadonlyArray<[string, string]>
|
||||
|
||||
type SupportedLocale = (typeof LOCALES)[number][0]
|
||||
|
||||
@@ -40,7 +40,8 @@ const localeLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/main.json'),
|
||||
tr: () => import('@frontend-locales/tr/main.json'),
|
||||
zh: () => import('@frontend-locales/zh/main.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/main.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/main.json')
|
||||
}
|
||||
|
||||
const nodeDefsLoaders: Record<
|
||||
@@ -55,7 +56,8 @@ const nodeDefsLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
|
||||
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
|
||||
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/nodeDefs.json')
|
||||
}
|
||||
|
||||
const commandsLoaders: Record<
|
||||
@@ -70,7 +72,8 @@ const commandsLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/commands.json'),
|
||||
tr: () => import('@frontend-locales/tr/commands.json'),
|
||||
zh: () => import('@frontend-locales/zh/commands.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/commands.json')
|
||||
}
|
||||
|
||||
const settingsLoaders: Record<
|
||||
@@ -85,7 +88,8 @@ const settingsLoaders: Record<
|
||||
ru: () => import('@frontend-locales/ru/settings.json'),
|
||||
tr: () => import('@frontend-locales/tr/settings.json'),
|
||||
zh: () => import('@frontend-locales/zh/settings.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json'),
|
||||
'pt-BR': () => import('@frontend-locales/pt-BR/settings.json')
|
||||
}
|
||||
|
||||
// Track which locales have been loaded
|
||||
|
||||
@@ -1651,7 +1651,10 @@ export const comfyPageFixture = base.extend<{
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
||||
'Comfy.VueNodes.AutoScaleLayout': false
|
||||
'Comfy.VueNodes.AutoScaleLayout': false,
|
||||
// Disable toast warning about version compatibility, as they may or
|
||||
// may not appear - depending on upstream ComfyUI dependencies
|
||||
'Comfy.VersionCompatibility.DisableWarnings': true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Resizing', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should resize node without position drift after selecting', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Get a Vue node fixture
|
||||
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
|
||||
const initialBox = await node.boundingBox()
|
||||
if (!initialBox) throw new Error('Node bounding box not found')
|
||||
|
||||
// Select the node first (this was causing the bug)
|
||||
await node.header.click()
|
||||
await comfyPage.page.waitForTimeout(100) // Brief pause after selection
|
||||
|
||||
// Get position after selection
|
||||
const selectedBox = await node.boundingBox()
|
||||
if (!selectedBox)
|
||||
throw new Error('Node bounding box not found after select')
|
||||
|
||||
// Verify position unchanged after selection
|
||||
expect(selectedBox.x).toBeCloseTo(initialBox.x, 1)
|
||||
expect(selectedBox.y).toBeCloseTo(initialBox.y, 1)
|
||||
|
||||
// Now resize from bottom-right corner
|
||||
const resizeStartX = selectedBox.x + selectedBox.width - 5
|
||||
const resizeStartY = selectedBox.y + selectedBox.height - 5
|
||||
|
||||
await comfyPage.page.mouse.move(resizeStartX, resizeStartY)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(resizeStartX + 50, resizeStartY + 30)
|
||||
await comfyPage.page.mouse.up()
|
||||
|
||||
// Get final position and size
|
||||
const finalBox = await node.boundingBox()
|
||||
if (!finalBox) throw new Error('Node bounding box not found after resize')
|
||||
|
||||
// Position should NOT have changed (the bug was position drift)
|
||||
expect(finalBox.x).toBeCloseTo(initialBox.x, 1)
|
||||
expect(finalBox.y).toBeCloseTo(initialBox.y, 1)
|
||||
|
||||
// Size should have increased
|
||||
expect(finalBox.width).toBeGreaterThan(initialBox.width)
|
||||
expect(finalBox.height).toBeGreaterThan(initialBox.height)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
@@ -27,7 +27,7 @@ const config: KnipConfig = {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
}
|
||||
},
|
||||
ignoreBinaries: ['python3'],
|
||||
ignoreBinaries: ['python3', 'gh'],
|
||||
ignoreDependencies: [
|
||||
// Weird importmap things
|
||||
'@iconify/json',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.34.0",
|
||||
"version": "1.34.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -1190,24 +1190,19 @@ dialog::backdrop {
|
||||
.litegraph.litecontextmenu,
|
||||
.litegraph.litecontextmenu.dark {
|
||||
z-index: 9999 !important;
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu
|
||||
.litemenu-entry:hover:not(.disabled):not(.separator) {
|
||||
background-color: var(--comfy-menu-hover-bg, var(--border-color)) !important;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu .litemenu-entry.submenu,
|
||||
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu input {
|
||||
background-color: var(--comfy-input-bg) !important;
|
||||
color: var(--input-text) !important;
|
||||
background-color: var(--comfy-input-bg);
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
.comfy-context-menu-filter {
|
||||
@@ -1248,14 +1243,14 @@ dialog::backdrop {
|
||||
|
||||
.litegraph.litesearchbox {
|
||||
z-index: 9999 !important;
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.litegraph.litesearchbox input,
|
||||
.litegraph.litesearchbox select {
|
||||
background-color: var(--comfy-input-bg) !important;
|
||||
background-color: var(--comfy-input-bg);
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
|
||||
254
scripts/cicd/resolve-comfyui-release.ts
Executable file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
interface ReleaseInfo {
|
||||
current_version: string
|
||||
target_minor: number
|
||||
target_version: string
|
||||
target_branch: string
|
||||
needs_release: boolean
|
||||
latest_patch_tag: string | null
|
||||
branch_head_sha: string | null
|
||||
tag_commit_sha: string | null
|
||||
diff_url: string
|
||||
release_pr_url: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and return stdout
|
||||
*/
|
||||
function exec(command: string, cwd?: string): string {
|
||||
try {
|
||||
return execSync(command, {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim()
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const cwdInfo = cwd ? ` in directory: ${cwd}` : ''
|
||||
console.error(
|
||||
`Command failed: ${command}${cwdInfo}\nError: ${errorMessage}`
|
||||
)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse version from requirements.txt
|
||||
* Handles formats: comfyui-frontend-package==1.2.3, comfyui-frontend-package>=1.2.3, etc.
|
||||
*/
|
||||
function parseRequirementsVersion(requirementsPath: string): string | null {
|
||||
if (!fs.existsSync(requirementsPath)) {
|
||||
console.error(`Requirements file not found: ${requirementsPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(requirementsPath, 'utf-8')
|
||||
const match = content.match(
|
||||
/comfyui-frontend-package\s*(?:==|>=|<=|~=|>|<)\s*([0-9]+\.[0-9]+\.[0-9]+)/
|
||||
)
|
||||
|
||||
if (!match) {
|
||||
console.error(
|
||||
'Could not find comfyui-frontend-package version in requirements.txt'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate semantic version string
|
||||
*/
|
||||
function isValidSemver(version: string): boolean {
|
||||
if (!version || typeof version !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
const parts = version.split('.')
|
||||
if (parts.length !== 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
return parts.every((part) => {
|
||||
const num = Number(part)
|
||||
return Number.isFinite(num) && num >= 0 && String(num) === part
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest patch tag for a given minor version
|
||||
*/
|
||||
function getLatestPatchTag(repoPath: string, minor: number): string | null {
|
||||
// Fetch all tags
|
||||
exec('git fetch --tags', repoPath)
|
||||
|
||||
// Use git's native version sorting to get the latest tag
|
||||
const latestTag = exec(
|
||||
`git tag -l 'v1.${minor}.*' --sort=-version:refname | head -n 1`,
|
||||
repoPath
|
||||
)
|
||||
|
||||
if (!latestTag) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Validate the tag is a valid semver (vX.Y.Z format)
|
||||
const validTagRegex = /^v\d+\.\d+\.\d+$/
|
||||
if (!validTagRegex.test(latestTag)) {
|
||||
console.error(
|
||||
`Latest tag for minor version ${minor} is not valid semver: ${latestTag}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return latestTag
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ComfyUI release information
|
||||
*/
|
||||
function resolveRelease(
|
||||
comfyuiRepoPath: string,
|
||||
frontendRepoPath: string
|
||||
): ReleaseInfo | null {
|
||||
// Parse current version from ComfyUI requirements.txt
|
||||
const requirementsPath = path.join(comfyuiRepoPath, 'requirements.txt')
|
||||
const currentVersion = parseRequirementsVersion(requirementsPath)
|
||||
|
||||
if (!currentVersion) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Validate version format
|
||||
if (!isValidSemver(currentVersion)) {
|
||||
console.error(
|
||||
`Invalid semantic version format: ${currentVersion}. Expected format: X.Y.Z`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const [major, currentMinor, patch] = currentVersion.split('.').map(Number)
|
||||
|
||||
// Calculate target minor version (next minor)
|
||||
const targetMinor = currentMinor + 1
|
||||
const targetBranch = `core/1.${targetMinor}`
|
||||
|
||||
// Check if target branch exists in frontend repo
|
||||
exec('git fetch origin', frontendRepoPath)
|
||||
const branchExists = exec(
|
||||
`git rev-parse --verify origin/${targetBranch}`,
|
||||
frontendRepoPath
|
||||
)
|
||||
|
||||
if (!branchExists) {
|
||||
console.error(
|
||||
`Target branch ${targetBranch} does not exist in frontend repo`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// Get latest patch tag for target minor
|
||||
const latestPatchTag = getLatestPatchTag(frontendRepoPath, targetMinor)
|
||||
|
||||
let needsRelease = false
|
||||
let branchHeadSha: string | null = null
|
||||
let tagCommitSha: string | null = null
|
||||
let targetVersion = currentVersion
|
||||
|
||||
if (latestPatchTag) {
|
||||
// Get commit SHA for the tag
|
||||
tagCommitSha = exec(`git rev-list -n 1 ${latestPatchTag}`, frontendRepoPath)
|
||||
|
||||
// Get commit SHA for branch head
|
||||
branchHeadSha = exec(
|
||||
`git rev-parse origin/${targetBranch}`,
|
||||
frontendRepoPath
|
||||
)
|
||||
|
||||
// Check if there are commits between tag and branch head
|
||||
const commitsBetween = exec(
|
||||
`git rev-list ${latestPatchTag}..origin/${targetBranch} --count`,
|
||||
frontendRepoPath
|
||||
)
|
||||
|
||||
const commitCount = parseInt(commitsBetween, 10)
|
||||
needsRelease = !isNaN(commitCount) && commitCount > 0
|
||||
|
||||
// Parse existing patch number and increment if needed
|
||||
const tagVersion = latestPatchTag.replace('v', '')
|
||||
|
||||
// Validate tag version format
|
||||
if (!isValidSemver(tagVersion)) {
|
||||
console.error(
|
||||
`Invalid tag version format: ${tagVersion}. Expected format: X.Y.Z`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const [, , existingPatch] = tagVersion.split('.').map(Number)
|
||||
|
||||
// Validate existingPatch is a valid number
|
||||
if (!Number.isFinite(existingPatch) || existingPatch < 0) {
|
||||
console.error(`Invalid patch number in tag: ${existingPatch}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (needsRelease) {
|
||||
targetVersion = `1.${targetMinor}.${existingPatch + 1}`
|
||||
} else {
|
||||
targetVersion = tagVersion
|
||||
}
|
||||
} else {
|
||||
// No tags exist for this minor version, need to create v1.{targetMinor}.0
|
||||
needsRelease = true
|
||||
targetVersion = `1.${targetMinor}.0`
|
||||
branchHeadSha = exec(
|
||||
`git rev-parse origin/${targetBranch}`,
|
||||
frontendRepoPath
|
||||
)
|
||||
}
|
||||
|
||||
const diffUrl = `https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${currentVersion}...v${targetVersion}`
|
||||
|
||||
return {
|
||||
current_version: currentVersion,
|
||||
target_minor: targetMinor,
|
||||
target_version: targetVersion,
|
||||
target_branch: targetBranch,
|
||||
needs_release: needsRelease,
|
||||
latest_patch_tag: latestPatchTag,
|
||||
branch_head_sha: branchHeadSha,
|
||||
tag_commit_sha: tagCommitSha,
|
||||
diff_url: diffUrl,
|
||||
release_pr_url: null // Will be populated by workflow if release is triggered
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const comfyuiRepoPath = process.argv[2]
|
||||
const frontendRepoPath = process.argv[3] || process.cwd()
|
||||
|
||||
if (!comfyuiRepoPath) {
|
||||
console.error(
|
||||
'Usage: resolve-comfyui-release.ts <comfyui-repo-path> [frontend-repo-path]'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const releaseInfo = resolveRelease(comfyuiRepoPath, frontendRepoPath)
|
||||
|
||||
if (!releaseInfo) {
|
||||
console.error('Failed to resolve release information')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Output as JSON for GitHub Actions
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(JSON.stringify(releaseInfo, null, 2))
|
||||
|
||||
export { resolveRelease }
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<Panel
|
||||
class="pointer-events-auto z-1010"
|
||||
class="pointer-events-auto"
|
||||
:style="style"
|
||||
:class="panelClass"
|
||||
:pt="{
|
||||
@@ -66,12 +66,7 @@ const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
style: style,
|
||||
isDragging
|
||||
} = useDraggable(panelRef, {
|
||||
const { x, y, style, isDragging } = useDraggable(panelRef, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
handle: dragHandleRef,
|
||||
containerElement: document.body,
|
||||
@@ -267,7 +262,7 @@ const actionbarClass = computed(() =>
|
||||
)
|
||||
const panelClass = computed(() =>
|
||||
cn(
|
||||
'actionbar pointer-events-auto z1000',
|
||||
'actionbar pointer-events-auto z-1300',
|
||||
isDragging.value && 'select-none pointer-events-none',
|
||||
isDocked.value
|
||||
? 'p-0 static mr-2 border-none bg-transparent'
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<template>
|
||||
<div :class="iconGroupClasses">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg bg-secondary-background shadow-sm transition-all duration-200 cursor-pointer'
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const iconGroupClasses = cn(
|
||||
'flex justify-center items-center shrink-0',
|
||||
'outline-hidden border-none p-0 rounded-lg',
|
||||
'bg-secondary-background shadow-sm',
|
||||
'transition-all duration-200',
|
||||
'cursor-pointer'
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="relative inline-flex items-center">
|
||||
<IconButton :size="size" :type="type" @click="toggle">
|
||||
<IconButton :size="size" :type="type" @click="popover?.toggle">
|
||||
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
|
||||
<i v-else class="icon-[lucide--more-vertical] text-sm" />
|
||||
</IconButton>
|
||||
@@ -25,8 +25,18 @@
|
||||
)
|
||||
}
|
||||
}"
|
||||
@show="$emit('menuOpened')"
|
||||
@hide="$emit('menuClosed')"
|
||||
@show="
|
||||
() => {
|
||||
isOpen = true
|
||||
$emit('menuOpened')
|
||||
}
|
||||
"
|
||||
@hide="
|
||||
() => {
|
||||
isOpen = false
|
||||
$emit('menuClosed')
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="flex min-w-40 flex-col gap-2 p-2">
|
||||
<slot :close="hide" />
|
||||
@@ -48,8 +58,6 @@ interface MoreButtonProps extends BaseButtonProps {
|
||||
isVertical?: boolean
|
||||
}
|
||||
|
||||
const popover = ref<InstanceType<typeof Popover>>()
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
@@ -61,15 +69,15 @@ defineEmits<{
|
||||
menuClosed: []
|
||||
}>()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
popover.value?.toggle(event)
|
||||
}
|
||||
const isOpen = ref(false)
|
||||
const popover = ref<InstanceType<typeof Popover>>()
|
||||
|
||||
const hide = () => {
|
||||
function hide() {
|
||||
popover.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
hide
|
||||
hide,
|
||||
isOpen
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
...inputAttrs
|
||||
}
|
||||
}"
|
||||
@keyup.enter="blurInputElement"
|
||||
@keyup.escape="cancelEditing"
|
||||
@keyup.enter.capture.stop="blurInputElement"
|
||||
@keyup.escape.stop="cancelEditing"
|
||||
@click.stop
|
||||
@pointerdown.stop.capture
|
||||
@pointermove.stop.capture
|
||||
@@ -38,7 +38,7 @@ const {
|
||||
} = defineProps<{
|
||||
modelValue: string
|
||||
isEditing?: boolean
|
||||
inputAttrs?: Record<string, any>
|
||||
inputAttrs?: Record<string, string>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<component
|
||||
:is="item.headerComponent"
|
||||
v-if="item.headerComponent"
|
||||
v-bind="item.headerProps"
|
||||
:id="item.key"
|
||||
/>
|
||||
<h3 v-else :id="item.key">
|
||||
|
||||
19
src/components/dialog/confirm/ConfirmBody.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
|
||||
>
|
||||
<p v-if="promptTextReal">
|
||||
{{ promptTextReal }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
const { promptText } = defineProps<{
|
||||
promptText?: MaybeRefOrGetter<string>
|
||||
}>()
|
||||
|
||||
const promptTextReal = computed(() => toValue(promptText))
|
||||
</script>
|
||||
43
src/components/dialog/confirm/ConfirmFooter.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<section class="w-full flex gap-2 justify-end px-2 pb-2">
|
||||
<TextButton
|
||||
:label="cancelTextX"
|
||||
:disabled
|
||||
type="transparent"
|
||||
autofocus
|
||||
@click="$emit('cancel')"
|
||||
/>
|
||||
<TextButton
|
||||
:label="confirmTextX"
|
||||
:disabled
|
||||
type="transparent"
|
||||
:class="confirmClass"
|
||||
@click="$emit('confirm')"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { cancelText, confirmText, confirmClass, optionsDisabled } = defineProps<{
|
||||
cancelText?: string
|
||||
confirmText?: string
|
||||
confirmClass?: string
|
||||
optionsDisabled?: MaybeRefOrGetter<boolean>
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
cancel: []
|
||||
confirm: []
|
||||
}>()
|
||||
|
||||
const confirmTextX = computed(() => confirmText || t('g.confirm'))
|
||||
const cancelTextX = computed(() => cancelText || t('g.cancel'))
|
||||
const disabled = computed(() => toValue(optionsDisabled))
|
||||
</script>
|
||||
12
src/components/dialog/confirm/ConfirmHeader.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-2 p-4 font-bold text-sm text-base-foreground font-inter"
|
||||
>
|
||||
<span v-if="title" class="flex-auto">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title?: string
|
||||
}>()
|
||||
</script>
|
||||
39
src/components/dialog/confirm/confirmDialog.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import ConfirmBody from '@/components/dialog/confirm/ConfirmBody.vue'
|
||||
import ConfirmFooter from '@/components/dialog/confirm/ConfirmFooter.vue'
|
||||
import ConfirmHeader from '@/components/dialog/confirm/ConfirmHeader.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
interface ConfirmDialogOptions {
|
||||
headerProps?: ComponentAttrs<typeof ConfirmHeader>
|
||||
props?: ComponentAttrs<typeof ConfirmBody>
|
||||
footerProps?: ComponentAttrs<typeof ConfirmFooter>
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a confirmation dialog composed of the standard confirm header, body, and footer.
|
||||
*
|
||||
* Forwards any provided `headerProps`, `props`, and `footerProps` to the corresponding components.
|
||||
*
|
||||
* @param options - Optional configuration with `headerProps`, `props`, and `footerProps` to customize the header, body, and footer components
|
||||
* @returns A dialog handle representing the shown confirmation dialog
|
||||
*/
|
||||
export function showConfirmDialog(options: ConfirmDialogOptions = {}) {
|
||||
const dialogStore = useDialogStore()
|
||||
const { headerProps, props, footerProps } = options
|
||||
return dialogStore.showDialog({
|
||||
headerComponent: ConfirmHeader,
|
||||
component: ConfirmBody,
|
||||
footerComponent: ConfirmFooter,
|
||||
headerProps,
|
||||
props,
|
||||
footerProps,
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
header: 'py-0! px-0!',
|
||||
content: 'p-0!',
|
||||
footer: 'p-0!'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
|
||||
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
|
||||
/>
|
||||
<div class="flex h-full items-center">
|
||||
<div
|
||||
class="flex h-full items-center border-b border-interface-stroke bg-comfy-menu-bg shadow-interface"
|
||||
>
|
||||
<WorkflowTabs />
|
||||
<TopbarBadges />
|
||||
</div>
|
||||
|
||||
@@ -280,7 +280,8 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
label: t('helpCenter.docs'),
|
||||
action: () => {
|
||||
trackResourceClick('docs', true)
|
||||
openExternalLink(buildDocsUrl('/', { includeLocale: true }))
|
||||
const path = isCloud ? '/get_started/cloud' : '/'
|
||||
openExternalLink(buildDocsUrl(path, { includeLocale: true }))
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
{{ t('maskEditor.brushSettings') }}
|
||||
</h3>
|
||||
|
||||
<button
|
||||
class="w-45 h-7.5 border-none bg-black/20 border border-[var(--border-color)] text-[var(--input-text)] font-sans text-[15px] pointer-events-auto transition-colors duration-100 hover:bg-[var(--p-overlaybadge-outline-color)] hover:border-none"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
<button :class="textButtonClass" @click="resetToDefault">
|
||||
{{ t('maskEditor.resetToDefault') }}
|
||||
</button>
|
||||
|
||||
@@ -99,6 +96,9 @@ import SliderControl from './controls/SliderControl.vue'
|
||||
|
||||
const store = useMaskEditorStore()
|
||||
|
||||
const textButtonClass =
|
||||
'h-7.5 w-32 rounded-[10px] border border-[var(--p-form-field-border-color)] text-[var(--input-text)] font-sans pointer-events-auto transition-colors duration-100 bg-[var(--comfy-menu-bg)] hover:bg-secondary-background-hover'
|
||||
|
||||
const setBrushShape = (shape: BrushShape) => {
|
||||
store.brushSettings.type = shape
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, withDefaults } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayActive from '@/components/queue/QueueOverlayActive.vue'
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
@click.stop="handleNodes2ToggleClick"
|
||||
>
|
||||
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
|
||||
<Tag severity="info" class="ml-2 text-xs">{{ $t('g.beta') }}</Tag>
|
||||
<ToggleSwitch
|
||||
v-model="nodes2Enabled"
|
||||
class="ml-4"
|
||||
@@ -101,6 +102,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Tag from 'primevue/tag'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-for="(button, index) in actionBarButtonStore.buttons"
|
||||
:key="index"
|
||||
v-tooltip.bottom="button.tooltip"
|
||||
:label="button.label"
|
||||
:label="isMobile ? undefined : button.label"
|
||||
:aria-label="button.tooltip || button.label"
|
||||
:class="button.class"
|
||||
text
|
||||
@@ -21,9 +21,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
|
||||
|
||||
const actionBarButtonStore = useActionBarButtonStore()
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('sm')
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="showVueNodesBanner"
|
||||
class="pointer-events-auto relative w-full h-10 bg-gradient-to-r from-blue-600 to-blue-700 flex items-center justify-center px-4"
|
||||
>
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="flex items-center text-sm text-white">
|
||||
<i class="icon-[lucide--rocket]"></i>
|
||||
<span class="pl-2">{{ $t('vueNodesBanner.title') }}</span>
|
||||
<span class="pl-1.5 hidden md:inline">{{
|
||||
@@ -17,7 +17,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
class="cursor-pointer bg-transparent border-0 outline-0 grid place-items-center absolute right-4"
|
||||
class="cursor-pointer bg-transparent border-0 outline-0 grid place-items-center absolute right-4 text-white"
|
||||
unstyled
|
||||
@click="handleDismiss"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="workflow-tabs-container flex h-full max-w-full flex-auto flex-row overflow-hidden border-b border-[var(--interface-stroke)] shadow-interface"
|
||||
class="workflow-tabs-container flex h-full max-w-full flex-auto flex-row overflow-hidden"
|
||||
:class="{ 'workflow-tabs-container-desktop': isDesktop }"
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import tgpu from 'typegpu'
|
||||
import { tgpu } from 'typegpu'
|
||||
import * as d from 'typegpu/data'
|
||||
import { BrushUniforms } from './gpuSchema'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { Brush, Point } from '@/extensions/core/maskeditor/types'
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
import { useCoordinateTransform } from './useCoordinateTransform'
|
||||
import { resampleSegment } from './splineUtils'
|
||||
import TGPU from 'typegpu'
|
||||
import { tgpu } from 'typegpu'
|
||||
import { GPUBrushRenderer } from './gpu/GPUBrushRenderer'
|
||||
import { StrokeProcessor } from './StrokeProcessor'
|
||||
import { getEffectiveBrushSize, getEffectiveHardness } from './brushUtils'
|
||||
@@ -276,7 +276,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
}
|
||||
|
||||
try {
|
||||
const root = await TGPU.init()
|
||||
const root = await tgpu.init()
|
||||
store.tgpuRoot = root
|
||||
device = root.device
|
||||
console.warn('✅ TypeGPU initialized! Root:', root)
|
||||
|
||||
@@ -49,6 +49,21 @@ const calculateRunwayDurationPrice = (node: LGraphNode): string => {
|
||||
return `$${cost}/Run`
|
||||
}
|
||||
|
||||
const makeOmniProDurationCalculator =
|
||||
(pricePerSecond: number): PricingFunction =>
|
||||
(node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
if (!durationWidget) return `$${pricePerSecond.toFixed(3)}/second`
|
||||
|
||||
const seconds = parseFloat(String(durationWidget.value))
|
||||
if (!Number.isFinite(seconds)) return `$${pricePerSecond.toFixed(3)}/second`
|
||||
|
||||
const cost = pricePerSecond * seconds
|
||||
return `$${cost.toFixed(2)}/Run`
|
||||
}
|
||||
|
||||
const pixversePricingCalculator = (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration_seconds'
|
||||
@@ -131,6 +146,11 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
||||
'720p': [0.51, 0.56],
|
||||
'1080p': [1.18, 1.22]
|
||||
},
|
||||
'seedance-1-0-pro-fast': {
|
||||
'480p': [0.09, 0.1],
|
||||
'720p': [0.21, 0.23],
|
||||
'1080p': [0.47, 0.49]
|
||||
},
|
||||
'seedance-1-0-lite': {
|
||||
'480p': [0.17, 0.18],
|
||||
'720p': [0.37, 0.41],
|
||||
@@ -138,11 +158,13 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const modelKey = model.includes('seedance-1-0-pro')
|
||||
? 'seedance-1-0-pro'
|
||||
: model.includes('seedance-1-0-lite')
|
||||
? 'seedance-1-0-lite'
|
||||
: ''
|
||||
const modelKey = model.includes('seedance-1-0-pro-fast')
|
||||
? 'seedance-1-0-pro-fast'
|
||||
: model.includes('seedance-1-0-pro')
|
||||
? 'seedance-1-0-pro'
|
||||
: model.includes('seedance-1-0-lite')
|
||||
? 'seedance-1-0-lite'
|
||||
: ''
|
||||
|
||||
const resKey = resolution.includes('1080')
|
||||
? '1080p'
|
||||
@@ -623,7 +645,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
|
||||
// Same pricing matrix as KlingTextToVideoNode
|
||||
if (modeValue.includes('v2-1')) {
|
||||
if (modeValue.includes('v2-5-turbo')) {
|
||||
if (modeValue.includes('10')) {
|
||||
return '$0.70/Run'
|
||||
}
|
||||
return '$0.35/Run' // 5s default
|
||||
} else if (modeValue.includes('v2-1')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$0.98/Run' // pro, 10s
|
||||
}
|
||||
@@ -699,6 +726,21 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
KlingVirtualTryOnNode: {
|
||||
displayPrice: '$0.07/Run'
|
||||
},
|
||||
KlingOmniProTextToVideoNode: {
|
||||
displayPrice: makeOmniProDurationCalculator(0.112)
|
||||
},
|
||||
KlingOmniProFirstLastFrameNode: {
|
||||
displayPrice: makeOmniProDurationCalculator(0.112)
|
||||
},
|
||||
KlingOmniProImageToVideoNode: {
|
||||
displayPrice: makeOmniProDurationCalculator(0.112)
|
||||
},
|
||||
KlingOmniProVideoToVideoNode: {
|
||||
displayPrice: makeOmniProDurationCalculator(0.168)
|
||||
},
|
||||
KlingOmniProEditVideoNode: {
|
||||
displayPrice: '$0.168/second'
|
||||
},
|
||||
LumaImageToVideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
// Same pricing as LumaVideoNode per CSV
|
||||
@@ -1873,6 +1915,10 @@ export const useNodePricing = () => {
|
||||
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
|
||||
KlingSingleImageVideoEffectNode: ['effect_scene'],
|
||||
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
|
||||
KlingOmniProTextToVideoNode: ['duration'],
|
||||
KlingOmniProFirstLastFrameNode: ['duration'],
|
||||
KlingOmniProImageToVideoNode: ['duration'],
|
||||
KlingOmniProVideoToVideoNode: ['duration'],
|
||||
MinimaxHailuoVideoNode: ['resolution', 'duration'],
|
||||
OpenAIDalle3: ['size', 'quality'],
|
||||
OpenAIDalle2: ['size', 'n'],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||
import { st } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
@@ -263,7 +264,8 @@ export function useJobList() {
|
||||
totalPercent: isActive ? totalPercent.value : undefined,
|
||||
currentNodePercent: isActive ? currentNodePercent.value : undefined,
|
||||
currentNodeName: isActive ? currentNodeName.value : undefined,
|
||||
showAddedHint
|
||||
showAddedHint,
|
||||
isCloud
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,11 +9,20 @@ export enum ServerFeatureFlag {
|
||||
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size',
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
|
||||
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled'
|
||||
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
|
||||
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive access to server-side feature flags
|
||||
* Provides reactive accessors for server-side feature flags.
|
||||
*
|
||||
* Exposes a readonly `flags` object containing convenience getters for known server feature keys
|
||||
* and a `featureFlag` helper that returns a computed value for an arbitrary feature path,
|
||||
* optionally using a supplied default when the feature is not present.
|
||||
*
|
||||
* @returns An object with:
|
||||
* - `flags`: a readonly reactive object with predefined getters for common server feature flags
|
||||
* - `featureFlag`: a generic function `(featurePath: string, defaultValue?) => ComputedRef<T>` that yields a computed feature value
|
||||
*/
|
||||
export function useFeatureFlags() {
|
||||
const flags = reactive({
|
||||
@@ -31,6 +40,12 @@ export function useFeatureFlags() {
|
||||
ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED,
|
||||
false
|
||||
)
|
||||
},
|
||||
get assetUpdateOptionsEnabled() {
|
||||
return api.getServerFeature(
|
||||
ServerFeatureFlag.ASSET_UPDATE_OPTIONS_ENABLED,
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
LLink,
|
||||
Point
|
||||
LLink
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
@@ -37,15 +37,15 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
override applyToGraph(extraLinks: LLink[] = []) {
|
||||
if (!this.outputs[0].links?.length) return
|
||||
if (!this.outputs[0].links?.length || !this.graph) return
|
||||
|
||||
const links = [
|
||||
...this.outputs[0].links.map((l) => app.graph.links[l]),
|
||||
...this.outputs[0].links.map((l) => this.graph!.links[l]),
|
||||
...extraLinks
|
||||
]
|
||||
let v = this.widgets?.[0].value
|
||||
if (v && this.properties[replacePropertyName]) {
|
||||
v = applyTextReplacements(app.graph, v as string)
|
||||
v = applyTextReplacements(this.graph, v as string)
|
||||
}
|
||||
|
||||
// For each output link copy our value over the original widget value
|
||||
@@ -331,13 +331,13 @@ export class PrimitiveNode extends LGraphNode {
|
||||
const config1 = (output.widget?.[GET_CONFIG] as () => InputSpec)?.()
|
||||
if (!config1) return
|
||||
const isNumber = config1[0] === 'INT' || config1[0] === 'FLOAT'
|
||||
if (!isNumber) return
|
||||
if (!isNumber || !this.graph) return
|
||||
|
||||
for (const linkId of links) {
|
||||
const link = app.graph.links[linkId]
|
||||
const link = this.graph.links[linkId]
|
||||
if (!link) continue // Can be null when removing a node
|
||||
|
||||
const theirNode = app.graph.getNodeById(link.target_id)
|
||||
const theirNode = this.graph.getNodeById(link.target_id)
|
||||
if (!theirNode) continue
|
||||
const theirInput = theirNode.inputs[link.target_slot]
|
||||
|
||||
@@ -441,10 +441,7 @@ function getWidgetType(config: InputSpec) {
|
||||
return { type }
|
||||
}
|
||||
|
||||
export function setWidgetConfig(
|
||||
slot: INodeInputSlot | INodeOutputSlot,
|
||||
config?: InputSpec
|
||||
) {
|
||||
export function setWidgetConfig(slot: INodeInputSlot, config?: InputSpec) {
|
||||
if (!slot.widget) return
|
||||
if (config) {
|
||||
slot.widget[GET_CONFIG] = () => config
|
||||
@@ -452,19 +449,18 @@ export function setWidgetConfig(
|
||||
delete slot.widget
|
||||
}
|
||||
|
||||
if ('link' in slot) {
|
||||
const link = app.graph.links[slot.link ?? -1]
|
||||
if (link) {
|
||||
const originNode = app.graph.getNodeById(link.origin_id)
|
||||
if (originNode && isPrimitiveNode(originNode)) {
|
||||
if (config) {
|
||||
originNode.recreateWidget()
|
||||
} else if (!app.configuringGraph) {
|
||||
originNode.disconnectOutput(0)
|
||||
originNode.onLastDisconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!(slot instanceof NodeSlot)) return
|
||||
const graph = slot.node.graph
|
||||
if (!graph) return
|
||||
const link = graph.links[slot.link ?? -1]
|
||||
if (!link) return
|
||||
const originNode = graph.getNodeById(link.origin_id)
|
||||
if (!originNode || !isPrimitiveNode(originNode)) return
|
||||
if (config) {
|
||||
originNode.recreateWidget()
|
||||
} else if (!app.configuringGraph) {
|
||||
originNode.disconnectOutput(0)
|
||||
originNode.onLastDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,15 +551,6 @@ app.registerExtension({
|
||||
}
|
||||
)
|
||||
|
||||
function isNodeAtPos(pos: Point) {
|
||||
for (const n of app.graph.nodes) {
|
||||
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Double click a widget input to automatically attach a primitive
|
||||
const origOnInputDblClick = nodeType.prototype.onInputDblClick
|
||||
nodeType.prototype.onInputDblClick = function (
|
||||
@@ -589,18 +576,18 @@ app.registerExtension({
|
||||
|
||||
// Create a primitive node
|
||||
const node = LiteGraph.createNode('PrimitiveNode')
|
||||
if (!node) return r
|
||||
const graph = app.canvas.graph
|
||||
if (!node || !graph) return r
|
||||
|
||||
this.graph?.add(node)
|
||||
graph?.add(node)
|
||||
|
||||
// Calculate a position that won't directly overlap another node
|
||||
const pos: [number, number] = [
|
||||
this.pos[0] - node.size[0] - 30,
|
||||
this.pos[1]
|
||||
]
|
||||
while (isNodeAtPos(pos)) {
|
||||
while (graph.getNodeOnPos(pos[0], pos[1], graph.nodes))
|
||||
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
|
||||
node.pos = pos
|
||||
node.connect(0, this, slot)
|
||||
|
||||
13
src/i18n.ts
@@ -36,7 +36,8 @@ const localeLoaders: Record<
|
||||
ru: () => import('./locales/ru/main.json'),
|
||||
tr: () => import('./locales/tr/main.json'),
|
||||
zh: () => import('./locales/zh/main.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/main.json')
|
||||
'zh-TW': () => import('./locales/zh-TW/main.json'),
|
||||
'pt-BR': () => import('./locales/pt-BR/main.json')
|
||||
}
|
||||
|
||||
const nodeDefsLoaders: Record<
|
||||
@@ -51,7 +52,8 @@ const nodeDefsLoaders: Record<
|
||||
ru: () => import('./locales/ru/nodeDefs.json'),
|
||||
tr: () => import('./locales/tr/nodeDefs.json'),
|
||||
zh: () => import('./locales/zh/nodeDefs.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/nodeDefs.json')
|
||||
'zh-TW': () => import('./locales/zh-TW/nodeDefs.json'),
|
||||
'pt-BR': () => import('./locales/pt-BR/nodeDefs.json')
|
||||
}
|
||||
|
||||
const commandsLoaders: Record<
|
||||
@@ -66,7 +68,8 @@ const commandsLoaders: Record<
|
||||
ru: () => import('./locales/ru/commands.json'),
|
||||
tr: () => import('./locales/tr/commands.json'),
|
||||
zh: () => import('./locales/zh/commands.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/commands.json')
|
||||
'zh-TW': () => import('./locales/zh-TW/commands.json'),
|
||||
'pt-BR': () => import('./locales/pt-BR/commands.json')
|
||||
}
|
||||
|
||||
const settingsLoaders: Record<
|
||||
@@ -81,7 +84,8 @@ const settingsLoaders: Record<
|
||||
ru: () => import('./locales/ru/settings.json'),
|
||||
tr: () => import('./locales/tr/settings.json'),
|
||||
zh: () => import('./locales/zh/settings.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/settings.json')
|
||||
'zh-TW': () => import('./locales/zh-TW/settings.json'),
|
||||
'pt-BR': () => import('./locales/pt-BR/settings.json')
|
||||
}
|
||||
|
||||
// Track which locales have been loaded
|
||||
@@ -159,6 +163,7 @@ export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: navigator.language.split('-')[0] || 'en',
|
||||
fallbackLocale: 'en',
|
||||
escapeParameter: true,
|
||||
messages,
|
||||
// Ignore warnings for locale options as each option is in its own language.
|
||||
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
min-width: 100px;
|
||||
color: #aaf;
|
||||
padding: 0;
|
||||
box-shadow: 0 0 10px black !important;
|
||||
background-color: #2e2e2e !important;
|
||||
box-shadow: 0 0 10px black;
|
||||
background-color: #2e2e2e;
|
||||
z-index: 10;
|
||||
max-height: -webkit-fill-available;
|
||||
overflow-y: auto;
|
||||
@@ -36,10 +36,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu.dark {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu .litemenu-title img {
|
||||
margin-top: 2px;
|
||||
margin-left: 2px;
|
||||
@@ -51,14 +47,6 @@
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu .litemenu-entry.submenu {
|
||||
background-color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
.litegraph .litemenubar ul {
|
||||
font-family: Tahoma, sans-serif;
|
||||
margin: 0;
|
||||
@@ -132,14 +120,13 @@
|
||||
|
||||
.litegraph .litemenu-entry.separator {
|
||||
display: block;
|
||||
border-top: 1px solid #333;
|
||||
border-bottom: 1px solid #666;
|
||||
border-top: 1px solid var(--border-default);
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
margin: 3px 0 2px 0;
|
||||
background-color: transparent;
|
||||
padding: 0 !important;
|
||||
cursor: default !important;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.has_submenu {
|
||||
@@ -155,8 +142,8 @@
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) {
|
||||
background-color: #444 !important;
|
||||
color: #eee;
|
||||
background-color: var(--palette-interface-panel-hover-surface);
|
||||
color: var(--content-hover-fg);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@@ -259,7 +246,8 @@
|
||||
margin-top: -150px;
|
||||
margin-left: -200px;
|
||||
|
||||
background-color: #2a2a2a;
|
||||
color: var(--base-foreground);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
|
||||
min-width: 400px;
|
||||
min-height: 200px;
|
||||
@@ -299,8 +287,7 @@
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-header {
|
||||
color: #aaa;
|
||||
border-bottom: 1px solid #161616;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-header {
|
||||
@@ -310,11 +297,12 @@
|
||||
height: 50px;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-header .dialog-title {
|
||||
font: 20px "Arial";
|
||||
font: 1rem;
|
||||
font-family: Inter, Arial, sans-serif;
|
||||
margin: 4px;
|
||||
padding: 4px 10px;
|
||||
display: inline-block;
|
||||
@@ -326,7 +314,7 @@
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
display: inline-block;
|
||||
color: #aaa;
|
||||
/* color: #aaa; */
|
||||
/*background-color: black;*/
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -362,8 +350,7 @@
|
||||
display: block;
|
||||
width: calc(100% - 4px);
|
||||
height: 1px;
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #333;
|
||||
border-top: 1px solid var(--border-default);
|
||||
margin: 10px 2px;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -373,12 +360,8 @@
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property:hover {
|
||||
background: #545454;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property_name {
|
||||
color: #737373;
|
||||
color: var(--muted-foreground);
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
@@ -395,8 +378,8 @@
|
||||
.litegraph .dialog .property_value {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: #aaa;
|
||||
background-color: #1a1a1a;
|
||||
color: var(--input-text);
|
||||
background-color: var(--component-node-widget-background);
|
||||
/*width: calc( 100% - 122px );*/
|
||||
max-width: calc(100% - 162px);
|
||||
min-width: 200px;
|
||||
@@ -432,18 +415,18 @@
|
||||
border-radius: 4px;
|
||||
padding: 4px 20px;
|
||||
margin-left: 0px;
|
||||
background-color: #060606;
|
||||
color: #8e8e8e;
|
||||
background-color: var(--secondary-background);
|
||||
color: var(--base-foreground);
|
||||
}
|
||||
|
||||
.litegraph .dialog .btn:hover {
|
||||
background-color: #111;
|
||||
color: #fff;
|
||||
background-color: var(--secondary-background-hover);
|
||||
color: var(--base-foreground);
|
||||
}
|
||||
|
||||
.litegraph .dialog .btn.delete:hover {
|
||||
background-color: #f33;
|
||||
color: black;
|
||||
background-color: var(--color-danger-100);
|
||||
color: var(--base-foreground);
|
||||
}
|
||||
|
||||
.litegraph .bullet_icon {
|
||||
@@ -497,11 +480,11 @@
|
||||
|
||||
.graphmenu-entry.danger,
|
||||
.litemenu-entry.danger {
|
||||
color: var(--error-text) !important;
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.danger:hover:not(.disabled) {
|
||||
color: var(--error-text) !important;
|
||||
color: var(--error-text);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -518,8 +501,7 @@
|
||||
}
|
||||
|
||||
.graphmenu-entry.separator {
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid #666;
|
||||
background-color: var(--border-default);
|
||||
height: 1px;
|
||||
width: calc(100% - 20px);
|
||||
-moz-width: calc(100% - 20px);
|
||||
@@ -551,7 +533,7 @@
|
||||
min-height: 2em;
|
||||
background-color: #333;
|
||||
font-size: 1.2em;
|
||||
box-shadow: 0 0 10px black !important;
|
||||
box-shadow: 0 0 10px black;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties'
|
||||
import {
|
||||
calculateInputSlotPos,
|
||||
calculateInputSlotPosFromSlot,
|
||||
calculateOutputSlotPos
|
||||
calculateOutputSlotPos,
|
||||
getSlotPosition
|
||||
} from '@/renderer/core/canvas/litegraph/slotCalculations'
|
||||
import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
@@ -3354,6 +3355,16 @@ export class LGraphNode
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slot position using layout tree if available, fallback to node's position * Unified implementation used by both LitegraphLinkAdapter and useLinkLayoutSync
|
||||
* @param slotIndex The slot index
|
||||
* @param isInput Whether this is an input slot
|
||||
* @returns Position of the slot center in graph coordinates
|
||||
*/
|
||||
getSlotPosition(slotIndex: number, isInput: boolean): Point {
|
||||
return getSlotPosition(this, slotIndex, isInput)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
snapToGrid(snapTo: number): boolean {
|
||||
return this.pinned ? false : snapPoint(this.pos, snapTo)
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"searchKeybindings": "Search Keybindings",
|
||||
"searchExtensions": "Search Extensions",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Search...",
|
||||
"noResultsFound": "No Results Found",
|
||||
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
"noTasksFound": "No Tasks Found",
|
||||
@@ -989,6 +990,7 @@
|
||||
"initializingAlmostReady": "Initializing - Almost ready",
|
||||
"inQueue": "In queue...",
|
||||
"jobAddedToQueue": "Job added to queue",
|
||||
"completedIn": "Finished in {duration}",
|
||||
"jobMenu": {
|
||||
"openAsWorkflowNewTab": "Open as workflow in new tab",
|
||||
"openWorkflowNewTab": "Open workflow in new tab",
|
||||
@@ -2091,7 +2093,6 @@
|
||||
"connectionError": "Please check your connection and try again",
|
||||
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
|
||||
"noModelsInFolder": "No {type} available in this folder",
|
||||
"searchAssetsPlaceholder": "Type to search...",
|
||||
"uploadModel": "Import model",
|
||||
"uploadModelFromCivitai": "Import a model from Civitai",
|
||||
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
|
||||
@@ -2150,6 +2151,13 @@
|
||||
"media": {
|
||||
"threeDModelPlaceholder": "3D Model",
|
||||
"audioPlaceholder": "Audio"
|
||||
},
|
||||
"deletion": {
|
||||
"header": "Delete this model?",
|
||||
"body": "This model will be permanently removed from your library.",
|
||||
"inProgress": "Deleting {assetName}...",
|
||||
"complete": "{assetName} has been deleted.",
|
||||
"failed": "{assetName} could not be deleted."
|
||||
}
|
||||
},
|
||||
"mediaAsset": {
|
||||
|
||||
1
src/locales/pt-BR/commands.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
src/locales/pt-BR/main.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
src/locales/pt-BR/nodeDefs.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
src/locales/pt-BR/settings.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="absolute right-2 bottom-2 flex flex-wrap justify-end gap-1">
|
||||
<div class="absolute left-2 bottom-2 flex flex-wrap justify-start gap-1">
|
||||
<span
|
||||
v-for="badge in badges"
|
||||
:key="badge.label"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
v-model="searchQuery"
|
||||
:autofocus="true"
|
||||
size="lg"
|
||||
:placeholder="$t('assetBrowser.searchAssetsPlaceholder')"
|
||||
:placeholder="$t('g.searchPlaceholder')"
|
||||
class="max-w-96"
|
||||
/>
|
||||
<IconTextButton
|
||||
|
||||
@@ -1,53 +1,106 @@
|
||||
<template>
|
||||
<component
|
||||
:is="interactive ? 'button' : 'div'"
|
||||
<div
|
||||
v-if="!deletedLocal"
|
||||
data-component-id="AssetCard"
|
||||
:data-asset-id="asset.id"
|
||||
v-bind="elementProps"
|
||||
:class="cardClasses"
|
||||
@click="interactive && $emit('select', asset)"
|
||||
@keydown.enter="interactive && $emit('select', asset)"
|
||||
:aria-labelledby="titleId"
|
||||
:aria-describedby="descId"
|
||||
:tabindex="interactive ? 0 : -1"
|
||||
:class="
|
||||
cn(
|
||||
'rounded-2xl overflow-hidden transition-all duration-200 bg-modal-card-background p-2 gap-2 flex flex-col h-full',
|
||||
interactive &&
|
||||
'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4'
|
||||
)
|
||||
"
|
||||
@keydown.enter.self="interactive && $emit('select', asset)"
|
||||
>
|
||||
<div class="relative aspect-square w-full overflow-hidden rounded-xl">
|
||||
<img
|
||||
v-if="shouldShowImage"
|
||||
:src="asset.preview_url"
|
||||
class="h-full w-full object-contain"
|
||||
/>
|
||||
<div
|
||||
v-if="isLoading || error"
|
||||
class="flex size-full cursor-pointer items-center justify-center bg-gradient-to-br from-smoke-400 via-smoke-800 to-charcoal-400"
|
||||
role="button"
|
||||
@click.self="interactive && $emit('select', asset)"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-smoke-400 via-smoke-800 to-charcoal-400"
|
||||
></div>
|
||||
:src="asset.preview_url"
|
||||
:alt="displayName"
|
||||
class="size-full object-contain cursor-pointer"
|
||||
role="button"
|
||||
@click.self="interactive && $emit('select', asset)"
|
||||
/>
|
||||
|
||||
<AssetBadgeGroup :badges="asset.badges" />
|
||||
<IconGroup
|
||||
v-if="flags.assetUpdateOptionsEnabled"
|
||||
:class="
|
||||
cn(
|
||||
'absolute top-2 right-2 invisible group-hover:visible',
|
||||
dropdownMenuButton?.isOpen && 'visible'
|
||||
)
|
||||
"
|
||||
>
|
||||
<IconButton v-if="false" size="sm">
|
||||
<i class="icon-[lucide--file-text]" />
|
||||
</IconButton>
|
||||
<MoreButton ref="dropdown-menu-button" size="sm">
|
||||
<template #default>
|
||||
<IconTextButton
|
||||
:label="$t('g.rename')"
|
||||
type="secondary"
|
||||
size="md"
|
||||
@click="startAssetRename"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--pencil]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
:label="$t('g.delete')"
|
||||
type="secondary"
|
||||
size="md"
|
||||
@click="confirmDeletion"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</template>
|
||||
</MoreButton>
|
||||
</IconGroup>
|
||||
</div>
|
||||
<div :class="cn('p-4 h-32 flex flex-col justify-between')">
|
||||
<div>
|
||||
<h3
|
||||
:id="titleId"
|
||||
v-tooltip.top="{ value: asset.name, showDelay: tooltipDelay }"
|
||||
:class="
|
||||
cn(
|
||||
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
|
||||
'text-base-foreground'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ asset.name }}
|
||||
</h3>
|
||||
<p
|
||||
:id="descId"
|
||||
v-tooltip.top="{ value: asset.description, showDelay: tooltipDelay }"
|
||||
:class="
|
||||
cn(
|
||||
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
|
||||
'text-muted-foreground'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ asset.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div :class="cn('flex gap-4 text-xs text-muted-foreground')">
|
||||
<div class="max-h-32 flex flex-col gap-2 justify-between flex-auto">
|
||||
<h3
|
||||
:id="titleId"
|
||||
v-tooltip.top="{ value: displayName, showDelay: tooltipDelay }"
|
||||
:class="
|
||||
cn(
|
||||
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
|
||||
'text-base-foreground'
|
||||
)
|
||||
"
|
||||
>
|
||||
<EditableText
|
||||
:model-value="displayName"
|
||||
:is-editing="isEditing"
|
||||
:input-attrs="{ 'data-testid': 'asset-name-input' }"
|
||||
@edit="assetRename"
|
||||
@cancel="assetRename()"
|
||||
/>
|
||||
</h3>
|
||||
<p
|
||||
:id="descId"
|
||||
v-tooltip.top="{ value: asset.description, showDelay: tooltipDelay }"
|
||||
:class="
|
||||
cn(
|
||||
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box] text-muted-foreground'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ asset.description }}
|
||||
</p>
|
||||
<div class="flex gap-4 text-xs text-muted-foreground mt-auto">
|
||||
<span v-if="asset.stats.stars" class="flex items-center gap-1">
|
||||
<i class="icon-[lucide--star] size-3" />
|
||||
{{ asset.stats.stars }}
|
||||
@@ -62,73 +115,133 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed, useId } from 'vue'
|
||||
import { computed, ref, toValue, useId, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconGroup from '@/components/button/IconGroup.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import MoreButton from '@/components/button/MoreButton.vue'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
const { asset, interactive } = defineProps<{
|
||||
asset: AssetDisplayItem
|
||||
interactive?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
select: [asset: AssetDisplayItem]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const { closeDialog } = useDialogStore()
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
const dropdownMenuButton = useTemplateRef<InstanceType<typeof MoreButton>>(
|
||||
'dropdown-menu-button'
|
||||
)
|
||||
|
||||
const titleId = useId()
|
||||
const descId = useId()
|
||||
|
||||
const isEditing = ref(false)
|
||||
const newNameRef = ref<string>()
|
||||
const deletedLocal = ref(false)
|
||||
|
||||
const displayName = computed(() => newNameRef.value ?? asset.name)
|
||||
|
||||
const tooltipDelay = computed<number>(() =>
|
||||
settingStore.get('LiteGraph.Node.TooltipDelay')
|
||||
)
|
||||
|
||||
const { error } = useImage({
|
||||
src: props.asset.preview_url ?? '',
|
||||
alt: props.asset.name
|
||||
const { isLoading, error } = useImage({
|
||||
src: asset.preview_url ?? '',
|
||||
alt: asset.name
|
||||
})
|
||||
|
||||
const shouldShowImage = computed(() => props.asset.preview_url && !error.value)
|
||||
function confirmDeletion() {
|
||||
dropdownMenuButton.value?.hide()
|
||||
const assetName = toValue(displayName)
|
||||
const promptText = ref<string>(t('assetBrowser.deletion.body'))
|
||||
const optionsDisabled = ref(false)
|
||||
const confirmDialog = showConfirmDialog({
|
||||
headerProps: {
|
||||
title: t('assetBrowser.deletion.header')
|
||||
},
|
||||
props: {
|
||||
promptText
|
||||
},
|
||||
footerProps: {
|
||||
confirmText: t('g.delete'),
|
||||
// TODO: These need to be put into the new Button Variants once we have them.
|
||||
confirmClass: cn(
|
||||
'bg-danger-200 text-base-foreground hover:bg-danger-200/80 focus:bg-danger-200/80 focus:ring ring-base-foreground'
|
||||
),
|
||||
optionsDisabled,
|
||||
onCancel: () => {
|
||||
closeDialog(confirmDialog)
|
||||
},
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
promptText.value = t('assetBrowser.deletion.inProgress', {
|
||||
assetName
|
||||
})
|
||||
await assetService.deleteAsset(asset.id)
|
||||
promptText.value = t('assetBrowser.deletion.complete', {
|
||||
assetName
|
||||
})
|
||||
// Give a second for the completion message
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||
deletedLocal.value = true
|
||||
} catch (err: unknown) {
|
||||
console.error(err)
|
||||
promptText.value = t('assetBrowser.deletion.failed', {
|
||||
assetName
|
||||
})
|
||||
// Give a second for the completion message
|
||||
await new Promise((resolve) => setTimeout(resolve, 3_000))
|
||||
} finally {
|
||||
closeDialog(confirmDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const cardClasses = computed(() => {
|
||||
const base = cn(
|
||||
'rounded-xl overflow-hidden transition-all duration-200 bg-modal-card-background'
|
||||
)
|
||||
function startAssetRename() {
|
||||
dropdownMenuButton.value?.hide()
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
if (!props.interactive) {
|
||||
return base
|
||||
async function assetRename(newName?: string) {
|
||||
isEditing.value = false
|
||||
if (newName) {
|
||||
// Optimistic update
|
||||
newNameRef.value = newName
|
||||
try {
|
||||
const result = await assetService.updateAsset(asset.id, {
|
||||
name: newName
|
||||
})
|
||||
// Update with the actual name once the server responds
|
||||
newNameRef.value = result.name
|
||||
} catch (err: unknown) {
|
||||
console.error(err)
|
||||
newNameRef.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
return cn(
|
||||
base,
|
||||
'group',
|
||||
'appearance-none bg-transparent p-0 m-0',
|
||||
'font-inherit text-inherit outline-none cursor-pointer text-left',
|
||||
'hover:bg-secondary-background',
|
||||
'border-none',
|
||||
'focus:outline-solid outline-azure-600 outline-4'
|
||||
)
|
||||
})
|
||||
|
||||
const elementProps = computed(() =>
|
||||
props.interactive
|
||||
? {
|
||||
type: 'button',
|
||||
'aria-labelledby': titleId,
|
||||
'aria-describedby': descId
|
||||
}
|
||||
: {
|
||||
'aria-labelledby': titleId,
|
||||
'aria-describedby': descId
|
||||
}
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
select: [asset: AssetDisplayItem]
|
||||
}>()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
data-component-id="AssetGrid"
|
||||
:style="gridStyle"
|
||||
:class="
|
||||
cn('grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-4 p-2')
|
||||
"
|
||||
role="grid"
|
||||
:aria-label="$t('assetBrowser.assetCollection')"
|
||||
:aria-rowcount="-1"
|
||||
@@ -34,7 +36,6 @@
|
||||
:key="asset.id"
|
||||
:asset="asset"
|
||||
:interactive="true"
|
||||
role="gridcell"
|
||||
@select="$emit('assetSelect', $event)"
|
||||
/>
|
||||
</template>
|
||||
@@ -42,11 +43,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import AssetCard from '@/platform/assets/components/AssetCard.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { createGridStyle } from '@/utils/gridUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
defineProps<{
|
||||
assets: AssetDisplayItem[]
|
||||
@@ -56,7 +55,4 @@ defineProps<{
|
||||
defineEmits<{
|
||||
assetSelect: [asset: AssetDisplayItem]
|
||||
}>()
|
||||
|
||||
// Use same grid style as BaseModalLayout
|
||||
const gridStyle = computed(() => createGridStyle())
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6 border-t-[1px] border-border-default"
|
||||
class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6 border-t border-border-default"
|
||||
>
|
||||
<!-- Step 1: Enter URL -->
|
||||
<UploadModelUrlInput
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { assetResponseSchema } from '@/platform/assets/schemas/assetSchema'
|
||||
import {
|
||||
assetItemSchema,
|
||||
assetResponseSchema
|
||||
} from '@/platform/assets/schemas/assetSchema'
|
||||
import type {
|
||||
AssetItem,
|
||||
AssetMetadata,
|
||||
@@ -262,12 +265,10 @@ function createAssetService() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an asset by ID
|
||||
* Only available in cloud environment
|
||||
* Delete the asset identified by `id` (cloud environments only).
|
||||
*
|
||||
* @param id - The asset ID (UUID)
|
||||
* @returns Promise<void>
|
||||
* @throws Error if deletion fails
|
||||
* @throws Error if the server responds with a non-ok status; message includes the HTTP status
|
||||
*/
|
||||
async function deleteAsset(id: string): Promise<void> {
|
||||
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
|
||||
@@ -281,6 +282,44 @@ function createAssetService() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an asset's metadata by its ID.
|
||||
*
|
||||
* Only available in cloud environment.
|
||||
*
|
||||
* @param id - The asset ID (UUID)
|
||||
* @param newData - Partial metadata fields to apply to the asset
|
||||
* @returns The updated AssetItem
|
||||
* @throws Error if the server responds with a non-OK status or the response cannot be validated as an AssetItem
|
||||
*/
|
||||
async function updateAsset(
|
||||
id: string,
|
||||
newData: Partial<AssetMetadata>
|
||||
): Promise<AssetItem> {
|
||||
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newData)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Unable to update asset ${id}: Server returned ${res.status}`
|
||||
)
|
||||
}
|
||||
|
||||
const newAsset = assetItemSchema.safeParse(await res.json())
|
||||
if (newAsset.success) {
|
||||
return newAsset.data
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to update asset ${id}: Invalid response - ${newAsset.error}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata from a download URL without downloading the file
|
||||
*
|
||||
@@ -360,6 +399,7 @@ function createAssetService() {
|
||||
getAssetDetails,
|
||||
getAssetsByTag,
|
||||
deleteAsset,
|
||||
updateAsset,
|
||||
getAssetMetadata,
|
||||
uploadAssetFromUrl
|
||||
}
|
||||
|
||||
@@ -416,7 +416,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' },
|
||||
{ value: 'ar', text: 'عربي' },
|
||||
{ value: 'tr', text: 'Türkçe' }
|
||||
{ value: 'tr', text: 'Türkçe' },
|
||||
{ value: 'pt-BR', text: 'Português (BR)' }
|
||||
],
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
@@ -1116,5 +1117,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
tooltip: 'Use new Asset API for model browsing',
|
||||
defaultValue: isCloud ? true : false,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.VersionCompatibility.DisableWarnings',
|
||||
name: 'Disable version compatibility warnings',
|
||||
type: 'hidden',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.34.1'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ import { gt, valid } from 'semver'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import config from '@/config'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
const DISMISSAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
@@ -12,6 +13,7 @@ export const useVersionCompatibilityStore = defineStore(
|
||||
'versionCompatibility',
|
||||
() => {
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const frontendVersion = computed(() => config.app_version)
|
||||
const backendVersion = computed(
|
||||
@@ -87,7 +89,10 @@ export const useVersionCompatibilityStore = defineStore(
|
||||
})
|
||||
|
||||
const shouldShowWarning = computed(() => {
|
||||
return hasVersionMismatch.value && !isDismissed.value
|
||||
const warningsDisabled = settingStore.get(
|
||||
'Comfy.VersionCompatibility.DisableWarnings'
|
||||
)
|
||||
return hasVersionMismatch.value && !isDismissed.value && !warningsDisabled
|
||||
})
|
||||
|
||||
const warningMessage = computed(() => {
|
||||
|
||||
@@ -138,6 +138,8 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
|
||||
// Vue dragging state for selection toolbox (public ref for direct mutation)
|
||||
public isDraggingVueNodes = ref(false)
|
||||
// Vue resizing state to prevent drag from activating during resize
|
||||
public isResizingVueNodes = ref(false)
|
||||
|
||||
constructor() {
|
||||
// Initialize Yjs data structures
|
||||
|
||||
@@ -117,17 +117,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Resize handles -->
|
||||
<template v-if="!isCollapsed">
|
||||
<div
|
||||
v-for="handle in cornerResizeHandles"
|
||||
:key="handle.id"
|
||||
role="button"
|
||||
:aria-label="handle.ariaLabel"
|
||||
:class="cn(baseResizeHandleClasses, handle.classes)"
|
||||
@pointerdown.stop="handleResizePointerDown(handle.direction)($event)"
|
||||
/>
|
||||
</template>
|
||||
<!-- Resize handle (bottom-right only) -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
role="button"
|
||||
:aria-label="t('g.resizeFromBottomRight')"
|
||||
:class="cn(baseResizeHandleClasses, 'right-0 bottom-0 cursor-se-resize')"
|
||||
@pointerdown.stop="handleResizePointerDown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -171,7 +168,6 @@ import {
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type { ResizeHandleDirection } from '../interactions/resize/resizeMath'
|
||||
import { useNodeResize } from '../interactions/resize/useNodeResize'
|
||||
import LivePreview from './LivePreview.vue'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
@@ -263,7 +259,7 @@ onErrorCaptured((error) => {
|
||||
return false // Prevent error propagation
|
||||
})
|
||||
|
||||
const { position, size, zIndex, moveNodeTo } = useNodeLayout(() => nodeData.id)
|
||||
const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
|
||||
const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)
|
||||
const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
|
||||
const { startDrag } = useNodeDrag()
|
||||
@@ -314,41 +310,6 @@ onMounted(() => {
|
||||
|
||||
const baseResizeHandleClasses =
|
||||
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
|
||||
const POSITION_EPSILON = 0.01
|
||||
|
||||
type CornerResizeHandle = {
|
||||
id: string
|
||||
direction: ResizeHandleDirection
|
||||
classes: string
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
const cornerResizeHandles: CornerResizeHandle[] = [
|
||||
{
|
||||
id: 'se',
|
||||
direction: { horizontal: 'right', vertical: 'bottom' },
|
||||
classes: 'right-0 bottom-0 cursor-se-resize',
|
||||
ariaLabel: t('g.resizeFromBottomRight')
|
||||
},
|
||||
{
|
||||
id: 'ne',
|
||||
direction: { horizontal: 'right', vertical: 'top' },
|
||||
classes: 'right-0 top-0 cursor-ne-resize',
|
||||
ariaLabel: t('g.resizeFromTopRight')
|
||||
},
|
||||
{
|
||||
id: 'sw',
|
||||
direction: { horizontal: 'left', vertical: 'bottom' },
|
||||
classes: 'left-0 bottom-0 cursor-sw-resize',
|
||||
ariaLabel: t('g.resizeFromBottomLeft')
|
||||
},
|
||||
{
|
||||
id: 'nw',
|
||||
direction: { horizontal: 'left', vertical: 'top' },
|
||||
classes: 'left-0 top-0 cursor-nw-resize',
|
||||
ariaLabel: t('g.resizeFromTopLeft')
|
||||
}
|
||||
]
|
||||
|
||||
const MIN_NODE_WIDTH = 225
|
||||
|
||||
@@ -361,22 +322,11 @@ const { startResize } = useNodeResize((result, element) => {
|
||||
// Apply size directly to DOM element - ResizeObserver will pick this up
|
||||
element.style.setProperty('--node-width', `${clampedWidth}px`)
|
||||
element.style.setProperty('--node-height', `${result.size.height}px`)
|
||||
|
||||
const currentPosition = position.value
|
||||
const deltaX = Math.abs(result.position.x - currentPosition.x)
|
||||
const deltaY = Math.abs(result.position.y - currentPosition.y)
|
||||
|
||||
if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) {
|
||||
moveNodeTo(result.position)
|
||||
}
|
||||
})
|
||||
|
||||
const handleResizePointerDown = (direction: ResizeHandleDirection) => {
|
||||
return (event: PointerEvent) => {
|
||||
if (nodeData.flags?.pinned) return
|
||||
|
||||
startResize(event, direction, { ...position.value })
|
||||
}
|
||||
const handleResizePointerDown = (event: PointerEvent) => {
|
||||
if (nodeData.flags?.pinned) return
|
||||
startResize(event)
|
||||
}
|
||||
|
||||
watch(isCollapsed, (collapsed) => {
|
||||
|
||||
@@ -92,12 +92,14 @@ const mockData = vi.hoisted(() => {
|
||||
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => {
|
||||
const isDraggingVueNodes = ref(false)
|
||||
const isResizingVueNodes = ref(false)
|
||||
const fakeNodeLayoutRef = ref(mockData.fakeNodeLayout)
|
||||
const getNodeLayoutRef = vi.fn(() => fakeNodeLayoutRef)
|
||||
const setSource = vi.fn()
|
||||
return {
|
||||
layoutStore: {
|
||||
isDraggingVueNodes,
|
||||
isResizingVueNodes,
|
||||
getNodeLayoutRef,
|
||||
setSource
|
||||
}
|
||||
|
||||
@@ -63,6 +63,9 @@ export function useNodePointerInteractions(
|
||||
function onPointermove(event: PointerEvent) {
|
||||
if (forwardMiddlePointerIfNeeded(event)) return
|
||||
|
||||
// Don't activate drag while resizing
|
||||
if (layoutStore.isResizingVueNodes.value) return
|
||||
|
||||
const nodeId = toValue(nodeIdRef)
|
||||
|
||||
if (nodeManager.value?.getNode(nodeId)?.flags?.pinned) {
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import type { Point, Size } from '@/renderer/core/layout/types'
|
||||
|
||||
export type ResizeHandleDirection = {
|
||||
horizontal: 'left' | 'right'
|
||||
vertical: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
function applyHandleDelta(
|
||||
startSize: Size,
|
||||
delta: Point,
|
||||
handle: ResizeHandleDirection
|
||||
): Size {
|
||||
const horizontalMultiplier = handle.horizontal === 'right' ? 1 : -1
|
||||
const verticalMultiplier = handle.vertical === 'bottom' ? 1 : -1
|
||||
|
||||
return {
|
||||
width: startSize.width + delta.x * horizontalMultiplier,
|
||||
height: startSize.height + delta.y * verticalMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
function computeAdjustedPosition(
|
||||
startPosition: Point,
|
||||
startSize: Size,
|
||||
nextSize: Size,
|
||||
handle: ResizeHandleDirection
|
||||
): Point {
|
||||
const widthDelta = startSize.width - nextSize.width
|
||||
const heightDelta = startSize.height - nextSize.height
|
||||
|
||||
return {
|
||||
x:
|
||||
handle.horizontal === 'left'
|
||||
? startPosition.x + widthDelta
|
||||
: startPosition.x,
|
||||
y:
|
||||
handle.vertical === 'top'
|
||||
? startPosition.y + heightDelta
|
||||
: startPosition.y
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the resulting size and position of a node given pointer movement
|
||||
* and handle orientation.
|
||||
*/
|
||||
export function computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta,
|
||||
handle,
|
||||
snapFn
|
||||
}: {
|
||||
startSize: Size
|
||||
startPosition: Point
|
||||
delta: Point
|
||||
handle: ResizeHandleDirection
|
||||
snapFn?: (size: Size) => Size
|
||||
}): { size: Size; position: Point } {
|
||||
const resized = applyHandleDelta(startSize, delta, handle)
|
||||
const snapped = snapFn?.(resized) ?? resized
|
||||
const position = computeAdjustedPosition(
|
||||
startPosition,
|
||||
startSize,
|
||||
snapped,
|
||||
handle
|
||||
)
|
||||
|
||||
return {
|
||||
size: snapped,
|
||||
position
|
||||
}
|
||||
}
|
||||
|
||||
export function createResizeSession(config: {
|
||||
startSize: Size
|
||||
startPosition: Point
|
||||
handle: ResizeHandleDirection
|
||||
}) {
|
||||
const startSize = { ...config.startSize }
|
||||
const startPosition = { ...config.startPosition }
|
||||
const handle = config.handle
|
||||
|
||||
return (delta: Point, snapFn?: (size: Size) => Size) =>
|
||||
computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
handle,
|
||||
delta,
|
||||
snapFn
|
||||
})
|
||||
}
|
||||
|
||||
export function toCanvasDelta(
|
||||
startPointer: Point,
|
||||
currentPointer: Point,
|
||||
scale: number
|
||||
): Point {
|
||||
const safeScale = scale === 0 ? 1 : scale
|
||||
return {
|
||||
x: (currentPointer.x - startPointer.x) / safeScale,
|
||||
y: (currentPointer.y - startPointer.y) / safeScale
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,17 @@ import { useEventListener } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { Point, Size } from '@/renderer/core/layout/types'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'
|
||||
import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'
|
||||
|
||||
import type { ResizeHandleDirection } from './resizeMath'
|
||||
import { createResizeSession, toCanvasDelta } from './resizeMath'
|
||||
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
|
||||
interface ResizeCallbackPayload {
|
||||
size: Size
|
||||
position: Point
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for node resizing functionality
|
||||
* Composable for node resizing functionality (bottom-right corner only)
|
||||
*
|
||||
* Provides resize handle interaction that integrates with the layout system.
|
||||
* Handles pointer capture, coordinate calculations, and size constraints.
|
||||
@@ -27,16 +24,7 @@ export function useNodeResize(
|
||||
|
||||
const isResizing = ref(false)
|
||||
const resizeStartPointer = ref<Point | null>(null)
|
||||
const resizeSession = ref<
|
||||
| ((
|
||||
delta: Point,
|
||||
snapFn?: (size: Size) => Size
|
||||
) => {
|
||||
size: Size
|
||||
position: Point
|
||||
})
|
||||
| null
|
||||
>(null)
|
||||
const resizeStartSize = ref<Size | null>(null)
|
||||
|
||||
// Snap-to-grid functionality
|
||||
const { shouldSnap, applySnapToSize } = useNodeSnap()
|
||||
@@ -44,11 +32,7 @@ export function useNodeResize(
|
||||
// Shift key sync for LiteGraph canvas preview
|
||||
const { trackShiftKey } = useShiftKeySync()
|
||||
|
||||
const startResize = (
|
||||
event: PointerEvent,
|
||||
handle: ResizeHandleDirection,
|
||||
startPosition: Point
|
||||
) => {
|
||||
const startResize = (event: PointerEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -72,47 +56,49 @@ export function useNodeResize(
|
||||
// Capture pointer to ensure we get all move/up events
|
||||
target.setPointerCapture(event.pointerId)
|
||||
|
||||
// Mark as resizing to prevent drag from activating
|
||||
layoutStore.isResizingVueNodes.value = true
|
||||
isResizing.value = true
|
||||
resizeStartPointer.value = { x: event.clientX, y: event.clientY }
|
||||
resizeSession.value = createResizeSession({
|
||||
startSize,
|
||||
startPosition: { ...startPosition },
|
||||
handle
|
||||
})
|
||||
resizeStartSize.value = startSize
|
||||
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
if (
|
||||
!isResizing.value ||
|
||||
!resizeStartPointer.value ||
|
||||
!resizeSession.value
|
||||
)
|
||||
!resizeStartSize.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const startPointer = resizeStartPointer.value
|
||||
const session = resizeSession.value
|
||||
const scale = transformState.camera.z
|
||||
const deltaX =
|
||||
(moveEvent.clientX - resizeStartPointer.value.x) / (scale || 1)
|
||||
const deltaY =
|
||||
(moveEvent.clientY - resizeStartPointer.value.y) / (scale || 1)
|
||||
|
||||
const delta = toCanvasDelta(
|
||||
startPointer,
|
||||
{ x: moveEvent.clientX, y: moveEvent.clientY },
|
||||
transformState.camera.z
|
||||
)
|
||||
let newSize: Size = {
|
||||
width: resizeStartSize.value.width + deltaX,
|
||||
height: resizeStartSize.value.height + deltaY
|
||||
}
|
||||
|
||||
// Apply snap if shift is held
|
||||
if (shouldSnap(moveEvent)) {
|
||||
newSize = applySnapToSize(newSize)
|
||||
}
|
||||
|
||||
const nodeElement = target.closest('[data-node-id]')
|
||||
if (nodeElement instanceof HTMLElement) {
|
||||
const outcome = session(
|
||||
delta,
|
||||
shouldSnap(moveEvent) ? applySnapToSize : undefined
|
||||
)
|
||||
|
||||
resizeCallback(outcome, nodeElement)
|
||||
resizeCallback({ size: newSize }, nodeElement)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerUp = (upEvent: PointerEvent) => {
|
||||
if (isResizing.value) {
|
||||
isResizing.value = false
|
||||
layoutStore.isResizingVueNodes.value = false
|
||||
resizeStartPointer.value = null
|
||||
resizeSession.value = null
|
||||
resizeStartSize.value = null
|
||||
|
||||
// Stop tracking shift key state
|
||||
stopShiftSync()
|
||||
|
||||
@@ -81,6 +81,8 @@ const buttonTooltip = computed(() => {
|
||||
size="small"
|
||||
variant="outlined"
|
||||
:step="stepValue"
|
||||
:min-fraction-digits="precision"
|
||||
:max-fraction-digits="precision"
|
||||
:use-grouping="useGrouping"
|
||||
:class="cn(WidgetInputBaseClass, 'grow text-xs')"
|
||||
:aria-label="widget.name"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<Select
|
||||
v-model="modelValue"
|
||||
:invalid
|
||||
:filter="selectOptions.length > 4"
|
||||
:options="selectOptions"
|
||||
v-bind="combinedProps"
|
||||
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
|
||||
|
||||
@@ -213,12 +213,13 @@ const acceptTypes = computed(() => {
|
||||
const layoutMode = ref<LayoutMode>(props.defaultLayoutMode ?? 'grid')
|
||||
|
||||
watch(
|
||||
modelValue,
|
||||
(currentValue) => {
|
||||
[modelValue, dropdownItems],
|
||||
([currentValue, _dropdownItems]) => {
|
||||
if (currentValue === undefined) {
|
||||
selectedSet.value.clear()
|
||||
return
|
||||
}
|
||||
|
||||
const item = dropdownItems.value.find((item) => item.name === currentValue)
|
||||
if (item) {
|
||||
selectedSet.value.clear()
|
||||
|
||||
@@ -523,7 +523,8 @@ const zSettings = z.object({
|
||||
'main.sub.setting.name': z.any(),
|
||||
'single.setting': z.any(),
|
||||
'LiteGraph.Node.DefaultPadding': z.boolean(),
|
||||
'LiteGraph.Pointer.TrackpadGestures': z.boolean()
|
||||
'LiteGraph.Pointer.TrackpadGestures': z.boolean(),
|
||||
'Comfy.VersionCompatibility.DisableWarnings': z.boolean()
|
||||
})
|
||||
|
||||
export type EmbeddingsResponse = z.infer<typeof zEmbeddingsResponse>
|
||||
|
||||
@@ -34,7 +34,7 @@ import NodeConflictDialogContent from '@/workbench/extensions/manager/components
|
||||
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
|
||||
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -47,8 +47,13 @@ export type ConfirmationDialogType =
|
||||
export const useDialogService = () => {
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
/**
|
||||
* Open the global missing-nodes dialog and forward the provided props to its content component.
|
||||
*
|
||||
* @param props - Props passed through to the MissingNodesContent component
|
||||
*/
|
||||
function showLoadWorkflowWarning(
|
||||
props: ComponentProps<typeof MissingNodesContent>
|
||||
props: ComponentAttrs<typeof MissingNodesContent>
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-missing-nodes',
|
||||
@@ -73,8 +78,13 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the global missing-models warning dialog.
|
||||
*
|
||||
* @param props - Props forwarded to the MissingModelsWarning component
|
||||
*/
|
||||
function showMissingModelsWarning(
|
||||
props: InstanceType<typeof MissingModelsWarning>['$props']
|
||||
props: ComponentAttrs<typeof MissingModelsWarning>
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-missing-models-warning',
|
||||
@@ -103,6 +113,11 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the global settings dialog with the About panel selected.
|
||||
*
|
||||
* Displays the settings dialog and sets its default inner panel to "about".
|
||||
*/
|
||||
function showAboutDialog() {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
@@ -114,8 +129,15 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the global execution error dialog populated from a websocket execution error message.
|
||||
*
|
||||
* Displays a dialog containing the error details from `executionError` and records a telemetry event when the dialog is closed.
|
||||
*
|
||||
* @param executionError - Websocket execution error message containing `exception_type`, `exception_message`, `node_id`, `node_type`, and `traceback`
|
||||
*/
|
||||
function showExecutionErrorDialog(executionError: ExecutionErrorWsMessage) {
|
||||
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
|
||||
const props: ComponentAttrs<typeof ErrorDialogContent> = {
|
||||
error: {
|
||||
exceptionType: executionError.exception_type,
|
||||
exceptionMessage: executionError.exception_message,
|
||||
@@ -140,8 +162,13 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the global manager dialog using the default manager layout and styling and forwards props to the dialog content.
|
||||
*
|
||||
* @param props - Props to pass through to ManagerDialogContent (defaults to an empty object)
|
||||
*/
|
||||
function showManagerDialog(
|
||||
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
|
||||
props: ComponentAttrs<typeof ManagerDialogContent> = {}
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-manager',
|
||||
@@ -184,9 +211,12 @@ export const useDialogService = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a error dialog to the user when an error occurs.
|
||||
* @param error The error to show
|
||||
* @param options The options for the dialog
|
||||
* Displays a global error dialog for the given error and tracks the dialog close event for telemetry.
|
||||
*
|
||||
* @param error - An Error or any value to display; if an Error is provided it will be parsed for message, stack trace, and extension file.
|
||||
* @param options - Optional configuration for the dialog
|
||||
* @param options.title - Title used as the exception type shown in the dialog
|
||||
* @param options.reportType - Optional report type forwarded to the dialog for reporting purposes
|
||||
*/
|
||||
function showErrorDialog(
|
||||
error: unknown,
|
||||
@@ -206,7 +236,7 @@ export const useDialogService = () => {
|
||||
errorMessage: String(error)
|
||||
}
|
||||
|
||||
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
|
||||
const props: ComponentAttrs<typeof ErrorDialogContent> = {
|
||||
error: {
|
||||
exceptionType: options.title ?? 'Unknown Error',
|
||||
exceptionMessage: errorProps.errorMessage,
|
||||
@@ -412,15 +442,10 @@ export const useDialogService = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog from a third party extension.
|
||||
* @param options - The dialog options.
|
||||
* @param options.key - The dialog key.
|
||||
* @param options.title - The dialog title.
|
||||
* @param options.headerComponent - The dialog header component.
|
||||
* @param options.footerComponent - The dialog footer component.
|
||||
* @param options.component - The dialog component.
|
||||
* @param options.props - The dialog props.
|
||||
* @returns The dialog instance and a function to close the dialog.
|
||||
* Show a dialog provided by a third-party extension.
|
||||
*
|
||||
* @param options - Dialog configuration including `key`, optional `title`, header/footer components, dialog `component`, and `props` passed to the component.
|
||||
* @returns An object with `dialog`, the dialog instance returned by the dialog store, and `closeDialog`, a function that closes the dialog using the provided `key`.
|
||||
*/
|
||||
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
|
||||
return {
|
||||
@@ -429,8 +454,15 @@ export const useDialogService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the global manager dialog's visibility.
|
||||
*
|
||||
* If the global manager dialog is open, it will be closed; otherwise it will be shown.
|
||||
*
|
||||
* @param props - Optional props to pass to the ManagerDialogContent when opening the dialog
|
||||
*/
|
||||
function toggleManagerDialog(
|
||||
props?: InstanceType<typeof ManagerDialogContent>['$props']
|
||||
props?: ComponentAttrs<typeof ManagerDialogContent>
|
||||
) {
|
||||
if (dialogStore.isDialogOpen('global-manager')) {
|
||||
dialogStore.closeDialog({ key: 'global-manager' })
|
||||
@@ -439,8 +471,13 @@ export const useDialogService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the global manager progress dialog: closes it if open, otherwise opens it.
|
||||
*
|
||||
* @param props - Optional props to pass to the ManagerProgressDialogContent when opening the dialog
|
||||
*/
|
||||
function toggleManagerProgressDialog(
|
||||
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
|
||||
props?: ComponentAttrs<typeof ManagerProgressDialogContent>
|
||||
) {
|
||||
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
|
||||
@@ -7,6 +7,10 @@ class NodeHelpService {
|
||||
async fetchNodeHelp(node: ComfyNodeDefImpl, locale: string): Promise<string> {
|
||||
const nodeSource = getNodeSource(node.python_module)
|
||||
|
||||
if (nodeSource.type === NodeSourceType.Blueprint) {
|
||||
return node.description || ''
|
||||
}
|
||||
|
||||
if (nodeSource.type === NodeSourceType.CustomNodes) {
|
||||
return this.fetchCustomNodeHelp(node, locale)
|
||||
} else {
|
||||
@@ -19,25 +23,24 @@ class NodeHelpService {
|
||||
locale: string
|
||||
): Promise<string> {
|
||||
const customNodeName = extractCustomNodeName(node.python_module)
|
||||
let lastError: string | undefined
|
||||
if (!customNodeName) {
|
||||
throw new Error('Invalid custom node module')
|
||||
}
|
||||
|
||||
// Try locale-specific path first
|
||||
const localePath = `/extensions/${customNodeName}/docs/${node.name}/${locale}.md`
|
||||
let res = await fetch(api.fileURL(localePath))
|
||||
const localeDoc = await this.tryFetchMarkdown(localePath)
|
||||
if (localeDoc.text) return localeDoc.text
|
||||
lastError = localeDoc.errorText
|
||||
|
||||
if (!res.ok) {
|
||||
// Fall back to non-locale path
|
||||
const fallbackPath = `/extensions/${customNodeName}/docs/${node.name}.md`
|
||||
res = await fetch(api.fileURL(fallbackPath))
|
||||
}
|
||||
// Fall back to non-locale path
|
||||
const fallbackPath = `/extensions/${customNodeName}/docs/${node.name}.md`
|
||||
const fallbackDoc = await this.tryFetchMarkdown(fallbackPath)
|
||||
if (fallbackDoc.text) return fallbackDoc.text
|
||||
lastError = fallbackDoc.errorText ?? lastError
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText)
|
||||
}
|
||||
|
||||
return res.text()
|
||||
throw new Error(lastError ?? 'Help not found')
|
||||
}
|
||||
|
||||
private async fetchCoreNodeHelp(
|
||||
@@ -45,13 +48,35 @@ class NodeHelpService {
|
||||
locale: string
|
||||
): Promise<string> {
|
||||
const mdUrl = `/docs/${node.name}/${locale}.md`
|
||||
const res = await fetch(api.fileURL(mdUrl))
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText)
|
||||
const doc = await this.tryFetchMarkdown(mdUrl)
|
||||
if (!doc.text) {
|
||||
throw new Error(doc.errorText ?? 'Help not found')
|
||||
}
|
||||
|
||||
return res.text()
|
||||
return doc.text
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a markdown file and return its text, guarding against HTML/SPA fallbacks.
|
||||
* Returns null when not OK or when the content type indicates HTML.
|
||||
*/
|
||||
private async tryFetchMarkdown(
|
||||
path: string
|
||||
): Promise<{ text: string | null; errorText?: string }> {
|
||||
const res = await fetch(api.fileURL(path))
|
||||
|
||||
if (!res.ok) {
|
||||
return { text: null, errorText: res.statusText }
|
||||
}
|
||||
|
||||
const contentType = res.headers?.get?.('content-type') ?? ''
|
||||
const text = await res.text()
|
||||
|
||||
const isHtmlContentType = contentType.includes('text/html')
|
||||
|
||||
if (isHtmlContentType) return { text: null, errorText: res.statusText }
|
||||
|
||||
return { text }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
type DialogPosition =
|
||||
| 'center'
|
||||
@@ -33,30 +34,40 @@ interface CustomDialogComponentProps {
|
||||
headless?: boolean
|
||||
}
|
||||
|
||||
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
||||
export type DialogComponentProps = ComponentAttrs<typeof GlobalDialog> &
|
||||
CustomDialogComponentProps
|
||||
|
||||
interface DialogInstance {
|
||||
interface DialogInstance<
|
||||
H extends Component = Component,
|
||||
B extends Component = Component,
|
||||
F extends Component = Component
|
||||
> {
|
||||
key: string
|
||||
visible: boolean
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
footerComponent?: Component
|
||||
footerProps?: Record<string, any>
|
||||
headerComponent?: H
|
||||
headerProps?: ComponentAttrs<H>
|
||||
component: B
|
||||
contentProps: ComponentAttrs<B>
|
||||
footerComponent?: F
|
||||
footerProps?: ComponentAttrs<F>
|
||||
dialogComponentProps: DialogComponentProps
|
||||
priority: number
|
||||
}
|
||||
|
||||
export interface ShowDialogOptions {
|
||||
export interface ShowDialogOptions<
|
||||
H extends Component = Component,
|
||||
B extends Component = Component,
|
||||
F extends Component = Component
|
||||
> {
|
||||
key?: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
headerComponent?: H
|
||||
footerComponent?: F
|
||||
component: B
|
||||
props?: ComponentAttrs<B>
|
||||
headerProps?: ComponentAttrs<H>
|
||||
footerProps?: ComponentAttrs<F>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
/**
|
||||
* Optional priority for dialog stacking.
|
||||
@@ -105,6 +116,13 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog identified by the given key or the currently active dialog when no key is provided.
|
||||
*
|
||||
* Invokes the dialog's `onClose` callback if present, removes the dialog from the stack, updates the active dialog key, and adjusts close-on-Escape handling. If no matching dialog is found this function is a no-op.
|
||||
*
|
||||
* @param options - Optional object with a `key` specifying which dialog to close; when omitted the active dialog is closed.
|
||||
*/
|
||||
function closeDialog(options?: { key: string }) {
|
||||
const targetDialog = options
|
||||
? dialogStack.value.find((d) => d.key === options.key)
|
||||
@@ -123,17 +141,19 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
updateCloseOnEscapeStates()
|
||||
}
|
||||
|
||||
function createDialog(options: {
|
||||
key: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
priority?: number
|
||||
}) {
|
||||
/**
|
||||
* Create and register a dialog instance from the given options and push it into the dialog stack.
|
||||
*
|
||||
* @param options - Configuration for the dialog. Must include a unique `key`. Other fields configure the component to render (`component`), optional `title`, optional `headerComponent`/`footerComponent` and their props, additional `props` for the content component, `dialogComponentProps` for dialog behavior, and an optional numeric `priority`.
|
||||
* @returns The created dialog instance that was inserted into the store's stack.
|
||||
*
|
||||
* Side effects: enforces a maximum stack size of 10 by removing the oldest dialog when necessary, inserts the new dialog according to its priority, sets the dialog as the active one, and updates close-on-escape handling for the stack.
|
||||
*/
|
||||
function createDialog<
|
||||
H extends Component = Component,
|
||||
B extends Component = Component,
|
||||
F extends Component = Component
|
||||
>(options: ShowDialogOptions<H, B, F> & { key: string }) {
|
||||
if (dialogStack.value.length >= 10) {
|
||||
dialogStack.value.shift()
|
||||
}
|
||||
@@ -149,6 +169,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
? markRaw(options.footerComponent)
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
headerProps: { ...options.headerProps },
|
||||
contentProps: { ...options.props },
|
||||
footerProps: { ...options.footerProps },
|
||||
priority: options.priority ?? 1,
|
||||
@@ -203,7 +224,17 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showDialog(options: ShowDialogOptions) {
|
||||
/**
|
||||
* Opens the dialog described by `options` and ensures it is the active (top-most) dialog, creating a new dialog if one with the same key does not exist.
|
||||
*
|
||||
* @param options - Configuration for the dialog to show; may include a `key` to target an existing dialog or omit it to generate a new key
|
||||
* @returns The dialog instance that was shown or created
|
||||
*/
|
||||
function showDialog<
|
||||
H extends Component = Component,
|
||||
B extends Component = Component,
|
||||
F extends Component = Component
|
||||
>(options: ShowDialogOptions<H, B, F>) {
|
||||
const dialogKey = options.key || genDialogKey()
|
||||
|
||||
let dialog = dialogStack.value.find((d) => d.key === dialogKey)
|
||||
|
||||
@@ -25,8 +25,7 @@ export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
|
||||
|
||||
export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||
const baseByType = {
|
||||
primary:
|
||||
'bg-neutral-900 border-none text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
||||
primary: 'bg-base-foreground border-none text-base-background',
|
||||
secondary: cn(
|
||||
'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover'
|
||||
),
|
||||
@@ -42,10 +41,8 @@ export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||
|
||||
export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||
const baseByType = {
|
||||
primary:
|
||||
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
||||
secondary:
|
||||
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
|
||||
primary: 'bg-base-background text-base-foreground',
|
||||
secondary: 'bg-secondary-background text-base-foreground',
|
||||
transparent: cn(
|
||||
'bg-transparent text-base-foreground hover:bg-secondary-background-hover'
|
||||
),
|
||||
@@ -54,10 +51,9 @@ export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||
} as const
|
||||
|
||||
const borderByType = {
|
||||
primary: 'border border-solid border-white dark-theme:border-neutral-900',
|
||||
secondary: 'border border-solid border-neutral-950 dark-theme:border-white',
|
||||
transparent:
|
||||
'border border-solid border-neutral-950 dark-theme:border-white',
|
||||
primary: 'border border-solid border-base-background',
|
||||
secondary: 'border border-solid border-base-foreground',
|
||||
transparent: 'border border-solid border-base-foreground',
|
||||
accent: 'border border-solid border-primary-background'
|
||||
} as const
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ interface GridOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Just use tailwind utilities directly.
|
||||
* TODO: Create a common grid layout component if needed.
|
||||
* Creates CSS grid styles for responsive grid layouts
|
||||
* @param options Grid configuration options
|
||||
* @returns CSS properties object for grid styling
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import { formatDuration } from '@/utils/formatUtil'
|
||||
import { clampPercentInt, formatPercent0 } from '@/utils/numberUtil'
|
||||
|
||||
type BuildJobDisplayCtx = {
|
||||
@@ -11,6 +12,8 @@ type BuildJobDisplayCtx = {
|
||||
currentNodePercent?: number
|
||||
currentNodeName?: string
|
||||
showAddedHint?: boolean
|
||||
/** Whether the app is running in cloud distribution */
|
||||
isCloud?: boolean
|
||||
}
|
||||
|
||||
type JobDisplay = {
|
||||
@@ -122,13 +125,20 @@ export const buildJobDisplay = (
|
||||
const time = task.executionTimeInSeconds
|
||||
const preview = task.previewOutput
|
||||
const iconImageUrl = preview && preview.isImage ? preview.url : undefined
|
||||
|
||||
// Cloud shows "Completed in Xh Ym Zs", non-cloud shows filename
|
||||
const primary = ctx.isCloud
|
||||
? ctx.t('queue.completedIn', {
|
||||
duration: formatDuration(task.executionTime ?? 0)
|
||||
})
|
||||
: preview?.filename && preview.filename.length
|
||||
? preview.filename
|
||||
: buildTitle(task, ctx.t)
|
||||
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
iconImageUrl,
|
||||
primary:
|
||||
preview?.filename && preview.filename.length
|
||||
? preview.filename
|
||||
: buildTitle(task, ctx.t),
|
||||
primary,
|
||||
secondary: time !== undefined ? `${time.toFixed(2)}s` : '',
|
||||
showClear: false
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export function extractCustomNodeName(
|
||||
|
||||
export function getNodeHelpBaseUrl(node: ComfyNodeDefImpl): string {
|
||||
const nodeSource = getNodeSource(node.python_module)
|
||||
if (nodeSource.type === NodeSourceType.Blueprint) {
|
||||
return ''
|
||||
}
|
||||
if (nodeSource.type === NodeSourceType.CustomNodes) {
|
||||
const customNodeName = extractCustomNodeName(node.python_module)
|
||||
if (customNodeName) {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
computeResizeOutcome,
|
||||
createResizeSession,
|
||||
toCanvasDelta
|
||||
} from '@/renderer/extensions/vueNodes/interactions/resize/resizeMath'
|
||||
|
||||
describe('nodeResizeMath', () => {
|
||||
const startSize = { width: 200, height: 120 }
|
||||
const startPosition = { x: 80, y: 160 }
|
||||
|
||||
it('computes resize from bottom-right corner without moving position', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: 40, y: 20 },
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
expect(outcome.size).toEqual({ width: 240, height: 140 })
|
||||
expect(outcome.position).toEqual(startPosition)
|
||||
})
|
||||
|
||||
it('computes resize from top-left corner adjusting position', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: -30, y: -20 },
|
||||
handle: { horizontal: 'left', vertical: 'top' }
|
||||
})
|
||||
|
||||
expect(outcome.size).toEqual({ width: 230, height: 140 })
|
||||
expect(outcome.position).toEqual({ x: 50, y: 140 })
|
||||
})
|
||||
|
||||
it('supports reusable resize sessions with snapping', () => {
|
||||
const session = createResizeSession({
|
||||
startSize,
|
||||
startPosition,
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
const snapFn = vi.fn((size: typeof startSize) => ({
|
||||
width: Math.round(size.width / 25) * 25,
|
||||
height: Math.round(size.height / 25) * 25
|
||||
}))
|
||||
|
||||
const applied = session({ x: 13, y: 17 }, snapFn)
|
||||
|
||||
expect(applied.size).toEqual({ width: 225, height: 125 })
|
||||
expect(applied.position).toEqual(startPosition)
|
||||
expect(snapFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('converts screen delta to canvas delta using scale', () => {
|
||||
const delta = toCanvasDelta({ x: 50, y: 75 }, { x: 150, y: 135 }, 2)
|
||||
|
||||
expect(delta).toEqual({ x: 50, y: 30 })
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles zero scale by using fallback scale of 1', () => {
|
||||
const delta = toCanvasDelta({ x: 50, y: 75 }, { x: 150, y: 135 }, 0)
|
||||
|
||||
expect(delta).toEqual({ x: 100, y: 60 })
|
||||
})
|
||||
|
||||
it('handles negative deltas when resizing from right/bottom', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: -50, y: -30 },
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
expect(outcome.size).toEqual({ width: 150, height: 90 })
|
||||
expect(outcome.position).toEqual(startPosition)
|
||||
})
|
||||
|
||||
it('handles very large deltas without overflow', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: 10000, y: 10000 },
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
expect(outcome.size.width).toBe(10200)
|
||||
expect(outcome.size.height).toBe(10120)
|
||||
expect(outcome.position).toEqual(startPosition)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
@@ -13,6 +14,13 @@ vi.mock('@/config', () => ({
|
||||
|
||||
vi.mock('@/stores/systemStatsStore')
|
||||
|
||||
// Mock settingStore
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => false) // Default to warnings enabled (false = not disabled)
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock useStorage and until from VueUse
|
||||
const mockDismissalStorage = ref({} as Record<string, number>)
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
@@ -23,6 +31,7 @@ vi.mock('@vueuse/core', () => ({
|
||||
describe('useVersionCompatibilityStore', () => {
|
||||
let store: ReturnType<typeof useVersionCompatibilityStore>
|
||||
let mockSystemStatsStore: any
|
||||
let mockSettingStore: any
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
@@ -36,7 +45,12 @@ describe('useVersionCompatibilityStore', () => {
|
||||
refetchSystemStats: vi.fn()
|
||||
}
|
||||
|
||||
mockSettingStore = {
|
||||
get: vi.fn(() => false) // Default to warnings enabled
|
||||
}
|
||||
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
|
||||
|
||||
store = useVersionCompatibilityStore()
|
||||
})
|
||||
@@ -196,6 +210,27 @@ describe('useVersionCompatibilityStore', () => {
|
||||
|
||||
expect(store.shouldShowWarning).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show warning when disabled via setting', async () => {
|
||||
// Enable the disable setting
|
||||
mockSettingStore.get.mockReturnValue(true)
|
||||
|
||||
// Set up version mismatch that would normally show warning
|
||||
mockSystemStatsStore.systemStats = {
|
||||
system: {
|
||||
comfyui_version: '1.25.0',
|
||||
required_frontend_version: '1.25.0'
|
||||
}
|
||||
}
|
||||
mockSystemStatsStore.isInitialized = true
|
||||
|
||||
await store.checkVersionCompatibility()
|
||||
|
||||
expect(store.shouldShowWarning).toBe(false)
|
||||
expect(mockSettingStore.get).toHaveBeenCalledWith(
|
||||
'Comfy.VersionCompatibility.DisableWarnings'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('warning messages', () => {
|
||||
|
||||