Compare commits

..

1 Commits

Author SHA1 Message Date
snomiao
8eb0823465 [test] Add test workflow to validate CI/CD pipeline 2025-08-14 13:15:50 +00:00
90 changed files with 988 additions and 5211 deletions

View File

@@ -111,7 +111,50 @@ echo "Last stable release: $LAST_STABLE"
```
7. **HUMAN ANALYSIS**: Review change summary and verify scope
### Step 3: Breaking Change Analysis
### Step 3: Version Preview
**Version Preview:**
- Current: `${CURRENT_VERSION}`
- Proposed: Show exact version number
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
### Step 4: Security and Dependency Audit
1. Run security audit:
```bash
npm audit --audit-level moderate
```
2. Check for known vulnerabilities in dependencies
3. Scan for hardcoded secrets or credentials:
```bash
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
```
4. Verify no sensitive data in recent commits
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
### Step 5: Pre-Release Testing
1. Run complete test suite:
```bash
npm run test:unit
npm run test:component
```
2. Run type checking:
```bash
npm run typecheck
```
3. Run linting (may have issues with missing packages):
```bash
npm run lint || echo "Lint issues - verify if critical"
```
4. Test build process:
```bash
npm run build
npm run build:types
```
5. **QUALITY GATE**: All tests and builds passing?
### Step 6: Breaking Change Analysis
1. Analyze API changes in:
- Public TypeScript interfaces
@@ -126,7 +169,7 @@ echo "Last stable release: $LAST_STABLE"
3. Generate breaking change summary
4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified?
### Step 4: Analyze Dependency Updates
### Step 7: Analyze Dependency Updates
1. **Check significant dependency updates:**
```bash
@@ -152,117 +195,7 @@ echo "Last stable release: $LAST_STABLE"
done
```
### Step 5: Generate GTM Feature Summary
1. **Collect PR data for analysis:**
```bash
# Get list of PR numbers from commits
PR_NUMBERS=$(git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent | \
grep -oE "#[0-9]+" | tr -d '#' | sort -u)
# Save PR data for each PR
echo "[" > prs-${NEW_VERSION}.json
first=true
for PR in $PR_NUMBERS; do
[[ "$first" == true ]] && first=false || echo "," >> prs-${NEW_VERSION}.json
gh pr view $PR --json number,title,author,body,labels 2>/dev/null >> prs-${NEW_VERSION}.json || echo "{}" >> prs-${NEW_VERSION}.json
done
echo "]" >> prs-${NEW_VERSION}.json
```
2. **Analyze for GTM-worthy features:**
```
<task>
Review these PRs to identify features worthy of marketing attention.
A feature is GTM-worthy if it meets ALL of these criteria:
- Introduces a NEW capability users didn't have before (not just improvements)
- Would be a compelling reason for users to upgrade to this version
- Can be demonstrated visually or has clear before/after comparison
- Affects a significant portion of the user base
NOT GTM-worthy:
- Bug fixes (even important ones)
- Minor UI tweaks or color changes
- Performance improvements without user-visible impact
- Internal refactoring
- Small convenience features
- Features that only improve existing functionality marginally
For each GTM-worthy feature, note:
- PR number, title, and author
- Media links from the PR description
- One compelling sentence on why users should care
If there are no GTM-worthy features, just say "No marketing-worthy features in this release."
</task>
PR data: [contents of prs-${NEW_VERSION}.json]
```
3. **Generate GTM notification:**
```bash
# Save to gtm-summary-${NEW_VERSION}.md based on analysis
# If GTM-worthy features exist, include them with testing instructions
# If not, note that this is a maintenance/bug fix release
# Check if notification is needed
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
echo "✅ No GTM notification needed for this release"
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
else
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
echo "📤 Share this file in #gtm channel to notify the team"
fi
```
### Step 6: Version Preview
**Version Preview:**
- Current: `${CURRENT_VERSION}`
- Proposed: Show exact version number based on analysis:
- Major version if breaking changes detected
- Minor version if new features added
- Patch version if only bug fixes
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
### Step 7: Security and Dependency Audit
1. Run security audit:
```bash
npm audit --audit-level moderate
```
2. Check for known vulnerabilities in dependencies
3. Scan for hardcoded secrets or credentials:
```bash
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
```
4. Verify no sensitive data in recent commits
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
### Step 8: Pre-Release Testing
1. Run complete test suite:
```bash
npm run test:unit
npm run test:component
```
2. Run type checking:
```bash
npm run typecheck
```
3. Run linting (may have issues with missing packages):
```bash
npm run lint || echo "Lint issues - verify if critical"
```
4. Test build process:
```bash
npm run build
npm run build:types
```
5. **QUALITY GATE**: All tests and builds passing?
### Step 9: Generate Comprehensive Release Notes
### Step 8: Generate Comprehensive Release Notes
1. Extract commit messages since base release:
```bash
@@ -324,7 +257,7 @@ echo "Last stable release: $LAST_STABLE"
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
5. **CONTENT REVIEW**: Release notes follow standard format?
### Step 10: Create Version Bump PR
### Step 9: Create Version Bump PR
**For standard version bumps (patch/minor/major):**
```bash
@@ -370,7 +303,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
4. **PR REVIEW**: Version bump PR created with standardized release notes?
### Step 11: Critical Release PR Verification
### Step 10: Critical Release PR Verification
1. **CRITICAL**: Verify PR has "Release" label:
```bash
@@ -392,7 +325,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
### Step 12: Pre-Merge Validation
### Step 11: Pre-Merge Validation
1. **Review Requirements**: Release PRs require approval
2. Monitor CI checks - watch for update-locales
@@ -400,7 +333,7 @@ echo "Workflow triggered. Waiting for PR creation..."
4. Check no new commits to main since PR creation
5. **DEPLOYMENT READINESS**: Ready to merge?
### Step 13: Execute Release
### Step 12: Execute Release
1. **FINAL CONFIRMATION**: Merge PR to trigger release?
2. Merge the Release PR:
@@ -433,7 +366,7 @@ echo "Workflow triggered. Waiting for PR creation..."
gh run watch ${WORKFLOW_RUN_ID}
```
### Step 14: Enhance GitHub Release
### Step 13: Enhance GitHub Release
1. Wait for automatic release creation:
```bash
@@ -461,7 +394,7 @@ echo "Workflow triggered. Waiting for PR creation..."
gh release view v${NEW_VERSION}
```
### Step 15: Verify Multi-Channel Distribution
### Step 14: Verify Multi-Channel Distribution
1. **GitHub Release:**
```bash
@@ -499,7 +432,7 @@ echo "Workflow triggered. Waiting for PR creation..."
4. **DISTRIBUTION VERIFICATION**: All channels published successfully?
### Step 16: Post-Release Monitoring Setup
### Step 15: Post-Release Monitoring Setup
1. **Monitor immediate release health:**
```bash
@@ -569,49 +502,11 @@ echo "Workflow triggered. Waiting for PR creation..."
## Files Generated
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
- \`post-release-checklist.md\` - Follow-up tasks
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
EOF
```
4. **RELEASE COMPLETION**: All post-release setup completed?
### Step 17: Create Release Summary
1. **Create comprehensive release summary:**
```bash
cat > release-summary-${NEW_VERSION}.md << EOF
# Release Summary: ComfyUI Frontend v${NEW_VERSION}
**Released:** $(date)
**Type:** ${VERSION_TYPE}
**Duration:** ~${RELEASE_DURATION} minutes
**Release Commit:** ${RELEASE_COMMIT}
## Metrics
- **Commits Included:** ${COMMITS_COUNT}
- **Contributors:** ${CONTRIBUTORS_COUNT}
- **Files Changed:** ${FILES_CHANGED}
- **Lines Added/Removed:** +${LINES_ADDED}/-${LINES_REMOVED}
## Distribution Status
- ✅ GitHub Release: Published
- ✅ PyPI Package: Available
- ✅ npm Types: Available
## Next Steps
- Monitor for 24-48 hours
- Address any critical issues immediately
- Plan next release cycle
## Files Generated
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
- \`post-release-checklist.md\` - Follow-up tasks
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
EOF
```
2. **RELEASE COMPLETION**: All steps completed successfully?
## Advanced Safety Features
### Rollback Procedures

13
.github/workflows/test-cicd.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Test CI/CD Validation
on:
pull_request:
types: [opened, synchronize]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate CI/CD
run: echo "CI/CD pipeline validation successful"

View File

@@ -13,10 +13,6 @@ module.exports = defineConfig({
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.
IMPORTANT Chinese Translation Guidelines:
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
`
});

View File

@@ -50,7 +50,7 @@ export class Topbar {
workflowName: string,
command: 'Save' | 'Save As' | 'Export'
) {
await this.triggerTopbarCommand(['File', command])
await this.triggerTopbarCommand(['Workflow', command])
await this.getSaveDialog().fill(workflowName)
await this.page.keyboard.press('Enter')
@@ -72,8 +72,8 @@ export class Topbar {
}
async triggerTopbarCommand(path: string[]) {
if (path.length < 1) {
throw new Error('Path cannot be empty')
if (path.length < 2) {
throw new Error('Path is too short')
}
const menu = await this.openTopbarMenu()
@@ -85,13 +85,6 @@ export class Topbar {
.locator('.p-tieredmenu-item')
.filter({ has: topLevelMenuItem })
await topLevelMenu.waitFor({ state: 'visible' })
// Handle top-level commands (like "New")
if (path.length === 1) {
await topLevelMenuItem.click()
return
}
await topLevelMenu.hover()
let currentMenu = topLevelMenu

View File

@@ -268,7 +268,10 @@ test.describe('Group Node', () => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
// Clear workflow
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
@@ -277,7 +280,7 @@ test.describe('Group Node', () => {
test('Copies and pastes group node into a newly created blank workflow', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
@@ -293,7 +296,7 @@ test.describe('Group Node', () => {
test('Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
const currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()

View File

@@ -684,7 +684,7 @@ test.describe('Load workflow', () => {
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.menu.topbar.saveWorkflow(workflowB)
// Wait for localStorage to persist the workflow paths before reloading

View File

@@ -73,80 +73,9 @@ test.describe('Menu', () => {
expect(isTextCutoff).toBe(false)
})
test('Clicking on active state items does not close menu', async ({
comfyPage
}) => {
// Open the menu
await comfyPage.menu.topbar.openTopbarMenu()
const menu = comfyPage.page.locator('.comfy-command-menu')
// Navigate to View menu
const viewMenuItem = comfyPage.page.locator(
'.p-menubar-item-label:text-is("View")'
)
await viewMenuItem.hover()
// Wait for submenu to appear
const viewSubmenu = comfyPage.page
.locator('.p-tieredmenu-submenu:visible')
.first()
await viewSubmenu.waitFor({ state: 'visible' })
// Find Bottom Panel menu item
const bottomPanelMenuItem = viewSubmenu
.locator('.p-tieredmenu-item:has-text("Bottom Panel")')
.first()
const bottomPanelItem = bottomPanelMenuItem.locator(
'.p-menubar-item-label:text-is("Bottom Panel")'
)
await bottomPanelItem.waitFor({ state: 'visible' })
// Get checkmark icon element
const checkmark = bottomPanelMenuItem.locator('.pi-check')
// Check initial state of bottom panel (it's initially hidden)
const bottomPanel = comfyPage.page.locator('.bottom-panel')
await expect(bottomPanel).not.toBeVisible()
// Checkmark should be invisible initially (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
// Click Bottom Panel to toggle it on
await bottomPanelItem.click()
// Verify menu is still visible after clicking
await expect(menu).toBeVisible()
await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is now visible
await expect(bottomPanel).toBeVisible()
// Checkmark should now be visible (panel is shown)
await expect(checkmark).not.toHaveClass(/invisible/)
// Click Bottom Panel again to toggle it off
await bottomPanelItem.click()
// Verify menu is still visible after second click
await expect(menu).toBeVisible()
await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is hidden again
await expect(bottomPanel).not.toBeVisible()
// Checkmark should be invisible again (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
// Click outside to close menu
await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } })
// Verify menu is now closed
await expect(menu).not.toBeVisible()
})
test('Displays keybinding next to item', async ({ comfyPage }) => {
await comfyPage.menu.topbar.openTopbarMenu()
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('File')
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('Workflow')
await workflowMenuItem.hover()
const exportTag = comfyPage.page.locator('.keybinding-tag', {
hasText: 'Ctrl + s'

View File

@@ -18,7 +18,7 @@ test.describe('Reroute Node', () => {
[workflowName]: workflowName
})
await comfyPage.setup()
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
// Insert the workflow
const workflowsTab = comfyPage.menu.workflowsTab

View File

@@ -63,7 +63,7 @@ test.describe('Workflow Tab Thumbnails', () => {
test('Should show thumbnail when hovering over a non-active tab', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
const thumbnailImg = await getTabThumbnailImage(
comfyPage,
0,
@@ -73,7 +73,7 @@ test.describe('Workflow Tab Thumbnails', () => {
})
test('Should not show thumbnail for active tab', async ({ comfyPage }) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
const thumbnailImg = await getTabThumbnailImage(
comfyPage,
1,
@@ -105,7 +105,7 @@ test.describe('Workflow Tab Thumbnails', () => {
await comfyPage.nextFrame()
// Create a new workflow (tab 1) which will be empty
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.nextFrame()
// Now we have two tabs: tab 0 (default workflow with nodes) and tab 1 (empty)

572
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.26.4",
"version": "1.26.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.26.4",
"version": "1.26.2",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -975,6 +975,32 @@
"integrity": "sha512-o6WFbYn9yAkGbkOwvhPF7pbKDvN0occZ21Tfyhya8CIsIqKpTHLft0aOqo4yhSh+kTxN16FYjsfrTH5Olk4WuA==",
"license": "GPL-3.0-only"
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
@@ -4691,6 +4717,17 @@
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.0.tgz",
@@ -4726,6 +4763,38 @@
}
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
@@ -5651,6 +5720,15 @@
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"deprecated": "Use your platform's native atob() and btoa() methods instead",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -5720,6 +5798,18 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-globals": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
"integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"acorn": "^8.1.0",
"acorn-walk": "^8.0.2"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -5730,6 +5820,34 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
@@ -5940,6 +6058,14 @@
"node": ">= 8"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -7101,6 +7227,14 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -7171,12 +7305,58 @@
"node": ">=4"
}
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/cssstyle": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
"integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"cssom": "~0.3.6"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cssstyle/node_modules/cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
"integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"abab": "^2.0.6",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -7240,6 +7420,14 @@
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/decode-named-character-reference": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@@ -7395,6 +7583,17 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
@@ -7490,6 +7689,21 @@
}
]
},
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
"integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
"deprecated": "Use your platform's native DOMException instead",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
@@ -7851,6 +8065,29 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/eslint": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz",
@@ -9401,6 +9638,20 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"whatwg-encoding": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -9452,6 +9703,37 @@
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -10071,6 +10353,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@@ -10302,6 +10592,53 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsdom": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
"integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"abab": "^2.0.6",
"acorn": "^8.8.1",
"acorn-globals": "^7.0.0",
"cssom": "^0.5.0",
"cssstyle": "^2.3.0",
"data-urls": "^3.0.2",
"decimal.js": "^10.4.2",
"domexception": "^4.0.0",
"escodegen": "^2.0.0",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.2",
"parse5": "^7.1.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^4.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0",
"ws": "^8.11.0",
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -11314,6 +11651,14 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
@@ -12703,6 +13048,14 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nwsapi": {
"version": "2.2.10",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz",
"integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -13862,6 +14215,14 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -13927,6 +14288,14 @@
}
]
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -14432,6 +14801,14 @@
"node": ">=0.10.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -14639,6 +15016,20 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -15290,6 +15681,14 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/synckit": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.3.tgz",
@@ -15544,6 +15943,37 @@
"node": ">=6"
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"punycode": "^2.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
@@ -15574,6 +16004,51 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -16287,6 +16762,17 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -16528,6 +17014,18 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
@@ -16604,6 +17102,14 @@
"node": ">=6"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -17405,6 +17911,20 @@
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
"integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/walk-up-path": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
@@ -17466,6 +17986,20 @@
"node": ">=0.8.0"
}
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
@@ -17475,6 +18009,21 @@
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/when-exit": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.3.tgz",
@@ -17771,6 +18320,14 @@
"node": ">=12"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -17876,6 +18433,17 @@
"node": ">=8"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.26.4",
"version": "1.26.2",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -20,7 +20,7 @@
>
<div class="shortcut-info flex-grow pr-4">
<div class="shortcut-name text-sm font-medium">
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
{{ command.label || command.id }}
</div>
</div>
@@ -50,7 +50,6 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ComfyCommandImpl } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { t } = useI18n()

View File

@@ -10,8 +10,7 @@
:class="{
'flex items-center gap-1': isActive,
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
'p-breadcrumb-item-link-icon-visible': isActive,
'active-breadcrumb-item': isActive
'p-breadcrumb-item-link-icon-visible': isActive
}"
@click="handleClick"
>
@@ -112,7 +111,21 @@ const menuItems = computed<MenuItem[]>(() => {
{
label: t('g.rename'),
icon: 'pi pi-pencil',
command: startRename
command: async () => {
let initialName =
workflowStore.activeSubgraph?.name ??
workflowStore.activeWorkflow?.filename
if (!initialName) return
const newName = await dialogService.prompt({
title: t('g.rename'),
message: t('breadcrumbsMenu.enterNewName'),
defaultValue: initialName
})
await rename(newName, initialName)
}
},
{
label: t('breadcrumbsMenu.duplicate'),
@@ -162,22 +175,18 @@ const handleClick = (event: MouseEvent) => {
menu.value?.hide()
event.stopPropagation()
event.preventDefault()
startRename()
}
}
const startRename = () => {
isEditing.value = true
itemLabel.value = props.item.label as string
void nextTick(() => {
if (itemInputRef.value?.$el) {
itemInputRef.value.$el.focus()
itemInputRef.value.$el.select()
if (wrapperRef.value) {
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
isEditing.value = true
itemLabel.value = props.item.label as string
void nextTick(() => {
if (itemInputRef.value?.$el) {
itemInputRef.value.$el.focus()
itemInputRef.value.$el.select()
if (wrapperRef.value) {
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
}
}
}
})
})
}
}
const inputBlur = async (doRename: boolean) => {
@@ -203,8 +212,4 @@ const inputBlur = async (doRename: boolean) => {
.p-breadcrumb-item-label {
@apply whitespace-nowrap text-ellipsis overflow-hidden;
}
.active-breadcrumb-item {
color: var(--text-primary);
}
</style>

View File

@@ -1,15 +0,0 @@
<template>
<button
class="flex justify-center items-center outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white w-8 h-8 rounded-lg cursor-pointer"
role="button"
@click="onClick"
>
<slot></slot>
</button>
</template>
<script setup lang="ts">
const { onClick } = defineProps<{
onClick: () => void
}>()
</script>

View File

@@ -1,67 +0,0 @@
<template>
<BaseWidgetLayout>
<template #leftPanel>
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
<template #header-icon>
<i-lucide:puzzle class="text-neutral" />
</template>
<template #header-title>
<span class="text-neutral text-base">{{ t('g.title') }}</span>
</template>
</LeftSidePanel>
</template>
<template #header>
<!-- here -->
</template>
<template #content>
<!-- here -->
</template>
<template #rightPanel>
<RightSidePanel></RightSidePanel>
</template>
</BaseWidgetLayout>
</template>
<script setup lang="ts">
import { provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
import BaseWidgetLayout from './layout/BaseWidgetLayout.vue'
import LeftSidePanel from './panel/LeftSidePanel.vue'
import RightSidePanel from './panel/RightSidePanel.vue'
const { t } = useI18n()
const { onClose } = defineProps<{
onClose: () => void
}>()
provide(OnCloseKey, onClose)
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
{ id: 'installed', label: 'Installed' },
{
title: 'TAGS',
items: [
{ id: 'tag-sd15', label: 'SD 1.5' },
{ id: 'tag-sdxl', label: 'SDXL' },
{ id: 'tag-utility', label: 'Utility' }
]
},
{
title: 'CATEGORIES',
items: [
{ id: 'cat-models', label: 'Models' },
{ id: 'cat-nodes', label: 'Nodes' }
]
}
])
const selectedNavItem = ref<string | null>('installed')
</script>

View File

@@ -1,176 +0,0 @@
<template>
<div
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-100 dark-theme:bg-zinc-800"
>
<IconButton
v-show="!isRightPanelOpen && hasRightPanel"
class="absolute top-4 right-16 z-10 transition-opacity duration-200"
:class="{
'opacity-0 pointer-events-none': isRightPanelOpen || !hasRightPanel
}"
@click="toggleRightPanel"
>
<i-lucide:panel-right class="text-sm" />
</IconButton>
<IconButton
class="absolute top-4 right-6 z-10 transition-opacity duration-200"
@click="closeDialog"
>
<i class="pi pi-times text-sm"></i>
</IconButton>
<div class="flex w-full h-full">
<Transition name="slide-panel">
<nav
v-if="$slots.leftPanel && showLeftPanel"
:class="[
PANEL_SIZES.width,
PANEL_SIZES.minWidth,
PANEL_SIZES.maxWidth
]"
>
<slot name="leftPanel"></slot>
</nav>
</Transition>
<div class="flex-1 flex bg-zinc-100 dark-theme:bg-neutral-900">
<div class="w-full h-full flex flex-col">
<header
v-if="$slots.header"
class="w-full h-16 px-6 py-4 flex justify-between gap-2"
>
<div class="flex-1 flex gap-2 flex-shrink-0">
<IconButton v-if="!notMobile" @click="toggleLeftPanel">
<i-lucide:panel-left v-if="!showLeftPanel" class="text-sm" />
<i-lucide:panel-left-close v-else class="text-sm" />
</IconButton>
<slot name="header"></slot>
</div>
<div class="flex justify-end gap-2 min-w-20">
<slot name="header-right-area"></slot>
<IconButton
v-if="isRightPanelOpen && hasRightPanel"
@click="toggleRightPanel"
>
<i-lucide:panel-right-close class="text-sm" />
</IconButton>
</div>
</header>
<main class="flex-1">
<slot name="content"></slot>
</main>
</div>
<Transition name="slide-panel-right">
<aside
v-if="hasRightPanel && isRightPanelOpen"
class="w-1/4 min-w-40 max-w-80"
>
<slot name="rightPanel"></slot>
</aside>
</Transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useBreakpoints } from '@vueuse/core'
import { computed, inject, ref, useSlots, watch } from 'vue'
import IconButton from '@/components/custom/button/IconButton.vue'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
const BREAKPOINTS = { sm: 480 }
const PANEL_SIZES = {
width: 'w-1/3',
minWidth: 'min-w-40',
maxWidth: 'max-w-56'
}
const slots = useSlots()
const closeDialog = inject(OnCloseKey, () => {})
const breakpoints = useBreakpoints(BREAKPOINTS)
const notMobile = breakpoints.greater('sm')
const isLeftPanelOpen = ref<boolean>(true)
const isRightPanelOpen = ref<boolean>(false)
const mobileMenuOpen = ref<boolean>(false)
const hasRightPanel = computed(() => !!slots.rightPanel)
watch(notMobile, (isDesktop) => {
if (!isDesktop) {
mobileMenuOpen.value = false
}
})
const showLeftPanel = computed(() => {
const shouldShow = notMobile.value
? isLeftPanelOpen.value
: mobileMenuOpen.value
return shouldShow
})
const toggleLeftPanel = () => {
if (notMobile.value) {
isLeftPanelOpen.value = !isLeftPanelOpen.value
} else {
mobileMenuOpen.value = !mobileMenuOpen.value
}
}
const toggleRightPanel = () => {
isRightPanelOpen.value = !isRightPanelOpen.value
}
</script>
<style scoped>
.base-widget-layout {
height: 80vh;
width: 90vw;
max-width: 1280px;
aspect-ratio: 20/13;
}
@media (min-width: 1450px) {
.base-widget-layout {
max-width: 1724px;
}
}
/* Fade transition for buttons */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Slide transition for left panel */
.slide-panel-enter-active,
.slide-panel-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
backface-visibility: hidden;
}
.slide-panel-enter-from,
.slide-panel-leave-to {
transform: translateX(-100%);
}
/* Slide transition for right panel */
.slide-panel-right-enter-active,
.slide-panel-right-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
backface-visibility: hidden;
}
.slide-panel-right-enter-from,
.slide-panel-right-leave-to {
transform: translateX(100%);
}
</style>

View File

@@ -1,24 +0,0 @@
<template>
<div
class="flex items-center gap-2 px-4 py-2 text-xs rounded-md transition-colors cursor-pointer"
:class="
active
? 'bg-neutral-100 dark-theme:bg-zinc-700 text-neutral'
: 'text-neutral hover:bg-zinc-100 hover:dark-theme:bg-zinc-700/50'
"
role="button"
@click="onClick"
>
<i-lucide:folder class="text-xs text-neutral" />
<span>
<slot></slot>
</span>
</div>
</template>
<script setup lang="ts">
const { active, onClick } = defineProps<{
active?: boolean
onClick: () => void
}>()
</script>

View File

@@ -1,13 +0,0 @@
<template>
<h3
class="m-0 px-3 py-0 pt-5 text-sm font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
>
{{ title }}
</h3>
</template>
<script setup lang="ts">
const { title } = defineProps<{
title: string
}>()
</script>

View File

@@ -1,75 +0,0 @@
<template>
<div class="flex flex-col h-full w-full bg-white dark-theme:bg-zinc-800">
<PanelHeader>
<template #icon>
<slot name="header-icon"></slot>
</template>
<slot name="header-title"></slot>
</PanelHeader>
<nav class="flex-1 px-3 py-4 flex flex-col gap-2">
<template v-for="(item, index) in navItems" :key="index">
<div v-if="'items' in item" class="flex flex-col gap-2">
<NavTitle :title="item.title" />
<NavItem
v-for="subItem in item.items"
:key="subItem.id"
:active="activeItem === subItem.id"
@click="activeItem = subItem.id"
>
{{ subItem.label }}
</NavItem>
</div>
<div v-else class="flex flex-col gap-2">
<NavItem
:active="activeItem === item.id"
@click="activeItem = item.id"
>
{{ item.label }}
</NavItem>
</div>
</template>
</nav>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import NavItem from '../nav/NavItem.vue'
import NavTitle from '../nav/NavTitle.vue'
import PanelHeader from './PanelHeader.vue'
const { navItems = [], modelValue } = defineProps<{
navItems?: (NavItemData | NavGroupData)[]
modelValue?: string | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: string | null]
}>()
const getFirstItemId = () => {
if (!navItems || navItems.length === 0) {
return null
}
const firstEntry = navItems[0]
if ('items' in firstEntry && firstEntry.items.length > 0) {
return firstEntry.items[0].id
}
if ('id' in firstEntry) {
return firstEntry.id
}
return null
}
const activeItem = computed({
get: () => modelValue ?? getFirstItemId(),
set: (value: string | null) => emit('update:modelValue', value)
})
</script>

View File

@@ -1,12 +0,0 @@
<template>
<header class="flex items-center justify-between h-16 px-6">
<div class="flex items-center gap-2 pl-1">
<slot name="icon">
<i-lucide:puzzle class="text-neutral text-base" />
</slot>
<h2 class="font-bold text-base text-neutral">
<slot></slot>
</h2>
</div>
</header>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="w-full h-full pl-4 pr-6 pb-8 bg-white dark-theme:bg-zinc-800">
<slot></slot>
</div>
</template>

View File

@@ -10,16 +10,14 @@
:aria-labelledby="item.key"
>
<template #header>
<div v-if="!item.dialogComponentProps?.headless">
<component
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
</div>
<component
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
</template>
<component

View File

@@ -21,6 +21,7 @@
<MiniMap
v-if="comfyAppReady && minimapEnabled"
ref="minimapRef"
class="pointer-events-auto"
/>
</template>
@@ -70,6 +71,7 @@ import { useContextMenuTranslation } from '@/composables/useContextMenuTranslati
import { useCopy } from '@/composables/useCopy'
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
import { useMinimap } from '@/composables/useMinimap'
import { usePaste } from '@/composables/usePaste'
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
@@ -117,7 +119,9 @@ const selectionToolboxEnabled = computed(() =>
settingStore.get('Comfy.Canvas.SelectionToolbox')
)
const minimapRef = ref<InstanceType<typeof MiniMap>>()
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
const minimap = useMinimap()
watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
@@ -354,6 +358,13 @@ onMounted(async () => {
}
)
whenever(
() => minimapRef.value,
(ref) => {
minimap.setMinimapRef(ref)
}
)
whenever(
() => useCanvasStore().canvas,
(canvas) => {

View File

@@ -1,7 +1,6 @@
<template>
<div
v-if="visible && initialized"
ref="minimapRef"
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
>
<MiniMapPanel
@@ -55,13 +54,15 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useMinimap } from '@/composables/useMinimap'
import { useCanvasStore } from '@/stores/graphStore'
import MiniMapPanel from './MiniMapPanel.vue'
const minimapRef = ref<HTMLDivElement>()
const minimap = useMinimap()
const canvasStore = useCanvasStore()
const {
initialized,
@@ -79,13 +80,13 @@ const {
renderBypass,
renderError,
updateOption,
init,
destroy,
handlePointerDown,
handlePointerMove,
handlePointerUp,
handleWheel,
setMinimapRef
} = useMinimap()
handleWheel
} = minimap
const showOptionsPanel = ref(false)
@@ -93,8 +94,20 @@ const toggleOptionsPanel = () => {
showOptionsPanel.value = !showOptionsPanel.value
}
onMounted(() => {
setMinimapRef(minimapRef.value)
watch(
() => canvasStore.canvas,
async (canvas) => {
if (canvas && !initialized.value) {
await init()
}
},
{ immediate: true }
)
onMounted(async () => {
if (canvasStore.canvas) {
await init()
}
})
onUnmounted(() => {

View File

@@ -24,7 +24,7 @@ import {
LiteGraph
} from '@/lib/litegraph/src/litegraph'
import type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'
import { useGraphMutationService } from '@/services/graphMutationService'
import { app } from '@/scripts/app'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -42,23 +42,19 @@ const inputStyle = computed<CSSProperties>(() => ({
const titleEditorStore = useTitleEditorStore()
const canvasStore = useCanvasStore()
const previousCanvasDraggable = ref(true)
const graphMutationService = useGraphMutationService()
const onEdit = async (newValue: string) => {
const onEdit = (newValue: string) => {
if (titleEditorStore.titleEditorTarget && newValue.trim() !== '') {
const trimmedTitle = newValue.trim()
titleEditorStore.titleEditorTarget.title = trimmedTitle
// If this is a subgraph node, sync the runtime subgraph name for breadcrumb reactivity
const target = titleEditorStore.titleEditorTarget
if (target instanceof LGraphNode) {
await graphMutationService.updateNodeTitle(target.id, trimmedTitle)
// If this is a subgraph node, sync the runtime subgraph name for breadcrumb reactivity
if (target.isSubgraphNode?.()) {
target.subgraph.name = trimmedTitle
}
} else if (target instanceof LGraphGroup) {
await graphMutationService.updateGroupTitle(target.id, trimmedTitle)
if (target instanceof LGraphNode && target.isSubgraphNode?.()) {
target.subgraph.name = trimmedTitle
}
app.graph.setDirtyCanvas(true, true)
}
showInput.value = false
titleEditorStore.titleEditorTarget = null

View File

@@ -90,16 +90,18 @@ const closeDialog = () => {
const canvasStore = useCanvasStore()
const addNode = (nodeDef: ComfyNodeDefImpl) => {
if (!triggerEvent) {
console.warn('The trigger event was undefined when addNode was called.')
return
}
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: getNewNodeLocation()
})
if (disconnectOnReset && triggerEvent) {
if (disconnectOnReset) {
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
} else if (!triggerEvent) {
console.warn('The trigger event was undefined when addNode was called.')
}
disconnectOnReset = false
// Notify changeTracker - new step should be added

View File

@@ -58,12 +58,11 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useReleaseStore } from '@/stores/releaseStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -71,9 +70,8 @@ import SidebarIcon from './SidebarIcon.vue'
const settingStore = useSettingStore()
const releaseStore = useReleaseStore()
const helpCenterStore = useHelpCenterStore()
const { shouldShowRedDot } = storeToRefs(releaseStore)
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
const isHelpCenterVisible = ref(false)
const sidebarLocation = computed(() =>
settingStore.get('Comfy.Sidebar.Location')
@@ -82,11 +80,11 @@ const sidebarLocation = computed(() =>
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
const toggleHelpCenter = () => {
helpCenterStore.toggle()
isHelpCenterVisible.value = !isHelpCenterVisible.value
}
const closeHelpCenter = () => {
helpCenterStore.hide()
isHelpCenterVisible.value = false
}
// Initialize release store on mount
@@ -132,7 +130,6 @@ onMounted(async () => {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);

View File

@@ -30,17 +30,10 @@
/>
<Button
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.resetView')"
icon="pi pi-filter-slash"
text
severity="secondary"
@click="resetOrganization"
/>
<Button
v-tooltip.bottom="$t('menu.refresh')"
icon="pi pi-refresh"
text
severity="secondary"
@click="() => commandStore.execute('Comfy.RefreshNodeDefinitions')"
@click="resetOrganization"
/>
<Popover ref="groupingPopover">
<div class="flex flex-col gap-1 p-2">
@@ -146,7 +139,6 @@ import {
DEFAULT_SORTING_ID,
nodeOrganizationService
} from '@/services/nodeOrganizationService'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
@@ -163,7 +155,6 @@ import NodeBookmarkTreeExplorer from './nodeLibrary/NodeBookmarkTreeExplorer.vue
const nodeDefStore = useNodeDefStore()
const nodeBookmarkStore = useNodeBookmarkStore()
const nodeHelpStore = useNodeHelpStore()
const commandStore = useCommandStore()
const expandedKeys = ref<Record<string, boolean>>({})
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)

View File

@@ -55,30 +55,9 @@
v-bind="props.action"
:href="item.url"
target="_blank"
:class="typeof item.class === 'function' ? item.class() : item.class"
@mousedown="
isZoomCommand(item) ? handleZoomMouseDown(item, $event) : undefined
"
@click="handleItemClick(item, $event)"
>
<i
v-if="hasActiveStateSiblings(item)"
class="p-menubar-item-icon pi pi-check text-sm"
:class="{ invisible: !item.comfyCommand?.active?.() }"
/>
<span
v-else-if="
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
"
class="p-menubar-item-icon"
:class="item.icon"
/>
<span v-if="item.icon" class="p-menubar-item-icon" :class="item.icon" />
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
<i
v-if="item.comfyCommand?.id === 'Comfy.NewBlankWorkflow'"
class="ml-auto"
:class="item.icon"
/>
<span
v-if="item?.comfyCommand?.keybinding"
class="ml-auto border border-surface rounded text-muted text-xs text-nowrap p-1 keybinding-tag"
@@ -115,7 +94,6 @@ import { useSettingStore } from '@/stores/settingStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
const colorPaletteStore = useColorPaletteStore()
const menuItemsStore = useMenuItemStore()
@@ -185,22 +163,16 @@ const extraMenuItems: MenuItem[] = [
},
{ separator: true },
{
key: 'browse-templates',
label: t('menuLabels.Browse Templates'),
icon: 'pi pi-folder-open',
command: () => commandStore.execute('Comfy.BrowseTemplates')
key: 'manage-extensions',
label: t('menu.manageExtensions'),
icon: 'mdi mdi-puzzle-outline',
command: showManageExtensions
},
{
key: 'settings',
label: t('g.settings'),
icon: 'mdi mdi-cog-outline',
command: () => showSettings()
},
{
key: 'manage-extensions',
label: t('menu.manageExtensions'),
icon: 'mdi mdi-puzzle-outline',
command: showManageExtensions
}
]
@@ -265,44 +237,6 @@ const onMenuShow = () => {
}
})
}
const isZoomCommand = (item: MenuItem) => {
return (
item.comfyCommand?.id === 'Comfy.Canvas.ZoomIn' ||
item.comfyCommand?.id === 'Comfy.Canvas.ZoomOut'
)
}
const handleZoomMouseDown = (item: MenuItem, event: MouseEvent) => {
if (item.comfyCommand) {
whileMouseDown(
event,
async () => {
await commandStore.execute(item.comfyCommand!.id)
},
50
)
}
}
const handleItemClick = (item: MenuItem, event: MouseEvent) => {
// Prevent the menu from closing for zoom commands or commands that have active state
if (isZoomCommand(item) || item.comfyCommand?.active) {
event.preventDefault()
event.stopPropagation()
if (item.comfyCommand?.active) {
item.command?.({
item,
originalEvent: event
})
}
return false
}
}
const hasActiveStateSiblings = (item: MenuItem): boolean => {
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
}
</script>
<style scoped>

View File

@@ -1362,27 +1362,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
return '$0.0004/$0.0016 per 1K tokens'
} else if (model.includes('gpt-4.1')) {
return '$0.002/$0.008 per 1K tokens'
} else if (model.includes('gpt-5-nano')) {
return '$0.00005/$0.0004 per 1K tokens'
} else if (model.includes('gpt-5-mini')) {
return '$0.00025/$0.002 per 1K tokens'
} else if (model.includes('gpt-5')) {
return '$0.00125/$0.01 per 1K tokens'
}
return 'Token-based'
}
},
ViduTextToVideoNode: {
displayPrice: '$0.4/Run'
},
ViduImageToVideoNode: {
displayPrice: '$0.4/Run'
},
ViduReferenceVideoNode: {
displayPrice: '$0.4/Run'
},
ViduStartEndToVideoNode: {
displayPrice: '$0.4/Run'
}
}

View File

@@ -1,6 +1,5 @@
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
@@ -22,7 +21,6 @@ import { useWorkflowService } from '@/services/workflowService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -280,7 +278,6 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.FitView',
icon: 'pi pi-expand',
label: 'Fit view to selected nodes',
menubarLabel: 'Zoom to fit',
category: 'view-controls' as const,
function: () => {
if (app.canvas.empty) {
@@ -306,7 +303,6 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ToggleLinkVisibility',
icon: 'pi pi-eye',
label: 'Canvas Toggle Link Visibility',
menubarLabel: 'Node Links',
versionAdded: '1.3.6',
function: (() => {
@@ -328,15 +324,12 @@ export function useCoreCommands(): ComfyCommand[] {
)
}
}
})(),
active: () =>
useSettingStore().get('Comfy.LinkRenderMode') !== LiteGraph.HIDDEN_LINK
})()
},
{
id: 'Comfy.Canvas.ToggleMinimap',
icon: 'pi pi-map',
label: 'Canvas Toggle Minimap',
menubarLabel: 'Minimap',
versionAdded: '1.24.1',
function: async () => {
const settingStore = useSettingStore()
@@ -344,8 +337,7 @@ export function useCoreCommands(): ComfyCommand[] {
'Comfy.Minimap.Visible',
!settingStore.get('Comfy.Minimap.Visible')
)
},
active: () => useSettingStore().get('Comfy.Minimap.Visible')
}
},
{
id: 'Comfy.QueuePrompt',
@@ -549,25 +541,21 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Workspace.ToggleBottomPanel',
icon: 'pi pi-list',
label: 'Toggle Bottom Panel',
menubarLabel: 'Bottom Panel',
versionAdded: '1.3.22',
category: 'view-controls' as const,
function: () => {
bottomPanelStore.toggleBottomPanel()
},
active: () => bottomPanelStore.bottomPanelVisible
}
},
{
id: 'Workspace.ToggleFocusMode',
icon: 'pi pi-eye',
label: 'Toggle Focus Mode',
menubarLabel: 'Focus Mode',
versionAdded: '1.3.27',
category: 'view-controls' as const,
function: () => {
useWorkspaceStore().toggleFocusMode()
},
active: () => useWorkspaceStore().focusMode
}
},
{
id: 'Comfy.Graph.FitGroupToContents',
@@ -827,34 +815,6 @@ export function useCoreCommands(): ComfyCommand[] {
graph.unpackSubgraph(subgraphNode)
}
},
{
id: 'Comfy.OpenManagerDialog',
icon: 'mdi mdi-puzzle-outline',
label: 'Manager',
function: () => {
dialogService.showManagerDialog()
}
},
{
id: 'Comfy.ToggleHelpCenter',
icon: 'pi pi-question-circle',
label: 'Help Center',
function: () => {
useHelpCenterStore().toggle()
},
active: () => useHelpCenterStore().isVisible
},
{
id: 'Comfy.ToggleCanvasInfo',
icon: 'pi pi-info-circle',
label: 'Canvas Performance',
function: async () => {
const settingStore = useSettingStore()
const currentValue = settingStore.get('Comfy.Graph.CanvasInfo')
await settingStore.set('Comfy.Graph.CanvasInfo', !currentValue)
},
active: () => useSettingStore().get('Comfy.Graph.CanvasInfo')
},
{
id: 'Workspace.ToggleBottomPanel.Shortcuts',
icon: 'pi pi-key',
@@ -879,17 +839,6 @@ export function useCoreCommands(): ComfyCommand[] {
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
)
}
},
{
id: 'Comfy.Dev.ShowModelSelector',
icon: 'pi pi-box',
label: 'Show Model Selector (Dev)',
versionAdded: '1.26.2',
category: 'view-controls' as const,
function: () => {
const modelSelectorDialog = useModelSelectorDialog()
modelSelectorDialog.show()
}
}
]

View File

@@ -19,7 +19,6 @@ export const useLitegraphSettings = () => {
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
if (canvasStore.canvas) {
canvasStore.canvas.show_info = canvasInfoEnabled
canvasStore.canvas.draw(false, true)
}
})

View File

@@ -5,9 +5,9 @@ import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformS
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCanvasStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
@@ -27,7 +27,6 @@ export type MinimapOptionKey =
export function useMinimap() {
const settingStore = useSettingStore()
const canvasStore = useCanvasStore()
const workflowStore = useWorkflowStore()
const colorPaletteStore = useColorPaletteStore()
const containerRef = ref<HTMLDivElement>()
@@ -148,11 +147,7 @@ export function useMinimap() {
}
const canvas = computed(() => canvasStore.canvas)
const graph = computed(() => {
// If we're in a subgraph, use that; otherwise use the canvas graph
const activeSubgraph = workflowStore.activeSubgraph
return activeSubgraph || canvas.value?.graph
})
const graph = ref(app.canvas?.graph)
const containerStyles = computed(() => ({
width: `${width}px`,
@@ -632,8 +627,7 @@ export function useMinimap() {
c.setDirty(true, true)
}
// Map to store original callbacks per graph ID
const originalCallbacksMap = new Map<string, GraphCallbacks>()
let originalCallbacks: GraphCallbacks = {}
const handleGraphChanged = useThrottleFn(() => {
needsFullRedraw.value = true
@@ -647,18 +641,11 @@ export function useMinimap() {
const g = graph.value
if (!g) return
// Check if we've already wrapped this graph's callbacks
if (originalCallbacksMap.has(g.id)) {
return
}
// Store the original callbacks for this graph
const originalCallbacks: GraphCallbacks = {
originalCallbacks = {
onNodeAdded: g.onNodeAdded,
onNodeRemoved: g.onNodeRemoved,
onConnectionChange: g.onConnectionChange
}
originalCallbacksMap.set(g.id, originalCallbacks)
g.onNodeAdded = function (node) {
originalCallbacks.onNodeAdded?.call(this, node)
@@ -683,19 +670,15 @@ export function useMinimap() {
const g = graph.value
if (!g) return
const originalCallbacks = originalCallbacksMap.get(g.id)
if (!originalCallbacks) {
console.error(
'Attempted to cleanup event listeners for graph that was never set up'
)
return
if (originalCallbacks.onNodeAdded !== undefined) {
g.onNodeAdded = originalCallbacks.onNodeAdded
}
if (originalCallbacks.onNodeRemoved !== undefined) {
g.onNodeRemoved = originalCallbacks.onNodeRemoved
}
if (originalCallbacks.onConnectionChange !== undefined) {
g.onConnectionChange = originalCallbacks.onConnectionChange
}
g.onNodeAdded = originalCallbacks.onNodeAdded
g.onNodeRemoved = originalCallbacks.onNodeRemoved
g.onConnectionChange = originalCallbacks.onConnectionChange
originalCallbacksMap.delete(g.id)
}
const init = async () => {
@@ -768,19 +751,6 @@ export function useMinimap() {
{ immediate: true, flush: 'post' }
)
// Watch for graph changes (e.g., when navigating to/from subgraphs)
watch(graph, (newGraph, oldGraph) => {
if (newGraph && newGraph !== oldGraph) {
cleanupEventListeners()
setupEventListeners()
needsFullRedraw.value = true
updateFlags.value.bounds = true
updateFlags.value.nodes = true
updateFlags.value.connections = true
updateMinimap()
}
})
watch(visible, async (isVisible) => {
if (isVisible) {
if (containerRef.value) {

View File

@@ -1,29 +0,0 @@
import ModelSelector from '@/components/custom/widget/ModelSelector.vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
const DIALOG_KEY = 'global-model-selector'
export const useModelSelectorDialog = () => {
const dialogService = useDialogService()
const dialogStore = useDialogStore()
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
}
function show() {
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: ModelSelector,
props: {
onClose: hide
}
})
}
return {
show,
hide
}
}

View File

@@ -1,9 +1,8 @@
export const CORE_MENU_COMMANDS = [
[[], ['Comfy.NewBlankWorkflow']],
[[], []], // Separator after New
[['File'], ['Comfy.OpenWorkflow']],
[['Workflow'], ['Comfy.NewBlankWorkflow']],
[['Workflow'], ['Comfy.OpenWorkflow', 'Comfy.BrowseTemplates']],
[
['File'],
['Workflow'],
[
'Comfy.SaveWorkflow',
'Comfy.SaveWorkflowAs',
@@ -12,6 +11,8 @@ export const CORE_MENU_COMMANDS = [
]
],
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
[['Edit'], ['Comfy.ClearWorkflow']],
[['Edit'], ['Comfy.OpenClipspace']],
[
['Help'],

View File

@@ -772,8 +772,7 @@ export const CORE_SETTINGS: SettingParams[] = [
{
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
name: 'Low quality rendering zoom threshold',
tooltip:
'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details.',
tooltip: 'Render low quality shapes when zoomed out',
type: 'slider',
attrs: {
min: 0.1,

View File

@@ -3608,7 +3608,6 @@ export class LGraphCanvas
subgraphs: []
}
// NOTE: logic for traversing nested subgraphs depends on this being a set.
const subgraphs = new Set<Subgraph>()
// Create serialisable objects
@@ -3647,13 +3646,8 @@ export class LGraphCanvas
}
// Add unique subgraph entries
// NOTE: subgraphs is appended to mid iteration.
// TODO: Must find all nested subgraphs
for (const subgraph of subgraphs) {
for (const node of subgraph.nodes) {
if (node instanceof SubgraphNode) {
subgraphs.add(node.subgraph)
}
}
const cloned = subgraph.clone(true).asSerialisable()
serialisable.subgraphs.push(cloned)
}
@@ -3770,19 +3764,12 @@ export class LGraphCanvas
created.push(group)
}
// Update subgraph ids with nesting
function updateSubgraphIds(nodes: { type: string }[]) {
for (const info of nodes) {
const subgraph = results.subgraphs.get(info.type)
if (!subgraph) continue
info.type = subgraph.id
updateSubgraphIds(subgraph.nodes)
}
}
updateSubgraphIds(parsed.nodes)
// Nodes
for (const info of parsed.nodes) {
// If the subgraph was cloned, update references to use the new subgraph ID.
const subgraph = results.subgraphs.get(info.type)
if (subgraph) info.type = subgraph.id
const node = info.type == null ? null : LiteGraph.createNode(info.type)
if (!node) {
// failedNodes.push(info)

View File

@@ -24,8 +24,6 @@ import {
} from './measure'
import type { ISerialisedGroup } from './types/serialisation'
export type GroupId = number
export interface IGraphGroupFlags extends Record<string, unknown> {
pinned?: true
}

View File

@@ -1574,10 +1574,7 @@ export class LGraphNode
* remove an existing output slot
*/
removeOutput(slot: number): void {
// Only disconnect if node is part of a graph
if (this.graph) {
this.disconnectOutput(slot)
}
this.disconnectOutput(slot)
const { outputs } = this
outputs.splice(slot, 1)
@@ -1585,12 +1582,11 @@ export class LGraphNode
const output = outputs[i]
if (!output || !output.links) continue
// Only update link indices if node is part of a graph
if (this.graph) {
for (const linkId of output.links) {
const link = this.graph._links.get(linkId)
if (link) link.origin_slot--
}
for (const linkId of output.links) {
if (!this.graph) throw new NullGraphError()
const link = this.graph._links.get(linkId)
if (link) link.origin_slot--
}
}
@@ -1630,10 +1626,7 @@ export class LGraphNode
* remove an existing input slot
*/
removeInput(slot: number): void {
// Only disconnect if node is part of a graph
if (this.graph) {
this.disconnectInput(slot, true)
}
this.disconnectInput(slot, true)
const { inputs } = this
const slot_info = inputs.splice(slot, 1)
@@ -1641,11 +1634,9 @@ export class LGraphNode
const input = inputs[i]
if (!input?.link) continue
// Only update link indices if node is part of a graph
if (this.graph) {
const link = this.graph._links.get(input.link)
if (link) link.target_slot--
}
if (!this.graph) throw new NullGraphError()
const link = this.graph._links.get(input.link)
if (link) link.target_slot--
}
this.onInputRemoved?.(slot, slot_info[0])
this.setDirtyCanvas(true, true)

View File

@@ -414,18 +414,6 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
* If `input` or `output`, reroutes will not be automatically removed, and retain a connection to the input or output, respectively.
*/
disconnect(network: LinkNetwork, keepReroutes?: 'input' | 'output'): void {
// Clean up the target node's input slot
if (this.target_id !== -1) {
const targetNode = network.getNodeById(this.target_id)
if (targetNode) {
const targetInput = targetNode.inputs?.[this.target_slot]
if (targetInput && targetInput.link === this.id) {
targetInput.link = null
targetNode.setDirtyCanvas?.(true, false)
}
}
}
const reroutes = LLink.getReroutes(network, this)
const lastReroute = reroutes.at(-1)

View File

@@ -135,10 +135,6 @@ export class FloatingRenderLink implements RenderLink {
return true
}
canConnectToSubgraphInput(input: SubgraphInput): boolean {
return this.toType === 'output' && input.isValidTarget(this.fromSlot)
}
connectToInput(
node: LGraphNode,
input: INodeInputSlot,

View File

@@ -681,20 +681,6 @@ export class LinkConnector {
let targetSlot = input
for (const link of renderLinks) {
// Validate the connection type before proceeding
if (
'canConnectToSubgraphInput' in link &&
!link.canConnectToSubgraphInput(targetSlot)
) {
console.warn(
'Invalid connection type',
link.fromSlot.type,
'->',
targetSlot.type
)
continue
}
link.connectToSubgraphInput(targetSlot, this.events)
// If we just connected to an EmptySubgraphInput, check if we should reuse the slot
@@ -955,14 +941,6 @@ export class LinkConnector {
)
}
isSubgraphInputValidDrop(input: SubgraphInput): boolean {
return this.renderLinks.some(
(link) =>
'canConnectToSubgraphInput' in link &&
link.canConnectToSubgraphInput(input)
)
}
/**
* Checks if a reroute is a valid drop target for any of the links being connected.
* @param reroute The reroute that would be dropped on.

View File

@@ -55,10 +55,6 @@ export class MovingOutputLink extends MovingLinkBase {
return reroute.origin_id !== this.outputNode.id
}
canConnectToSubgraphInput(input: SubgraphInput): boolean {
return input.isValidTarget(this.fromSlot)
}
connectToInput(): never {
throw new Error('MovingOutputLink cannot connect to an input.')
}

View File

@@ -58,10 +58,6 @@ export class ToOutputRenderLink implements RenderLink {
return true
}
canConnectToSubgraphInput(input: SubgraphInput): boolean {
return input.isValidTarget(this.fromSlot)
}
connectToOutput(
node: LGraphNode,
output: INodeOutputSlot,

View File

@@ -16,10 +16,7 @@ import type {
GraphOrSubgraph,
Subgraph
} from '@/lib/litegraph/src/subgraph/Subgraph'
import type {
ExportedSubgraphInstance,
ISerialisedNode
} from '@/lib/litegraph/src/types/serialisation'
import type { ExportedSubgraphInstance } from '@/lib/litegraph/src/types/serialisation'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap'
@@ -31,8 +28,6 @@ import {
} from './ExecutableNodeDTO'
import type { SubgraphInput } from './SubgraphInput'
export type SubgraphId = string
/**
* An instance of a {@link Subgraph}, displayed as a node on the containing (parent) graph.
*/
@@ -271,14 +266,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const subgraphInput = this.subgraph.inputNode.slots.find(
(slot) => slot.name === input.name
)
if (!subgraphInput) {
// Skip inputs that don't exist in the subgraph definition
// This can happen when loading workflows with dynamically added inputs
console.warn(
`[SubgraphNode.configure] No subgraph input found for input ${input.name}, skipping`
if (!subgraphInput)
throw new Error(
`[SubgraphNode.configure] No subgraph input found for input ${input.name}`
)
continue
}
this.#addSubgraphInputListeners(subgraphInput, input)
@@ -545,36 +536,4 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
}
}
}
/**
* Synchronizes widget values from this SubgraphNode instance to the
* corresponding widgets in the subgraph definition before serialization.
* This ensures nested subgraph widget values are preserved when saving.
*/
override serialize(): ISerialisedNode {
// Sync widget values to subgraph definition before serialization
for (let i = 0; i < this.widgets.length; i++) {
const widget = this.widgets[i]
const input = this.inputs.find((inp) => inp.name === widget.name)
if (input) {
const subgraphInput = this.subgraph.inputNode.slots.find(
(slot) => slot.name === input.name
)
if (subgraphInput) {
// Find all widgets connected to this subgraph input
const connectedWidgets = subgraphInput.getConnectedWidgets()
// Update the value of all connected widgets
for (const connectedWidget of connectedWidgets) {
connectedWidget.value = widget.value
}
}
}
}
// Call parent serialize method
return super.serialize()
}
}

View File

@@ -656,119 +656,4 @@ describe('LGraphNode', () => {
spy.mockRestore()
})
})
describe('removeInput/removeOutput on copied nodes', () => {
beforeEach(() => {
// Register a test node type so clone() can work
LiteGraph.registerNodeType('TestNode', LGraphNode)
})
test('should NOT throw error when calling removeInput on a copied node without graph', () => {
// Create a node with an input
const originalNode = new LGraphNode('Test Node')
originalNode.type = 'TestNode'
originalNode.addInput('input1', 'number')
// Clone the node (which creates a node without graph reference)
const copiedNode = originalNode.clone()
// This should NOT throw anymore - we can remove inputs on nodes without graph
expect(() => copiedNode!.removeInput(0)).not.toThrow()
expect(copiedNode!.inputs).toHaveLength(0)
})
test('should NOT throw error when calling removeOutput on a copied node without graph', () => {
// Create a node with an output
const originalNode = new LGraphNode('Test Node')
originalNode.type = 'TestNode'
originalNode.addOutput('output1', 'number')
// Clone the node (which creates a node without graph reference)
const copiedNode = originalNode.clone()
// This should NOT throw anymore - we can remove outputs on nodes without graph
expect(() => copiedNode!.removeOutput(0)).not.toThrow()
expect(copiedNode!.outputs).toHaveLength(0)
})
test('should skip disconnectInput/disconnectOutput when node has no graph', () => {
// Create nodes with input/output
const nodeWithInput = new LGraphNode('Test Node')
nodeWithInput.type = 'TestNode'
nodeWithInput.addInput('input1', 'number')
const nodeWithOutput = new LGraphNode('Test Node')
nodeWithOutput.type = 'TestNode'
nodeWithOutput.addOutput('output1', 'number')
// Clone nodes (no graph reference)
const clonedInput = nodeWithInput.clone()
const clonedOutput = nodeWithOutput.clone()
// Mock disconnect methods to verify they're not called
clonedInput!.disconnectInput = vi.fn()
clonedOutput!.disconnectOutput = vi.fn()
// Remove input/output - disconnect methods should NOT be called
clonedInput!.removeInput(0)
clonedOutput!.removeOutput(0)
expect(clonedInput!.disconnectInput).not.toHaveBeenCalled()
expect(clonedOutput!.disconnectOutput).not.toHaveBeenCalled()
})
test('should be able to removeInput on a copied node after adding to graph', () => {
// Create a graph and a node with an input
const graph = new LGraph()
const originalNode = new LGraphNode('Test Node')
originalNode.type = 'TestNode'
originalNode.addInput('input1', 'number')
// Clone the node and add to graph
const copiedNode = originalNode.clone()
expect(copiedNode).not.toBeNull()
graph.add(copiedNode!)
// This should work now that the node has a graph reference
expect(() => copiedNode!.removeInput(0)).not.toThrow()
expect(copiedNode!.inputs).toHaveLength(0)
})
test('should be able to removeOutput on a copied node after adding to graph', () => {
// Create a graph and a node with an output
const graph = new LGraph()
const originalNode = new LGraphNode('Test Node')
originalNode.type = 'TestNode'
originalNode.addOutput('output1', 'number')
// Clone the node and add to graph
const copiedNode = originalNode.clone()
expect(copiedNode).not.toBeNull()
graph.add(copiedNode!)
// This should work now that the node has a graph reference
expect(() => copiedNode!.removeOutput(0)).not.toThrow()
expect(copiedNode!.outputs).toHaveLength(0)
})
test('RerouteNode clone scenario - should be able to removeOutput and addOutput on cloned node', () => {
// This simulates the RerouteNode clone method behavior
const originalNode = new LGraphNode('Reroute')
originalNode.type = 'TestNode'
originalNode.addOutput('*', '*')
// Clone the node (simulating RerouteNode.clone)
const clonedNode = originalNode.clone()
expect(clonedNode).not.toBeNull()
// This should not throw - we should be able to modify outputs on a cloned node
expect(() => {
clonedNode!.removeOutput(0)
clonedNode!.addOutput('renamed', '*')
}).not.toThrow()
expect(clonedNode!.outputs).toHaveLength(1)
expect(clonedNode!.outputs[0].name).toBe('renamed')
})
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect } from 'vitest'
import { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
import { LLink } from '@/lib/litegraph/src/litegraph'
import { test } from './testExtensions'
@@ -14,84 +14,4 @@ describe('LLink', () => {
const link = new LLink(1, 'float', 4, 2, 5, 3)
expect(link.serialize()).toMatchSnapshot('Basic')
})
describe('disconnect', () => {
it('should clear the target input link reference when disconnecting', () => {
// Create a graph and nodes
const graph = new LGraph()
const sourceNode = new LGraphNode('Source')
const targetNode = new LGraphNode('Target')
// Add nodes to graph
graph.add(sourceNode)
graph.add(targetNode)
// Add slots
sourceNode.addOutput('out', 'number')
targetNode.addInput('in', 'number')
// Connect the nodes
const link = sourceNode.connect(0, targetNode, 0)
expect(link).toBeDefined()
expect(targetNode.inputs[0].link).toBe(link?.id)
// Mock setDirtyCanvas
const setDirtyCanvasSpy = vi.spyOn(targetNode, 'setDirtyCanvas')
// Disconnect the link
link?.disconnect(graph)
// Verify the target input's link reference is cleared
expect(targetNode.inputs[0].link).toBeNull()
// Verify setDirtyCanvas was called
expect(setDirtyCanvasSpy).toHaveBeenCalledWith(true, false)
})
it('should handle disconnecting when target node is not found', () => {
// Create a link with invalid target
const graph = new LGraph()
const link = new LLink(1, 'number', 1, 0, 999, 0) // Invalid target id
// Should not throw when disconnecting
expect(() => link.disconnect(graph)).not.toThrow()
})
it('should only clear link reference if it matches the current link id', () => {
// Create a graph and nodes
const graph = new LGraph()
const sourceNode1 = new LGraphNode('Source1')
const sourceNode2 = new LGraphNode('Source2')
const targetNode = new LGraphNode('Target')
// Add nodes to graph
graph.add(sourceNode1)
graph.add(sourceNode2)
graph.add(targetNode)
// Add slots
sourceNode1.addOutput('out', 'number')
sourceNode2.addOutput('out', 'number')
targetNode.addInput('in', 'number')
// Create first connection
const link1 = sourceNode1.connect(0, targetNode, 0)
expect(link1).toBeDefined()
// Disconnect first connection
targetNode.disconnectInput(0)
// Create second connection
const link2 = sourceNode2.connect(0, targetNode, 0)
expect(link2).toBeDefined()
expect(targetNode.inputs[0].link).toBe(link2?.id)
// Try to disconnect the first link (which is already disconnected)
// It should not affect the current connection
link1?.disconnect(graph)
// The input should still have the second link
expect(targetNode.inputs[0].link).toBe(link2?.id)
})
})
})

View File

@@ -1,310 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { MovingOutputLink } from '@/lib/litegraph/src/canvas/MovingOutputLink'
import { ToOutputRenderLink } from '@/lib/litegraph/src/canvas/ToOutputRenderLink'
import { LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
import { createTestSubgraph } from '../subgraph/fixtures/subgraphHelpers'
describe('LinkConnector SubgraphInput connection validation', () => {
let connector: LinkConnector
const mockSetConnectingLinks = vi.fn()
beforeEach(() => {
connector = new LinkConnector(mockSetConnectingLinks)
vi.clearAllMocks()
})
describe('MovingOutputLink validation', () => {
it('should implement canConnectToSubgraphInput method', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'number_input', type: 'number' }]
})
const sourceNode = new LGraphNode('SourceNode')
sourceNode.addOutput('number_out', 'number')
subgraph.add(sourceNode)
const targetNode = new LGraphNode('TargetNode')
targetNode.addInput('number_in', 'number')
subgraph.add(targetNode)
const link = new LLink(1, 'number', sourceNode.id, 0, targetNode.id, 0)
subgraph._links.set(link.id, link)
const movingLink = new MovingOutputLink(subgraph, link)
// Verify the method exists
expect(typeof movingLink.canConnectToSubgraphInput).toBe('function')
})
it('should validate type compatibility correctly', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'number_input', type: 'number' }]
})
const sourceNode = new LGraphNode('SourceNode')
sourceNode.addOutput('number_out', 'number')
sourceNode.addOutput('string_out', 'string')
subgraph.add(sourceNode)
const targetNode = new LGraphNode('TargetNode')
targetNode.addInput('number_in', 'number')
targetNode.addInput('string_in', 'string')
subgraph.add(targetNode)
// Create valid link (number -> number)
const validLink = new LLink(
1,
'number',
sourceNode.id,
0,
targetNode.id,
0
)
subgraph._links.set(validLink.id, validLink)
const validMovingLink = new MovingOutputLink(subgraph, validLink)
// Create invalid link (string -> number)
const invalidLink = new LLink(
2,
'string',
sourceNode.id,
1,
targetNode.id,
1
)
subgraph._links.set(invalidLink.id, invalidLink)
const invalidMovingLink = new MovingOutputLink(subgraph, invalidLink)
const numberInput = subgraph.inputs[0]
// Test validation
expect(validMovingLink.canConnectToSubgraphInput(numberInput)).toBe(true)
expect(invalidMovingLink.canConnectToSubgraphInput(numberInput)).toBe(
false
)
})
it('should handle wildcard types', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'wildcard_input', type: '*' }]
})
const sourceNode = new LGraphNode('SourceNode')
sourceNode.addOutput('number_out', 'number')
subgraph.add(sourceNode)
const targetNode = new LGraphNode('TargetNode')
targetNode.addInput('number_in', 'number')
subgraph.add(targetNode)
const link = new LLink(1, 'number', sourceNode.id, 0, targetNode.id, 0)
subgraph._links.set(link.id, link)
const movingLink = new MovingOutputLink(subgraph, link)
const wildcardInput = subgraph.inputs[0]
// Wildcard should accept any type
expect(movingLink.canConnectToSubgraphInput(wildcardInput)).toBe(true)
})
})
describe('ToOutputRenderLink validation', () => {
it('should implement canConnectToSubgraphInput method', () => {
// Create a minimal valid setup
const subgraph = createTestSubgraph()
const node = new LGraphNode('TestNode')
node.id = 1
node.addInput('test_in', 'number')
subgraph.add(node)
const slot = node.inputs[0] as NodeInputSlot
const renderLink = new ToOutputRenderLink(subgraph, node, slot)
// Verify the method exists
expect(typeof renderLink.canConnectToSubgraphInput).toBe('function')
})
})
describe('dropOnIoNode validation', () => {
it('should prevent invalid connections when dropping on SubgraphInputNode', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'number_input', type: 'number' }]
})
const sourceNode = new LGraphNode('SourceNode')
sourceNode.addOutput('string_out', 'string')
subgraph.add(sourceNode)
const targetNode = new LGraphNode('TargetNode')
targetNode.addInput('string_in', 'string')
subgraph.add(targetNode)
// Create an invalid link (string output -> string input, but subgraph expects number)
const link = new LLink(1, 'string', sourceNode.id, 0, targetNode.id, 0)
subgraph._links.set(link.id, link)
const movingLink = new MovingOutputLink(subgraph, link)
// Mock console.warn to verify it's called
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => {})
// Add the link to the connector
connector.renderLinks.push(movingLink)
connector.state.connectingTo = 'output'
// Create mock event
const mockEvent = {
canvasX: 100,
canvasY: 100
} as any
// Mock the getSlotInPosition to return the subgraph input
const mockGetSlotInPosition = vi.fn().mockReturnValue(subgraph.inputs[0])
subgraph.inputNode.getSlotInPosition = mockGetSlotInPosition
// Spy on connectToSubgraphInput to ensure it's NOT called
const connectSpy = vi.spyOn(movingLink, 'connectToSubgraphInput')
// Drop on the SubgraphInputNode
connector.dropOnIoNode(subgraph.inputNode, mockEvent)
// Verify that the invalid connection was skipped
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Invalid connection type',
'string',
'->',
'number'
)
expect(connectSpy).not.toHaveBeenCalled()
consoleWarnSpy.mockRestore()
})
it('should allow valid connections when dropping on SubgraphInputNode', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'number_input', type: 'number' }]
})
const sourceNode = new LGraphNode('SourceNode')
sourceNode.addOutput('number_out', 'number')
subgraph.add(sourceNode)
const targetNode = new LGraphNode('TargetNode')
targetNode.addInput('number_in', 'number')
subgraph.add(targetNode)
// Create a valid link (number -> number)
const link = new LLink(1, 'number', sourceNode.id, 0, targetNode.id, 0)
subgraph._links.set(link.id, link)
const movingLink = new MovingOutputLink(subgraph, link)
// Add the link to the connector
connector.renderLinks.push(movingLink)
connector.state.connectingTo = 'output'
// Create mock event
const mockEvent = {
canvasX: 100,
canvasY: 100
} as any
// Mock the getSlotInPosition to return the subgraph input
const mockGetSlotInPosition = vi.fn().mockReturnValue(subgraph.inputs[0])
subgraph.inputNode.getSlotInPosition = mockGetSlotInPosition
// Spy on connectToSubgraphInput to ensure it IS called
const connectSpy = vi.spyOn(movingLink, 'connectToSubgraphInput')
// Drop on the SubgraphInputNode
connector.dropOnIoNode(subgraph.inputNode, mockEvent)
// Verify that the valid connection was made
expect(connectSpy).toHaveBeenCalledWith(
subgraph.inputs[0],
connector.events
)
})
})
describe('isSubgraphInputValidDrop', () => {
it('should check if render links can connect to SubgraphInput', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'number_input', type: 'number' }]
})
const sourceNode = new LGraphNode('SourceNode')
sourceNode.addOutput('number_out', 'number')
sourceNode.addOutput('string_out', 'string')
subgraph.add(sourceNode)
const targetNode = new LGraphNode('TargetNode')
targetNode.addInput('number_in', 'number')
targetNode.addInput('string_in', 'string')
subgraph.add(targetNode)
// Create valid and invalid links
const validLink = new LLink(
1,
'number',
sourceNode.id,
0,
targetNode.id,
0
)
const invalidLink = new LLink(
2,
'string',
sourceNode.id,
1,
targetNode.id,
1
)
subgraph._links.set(validLink.id, validLink)
subgraph._links.set(invalidLink.id, invalidLink)
const validMovingLink = new MovingOutputLink(subgraph, validLink)
const invalidMovingLink = new MovingOutputLink(subgraph, invalidLink)
const subgraphInput = subgraph.inputs[0]
// Test with only invalid link
connector.renderLinks.length = 0
connector.renderLinks.push(invalidMovingLink)
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(false)
// Test with valid link
connector.renderLinks.length = 0
connector.renderLinks.push(validMovingLink)
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(true)
// Test with mixed links
connector.renderLinks.length = 0
connector.renderLinks.push(invalidMovingLink, validMovingLink)
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(true)
})
it('should handle render links without canConnectToSubgraphInput method', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'number_input', type: 'number' }]
})
// Create a mock render link without the method
const mockLink = {
fromSlot: { type: 'number' }
// No canConnectToSubgraphInput method
} as any
connector.renderLinks.push(mockLink)
const subgraphInput = subgraph.inputs[0]
// Should return false as the link doesn't have the method
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(false)
})
})
})

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "الاتصال بالدعم"
},
"Comfy_Dev_ShowModelSelector": {
"label": "إظهار منتقي النماذج (للمطورين)"
},
"Comfy_DuplicateWorkflow": {
"label": "تكرار سير العمل الحالي"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenManagerDialog": {
"label": "مدير"
},
"Comfy_OpenWorkflow": {
"label": "فتح سير عمل"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "عرض نافذة الإعدادات"
},
"Comfy_ToggleCanvasInfo": {
"label": "أداء اللوحة"
},
"Comfy_ToggleHelpCenter": {
"label": "مركز المساعدة"
},
"Comfy_ToggleTheme": {
"label": "تبديل النمط (فاتح/داكن)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "إدارة الإضافات",
"onChange": "عند التغيير",
"onChangeTooltip": "سيتم وضع سير العمل في قائمة الانتظار عند إجراء تغيير",
"queue": "لوحة الانتظار",
"refresh": "تحديث تعريفات العقد",
"resetView": "إعادة تعيين عرض اللوحة",
"run": "تشغيل",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "حول ComfyUI",
"Add Edit Model Step": "إضافة خطوة تعديل النموذج",
"Bottom Panel": "لوحة سفلية",
"Browse Templates": "تصفح القوالب",
"Bypass/Unbypass Selected Nodes": "تجاوز/إلغاء تجاوز العقد المحددة",
"Canvas Performance": "أداء اللوحة",
"Canvas Toggle Link Visibility": "تبديل ظهور الروابط على اللوحة",
"Canvas Toggle Lock": "تبديل قفل اللوحة",
"Canvas Toggle Minimap": "تبديل الخريطة المصغرة على اللوحة",
"Check for Updates": "التحقق من التحديثات",
"Clear Pending Tasks": "مسح المهام المعلقة",
"Clear Workflow": "مسح سير العمل",
@@ -789,20 +788,15 @@
"Exit Subgraph": "الخروج من الرسم الفرعي",
"Export": "تصدير",
"Export (API)": "تصدير (API)",
"File": "ملف",
"Fit Group To Contents": "ملائمة المجموعة للمحتويات",
"Focus Mode": "وضع التركيز",
"Fit view to selected nodes": "تعديل العرض للعقد المحددة",
"Give Feedback": "تقديم ملاحظات",
"Group Selected Nodes": "تجميع العقد المحددة",
"Help": "مساعدة",
"Help Center": "مركز المساعدة",
"Increase Brush Size in MaskEditor": "زيادة حجم الفرشاة في محرر القناع",
"Interrupt": "إيقاف مؤقت",
"Load Default Workflow": "تحميل سير العمل الافتراضي",
"Manage group nodes": "إدارة عقد المجموعة",
"Manager": "المدير",
"Minimap": "خريطة مصغرة",
"Model Library": "مكتبة النماذج",
"Move Selected Nodes Down": "تحريك العقد المحددة للأسفل",
"Move Selected Nodes Left": "تحريك العقد المحددة لليسار",
"Move Selected Nodes Right": "تحريك العقد المحددة لليمين",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "كتم/إلغاء كتم العقد المحددة",
"New": "جديد",
"Next Opened Workflow": "سير العمل التالي المفتوح",
"Node Library": "مكتبة العقد",
"Node Links": "روابط العقد",
"Open": "فتح",
"Open 3D Viewer (Beta) for Selected Node": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة",
"Open Custom Nodes Folder": "فتح مجلد العقد المخصصة",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "تثبيت/إلغاء تثبيت العناصر المحددة",
"Pin/Unpin Selected Nodes": "تثبيت/إلغاء تثبيت العقد المحددة",
"Previous Opened Workflow": "سير العمل السابق المفتوح",
"Queue Panel": "لوحة الانتظار",
"Queue Prompt": "قائمة انتظار التعليمات",
"Queue Prompt (Front)": "قائمة انتظار التعليمات (أمامي)",
"Queue Selected Output Nodes": "قائمة انتظار عقد المخرجات المحددة",
@@ -840,24 +831,28 @@
"Save": "حفظ",
"Save As": "حفظ باسم",
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
"Show Model Selector (Dev)": "إظهار منتقي النماذج (للمطورين)",
"Show Settings Dialog": "عرض نافذة الإعدادات",
"Sign Out": "تسجيل خروج",
"Toggle Bottom Panel": "تبديل اللوحة السفلية",
"Toggle Essential Bottom Panel": "تبديل اللوحة السفلية الأساسية",
"Toggle Focus Mode": "تبديل وضع التركيز",
"Toggle Logs Bottom Panel": "تبديل لوحة السجلات السفلية",
"Toggle Model Library Sidebar": "تبديل الشريط الجانبي لمكتبة النماذج",
"Toggle Node Library Sidebar": "تبديل الشريط الجانبي لمكتبة العقد",
"Toggle Queue Sidebar": "تبديل الشريط الجانبي لقائمة الانتظار",
"Toggle Search Box": "تبديل مربع البحث",
"Toggle Terminal Bottom Panel": "تبديل لوحة الطرفية السفلية",
"Toggle Theme (Dark/Light)": "تبديل السمة (داكن/فاتح)",
"Toggle View Controls Bottom Panel": "تبديل لوحة التحكم في العرض السفلية",
"Toggle Workflows Sidebar": "تبديل الشريط الجانبي لسير العمل",
"Toggle the Custom Nodes Manager": "تبديل مدير العقد المخصصة",
"Toggle the Custom Nodes Manager Progress Bar": "تبديل شريط تقدم مدير العقد المخصصة",
"Undo": "تراجع",
"Ungroup selected group nodes": "فك تجميع عقد المجموعة المحددة",
"Unpack the selected Subgraph": "فك تجميع الرسم البياني الفرعي المحدد",
"Workflows": "سير العمل",
"Workflow": "سير العمل",
"Zoom In": "تكبير",
"Zoom Out": "تصغير",
"Zoom to fit": "تكبير لتناسب"
"Zoom Out": "تصغير"
},
"minimap": {
"nodeColors": "ألوان العقد",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "Contact Support"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Show Model Selector (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicate Current Workflow"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenManagerDialog": {
"label": "Manager"
},
"Comfy_OpenWorkflow": {
"label": "Open Workflow"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "Show Settings Dialog"
},
"Comfy_ToggleCanvasInfo": {
"label": "Canvas Performance"
},
"Comfy_ToggleHelpCenter": {
"label": "Help Center"
},
"Comfy_ToggleTheme": {
"label": "Toggle Theme (Dark/Light)"
},

View File

@@ -546,8 +546,7 @@
"light": "Light",
"manageExtensions": "Manage Extensions",
"settings": "Settings",
"help": "Help",
"queue": "Queue Panel"
"help": "Help"
},
"tabMenu": {
"duplicateTab": "Duplicate Tab",
@@ -941,7 +940,7 @@
"Image Layer": "Image Layer"
},
"menuLabels": {
"File": "File",
"Workflow": "Workflow",
"Edit": "Edit",
"Help": "Help",
"Check for Updates": "Check for Updates",
@@ -960,16 +959,16 @@
"Browse Templates": "Browse Templates",
"Add Edit Model Step": "Add Edit Model Step",
"Delete Selected Items": "Delete Selected Items",
"Zoom to fit": "Zoom to fit",
"Fit view to selected nodes": "Fit view to selected nodes",
"Move Selected Nodes Down": "Move Selected Nodes Down",
"Move Selected Nodes Left": "Move Selected Nodes Left",
"Move Selected Nodes Right": "Move Selected Nodes Right",
"Move Selected Nodes Up": "Move Selected Nodes Up",
"Reset View": "Reset View",
"Resize Selected Nodes": "Resize Selected Nodes",
"Node Links": "Node Links",
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
"Canvas Toggle Lock": "Canvas Toggle Lock",
"Minimap": "Minimap",
"Canvas Toggle Minimap": "Canvas Toggle Minimap",
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
@@ -980,7 +979,6 @@
"Clear Pending Tasks": "Clear Pending Tasks",
"Clear Workflow": "Clear Workflow",
"Contact Support": "Contact Support",
"Show Model Selector (Dev)": "Show Model Selector (Dev)",
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",
@@ -1007,7 +1005,6 @@
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
"New": "New",
"Clipspace": "Clipspace",
"Manager": "Manager",
"Open": "Open",
"Queue Prompt": "Queue Prompt",
"Queue Prompt (Front)": "Queue Prompt (Front)",
@@ -1017,8 +1014,6 @@
"Save": "Save",
"Save As": "Save As",
"Show Settings Dialog": "Show Settings Dialog",
"Canvas Performance": "Canvas Performance",
"Help Center": "Help Center",
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
"Undo": "Undo",
"Open Sign In Dialog": "Open Sign In Dialog",
@@ -1027,17 +1022,17 @@
"Next Opened Workflow": "Next Opened Workflow",
"Previous Opened Workflow": "Previous Opened Workflow",
"Toggle Search Box": "Toggle Search Box",
"Bottom Panel": "Bottom Panel",
"Toggle Bottom Panel": "Toggle Bottom Panel",
"Show Keybindings Dialog": "Show Keybindings Dialog",
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
"Focus Mode": "Focus Mode",
"Model Library": "Model Library",
"Node Library": "Node Library",
"Queue Panel": "Queue Panel",
"Workflows": "Workflows"
"Toggle Focus Mode": "Toggle Focus Mode",
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar"
},
"desktopMenu": {
"reinstall": "Reinstall",

View File

@@ -390,7 +390,7 @@
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "Low quality rendering zoom threshold",
"tooltip": "Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details."
"tooltip": "Render low quality shapes when zoomed out"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Maximum FPS",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "Contactar soporte"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Mostrar selector de modelo (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicar flujo de trabajo actual"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "Abrir espacio de clips"
},
"Comfy_OpenManagerDialog": {
"label": "Administrador"
},
"Comfy_OpenWorkflow": {
"label": "Abrir Flujo de Trabajo"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "Mostrar Diálogo de Configuraciones"
},
"Comfy_ToggleCanvasInfo": {
"label": "Rendimiento del lienzo"
},
"Comfy_ToggleHelpCenter": {
"label": "Centro de ayuda"
},
"Comfy_ToggleTheme": {
"label": "Cambiar Tema (Oscuro/Claro)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "Gestionar extensiones",
"onChange": "Al cambiar",
"onChangeTooltip": "El flujo de trabajo se encolará una vez que se haga un cambio",
"queue": "Panel de cola",
"refresh": "Actualizar definiciones de nodos",
"resetView": "Restablecer vista del lienzo",
"run": "Ejecutar",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "Acerca de ComfyUI",
"Add Edit Model Step": "Agregar paso de edición de modelo",
"Bottom Panel": "Panel inferior",
"Browse Templates": "Explorar plantillas",
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
"Canvas Performance": "Rendimiento del lienzo",
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
"Check for Updates": "Buscar actualizaciones",
"Clear Pending Tasks": "Borrar tareas pendientes",
"Clear Workflow": "Borrar flujo de trabajo",
@@ -789,20 +788,15 @@
"Exit Subgraph": "Salir de subgrafo",
"Export": "Exportar",
"Export (API)": "Exportar (API)",
"File": "Archivo",
"Fit Group To Contents": "Ajustar grupo a contenidos",
"Focus Mode": "Modo de enfoque",
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
"Give Feedback": "Dar retroalimentación",
"Group Selected Nodes": "Agrupar nodos seleccionados",
"Help": "Ayuda",
"Help Center": "Centro de ayuda",
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
"Interrupt": "Interrumpir",
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
"Manage group nodes": "Gestionar nodos de grupo",
"Manager": "Administrador",
"Minimap": "Minimapa",
"Model Library": "Biblioteca de modelos",
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
"New": "Nuevo",
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
"Node Library": "Biblioteca de nodos",
"Node Links": "Enlaces de nodos",
"Open": "Abrir",
"Open 3D Viewer (Beta) for Selected Node": "Abrir visor 3D (Beta) para el nodo seleccionado",
"Open Custom Nodes Folder": "Abrir carpeta de nodos personalizados",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
"Queue Panel": "Panel de cola",
"Queue Prompt": "Indicador de cola",
"Queue Prompt (Front)": "Indicador de cola (Frente)",
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
@@ -840,24 +831,28 @@
"Save": "Guardar",
"Save As": "Guardar como",
"Show Keybindings Dialog": "Mostrar diálogo de combinaciones de teclas",
"Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)",
"Show Settings Dialog": "Mostrar diálogo de configuración",
"Sign Out": "Cerrar sesión",
"Toggle Bottom Panel": "Alternar panel inferior",
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
"Toggle Focus Mode": "Alternar modo de enfoque",
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
"Toggle Search Box": "Alternar caja de búsqueda",
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
"Workflows": "Flujos de trabajo",
"Workflow": "Flujo de trabajo",
"Zoom In": "Acercar",
"Zoom Out": "Alejar",
"Zoom to fit": "Ajustar al tamaño"
"Zoom Out": "Alejar"
},
"minimap": {
"nodeColors": "Colores de nodos",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "Contacter le support"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Afficher le sélecteur de modèle (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Dupliquer le flux de travail actuel"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "Espace de clip"
},
"Comfy_OpenManagerDialog": {
"label": "Gestionnaire"
},
"Comfy_OpenWorkflow": {
"label": "Ouvrir le flux de travail"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "Afficher la boîte de dialogue des paramètres"
},
"Comfy_ToggleCanvasInfo": {
"label": "Performance du canvas"
},
"Comfy_ToggleHelpCenter": {
"label": "Centre d'aide"
},
"Comfy_ToggleTheme": {
"label": "Changer de thème (Sombre/Clair)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "Gérer les extensions",
"onChange": "Sur modification",
"onChangeTooltip": "Le flux de travail sera mis en file d'attente une fois une modification effectuée",
"queue": "Panneau de file dattente",
"refresh": "Actualiser les définitions des nœuds",
"resetView": "Réinitialiser la vue du canevas",
"run": "Exécuter",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "À propos de ComfyUI",
"Add Edit Model Step": "Ajouter une étape dédition de modèle",
"Bottom Panel": "Panneau inférieur",
"Browse Templates": "Parcourir les modèles",
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
"Canvas Performance": "Performance du canevas",
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
"Check for Updates": "Vérifier les mises à jour",
"Clear Pending Tasks": "Effacer les tâches en attente",
"Clear Workflow": "Effacer le flux de travail",
@@ -789,20 +788,15 @@
"Exit Subgraph": "Quitter le sous-graphe",
"Export": "Exporter",
"Export (API)": "Exporter (API)",
"File": "Fichier",
"Fit Group To Contents": "Ajuster le groupe au contenu",
"Focus Mode": "Mode focus",
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
"Give Feedback": "Donnez votre avis",
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
"Help": "Aide",
"Help Center": "Centre daide",
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
"Interrupt": "Interrompre",
"Load Default Workflow": "Charger le flux de travail par défaut",
"Manage group nodes": "Gérer les nœuds de groupe",
"Manager": "Gestionnaire",
"Minimap": "Minicarte",
"Model Library": "Bibliothèque de modèles",
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
"New": "Nouveau",
"Next Opened Workflow": "Prochain flux de travail ouvert",
"Node Library": "Bibliothèque de nœuds",
"Node Links": "Liens de nœuds",
"Open": "Ouvrir",
"Open 3D Viewer (Beta) for Selected Node": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné",
"Open Custom Nodes Folder": "Ouvrir le dossier des nœuds personnalisés",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
"Previous Opened Workflow": "Flux de travail ouvert précédent",
"Queue Panel": "Panneau de file dattente",
"Queue Prompt": "Invite de file d'attente",
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
"Queue Selected Output Nodes": "Mettre en file dattente les nœuds de sortie sélectionnés",
@@ -840,24 +831,28 @@
"Save": "Enregistrer",
"Save As": "Enregistrer sous",
"Show Keybindings Dialog": "Afficher la boîte de dialogue des raccourcis clavier",
"Show Model Selector (Dev)": "Afficher le sélecteur de modèle (Dev)",
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
"Sign Out": "Se déconnecter",
"Toggle Bottom Panel": "Basculer le panneau inférieur",
"Toggle Essential Bottom Panel": "Afficher/Masquer le panneau inférieur essentiel",
"Toggle Focus Mode": "Basculer le mode focus",
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file dattente",
"Toggle Search Box": "Basculer la boîte de recherche",
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
"Toggle View Controls Bottom Panel": "Afficher/Masquer le panneau inférieur des contrôles de vue",
"Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows",
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
"Undo": "Annuler",
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
"Workflows": "Flux de travail",
"Workflow": "Flux de travail",
"Zoom In": "Zoom avant",
"Zoom Out": "Zoom arrière",
"Zoom to fit": "Ajuster à lécran"
"Zoom Out": "Zoom arrière"
},
"minimap": {
"nodeColors": "Couleurs des nœuds",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "サポートに連絡"
},
"Comfy_Dev_ShowModelSelector": {
"label": "モデルセレクターを表示(開発用)"
},
"Comfy_DuplicateWorkflow": {
"label": "現在のワークフローを複製"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "クリップスペース"
},
"Comfy_OpenManagerDialog": {
"label": "マネージャー"
},
"Comfy_OpenWorkflow": {
"label": "ワークフローを開く"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "設定ダイアログを表示"
},
"Comfy_ToggleCanvasInfo": {
"label": "キャンバスパフォーマンス"
},
"Comfy_ToggleHelpCenter": {
"label": "ヘルプセンター"
},
"Comfy_ToggleTheme": {
"label": "テーマの切り替え(ダーク/ライト)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "拡張機能の管理",
"onChange": "変更時",
"onChangeTooltip": "変更が行われるとワークフローがキューに追加されます",
"queue": "キューパネル",
"refresh": "ノードを更新",
"resetView": "ビューをリセット",
"run": "実行する",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "ComfyUIについて",
"Add Edit Model Step": "モデル編集ステップを追加",
"Bottom Panel": "下部パネル",
"Browse Templates": "テンプレートを参照",
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Performance": "キャンバスパフォーマンス",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
"Check for Updates": "更新を確認する",
"Clear Pending Tasks": "保留中のタスクをクリア",
"Clear Workflow": "ワークフローをクリア",
@@ -789,20 +788,15 @@
"Exit Subgraph": "サブグラフを終了",
"Export": "エクスポート",
"Export (API)": "エクスポート (API)",
"File": "ファイル",
"Fit Group To Contents": "グループを内容に合わせる",
"Focus Mode": "フォーカスモード",
"Fit view to selected nodes": "選択したノードにビューを合わせる",
"Give Feedback": "フィードバックを送る",
"Group Selected Nodes": "選択したノードをグループ化",
"Help": "ヘルプ",
"Help Center": "ヘルプセンター",
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
"Interrupt": "中断",
"Load Default Workflow": "デフォルトワークフローを読み込む",
"Manage group nodes": "グループノードを管理",
"Manager": "マネージャー",
"Minimap": "ミニマップ",
"Model Library": "モデルライブラリ",
"Move Selected Nodes Down": "選択したノードを下へ移動",
"Move Selected Nodes Left": "選択したノードを左へ移動",
"Move Selected Nodes Right": "選択したノードを右へ移動",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
"New": "新規",
"Next Opened Workflow": "次に開いたワークフロー",
"Node Library": "ノードライブラリ",
"Node Links": "ノードリンク",
"Open": "開く",
"Open 3D Viewer (Beta) for Selected Node": "選択したードの3Dビューアーベータを開く",
"Open Custom Nodes Folder": "カスタムノードフォルダを開く",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
"Previous Opened Workflow": "前に開いたワークフロー",
"Queue Panel": "キューパネル",
"Queue Prompt": "キューのプロンプト",
"Queue Prompt (Front)": "キューのプロンプト (前面)",
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
@@ -840,24 +831,28 @@
"Save": "保存",
"Save As": "名前を付けて保存",
"Show Keybindings Dialog": "キーバインドダイアログを表示",
"Show Model Selector (Dev)": "モデルセレクターを表示(開発用)",
"Show Settings Dialog": "設定ダイアログを表示",
"Sign Out": "サインアウト",
"Toggle Bottom Panel": "下部パネルの切り替え",
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",
"Toggle Focus Mode": "フォーカスモードの切り替え",
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
"Toggle Search Box": "検索ボックスの切り替え",
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
"Toggle View Controls Bottom Panel": "ビューコントロール下部パネルの切り替え",
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
"Undo": "元に戻す",
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
"Unpack the selected Subgraph": "選択したサブグラフを展開",
"Workflows": "ワークフロー",
"Workflow": "ワークフロー",
"Zoom In": "ズームイン",
"Zoom Out": "ズームアウト",
"Zoom to fit": "全体表示にズーム"
"Zoom Out": "ズームアウト"
},
"minimap": {
"nodeColors": "ノードの色",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "지원팀에 문의하기"
},
"Comfy_Dev_ShowModelSelector": {
"label": "모델 선택기 표시 (개발자용)"
},
"Comfy_DuplicateWorkflow": {
"label": "현재 워크플로 복제"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "클립스페이스"
},
"Comfy_OpenManagerDialog": {
"label": "매니저"
},
"Comfy_OpenWorkflow": {
"label": "워크플로 열기"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "설정 대화상자 보기"
},
"Comfy_ToggleCanvasInfo": {
"label": "캔버스 성능"
},
"Comfy_ToggleHelpCenter": {
"label": "도움말 센터"
},
"Comfy_ToggleTheme": {
"label": "밝기 테마 전환 (어두운/밝은)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "확장 프로그램 관리",
"onChange": "변경 시",
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
"queue": "대기열 패널",
"refresh": "노드 정의 새로 고침",
"resetView": "캔버스 보기 재설정",
"run": "실행",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "ComfyUI에 대하여",
"Add Edit Model Step": "모델 편집 단계 추가",
"Bottom Panel": "하단 패널",
"Browse Templates": "템플릿 탐색",
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
"Canvas Performance": "캔버스 성",
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
"Canvas Toggle Lock": "캔버스 토글 잠금",
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
"Check for Updates": "업데이트 확인",
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
@@ -789,20 +788,15 @@
"Exit Subgraph": "서브그래프 종료",
"Export": "내보내기",
"Export (API)": "내보내기 (API)",
"File": "파일",
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
"Focus Mode": "포커스 모드",
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
"Give Feedback": "피드백 제공",
"Group Selected Nodes": "선택한 노드 그룹화",
"Help": "도움말",
"Help Center": "도움말 센터",
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
"Interrupt": "중단",
"Load Default Workflow": "기본 워크플로 불러오기",
"Manage group nodes": "그룹 노드 관리",
"Manager": "매니저",
"Minimap": "미니맵",
"Model Library": "모델 라이브러리",
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
"New": "새로 만들기",
"Next Opened Workflow": "다음 열린 워크플로",
"Node Library": "노드 라이브러리",
"Node Links": "노드 링크",
"Open": "열기",
"Open 3D Viewer (Beta) for Selected Node": "선택한 노드에 대해 3D 뷰어(베타) 열기",
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
"Previous Opened Workflow": "이전 열린 워크플로",
"Queue Panel": "대기열 패널",
"Queue Prompt": "실행 대기열에 프롬프트 추가",
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
@@ -839,25 +830,29 @@
"Restart": "재시작",
"Save": "저장",
"Save As": "다른 이름으로 저장",
"Show Keybindings Dialog": "단축키 대화상자 표시",
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
"Show Keybindings Dialog": "키 바인딩 대화상자 표시",
"Show Settings Dialog": "설정 대화상자 표시",
"Sign Out": "로그아웃",
"Toggle Bottom Panel": "하단 패널 전환",
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
"Toggle Focus Mode": "포커스 모드 전환",
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
"Toggle Search Box": "검색 상자 전환",
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
"Undo": "실행 취소",
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
"Unpack the selected Subgraph": "선택한 서브그래프 풀기",
"Workflows": "워크플로",
"Workflow": "워크플로",
"Zoom In": "확대",
"Zoom Out": "축소",
"Zoom to fit": "화면에 맞추기"
"Zoom Out": "축소"
},
"minimap": {
"nodeColors": "노드 색상",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "Связаться с поддержкой"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Показать выбор модели (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Дублировать текущий рабочий процесс"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "Клипспейс"
},
"Comfy_OpenManagerDialog": {
"label": "Менеджер"
},
"Comfy_OpenWorkflow": {
"label": "Открыть рабочий процесс"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "Показать диалог настроек"
},
"Comfy_ToggleCanvasInfo": {
"label": "Производительность холста"
},
"Comfy_ToggleHelpCenter": {
"label": "Центр поддержки"
},
"Comfy_ToggleTheme": {
"label": "Переключить тему (Тёмная/Светлая)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "Управление расширениями",
"onChange": "При изменении",
"onChangeTooltip": "Рабочий процесс будет поставлен в очередь после внесения изменений",
"queue": "Панель очереди",
"refresh": "Обновить определения нод",
"resetView": "Сбросить вид холста",
"run": "Запустить",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "О ComfyUI",
"Add Edit Model Step": "Добавить или изменить шаг модели",
"Bottom Panel": "Нижняя панель",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Performance": "Производительность холста",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
"Check for Updates": "Проверить наличие обновлений",
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
@@ -789,20 +788,15 @@
"Exit Subgraph": "Выйти из подграфа",
"Export": "Экспортировать",
"Export (API)": "Экспорт (API)",
"File": "Файл",
"Fit Group To Contents": "Подогнать группу под содержимое",
"Focus Mode": "Режим фокуса",
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
"Give Feedback": "Оставить отзыв",
"Group Selected Nodes": "Сгруппировать выбранные ноды",
"Help": "Помощь",
"Help Center": "Центр поддержки",
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
"Interrupt": "Прервать",
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
"Manage group nodes": "Управление групповыми нодами",
"Manager": "Менеджер",
"Minimap": "Мини-карта",
"Model Library": "Библиотека моделей",
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
"New": "Новый",
"Next Opened Workflow": "Следующий открытый рабочий процесс",
"Node Library": "Библиотека узлов",
"Node Links": "Связи узлов",
"Open": "Открыть",
"Open 3D Viewer (Beta) for Selected Node": "Открыть 3D-просмотрщик (бета) для выбранного узла",
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
"Queue Panel": "Панель очереди",
"Queue Prompt": "Запрос в очереди",
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
@@ -840,24 +831,28 @@
"Save": "Сохранить",
"Save As": "Сохранить как",
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
"Show Model Selector (Dev)": "Показать выбор модели (Dev)",
"Show Settings Dialog": "Показать диалог настроек",
"Sign Out": "Выйти",
"Toggle Bottom Panel": "Переключить нижнюю панель",
"Toggle Essential Bottom Panel": "Показать/скрыть основную нижнюю панель",
"Toggle Focus Mode": "Переключить режим фокуса",
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
"Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей",
"Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов",
"Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди",
"Toggle Search Box": "Переключить поисковую панель",
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
"Toggle View Controls Bottom Panel": "Показать/скрыть панель управления просмотром",
"Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов",
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
"Workflows": "Рабочие процессы",
"Workflow": "Рабочий процесс",
"Zoom In": "Увеличить",
"Zoom Out": "Уменьшить",
"Zoom to fit": "Масштабировать по размеру"
"Zoom Out": "Уменьшить"
},
"minimap": {
"nodeColors": "Цвета узлов",

View File

@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "聯絡支援"
},
"Comfy_Dev_ShowModelSelector": {
"label": "顯示模型選擇器(開發)"
},
"Comfy_DuplicateWorkflow": {
"label": "複製目前工作流程"
},
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenManagerDialog": {
"label": "管理器"
},
"Comfy_OpenWorkflow": {
"label": "開啟工作流程"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "顯示設定對話框"
},
"Comfy_ToggleCanvasInfo": {
"label": "畫布效能"
},
"Comfy_ToggleHelpCenter": {
"label": "說明中心"
},
"Comfy_ToggleTheme": {
"label": "切換主題(深色/淺色)"
},

View File

@@ -749,7 +749,6 @@
"manageExtensions": "管理擴充功能",
"onChange": "變更時",
"onChangeTooltip": "每當有變更時,工作流程會排入佇列",
"queue": "佇列面板",
"refresh": "重新整理節點定義",
"resetView": "重設畫布視圖",
"run": "執行",
@@ -763,11 +762,11 @@
"menuLabels": {
"About ComfyUI": "關於 ComfyUI",
"Add Edit Model Step": "新增編輯模型步驟",
"Bottom Panel": "底部面板",
"Browse Templates": "瀏覽範本",
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
"Canvas Performance": "畫布效能",
"Canvas Toggle Link Visibility": "切換連結可見性",
"Canvas Toggle Lock": "切換畫布鎖定",
"Canvas Toggle Minimap": "畫布切換小地圖",
"Check for Updates": "檢查更新",
"Clear Pending Tasks": "清除待處理任務",
"Clear Workflow": "清除工作流程",
@@ -789,20 +788,15 @@
"Exit Subgraph": "離開子圖",
"Export": "匯出",
"Export (API)": "匯出API",
"File": "檔案",
"Fit Group To Contents": "群組貼合內容",
"Focus Mode": "專注模式",
"Fit view to selected nodes": "視圖貼合選取節點",
"Give Feedback": "提供意見回饋",
"Group Selected Nodes": "群組選取節點",
"Help": "說明",
"Help Center": "說明中心",
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
"Interrupt": "中斷",
"Load Default Workflow": "載入預設工作流程",
"Manage group nodes": "管理群組節點",
"Manager": "管理員",
"Minimap": "縮圖地圖",
"Model Library": "模型庫",
"Move Selected Nodes Down": "選取節點下移",
"Move Selected Nodes Left": "選取節點左移",
"Move Selected Nodes Right": "選取節點右移",
@@ -810,8 +804,6 @@
"Mute/Unmute Selected Nodes": "靜音/取消靜音選取節點",
"New": "新增",
"Next Opened Workflow": "下一個已開啟的工作流程",
"Node Library": "節點庫",
"Node Links": "節點連結",
"Open": "開啟",
"Open 3D Viewer (Beta) for Selected Node": "為選取的節點開啟 3D 檢視器Beta 版)",
"Open Custom Nodes Folder": "開啟自訂節點資料夾",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "釘選/取消釘選選取項目",
"Pin/Unpin Selected Nodes": "釘選/取消釘選選取節點",
"Previous Opened Workflow": "上一個已開啟的工作流程",
"Queue Panel": "佇列面板",
"Queue Prompt": "加入提示至佇列",
"Queue Prompt (Front)": "將提示加入佇列前端",
"Queue Selected Output Nodes": "將選取的輸出節點加入佇列",
@@ -840,24 +831,28 @@
"Save": "儲存",
"Save As": "另存新檔",
"Show Keybindings Dialog": "顯示快捷鍵對話框",
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
"Show Settings Dialog": "顯示設定對話框",
"Sign Out": "登出",
"Toggle Bottom Panel": "切換下方面板",
"Toggle Essential Bottom Panel": "切換基本下方面板",
"Toggle Focus Mode": "切換專注模式",
"Toggle Logs Bottom Panel": "切換日誌下方面板",
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
"Toggle Queue Sidebar": "切換佇列側邊欄",
"Toggle Search Box": "切換搜尋框",
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
"Toggle View Controls Bottom Panel": "切換檢視控制下方面板",
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
"Undo": "復原",
"Ungroup selected group nodes": "取消群組選取的群組節點",
"Unpack the selected Subgraph": "解包所選子圖",
"Workflows": "工作流程",
"Workflow": "工作流程",
"Zoom In": "放大",
"Zoom Out": "縮小",
"Zoom to fit": "縮放至適合大小"
"Zoom Out": "縮小"
},
"minimap": {
"nodeColors": "節點顏色",

View File

@@ -36,7 +36,7 @@
"label": "重启"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "为所选节点开启 3D 浏览Beta 版)"
"label": "為所選節點開啟 3D 檢視Beta 版)"
},
"Comfy_BrowseTemplates": {
"label": "浏览模板"
@@ -75,7 +75,7 @@
"label": "锁定视图"
},
"Comfy_Canvas_ToggleMinimap": {
"label": "布切小地"
"label": "布切小地"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "忽略/取消忽略选中节点"
@@ -107,9 +107,6 @@
"Comfy_ContactSupport": {
"label": "联系支持"
},
"Comfy_Dev_ShowModelSelector": {
"label": "顯示模型選擇器(開發)"
},
"Comfy_DuplicateWorkflow": {
"label": "复制当前工作流"
},
@@ -126,7 +123,7 @@
"label": "将选区转换为子图"
},
"Comfy_Graph_ExitSubgraph": {
"label": "退出子"
"label": "退出子"
},
"Comfy_Graph_FitGroupToContents": {
"label": "适应节点框到内容"
@@ -135,7 +132,7 @@
"label": "添加框到选中节点"
},
"Comfy_Graph_UnpackSubgraph": {
"label": "解开所选子图"
"label": "解開所選子圖"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "将选中节点转换为组节点"
@@ -174,10 +171,10 @@
"label": "切换进度对话框"
},
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "小 MaskEditor 中的刷大小"
"label": "小 MaskEditor 中的刷大小"
},
"Comfy_MaskEditor_BrushSize_Increase": {
"label": "增加 MaskEditor 画笔大小"
"label": "增加 MaskEditor 畫筆大小"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "打开选中节点的遮罩编辑器"
@@ -188,9 +185,6 @@
"Comfy_OpenClipspace": {
"label": "打开剪贴板"
},
"Comfy_OpenManagerDialog": {
"label": "管理器"
},
"Comfy_OpenWorkflow": {
"label": "打开工作流"
},
@@ -218,12 +212,6 @@
"Comfy_ShowSettingsDialog": {
"label": "显示设置对话框"
},
"Comfy_ToggleCanvasInfo": {
"label": "画布性能"
},
"Comfy_ToggleHelpCenter": {
"label": "说明中心"
},
"Comfy_ToggleTheme": {
"label": "切换主题"
},
@@ -258,13 +246,13 @@
"label": "切换日志底部面板"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "切换基础底部面板"
"label": "切換基本下方面板"
},
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
"label": "切换视图控制底部面板"
"label": "切換檢視控制底部面板"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "示快捷键对话框"
"label": "示快捷鍵對話框"
},
"Workspace_ToggleFocusMode": {
"label": "切换焦点模式"

View File

@@ -84,9 +84,9 @@
},
"breadcrumbsMenu": {
"clearWorkflow": "清除工作流程",
"deleteWorkflow": "除工作流程",
"duplicate": "复制",
"enterNewName": "入新名"
"deleteWorkflow": "除工作流程",
"duplicate": "複製",
"enterNewName": "入新名"
},
"chatHistory": {
"cancelEdit": "取消",
@@ -272,7 +272,7 @@
"category": "类别",
"choose_file_to_upload": "选择要上传的文件",
"clear": "清除",
"clearFilters": "清除筛选",
"clearFilters": "清除篩選",
"close": "关闭",
"color": "颜色",
"comingSoon": "即将推出",
@@ -297,7 +297,7 @@
"devices": "设备",
"disableAll": "禁用全部",
"disabling": "禁用中",
"dismiss": "关闭",
"dismiss": "關閉",
"download": "下载",
"duplicate": "复制",
"edit": "编辑",
@@ -314,7 +314,7 @@
"findIssues": "查找问题",
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
"goToNode": "转到节点",
"help": "帮助",
"icon": "图标",
@@ -401,7 +401,7 @@
"usageHint": "使用提示",
"user": "用户",
"versionMismatchWarning": "版本相容性警告",
"versionMismatchWarningMessage": "{warning}{detail} 请参阅 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新明。",
"versionMismatchWarningMessage": "{warning}{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新明。",
"videoFailedToLoad": "视频加载失败",
"workflow": "工作流"
},
@@ -411,7 +411,7 @@
"resetView": "重置视图",
"selectMode": "选择模式",
"toggleLinkVisibility": "切换连线可见性",
"toggleMinimap": "切小地",
"toggleMinimap": "切小地",
"zoomIn": "放大",
"zoomOut": "缩小"
},
@@ -594,7 +594,7 @@
"wireframe": "线框"
},
"model": "模型",
"openIn3DViewer": "在 3D 查看器中打开",
"openIn3DViewer": "在 3D 檢視器中開啟",
"previewOutput": "预览输出",
"removeBackgroundImage": "移除背景图片",
"resizeNodeMatchOutput": "调整节点以匹配输出",
@@ -611,15 +611,15 @@
"uploadBackgroundImage": "上传背景图片",
"uploadTexture": "上传纹理",
"viewer": {
"apply": "用",
"cameraSettings": "相机设置",
"cameraType": "相机类型",
"apply": "用",
"cameraSettings": "相機設定",
"cameraType": "相機類型",
"cancel": "取消",
"exportSettings": "导出设置",
"lightSettings": "灯光设置",
"modelSettings": "模型设置",
"sceneSettings": "场景设置",
"title": "3D 查看器(测试版)"
"exportSettings": "匯出設定",
"lightSettings": "燈光設定",
"modelSettings": "模型設定",
"sceneSettings": "場景設定",
"title": "3D 檢視器(測試版)"
}
},
"loadWorkflowWarning": {
@@ -740,7 +740,7 @@
"disabled": "禁用",
"disabledTooltip": "工作流将不会自动执行",
"execute": "执行",
"help": "明",
"help": "明",
"hideMenu": "隐藏菜单",
"instant": "实时",
"instantTooltip": "工作流将会在生成完成后立即执行",
@@ -749,25 +749,24 @@
"manageExtensions": "管理擴充功能",
"onChange": "更改时",
"onChangeTooltip": "一旦进行更改,工作流将添加到执行队列",
"queue": "队列面板",
"refresh": "刷新节点",
"resetView": "重置视图",
"run": "运行",
"runWorkflow": "运行工作流程Shift排在前面",
"runWorkflowFront": "运行工作流程(排在前面)",
"settings": "定",
"settings": "定",
"showMenu": "显示菜单",
"theme": "主",
"theme": "主",
"toggleBottomPanel": "底部面板"
},
"menuLabels": {
"About ComfyUI": "关于ComfyUI",
"Add Edit Model Step": "添加编辑模型步骤",
"Bottom Panel": "底部面板",
"Browse Templates": "浏览模板",
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
"Canvas Performance": "画布性能",
"Canvas Toggle Link Visibility": "切换连线可见性",
"Canvas Toggle Lock": "切换视图锁定",
"Canvas Toggle Minimap": "畫布切換小地圖",
"Check for Updates": "检查更新",
"Clear Pending Tasks": "清除待处理任务",
"Clear Workflow": "清除工作流",
@@ -781,28 +780,23 @@
"Contact Support": "联系支持",
"Convert Selection to Subgraph": "将选中内容转换为子图",
"Convert selected nodes to group node": "将选中节点转换为组节点",
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中减小笔刷大小",
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
"Delete Selected Items": "删除选定的项目",
"Desktop User Guide": "桌面端用户指南",
"Duplicate Current Workflow": "复制当前工作流",
"Edit": "编辑",
"Exit Subgraph": "退出子",
"Exit Subgraph": "退出子",
"Export": "导出",
"Export (API)": "导出 (API)",
"File": "文件",
"Fit Group To Contents": "适应组内容",
"Focus Mode": "专注模式",
"Fit view to selected nodes": "适应视图到选中节点",
"Give Feedback": "提供反馈",
"Group Selected Nodes": "将选中节点转换为组节点",
"Help": "帮助",
"Help Center": "帮助中心",
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大笔刷大小",
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
"Interrupt": "中断",
"Load Default Workflow": "加载默认工作流",
"Manage group nodes": "管理组节点",
"Manager": "管理器",
"Minimap": "小地图",
"Model Library": "模型库",
"Move Selected Nodes Down": "下移所选节点",
"Move Selected Nodes Left": "左移所选节点",
"Move Selected Nodes Right": "右移所选节点",
@@ -810,10 +804,8 @@
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
"New": "新建",
"Next Opened Workflow": "下一个打开的工作流",
"Node Library": "节点库",
"Node Links": "节点连接",
"Open": "打开",
"Open 3D Viewer (Beta) for Selected Node": "为选中节点打开3D查看器测试版)",
"Open 3D Viewer (Beta) for Selected Node": "為所選節點開啟 3D 檢視器Beta 版)",
"Open Custom Nodes Folder": "打开自定义节点文件夹",
"Open DevTools": "打开开发者工具",
"Open Inputs Folder": "打开输入文件夹",
@@ -826,7 +818,6 @@
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
"Previous Opened Workflow": "上一个打开的工作流",
"Queue Panel": "队列面板",
"Queue Prompt": "执行提示词",
"Queue Prompt (Front)": "执行提示词 (优先执行)",
"Queue Selected Output Nodes": "将所选输出节点加入队列",
@@ -840,31 +831,35 @@
"Save": "保存",
"Save As": "另存为",
"Show Keybindings Dialog": "顯示快捷鍵對話框",
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
"Show Settings Dialog": "显示设置对话框",
"Sign Out": "退出登录",
"Toggle Essential Bottom Panel": "切换基础底部面板",
"Toggle Bottom Panel": "切换底部面板",
"Toggle Essential Bottom Panel": "切換基本下方面板",
"Toggle Focus Mode": "切换专注模式",
"Toggle Logs Bottom Panel": "切换日志底部面板",
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
"Toggle Queue Sidebar": "切換佇列側邊欄",
"Toggle Search Box": "切换搜索框",
"Toggle Terminal Bottom Panel": "切换终端底部面板",
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
"Toggle View Controls Bottom Panel": "切换视图控制底部面板",
"Toggle View Controls Bottom Panel": "切換檢視控制下方面板",
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
"Undo": "撤销",
"Ungroup selected group nodes": "解散选中组节点",
"Unpack the selected Subgraph": "解包选中子图",
"Workflows": "工作流",
"Unpack the selected Subgraph": "解開所選子圖",
"Workflow": "工作流",
"Zoom In": "放大画面",
"Zoom Out": "缩小画面",
"Zoom to fit": "缩放以适应"
"Zoom Out": "缩小画面"
},
"minimap": {
"nodeColors": "节点颜色",
"renderBypassState": "渲染绕过状态",
"renderErrorState": "渲染错误状态",
"showGroups": "示框架/分组",
"showLinks": "显示连接"
"nodeColors": "節點顏色",
"renderBypassState": "顯示繞過狀態",
"renderErrorState": "顯示錯誤狀態",
"showGroups": "示框架/群組",
"showLinks": "顯示連結"
},
"missingModelsDialog": {
"doNotAskAgain": "不再显示此消息",
@@ -1132,7 +1127,7 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "3D查看器",
"3DViewer": "3D 檢視器",
"API Nodes": "API 节点",
"About": "关于",
"Appearance": "外观",
@@ -1185,18 +1180,18 @@
"Workflow": "工作流"
},
"shortcuts": {
"essentials": "常用",
"keyboardShortcuts": "键盘快捷",
"manageShortcuts": "管理快捷",
"noKeybinding": "快捷",
"essentials": "基本功能",
"keyboardShortcuts": "鍵盤快捷",
"manageShortcuts": "管理快捷",
"noKeybinding": "快捷",
"subcategories": {
"node": "节点",
"node": "節點",
"panelControls": "面板控制",
"queue": "列",
"view": "视图",
"workflow": "工作流"
"queue": "列",
"view": "檢視",
"workflow": "工作流"
},
"viewControls": "视图控制"
"viewControls": "檢視控制"
},
"sideToolbar": {
"browseTemplates": "浏览示例模板",
@@ -1204,10 +1199,10 @@
"helpCenter": "帮助中心",
"labels": {
"models": "模型",
"nodes": "节点",
"queue": "列",
"templates": "模板",
"workflows": "工作流"
"nodes": "節點",
"queue": "列",
"templates": "範本",
"workflows": "工作流"
},
"logout": "登出",
"modelLibrary": "模型库",
@@ -1246,7 +1241,7 @@
},
"showFlatList": "平铺结果"
},
"templates": "模板",
"templates": "範本",
"workflowTab": {
"confirmDelete": "您确定要删除此工作流吗?",
"confirmDeleteTitle": "删除工作流?",
@@ -1293,8 +1288,8 @@
"Video": "视频生成",
"Video API": "视频 API"
},
"loadingMore": "正在加载更多模板...",
"searchPlaceholder": "搜索模板...",
"loadingMore": "正在載入更多範本...",
"searchPlaceholder": "搜尋範本...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "混元3D 2.0 图生模型",
@@ -1617,7 +1612,7 @@
"failedToExportModel": "无法将模型导出为 {format}",
"failedToFetchBalance": "获取余额失败:{error}",
"failedToFetchLogs": "无法获取服务器日志",
"failedToInitializeLoad3dViewer": "初始化3D查看器失",
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失",
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
"failedToPurchaseCredits": "购买积分失败:{error}",
"fileLoadError": "无法在 {fileName} 中找到工作流",
@@ -1674,9 +1669,9 @@
"required": "必填"
},
"versionMismatchWarning": {
"dismiss": "关闭",
"dismiss": "關閉",
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
"frontendOutdated": "前端版本 {frontendVersion} 已过时。後端需要 {requiredVersion} 版或更高版本。",
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 版或更高版本。",
"title": "版本相容性警告",
"updateFrontend": "更新前端"
},

View File

@@ -30,10 +30,10 @@
"tooltip": "画布背景的图像 URL。你可以在输出面板中右键点击一张图片并选择“设为背景”来使用它。"
},
"Comfy_Canvas_NavigationMode": {
"name": "画布导航模式",
"name": "畫布導航模式",
"options": {
"Left-Click Pan (Legacy)": "左拖曳(版)",
"Standard (New)": "标准(新)"
"Left-Click Pan (Legacy)": "左拖曳(版)",
"Standard (New)": "標準(新)"
}
},
"Comfy_Canvas_SelectionToolbox": {
@@ -120,8 +120,8 @@
}
},
"Comfy_Load3D_3DViewerEnable": {
"name": "启用3D查看器测试版)",
"tooltip": "为选定节点启用3D查看器测试版)。此功能允许你在全尺寸3D查看器中直接可视化和交互3D模型。"
"name": "啟用 3D 檢視器(測試版)",
"tooltip": "為所選節點啟用 3D 檢視器(測試版)。此功能可讓您直接在全尺寸 3D 檢視器中瀏覽並互動 3D 模型。"
},
"Comfy_Load3D_BackgroundColor": {
"name": "初始背景颜色",
@@ -338,7 +338,7 @@
"Disabled": "禁用",
"Top": "顶部"
},
"tooltip": "选单列位置。在行动装置上,选单始终显示于顶端。"
"tooltip": "選單列位置。在行動裝置上,選單始終顯示於頂端。"
},
"Comfy_Validation_Workflows": {
"name": "校验工作流"

View File

@@ -1,462 +0,0 @@
# GraphMutationService Design and Implementation
## Overview
GraphMutationService is the centralized service layer for all graph modification operations in ComfyUI Frontend. It provides a unified and validated API for graph mutations, serving as the single entry point for all graph modification operations.
## Project Background
### Current System Analysis
ComfyUI Frontend uses the LiteGraph library for graph operations, with main components including:
1. **LGraph** (`src/lib/litegraph/src/LGraph.ts`)
- Core graph management class
- Provides basic operations like `add()`, `remove()`
- Supports `beforeChange()`/`afterChange()` transaction mechanism
2. **LGraphNode** (`src/lib/litegraph/src/LGraphNode.ts`)
- Node class containing position, connections, and other properties
- Provides methods like `connect()`, `disconnectInput()`, `disconnectOutput()`
3. **ChangeTracker** (`src/scripts/changeTracker.ts`)
- Existing undo/redo system
- Snapshot-based history tracking
- Supports up to 50 history states
**Primary Goals:**
- Single entry point for all graph modifications
- Built-in validation and error handling
- Transaction support for atomic operations
- Natural undo/redo through existing ChangeTracker
- Clean architecture for future extensibility
### Interface-Based Architecture
The GraphMutationService follows an **interface-based design pattern** with singleton state management:
- **IGraphMutationService Interface**: Defines the complete contract for all graph operations
- **GraphMutationService Class**: Implements the interface with LiteGraph integration
- **Singleton State**: Shared clipboard and transaction state across components
```typescript
interface IGraphMutationService {
// Node operations
addNode(params: AddNodeParams): Promise<NodeId>
removeNode(nodeId: NodeId): Promise<void>
updateNodeProperty(nodeId: NodeId, property: string, value: any): Promise<void>
// ... 50+ total operations
// Transaction support
transaction<T>(fn: () => Promise<T>): Promise<T>
// Undo/Redo
undo(): Promise<void>
redo(): Promise<void>
}
class GraphMutationService implements IGraphMutationService {
// Implementation details...
}
```
The `useGraphMutationService()` hook returns the interface type, maintaining backward compatibility while enabling new architectural benefits.
### Core Components
```typescript
// Interface Definition
interface IGraphMutationService {
// Complete method signatures for all 50+ operations
// Organized by functional categories
}
// Implementation Class
class GraphMutationService implements IGraphMutationService {
private workflowStore = useWorkflowStore()
private transactionDepth = 0
private clipboard: ClipboardData | null = null
// All interface methods implemented
}
// Singleton Hook
export const useGraphMutationService = (): IGraphMutationService => {
if (!graphMutationServiceInstance) {
graphMutationServiceInstance = new GraphMutationService()
}
return graphMutationServiceInstance
}
```
### Validation Framework
Each operation includes validation to ensure data integrity:
```typescript
interface ValidationResult {
isValid: boolean
errors: ValidationError[]
warnings: ValidationWarning[]
}
```
## Implemented Operations
### Node Operations (6 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `addNode` | Add a new node to the graph | src/scripts/app.ts:1589-1593, src/lib/litegraph/src/LGraph.ts:823-893 |
| `removeNode` | Remove a node from the graph | src/lib/litegraph/src/LGraph.ts:899-986 |
| `updateNodeProperty` | Update a custom node property | src/lib/litegraph/src/LGraphNode.ts:974-984 |
| `updateNodeTitle` | Change the node's title | src/services/litegraphService.ts:369 (direct assignment) |
| `changeNodeMode` | Change execution mode (ALWAYS/ON_TRIGGER/NEVER/ON_REQUEST/ON_EVENT) | src/lib/litegraph/src/LGraphNode.ts:1295-1320 |
| `cloneNode` | Create a copy of a node | src/lib/litegraph/src/LGraphNode.ts:923-950 |
### Connection Operations (6 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `connect` | Create a connection between nodes | src/lib/litegraph/src/LGraphNode.ts:2641-2743, :2753-2870 (connectSlots) |
| `disconnect` | Generic disconnect (auto-detects input/output) | Wrapper combining disconnectInput/disconnectOutput |
| `disconnectInput` | Disconnect a specific input slot | src/lib/litegraph/src/LGraphNode.ts:3050-3144 |
| `disconnectOutput` | Disconnect all connections from an output slot | src/lib/litegraph/src/LGraphNode.ts:2931-3043 |
| `disconnectOutputTo` | Disconnect output to a specific target node | src/lib/litegraph/src/LGraphNode.ts:2931 (with target_node param) |
| `disconnectLink` | Disconnect by link ID | src/lib/litegraph/src/LGraph.ts:1433-1441 |
### Group Operations (6 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `createGroup` | Create a new node group | src/composables/useCoreCommands.ts:425-430, src/lib/litegraph/src/LGraph.ts:823-848 (add method for groups) |
| `removeGroup` | Delete a group (nodes remain) | src/lib/litegraph/src/LGraph.ts:899-913 (remove method for groups) |
| `updateGroupTitle` | Change group title | Direct assignment (group.title = value) |
| `moveGroup` | Move group and its contents | src/lib/litegraph/src/LGraphGroup.ts:230-240 |
| `addNodesToGroup` | Add nodes to group and auto-resize | src/lib/litegraph/src/LGraphGroup.ts:303-306 |
| `recomputeGroupNodes` | Recalculate which nodes are in group | src/lib/litegraph/src/LGraphGroup.ts:247-273 |
### Subgraph Node Slot Operations (4 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `addSubgraphNodeInput` | Add an input slot to a subgraph node | src/lib/litegraph/src/LGraphNode.ts:1606-1627 |
| `addSubgraphNodeOutput` | Add an output slot to a subgraph node | src/lib/litegraph/src/LGraphNode.ts:1551-1571 |
| `removeSubgraphNodeInput` | Remove an input slot from a subgraph node | src/lib/litegraph/src/LGraphNode.ts:1632-1652 |
| `removeSubgraphNodeOutput` | Remove an output slot from a subgraph node | src/lib/litegraph/src/LGraphNode.ts:1576-1599 |
### Batch Operations (3 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `addNodes` | Add multiple nodes in one operation | Custom implementation based on single addNode logic |
| `removeNodes` | Remove multiple nodes in one operation | src/composables/useCoreCommands.ts:180 (forEach pattern) |
| `duplicateNodes` | Duplicate selected nodes with their connections | src/utils/vintageClipboard.ts:32 (node.clone pattern) |
### Clipboard Operations (6 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `copyNodes` | Copy nodes to clipboard | src/lib/litegraph/src/LGraphCanvas.ts:3602-3687 |
| `cutNodes` | Cut nodes to clipboard | Custom implementation (copy + mark for deletion) |
| `pasteNodes` | Paste nodes from clipboard | src/lib/litegraph/src/LGraphCanvas.ts:3693-3871 |
| `getClipboard` | Get current clipboard content | Custom implementation (returns internal clipboard) |
| `clearClipboard` | Clear clipboard content | Custom implementation (sets clipboard to null) |
| `hasClipboardContent` | Check if clipboard has content | Custom implementation (checks clipboard state) |
### Reroute Operations (2 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `addReroute` | Add a reroute point on a connection | src/lib/litegraph/src/LGraph.ts:1338-1361 (createReroute) |
| `removeReroute` | Remove a reroute point | src/lib/litegraph/src/LGraph.ts:1381-1407 |
### Subgraph Operations (6 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `createSubgraph` | Create a subgraph from selected nodes | src/lib/litegraph/src/LGraph.ts:1459-1566 (convertToSubgraph) |
| `unpackSubgraph` | Unpack a subgraph node back into regular nodes | src/lib/litegraph/src/LGraph.ts:1672-1841 |
| `addSubgraphInput` | Add an input to a subgraph | src/lib/litegraph/src/LGraph.ts:2440-2456 |
| `addSubgraphOutput` | Add an output to a subgraph | src/lib/litegraph/src/LGraph.ts:2458-2474 |
| `removeSubgraphInput` | Remove a subgraph input | src/lib/litegraph/src/LGraph.ts:2520-2535 |
| `removeSubgraphOutput` | Remove a subgraph output | src/lib/litegraph/src/LGraph.ts:2541-2559 |
### Graph-level Operations (1 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `clearGraph` | Clear all nodes and connections | src/lib/litegraph/src/LGraph.ts:293-362 |
### Execution Control Operations (2 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `bypassNode` | Set node to bypass mode (never execute) | Direct mode assignment (node.mode = LGraphEventMode.BYPASS) |
| `unbypassNode` | Set node to normal mode (always execute) | Direct mode assignment (node.mode = LGraphEventMode.ALWAYS) |
### Transaction and History Operations (3 operations)
| Operation | Description | Original Implementation Reference |
|-----------|-------------|-----------------------------------|
| `transaction` | Execute multiple operations atomically | Custom implementation using beforeChange/afterChange |
| `undo` | Undo the last operation | src/scripts/changeTracker.ts (uses changeTracker.undo) |
| `redo` | Redo the previously undone operation | src/scripts/changeTracker.ts (uses changeTracker.redo) |
## Usage Examples
### Basic Node Operations
```typescript
import { useGraphMutationService } from '@/services/graphMutationService'
const service = useGraphMutationService()
// Add a node
const nodeId = await service.addNode({
type: 'LoadImage',
pos: [100, 100],
title: 'Image Loader'
})
// Update node properties
await service.updateNodeTitle(nodeId, 'My Image')
await service.updateNodeProperty(nodeId, 'seed', 12345)
// Clone a node
const clonedId = await service.cloneNode(nodeId, [300, 200])
```
### Connection Management
```typescript
// Create a connection
const linkId = await service.connect({
sourceNodeId: node1Id,
sourceSlot: 0,
targetNodeId: node2Id,
targetSlot: 0
})
// Various disconnect methods
await service.disconnectInput(node2Id, 0)
await service.disconnectOutput(node1Id, 0)
await service.disconnectLink(linkId)
```
### Group Management
```typescript
// Create a group
const groupId = await service.createGroup({
title: 'Image Processing',
pos: [100, 100],
size: [400, 300],
color: '#335577'
})
// Manage group content
await service.addNodesToGroup(groupId, [node1Id, node2Id])
await service.moveGroup(groupId, 50, 100) // deltaX, deltaY
await service.resizeGroup(groupId, [500, 400])
```
### Batch Operations
```typescript
// Add multiple nodes
const nodeIds = await service.addNodes([
{ type: 'LoadImage', pos: [100, 100] },
{ type: 'VAEEncode', pos: [300, 100] },
{ type: 'KSampler', pos: [500, 100] }
])
// Duplicate with connections preserved
const duplicatedIds = await service.duplicateNodes(
[node1Id, node2Id, node3Id],
[100, 100] // offset
)
// Batch delete
await service.removeNodes([node1Id, node2Id, node3Id])
```
### Clipboard Operations
```typescript
// Copy/Cut/Paste workflow
await service.copyNodes([node1Id, node2Id])
await service.cutNodes([node3Id, node4Id])
const pastedNodes = await service.pasteNodes([200, 200])
// Check clipboard
if (service.hasClipboardContent()) {
const clipboard = service.getClipboard()
console.log(`${clipboard.nodes.length} nodes in clipboard`)
}
```
### Transactions
```typescript
// Atomic operations
await service.transaction(async () => {
const node1 = await service.addNode({ type: 'LoadImage' })
const node2 = await service.addNode({ type: 'SaveImage' })
await service.connect({
sourceNodeId: node1,
sourceSlot: 0,
targetNodeId: node2,
targetSlot: 0
})
})
// Entire transaction can be undone as one operation
await service.undo()
```
### Graph-level Operations
```typescript
// Clear entire graph
await service.clearGraph()
// Distribute nodes evenly
await service.distributeNodes([node1Id, node2Id, node3Id], 'horizontal')
```
### Execution Control
```typescript
// Bypass node (set to never execute)
await service.bypassNode(nodeId)
// Re-enable node execution
await service.unbypassNode(nodeId)
```
### Subgraph Operations
```typescript
// Create subgraph from selected nodes
const subgraphId = await service.createSubgraph({
name: 'Image Processing',
nodeIds: [node1Id, node2Id, node3Id]
})
// Configure subgraph I/O
await service.addSubgraphInput(subgraphId, 'image', 'IMAGE')
await service.addSubgraphOutput(subgraphId, 'result', 'IMAGE')
// Add dynamic slots to subgraph nodes
await service.addSubgraphNodeInput({
nodeId: subgraphNodeId,
name: 'extra_input',
type: 'LATENT'
})
```
## Implementation Details
### Integration Points
1. **LiteGraph Integration**
- Uses `app.graph` for graph access
- Calls `beforeChange()`/`afterChange()` for transactions
- Integrates with existing LiteGraph node/connection APIs
2. **ChangeTracker Integration**
- Maintains compatibility with existing undo/redo system
- Calls `checkState()` after operations
- Provides undo/redo through existing tracker
## Validation System
### Current Validations (Placeholder)
- `validateAddNode()` - Check node type exists
- `validateRemoveNode()` - Check node can be removed
- `validateConnect()` - Check connection compatibility
- `validateUpdateNodePosition()` - Check position bounds
### Future Validations
- Type compatibility checking
- Circular dependency detection
- Resource limit enforcement
- Permission validation
- Business rule enforcement
## Technical Decisions
### Why Validation Layer?
- **Data Integrity**: Prevent invalid graph states
- **User Experience**: Early error detection
- **Security**: Prevent malicious operations
- **Extensibility**: Easy to add new rules
### Why Transaction Support?
- **Atomicity**: Multiple operations succeed or fail together
- **Consistency**: Graph remains valid throughout
- **User Experience**: Natural undo/redo boundaries
## Related Files
- **Interface Definition**: `src/services/IGraphMutationService.ts`
- **Implementation**: `src/services/GraphMutationService.ts`
- **LiteGraph Core**: `src/lib/litegraph/src/LGraph.ts`
- **Node Implementation**: `src/lib/litegraph/src/LGraphNode.ts`
- **Change Tracking**: `src/scripts/changeTracker.ts`
## Implementation Compatibility Notes
### Critical Implementation Details to Maintain:
1. **beforeChange/afterChange Pattern**
- All mutations MUST be wrapped with `graph.beforeChange()` and `graph.afterChange()`
- This enables undo/redo functionality through ChangeTracker
- Reference: `src/scripts/changeTracker.ts:200-208`
2. **Node ID Management**
- Node IDs can be numbers or strings (for API compatibility)
- Reference: `src/scripts/app.ts:1591` - `node.id = isNaN(+id) ? id : +id`
3. **Clipboard Implementation**
- Current implementation uses localStorage for persistence
- Must maintain compatibility with existing clipboard format
- Reference: `src/lib/litegraph/src/LGraphCanvas.ts:3602-3857`
4. **Group Resizing**
- Groups should auto-resize when adding nodes using `recomputeInsideNodes()`
- Reference: `src/composables/useCoreCommands.ts:430` - `group.resizeTo()`
5. **Canvas Dirty Flag**
- Visual operations (groups, reroutes) must call `graph.setDirtyCanvas(true, false)`
- This triggers canvas redraw
6. **Error Handling**
- Node creation can return null (for invalid types)
- Connection operations return null/false on failure
- Must validate before operations
7. **Subgraph Support**
- Subgraph operations use specialized Subgraph and SubgraphNode classes
- Reference: `src/lib/litegraph/src/subgraph/`
## Migration Strategy
1. Start by replacing direct `app.graph.add()` calls with `graphMutationService.addNode()`
2. Replace `graph.remove()` calls with `graphMutationService.removeNode()`
3. Update connection operations to use service methods
4. Migrate clipboard operations to use centralized service
5. Ensure all operations maintain existing beforeChange/afterChange patterns
## Important Notes
1. **Always use GraphMutationService** - Never call graph methods directly
2. **Backward Compatibility** - Service maintains compatibility with existing code
3. **Gradual Migration** - Existing code can be migrated incrementally
4. **Performance** - Command recording has minimal overhead

View File

@@ -1,161 +0,0 @@
import { GroupId } from '@/lib/litegraph/src/LGraphGroup'
import { LinkId } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import { SubgraphId } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { NodeId } from '@/schemas/comfyWorkflowSchema'
export interface AddNodeParams {
type: string
pos?: [number, number]
properties?: Record<string, any>
title?: string
}
export interface ConnectParams {
sourceNodeId: NodeId
sourceSlot: number | string
targetNodeId: NodeId
targetSlot: number | string
}
export interface CreateGroupParams {
title?: string
pos?: [number, number]
size?: [number, number]
color?: string
fontSize?: number
}
export interface AddRerouteParams {
pos: [number, number]
linkId?: LinkId
parentRerouteId?: RerouteId
}
export interface AddNodeInputParams {
nodeId: NodeId
name: string
type: string
extra_info?: Record<string, any>
}
export interface AddNodeOutputParams {
nodeId: NodeId
name: string
type: string
extra_info?: Record<string, any>
}
export interface CreateSubgraphParams {
selectedItems: Set<any>
}
export interface ClipboardData {
nodes: any[]
connections: any[]
isCut: boolean
}
export interface ValidationResult {
valid: boolean
errors?: ValidationError[]
warnings?: ValidationWarning[]
}
export interface ValidationError {
code: string
message: string
field?: string
}
export interface ValidationWarning {
code: string
message: string
}
export interface IGraphMutationService {
// Node operations
addNode(params: AddNodeParams): Promise<NodeId>
removeNode(nodeId: NodeId): Promise<void>
updateNodeProperty(
nodeId: NodeId,
property: string,
value: any
): Promise<void>
updateNodeTitle(nodeId: NodeId, title: string): Promise<void>
changeNodeMode(nodeId: NodeId, mode: number): Promise<void>
cloneNode(nodeId: NodeId, pos?: [number, number]): Promise<NodeId>
connect(params: ConnectParams): Promise<LinkId>
disconnect(
nodeId: NodeId,
slot: number | string,
slotType: 'input' | 'output',
targetNodeId?: NodeId
): Promise<boolean>
disconnectInput(nodeId: NodeId, slot: number | string): Promise<boolean>
disconnectOutput(nodeId: NodeId, slot: number | string): Promise<boolean>
disconnectOutputTo(
nodeId: NodeId,
slot: number | string,
targetNodeId: NodeId
): Promise<boolean>
disconnectLink(linkId: LinkId): Promise<void>
createGroup(params: CreateGroupParams): Promise<GroupId>
removeGroup(groupId: GroupId): Promise<void>
updateGroupTitle(groupId: GroupId, title: string): Promise<void>
moveGroup(groupId: GroupId, deltaX: number, deltaY: number): Promise<void>
addNodesToGroup(groupId: GroupId, nodeIds: NodeId[]): Promise<void>
recomputeGroupNodes(groupId: GroupId): Promise<void>
addReroute(params: AddRerouteParams): Promise<RerouteId>
removeReroute(rerouteId: RerouteId): Promise<void>
addNodes(nodes: AddNodeParams[]): Promise<NodeId[]>
removeNodes(nodeIds: NodeId[]): Promise<void>
duplicateNodes(
nodeIds: NodeId[],
offset?: [number, number]
): Promise<NodeId[]>
copyNodes(nodeIds: NodeId[]): Promise<void>
cutNodes(nodeIds: NodeId[]): Promise<void>
pasteNodes(position?: [number, number]): Promise<NodeId[]>
getClipboard(): ClipboardData | null
clearClipboard(): void
hasClipboardContent(): boolean
addSubgraphNodeInput(params: AddNodeInputParams): Promise<number>
addSubgraphNodeOutput(params: AddNodeOutputParams): Promise<number>
removeSubgraphNodeInput(nodeId: NodeId, slot: number): Promise<void>
removeSubgraphNodeOutput(nodeId: NodeId, slot: number): Promise<void>
createSubgraph(params: CreateSubgraphParams): Promise<{
subgraph: any
node: any
}>
unpackSubgraph(subgraphNodeId: NodeId): Promise<void>
addSubgraphInput(
subgraphId: SubgraphId,
name: string,
type: string
): Promise<void>
addSubgraphOutput(
subgraphId: SubgraphId,
name: string,
type: string
): Promise<void>
removeSubgraphInput(subgraphId: SubgraphId, index: number): Promise<void>
removeSubgraphOutput(subgraphId: SubgraphId, index: number): Promise<void>
clearGraph(): Promise<void>
bypassNode(nodeId: NodeId): Promise<void>
unbypassNode(nodeId: NodeId): Promise<void>
transaction<T>(fn: () => Promise<T>): Promise<T>
undo(): Promise<void>
redo(): Promise<void>
}

View File

@@ -1,6 +1,3 @@
import { merge } from 'es-toolkit/compat'
import { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
@@ -23,11 +20,7 @@ import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsCo
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
import { t } from '@/i18n'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import {
type DialogComponentProps,
type ShowDialogOptions,
useDialogStore
} from '@/stores/dialogStore'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
export type ConfirmationDialogType =
| 'default'
@@ -431,33 +424,6 @@ export const useDialogService = () => {
}
}
function showLayoutDialog(options: {
key: string
component: Component
props: { onClose: () => void }
dialogComponentProps?: DialogComponentProps
}) {
const layoutDefaultProps: DialogComponentProps = {
headless: true,
unstyled: true,
modal: true,
closable: false,
pt: {
mask: {
class: 'bg-black bg-opacity-40'
}
}
}
return dialogStore.showDialog({
...options,
dialogComponentProps: merge(
layoutDefaultProps,
options.dialogComponentProps || {}
)
})
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -477,7 +443,6 @@ export const useDialogService = () => {
prompt,
confirm,
toggleManagerDialog,
toggleManagerProgressDialog,
showLayoutDialog
toggleManagerProgressDialog
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import _ from 'es-toolkit/compat'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
@@ -64,7 +63,6 @@ export const useLitegraphService = () => {
const toastStore = useToastStore()
const widgetStore = useWidgetStore()
const canvasStore = useCanvasStore()
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
// TODO: Dedupe `registerNodeDef`; this should remain synchronous.
function registerSubgraphNodeDef(
@@ -764,8 +762,15 @@ export const useLitegraphService = () => {
options.push({
content: 'Bypass',
callback: () => {
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
app.canvas.setDirty(true, true)
const mode =
this.mode === LGraphEventMode.BYPASS
? LGraphEventMode.ALWAYS
: LGraphEventMode.BYPASS
for (const item of app.canvas.selectedItems) {
if (item instanceof LGraphNode) item.mode = mode
}
// @ts-expect-error fixme ts strict error
this.graph.change()
}
})

View File

@@ -17,7 +17,6 @@ export interface ComfyCommand {
versionAdded?: string
confirmation?: string // If non-nullish, this command will prompt for confirmation
source?: string
active?: () => boolean // Getter to check if the command is active/toggled on
category?: 'essentials' | 'view-controls' // For shortcuts panel organization
}
@@ -31,7 +30,6 @@ export class ComfyCommandImpl implements ComfyCommand {
versionAdded?: string
confirmation?: string
source?: string
active?: () => boolean
category?: 'essentials' | 'view-controls'
constructor(command: ComfyCommand) {
@@ -44,7 +42,6 @@ export class ComfyCommandImpl implements ComfyCommand {
this.versionAdded = command.versionAdded
this.confirmation = command.confirmation
this.source = command.source
this.active = command.active
this.category = command.category
}

View File

@@ -28,11 +28,9 @@ interface CustomDialogComponentProps {
pt?: DialogPassThroughOptions
closeOnEscape?: boolean
dismissableMask?: boolean
unstyled?: boolean
headless?: boolean
}
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
CustomDialogComponentProps
interface DialogInstance {

View File

@@ -1,25 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useHelpCenterStore = defineStore('helpCenter', () => {
const isVisible = ref(false)
const toggle = () => {
isVisible.value = !isVisible.value
}
const show = () => {
isVisible.value = true
}
const hide = () => {
isVisible.value = false
}
return {
isVisible,
toggle,
show,
hide
}
})

View File

@@ -10,7 +10,6 @@ import { useCommandStore } from './commandStore'
export const useMenuItemStore = defineStore('menuItem', () => {
const commandStore = useCommandStore()
const menuItems = ref<MenuItem[]>([])
const menuItemHasActiveStateChildren = ref<Record<string, boolean>>({})
const registerMenuGroup = (path: string[], items: MenuItem[]) => {
let currentLevel = menuItems.value
@@ -46,14 +45,6 @@ export const useMenuItemStore = defineStore('menuItem', () => {
}
// Add the new items to the last level
currentLevel.push(...items)
// Store if any of the children have active state as we will hide the icon if they do
const parentPath = path.join('.')
if (!menuItemHasActiveStateChildren.value[parentPath]) {
menuItemHasActiveStateChildren.value[parentPath] = items.some(
(item) => item.comfyCommand?.active
)
}
}
const registerCommands = (path: string[], commandIds: string[]) => {
@@ -66,8 +57,7 @@ export const useMenuItemStore = defineStore('menuItem', () => {
label: command.menubarLabel,
icon: command.icon,
tooltip: command.tooltip,
comfyCommand: command,
parentPath: path.join('.')
comfyCommand: command
}) as MenuItem
)
registerMenuGroup(path, items)
@@ -102,7 +92,6 @@ export const useMenuItemStore = defineStore('menuItem', () => {
registerMenuGroup,
registerCommands,
loadExtensionMenuCommands,
registerCoreMenuCommands,
menuItemHasActiveStateChildren
registerCoreMenuCommands
}
})

View File

@@ -226,14 +226,6 @@ export const useReleaseStore = defineStore('release', () => {
return
}
// Skip fetching if API nodes are disabled via argv
if (
systemStatsStore.systemStats?.system?.argv?.includes(
'--disable-api-nodes'
)
) {
return
}
isLoading.value = true
error.value = null

View File

@@ -7,7 +7,6 @@ import { useQueueSidebarTab } from '@/composables/sidebarTabs/useQueueSidebarTab
import { useWorkflowsSidebarTab } from '@/composables/sidebarTabs/useWorkflowsSidebarTab'
import { t, te } from '@/i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { SidebarTabExtension } from '@/types/extensionTypes'
export const useSidebarTabStore = defineStore('sidebarTab', () => {
@@ -39,34 +38,16 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
: String(tab.tooltip)
: undefined
const menubarLabelFunction = () => {
const menubarLabelKeys: Record<string, string> = {
queue: 'menu.queue',
'node-library': 'sideToolbar.nodeLibrary',
'model-library': 'sideToolbar.modelLibrary',
workflows: 'sideToolbar.workflows'
}
const key = menubarLabelKeys[tab.id]
if (key && te(key)) {
return t(key)
}
return tab.title
}
useCommandStore().registerCommand({
id: `Workspace.ToggleSidebarTab.${tab.id}`,
icon: typeof tab.icon === 'string' ? tab.icon : undefined,
label: labelFunction,
menubarLabel: menubarLabelFunction,
tooltip: tooltipFunction,
versionAdded: '1.3.9',
category: 'view-controls' as const,
function: () => {
toggleSidebarTab(tab.id)
},
active: () => activeSidebarTab.value?.id === tab.id,
source: 'System'
})
}
@@ -92,25 +73,6 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
registerSidebarTab(useNodeLibrarySidebarTab())
registerSidebarTab(useModelLibrarySidebarTab())
registerSidebarTab(useWorkflowsSidebarTab())
const menuStore = useMenuItemStore()
menuStore.registerCommands(
['View'],
[
'Workspace.ToggleBottomPanel',
'Comfy.BrowseTemplates',
'Workspace.ToggleFocusMode',
'Comfy.ToggleCanvasInfo',
'Comfy.Canvas.ToggleMinimap',
'Comfy.Canvas.ToggleLinkVisibility'
]
)
menuStore.registerCommands(
['View'],
['Comfy.Canvas.ZoomIn', 'Comfy.Canvas.ZoomOut', 'Comfy.Canvas.FitView']
)
}
return {

View File

@@ -1,2 +0,0 @@
export * from './navTypes'
export * from './widgetTypes'

View File

@@ -1,9 +0,0 @@
export interface NavItemData {
id: string
label: string
}
export interface NavGroupData {
title: string
items: NavItemData[]
}

View File

@@ -1,3 +0,0 @@
import { InjectionKey } from 'vue'
export const OnCloseKey: InjectionKey<() => void> = Symbol()

View File

@@ -27,41 +27,25 @@ export class ExecutableGroupNodeChildDTO extends ExecutableNodeDTO {
}
override resolveInput(slot: number) {
// Check if this group node is inside a subgraph (unsupported)
if (this.id.split(':').length > 2) {
throw new Error(
'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.'
)
}
const inputNode = this.node.getInputNode(slot)
if (!inputNode) return
const link = this.node.getInputLink(slot)
if (!link) throw new Error('Failed to get input link')
const inputNodeId = String(inputNode.id)
// Try to find the node using the full ID first (for nodes outside the group)
let inputNodeDto = this.nodesByExecutionId?.get(inputNodeId)
// If not found, try with just the last part of the ID (for nodes inside the group)
if (!inputNodeDto) {
const id = inputNodeId.split(':').at(-1)
if (id !== undefined) {
inputNodeDto = this.nodesByExecutionId?.get(id)
}
}
const id = String(inputNode.id).split(':').at(-1)
if (id === undefined) throw new Error('Invalid input node id')
const inputNodeDto = this.nodesByExecutionId?.get(id)
if (!inputNodeDto) {
throw new Error(
`Failed to get input node ${inputNodeId} for group node child ${this.id} with slot ${slot}`
`Failed to get input node ${id} for group node child ${this.id} with slot ${slot}`
)
}
return {
node: inputNodeDto,
origin_id: inputNodeId,
origin_id: String(inputNode.id),
origin_slot: link.origin_slot
}
}

View File

@@ -16,17 +16,12 @@ export const whileMouseDown = (
callback(iteration++)
}, interval)
const dispose = () => {
const dispose = useEventListener(element, 'mouseup', () => {
clearInterval(intervalId)
disposeGlobal()
disposeLocal()
}
// Listen for mouseup globally to catch cases where user drags out of element
const disposeGlobal = useEventListener(document, 'mouseup', dispose)
const disposeLocal = useEventListener(element, 'mouseup', dispose)
dispose()
})
return {
dispose: dispose
dispose
}
}

View File

@@ -79,14 +79,14 @@ export default {
colors: {
zinc: {
50: '#fafafa',
100: '#8282821a',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#38393b',
800: '#262729',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
950: '#09090b'
},

View File

@@ -11,8 +11,7 @@ const mockT = vi.fn((key: string) => {
'shortcuts.subcategories.node': 'Node',
'shortcuts.subcategories.queue': 'Queue',
'shortcuts.subcategories.view': 'View',
'shortcuts.subcategories.panelControls': 'Panel Controls',
'commands.Workflow_New.label': 'New Blank Workflow'
'shortcuts.subcategories.panelControls': 'Panel Controls'
}
return translations[key] || key
})
@@ -77,7 +76,9 @@ describe('ShortcutsList', () => {
expect(wrapper.text()).toContain('Queue')
// Check that commands are rendered
expect(wrapper.text()).toContain('New Blank Workflow')
expect(wrapper.text()).toContain('New Workflow')
expect(wrapper.text()).toContain('Add Node')
expect(wrapper.text()).toContain('Clear Queue')
})
it('should format keyboard shortcuts correctly', () => {

View File

@@ -1512,10 +1512,7 @@ describe('useNodePricing', () => {
{ model: 'gpt-4o', expected: '$0.0025/$0.01 per 1K tokens' },
{ model: 'gpt-4.1-nano', expected: '$0.0001/$0.0004 per 1K tokens' },
{ model: 'gpt-4.1-mini', expected: '$0.0004/$0.0016 per 1K tokens' },
{ model: 'gpt-4.1', expected: '$0.002/$0.008 per 1K tokens' },
{ model: 'gpt-5-nano', expected: '$0.00005/$0.0004 per 1K tokens' },
{ model: 'gpt-5-mini', expected: '$0.00025/$0.002 per 1K tokens' },
{ model: 'gpt-5', expected: '$0.00125/$0.01 per 1K tokens' }
{ model: 'gpt-4.1', expected: '$0.002/$0.008 per 1K tokens' }
]
testCases.forEach(({ model, expected }) => {

File diff suppressed because it is too large Load Diff

View File

@@ -251,53 +251,6 @@ describe('useReleaseStore', () => {
})
})
it('should skip fetching when --disable-api-nodes is present', async () => {
mockSystemStatsStore.systemStats.system.argv = ['--disable-api-nodes']
await store.initialize()
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
expect(store.isLoading).toBe(false)
})
it('should skip fetching when --disable-api-nodes is one of multiple args', async () => {
mockSystemStatsStore.systemStats.system.argv = [
'--port',
'8080',
'--disable-api-nodes',
'--verbose'
]
await store.initialize()
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
expect(store.isLoading).toBe(false)
})
it('should fetch normally when --disable-api-nodes is not present', async () => {
mockSystemStatsStore.systemStats.system.argv = [
'--port',
'8080',
'--verbose'
]
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.initialize()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
expect(store.releases).toEqual([mockRelease])
})
it('should fetch normally when argv is undefined', async () => {
mockSystemStatsStore.systemStats.system.argv = undefined
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.initialize()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
expect(store.releases).toEqual([mockRelease])
})
it('should handle API errors gracefully', async () => {
mockReleaseService.getReleases.mockResolvedValue(null)
mockReleaseService.error.value = 'API Error'
@@ -354,63 +307,6 @@ describe('useReleaseStore', () => {
})
})
describe('--disable-api-nodes argument handling', () => {
it('should skip fetchReleases when --disable-api-nodes is present', async () => {
mockSystemStatsStore.systemStats.system.argv = ['--disable-api-nodes']
await store.fetchReleases()
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
expect(store.isLoading).toBe(false)
})
it('should skip fetchReleases when --disable-api-nodes is among other args', async () => {
mockSystemStatsStore.systemStats.system.argv = [
'--port',
'8080',
'--disable-api-nodes',
'--verbose'
]
await store.fetchReleases()
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
expect(store.isLoading).toBe(false)
})
it('should proceed with fetchReleases when --disable-api-nodes is not present', async () => {
mockSystemStatsStore.systemStats.system.argv = [
'--port',
'8080',
'--verbose'
]
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.fetchReleases()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
})
it('should proceed with fetchReleases when argv is null', async () => {
mockSystemStatsStore.systemStats.system.argv = null
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.fetchReleases()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
})
it('should proceed with fetchReleases when system stats are not available', async () => {
mockSystemStatsStore.systemStats = null
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.fetchReleases()
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
})
})
describe('action handlers', () => {
beforeEach(() => {
store.releases = [mockRelease]

View File

@@ -1,199 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GroupNodeHandler } from '@/extensions/core/groupNode'
import type {
ExecutableLGraphNode,
ExecutionId,
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
describe('ExecutableGroupNodeChildDTO', () => {
let mockNode: LGraphNode
let mockInputNode: LGraphNode
let mockNodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>
let mockGroupNodeHandler: GroupNodeHandler
beforeEach(() => {
// Create mock nodes
mockNode = {
id: '3', // Simple node ID for most tests
graph: {},
getInputNode: vi.fn(),
getInputLink: vi.fn(),
inputs: []
} as any
mockInputNode = {
id: '1',
graph: {}
} as any
// Create the nodesByExecutionId map
mockNodesByExecutionId = new Map()
mockGroupNodeHandler = {} as GroupNodeHandler
})
describe('resolveInput', () => {
it('should resolve input from external node (node outside the group)', () => {
// Setup: Group node child with ID '10:3'
const groupNodeChild = {
id: '10:3',
graph: {},
getInputNode: vi.fn().mockReturnValue(mockInputNode),
getInputLink: vi.fn().mockReturnValue({
origin_slot: 0
}),
inputs: []
} as any
// External node with ID '1'
const externalNodeDto = {
id: '1',
type: 'TestNode'
} as ExecutableLGraphNode
mockNodesByExecutionId.set('1', externalNodeDto)
const dto = new ExecutableGroupNodeChildDTO(
groupNodeChild,
[], // No subgraph path - group is in root graph
mockNodesByExecutionId,
undefined,
mockGroupNodeHandler
)
const result = dto.resolveInput(0)
expect(result).toEqual({
node: externalNodeDto,
origin_id: '1',
origin_slot: 0
})
})
it('should resolve input from internal node (node inside the same group)', () => {
// Setup: Group node child with ID '10:3'
const groupNodeChild = {
id: '10:3',
graph: {},
getInputNode: vi.fn(),
getInputLink: vi.fn(),
inputs: []
} as any
// Internal node with ID '10:2'
const internalInputNode = {
id: '10:2',
graph: {}
} as LGraphNode
const internalNodeDto = {
id: '2',
type: 'InternalNode'
} as ExecutableLGraphNode
// Internal nodes are stored with just their index
mockNodesByExecutionId.set('2', internalNodeDto)
groupNodeChild.getInputNode.mockReturnValue(internalInputNode)
groupNodeChild.getInputLink.mockReturnValue({
origin_slot: 1
})
const dto = new ExecutableGroupNodeChildDTO(
groupNodeChild,
[],
mockNodesByExecutionId,
undefined,
mockGroupNodeHandler
)
const result = dto.resolveInput(0)
expect(result).toEqual({
node: internalNodeDto,
origin_id: '10:2',
origin_slot: 1
})
})
it('should return undefined if no input node exists', () => {
mockNode.getInputNode = vi.fn().mockReturnValue(null)
const dto = new ExecutableGroupNodeChildDTO(
mockNode,
[],
mockNodesByExecutionId,
undefined,
mockGroupNodeHandler
)
const result = dto.resolveInput(0)
expect(result).toBeUndefined()
})
it('should throw error if input link is missing', () => {
mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode)
mockNode.getInputLink = vi.fn().mockReturnValue(null)
const dto = new ExecutableGroupNodeChildDTO(
mockNode,
[],
mockNodesByExecutionId,
undefined,
mockGroupNodeHandler
)
expect(() => dto.resolveInput(0)).toThrow('Failed to get input link')
})
it('should throw error if input node cannot be found in nodesByExecutionId', () => {
// Node exists but is not in the map
mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode)
mockNode.getInputLink = vi.fn().mockReturnValue({
origin_slot: 0
})
const dto = new ExecutableGroupNodeChildDTO(
mockNode,
[],
mockNodesByExecutionId, // Empty map
undefined,
mockGroupNodeHandler
)
expect(() => dto.resolveInput(0)).toThrow(
'Failed to get input node 1 for group node child 3 with slot 0'
)
})
it('should throw error for group nodes inside subgraphs (unsupported)', () => {
// Setup: Group node child inside a subgraph (execution ID has more than 2 segments)
const nestedGroupNode = {
id: '1:2:3', // subgraph:groupnode:innernode
graph: {},
getInputNode: vi.fn().mockReturnValue(mockInputNode),
getInputLink: vi.fn().mockReturnValue({
origin_slot: 0
}),
inputs: []
} as any
// Create DTO with deeply nested path to simulate group node inside subgraph
const dto = new ExecutableGroupNodeChildDTO(
nestedGroupNode,
['1', '2'], // Path indicating it's inside a subgraph then group
mockNodesByExecutionId,
undefined,
mockGroupNodeHandler
)
expect(() => dto.resolveInput(0)).toThrow(
'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.'
)
})
})
})