mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 09:57:33 +00:00
Compare commits
10 Commits
issue-5033
...
graphMutat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ea615fb8c | ||
|
|
ceac8f3741 | ||
|
|
b1057f164b | ||
|
|
4a189bdc93 | ||
|
|
f0adb4c9d3 | ||
|
|
d5d0aa52c2 | ||
|
|
69c660b3b7 | ||
|
|
88579c2a40 | ||
|
|
7ab247aa1d | ||
|
|
c78d03dd2c |
@@ -111,50 +111,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
```
|
||||
7. **HUMAN ANALYSIS**: Review change summary and verify scope
|
||||
|
||||
### 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
|
||||
### Step 3: Breaking Change Analysis
|
||||
|
||||
1. Analyze API changes in:
|
||||
- Public TypeScript interfaces
|
||||
@@ -169,7 +126,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
3. Generate breaking change summary
|
||||
4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified?
|
||||
|
||||
### Step 7: Analyze Dependency Updates
|
||||
### Step 4: Analyze Dependency Updates
|
||||
|
||||
1. **Check significant dependency updates:**
|
||||
```bash
|
||||
@@ -195,7 +152,117 @@ echo "Last stable release: $LAST_STABLE"
|
||||
done
|
||||
```
|
||||
|
||||
### Step 8: Generate Comprehensive Release Notes
|
||||
### 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
|
||||
|
||||
1. Extract commit messages since base release:
|
||||
```bash
|
||||
@@ -257,7 +324,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
|
||||
5. **CONTENT REVIEW**: Release notes follow standard format?
|
||||
|
||||
### Step 9: Create Version Bump PR
|
||||
### Step 10: Create Version Bump PR
|
||||
|
||||
**For standard version bumps (patch/minor/major):**
|
||||
```bash
|
||||
@@ -303,7 +370,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
4. **PR REVIEW**: Version bump PR created with standardized release notes?
|
||||
|
||||
### Step 10: Critical Release PR Verification
|
||||
### Step 11: Critical Release PR Verification
|
||||
|
||||
1. **CRITICAL**: Verify PR has "Release" label:
|
||||
```bash
|
||||
@@ -325,7 +392,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
|
||||
|
||||
### Step 11: Pre-Merge Validation
|
||||
### Step 12: Pre-Merge Validation
|
||||
|
||||
1. **Review Requirements**: Release PRs require approval
|
||||
2. Monitor CI checks - watch for update-locales
|
||||
@@ -333,7 +400,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 12: Execute Release
|
||||
### Step 13: Execute Release
|
||||
|
||||
1. **FINAL CONFIRMATION**: Merge PR to trigger release?
|
||||
2. Merge the Release PR:
|
||||
@@ -366,7 +433,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
### Step 13: Enhance GitHub Release
|
||||
### Step 14: Enhance GitHub Release
|
||||
|
||||
1. Wait for automatic release creation:
|
||||
```bash
|
||||
@@ -394,7 +461,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
gh release view v${NEW_VERSION}
|
||||
```
|
||||
|
||||
### Step 14: Verify Multi-Channel Distribution
|
||||
### Step 15: Verify Multi-Channel Distribution
|
||||
|
||||
1. **GitHub Release:**
|
||||
```bash
|
||||
@@ -432,7 +499,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
|
||||
4. **DISTRIBUTION VERIFICATION**: All channels published successfully?
|
||||
|
||||
### Step 15: Post-Release Monitoring Setup
|
||||
### Step 16: Post-Release Monitoring Setup
|
||||
|
||||
1. **Monitor immediate release health:**
|
||||
```bash
|
||||
@@ -508,87 +575,42 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
|
||||
4. **RELEASE COMPLETION**: All post-release setup completed?
|
||||
|
||||
### Step 16: Generate GTM Feature Summary
|
||||
### Step 17: Create Release Summary
|
||||
|
||||
1. **Extract and analyze PR data:**
|
||||
1. **Create comprehensive release summary:**
|
||||
```bash
|
||||
echo "📊 Checking for marketing-worthy features..."
|
||||
|
||||
# Extract all PR data inline
|
||||
PR_DATA=$(
|
||||
PR_LIST=$(git log ${BASE_TAG}..HEAD --grep="Merge pull request" --pretty=format:"%s" | grep -oE "#[0-9]+" | tr -d '#' | sort -u)
|
||||
|
||||
echo "["
|
||||
first=true
|
||||
for PR in $PR_LIST; do
|
||||
[[ "$first" == true ]] && first=false || echo ","
|
||||
gh pr view $PR --json number,title,author,body,files,labels,closedAt 2>/dev/null || continue
|
||||
done
|
||||
echo "]"
|
||||
)
|
||||
|
||||
# Save for analysis
|
||||
echo "$PR_DATA" > prs-${NEW_VERSION}.json
|
||||
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. **Analyze for GTM-worthy features:**
|
||||
```
|
||||
<task>
|
||||
Review these PRs to identify if ANY would interest a marketing/growth team.
|
||||
|
||||
Consider if a PR:
|
||||
- Changes something users directly interact with or experience
|
||||
- Makes something noticeably better, faster, or easier
|
||||
- Introduces capabilities users have been asking for
|
||||
- Has visual assets (screenshots, GIFs, videos) that could be shared
|
||||
- Tells a compelling story about improvement or innovation
|
||||
- Would make users excited if they heard about it
|
||||
|
||||
Many releases contain only technical improvements, bug fixes, or internal changes -
|
||||
that's perfectly normal. Only flag PRs that would genuinely interest end users.
|
||||
|
||||
If you find marketing-worthy PRs, note:
|
||||
- PR number, title, and author
|
||||
- Any media links from the description
|
||||
- One sentence on why it's worth showcasing
|
||||
|
||||
If nothing is marketing-worthy, just say "No marketing-worthy features in this release."
|
||||
</task>
|
||||
|
||||
PR data: [contents of prs-${NEW_VERSION}.json]
|
||||
```
|
||||
|
||||
3. **Generate GTM notification (only if needed):**
|
||||
```
|
||||
If there are marketing-worthy features, create a message for #gtm with:
|
||||
|
||||
🚀 Frontend Release v${NEW_VERSION}
|
||||
|
||||
Timeline: Available now in nightly, ~2-3 weeks for core
|
||||
|
||||
Features worth showcasing:
|
||||
[List the selected PRs with media links and authors]
|
||||
|
||||
Testing: --front-end-version ${NEW_VERSION}
|
||||
|
||||
If there are NO marketing-worthy features, generate:
|
||||
"No marketing-worthy features in v${NEW_VERSION} - mostly internal improvements and bug fixes."
|
||||
```
|
||||
|
||||
4. **Save the output:**
|
||||
```bash
|
||||
# Claude generates the GTM summary and saves it
|
||||
# Save to gtm-summary-${NEW_VERSION}.md
|
||||
|
||||
# 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
|
||||
```
|
||||
2. **RELEASE COMPLETION**: All steps completed successfully?
|
||||
|
||||
## Advanced Safety Features
|
||||
|
||||
|
||||
@@ -73,6 +73,77 @@ 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')
|
||||
|
||||
568
package-lock.json
generated
568
package-lock.json
generated
@@ -975,32 +975,6 @@
|
||||
"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",
|
||||
@@ -4717,17 +4691,6 @@
|
||||
"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",
|
||||
@@ -4763,38 +4726,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -5720,15 +5651,6 @@
|
||||
"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",
|
||||
@@ -5798,18 +5720,6 @@
|
||||
"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",
|
||||
@@ -5820,34 +5730,6 @@
|
||||
"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",
|
||||
@@ -6058,14 +5940,6 @@
|
||||
"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",
|
||||
@@ -7227,14 +7101,6 @@
|
||||
"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",
|
||||
@@ -7305,58 +7171,12 @@
|
||||
"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",
|
||||
@@ -7420,14 +7240,6 @@
|
||||
"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",
|
||||
@@ -7583,17 +7395,6 @@
|
||||
"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",
|
||||
@@ -7689,21 +7490,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"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",
|
||||
@@ -8065,29 +7851,6 @@
|
||||
"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",
|
||||
@@ -9638,20 +9401,6 @@
|
||||
"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",
|
||||
@@ -9703,37 +9452,6 @@
|
||||
"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",
|
||||
@@ -10353,14 +10071,6 @@
|
||||
"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",
|
||||
@@ -10592,53 +10302,6 @@
|
||||
"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",
|
||||
@@ -11651,14 +11314,6 @@
|
||||
"@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",
|
||||
@@ -13048,14 +12703,6 @@
|
||||
"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",
|
||||
@@ -14215,14 +13862,6 @@
|
||||
"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",
|
||||
@@ -14288,14 +13927,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"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",
|
||||
@@ -14801,14 +14432,6 @@
|
||||
"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",
|
||||
@@ -15016,20 +14639,6 @@
|
||||
"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",
|
||||
@@ -15681,14 +15290,6 @@
|
||||
"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",
|
||||
@@ -15943,37 +15544,6 @@
|
||||
"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",
|
||||
@@ -16004,51 +15574,6 @@
|
||||
"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",
|
||||
@@ -16762,17 +16287,6 @@
|
||||
"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",
|
||||
@@ -17014,18 +16528,6 @@
|
||||
"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",
|
||||
@@ -17102,14 +16604,6 @@
|
||||
"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",
|
||||
@@ -17911,20 +17405,6 @@
|
||||
"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",
|
||||
@@ -17986,20 +17466,6 @@
|
||||
"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",
|
||||
@@ -18009,21 +17475,6 @@
|
||||
"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",
|
||||
@@ -18320,14 +17771,6 @@
|
||||
"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",
|
||||
@@ -18433,17 +17876,6 @@
|
||||
"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",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
:class="{
|
||||
'flex items-center gap-1': isActive,
|
||||
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
|
||||
'p-breadcrumb-item-link-icon-visible': isActive
|
||||
'p-breadcrumb-item-link-icon-visible': isActive,
|
||||
'active-breadcrumb-item': isActive
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
@@ -111,21 +112,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
{
|
||||
label: t('g.rename'),
|
||||
icon: 'pi pi-pencil',
|
||||
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)
|
||||
}
|
||||
command: startRename
|
||||
},
|
||||
{
|
||||
label: t('breadcrumbsMenu.duplicate'),
|
||||
@@ -175,20 +162,24 @@ const handleClick = (event: MouseEvent) => {
|
||||
menu.value?.hide()
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
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`
|
||||
}
|
||||
}
|
||||
})
|
||||
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`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const inputBlur = async (doRename: boolean) => {
|
||||
if (doRename) {
|
||||
await rename(itemLabel.value, props.item.label as string)
|
||||
@@ -212,4 +203,8 @@ const inputBlur = async (doRename: boolean) => {
|
||||
.p-breadcrumb-item-label {
|
||||
@apply whitespace-nowrap text-ellipsis overflow-hidden;
|
||||
}
|
||||
|
||||
.active-breadcrumb-item {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/components/custom/button/IconButton.vue
Normal file
15
src/components/custom/button/IconButton.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
67
src/components/custom/widget/ModelSelector.vue
Normal file
67
src/components/custom/widget/ModelSelector.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<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>
|
||||
176
src/components/custom/widget/layout/BaseWidgetLayout.vue
Normal file
176
src/components/custom/widget/layout/BaseWidgetLayout.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<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>
|
||||
24
src/components/custom/widget/nav/NavItem.vue
Normal file
24
src/components/custom/widget/nav/NavItem.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<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>
|
||||
13
src/components/custom/widget/nav/NavTitle.vue
Normal file
13
src/components/custom/widget/nav/NavTitle.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
75
src/components/custom/widget/panel/LeftSidePanel.vue
Normal file
75
src/components/custom/widget/panel/LeftSidePanel.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<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>
|
||||
12
src/components/custom/widget/panel/PanelHeader.vue
Normal file
12
src/components/custom/widget/panel/PanelHeader.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<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>
|
||||
5
src/components/custom/widget/panel/RightSidePanel.vue
Normal file
5
src/components/custom/widget/panel/RightSidePanel.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="w-full h-full pl-4 pr-6 pb-8 bg-white dark-theme:bg-zinc-800">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,14 +10,16 @@
|
||||
:aria-labelledby="item.key"
|
||||
>
|
||||
<template #header>
|
||||
<component
|
||||
:is="item.headerComponent"
|
||||
v-if="item.headerComponent"
|
||||
:id="item.key"
|
||||
/>
|
||||
<h3 v-else :id="item.key">
|
||||
{{ item.title || ' ' }}
|
||||
</h3>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<component
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<MiniMap
|
||||
v-if="comfyAppReady && minimapEnabled"
|
||||
ref="minimapRef"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
</template>
|
||||
@@ -71,7 +70,6 @@ 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'
|
||||
@@ -119,9 +117,7 @@ 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')
|
||||
@@ -358,13 +354,6 @@ onMounted(async () => {
|
||||
}
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => minimapRef.value,
|
||||
(ref) => {
|
||||
minimap.setMinimapRef(ref)
|
||||
}
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => useCanvasStore().canvas,
|
||||
(canvas) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
ref="minimapRef"
|
||||
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
>
|
||||
<MiniMapPanel
|
||||
@@ -54,15 +55,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
import MiniMapPanel from './MiniMapPanel.vue'
|
||||
|
||||
const minimap = useMinimap()
|
||||
const canvasStore = useCanvasStore()
|
||||
const minimapRef = ref<HTMLDivElement>()
|
||||
|
||||
const {
|
||||
initialized,
|
||||
@@ -80,13 +79,13 @@ const {
|
||||
renderBypass,
|
||||
renderError,
|
||||
updateOption,
|
||||
init,
|
||||
destroy,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel
|
||||
} = minimap
|
||||
handleWheel,
|
||||
setMinimapRef
|
||||
} = useMinimap()
|
||||
|
||||
const showOptionsPanel = ref(false)
|
||||
|
||||
@@ -94,20 +93,8 @@ const toggleOptionsPanel = () => {
|
||||
showOptionsPanel.value = !showOptionsPanel.value
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
async (canvas) => {
|
||||
if (canvas && !initialized.value) {
|
||||
await init()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (canvasStore.canvas) {
|
||||
await init()
|
||||
}
|
||||
onMounted(() => {
|
||||
setMinimapRef(minimapRef.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useGraphMutationService } from '@/services/graphMutationService'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -42,19 +42,23 @@ const inputStyle = computed<CSSProperties>(() => ({
|
||||
const titleEditorStore = useTitleEditorStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const previousCanvasDraggable = ref(true)
|
||||
const graphMutationService = useGraphMutationService()
|
||||
|
||||
const onEdit = (newValue: string) => {
|
||||
const onEdit = async (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 && target.isSubgraphNode?.()) {
|
||||
target.subgraph.name = trimmedTitle
|
||||
}
|
||||
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
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)
|
||||
}
|
||||
}
|
||||
showInput.value = false
|
||||
titleEditorStore.titleEditorTarget = null
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
@mousedown="
|
||||
isZoomCommand(item) ? handleZoomMouseDown(item, $event) : undefined
|
||||
"
|
||||
@click="isZoomCommand(item) ? handleZoomClick($event) : undefined"
|
||||
@click="handleItemClick(item, $event)"
|
||||
>
|
||||
<i
|
||||
v-if="hasActiveStateSiblings(item)"
|
||||
@@ -285,11 +285,19 @@ const handleZoomMouseDown = (item: MenuItem, event: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomClick = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Prevent the menu from closing for zoom commands
|
||||
return false
|
||||
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 => {
|
||||
|
||||
@@ -1371,6 +1371,18 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
}
|
||||
return 'Token-based'
|
||||
}
|
||||
},
|
||||
ViduTextToVideoNode: {
|
||||
displayPrice: '$0.4/Run'
|
||||
},
|
||||
ViduImageToVideoNode: {
|
||||
displayPrice: '$0.4/Run'
|
||||
},
|
||||
ViduReferenceVideoNode: {
|
||||
displayPrice: '$0.4/Run'
|
||||
},
|
||||
ViduStartEndToVideoNode: {
|
||||
displayPrice: '$0.4/Run'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -878,6 +879,17 @@ 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()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -685,9 +685,10 @@ export function useMinimap() {
|
||||
|
||||
const originalCallbacks = originalCallbacksMap.get(g.id)
|
||||
if (!originalCallbacks) {
|
||||
throw new Error(
|
||||
console.error(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
|
||||
29
src/composables/useModelSelectorDialog.ts
Normal file
29
src/composables/useModelSelectorDialog.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
} from './measure'
|
||||
import type { ISerialisedGroup } from './types/serialisation'
|
||||
|
||||
export type GroupId = number
|
||||
|
||||
export interface IGraphGroupFlags extends Record<string, unknown> {
|
||||
pinned?: true
|
||||
}
|
||||
|
||||
@@ -1574,7 +1574,10 @@ export class LGraphNode
|
||||
* remove an existing output slot
|
||||
*/
|
||||
removeOutput(slot: number): void {
|
||||
this.disconnectOutput(slot)
|
||||
// Only disconnect if node is part of a graph
|
||||
if (this.graph) {
|
||||
this.disconnectOutput(slot)
|
||||
}
|
||||
const { outputs } = this
|
||||
outputs.splice(slot, 1)
|
||||
|
||||
@@ -1582,11 +1585,12 @@ export class LGraphNode
|
||||
const output = outputs[i]
|
||||
if (!output || !output.links) continue
|
||||
|
||||
for (const linkId of output.links) {
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
|
||||
const link = this.graph._links.get(linkId)
|
||||
if (link) link.origin_slot--
|
||||
// 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--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,7 +1630,10 @@ export class LGraphNode
|
||||
* remove an existing input slot
|
||||
*/
|
||||
removeInput(slot: number): void {
|
||||
this.disconnectInput(slot, true)
|
||||
// Only disconnect if node is part of a graph
|
||||
if (this.graph) {
|
||||
this.disconnectInput(slot, true)
|
||||
}
|
||||
const { inputs } = this
|
||||
const slot_info = inputs.splice(slot, 1)
|
||||
|
||||
@@ -1634,9 +1641,11 @@ export class LGraphNode
|
||||
const input = inputs[i]
|
||||
if (!input?.link) continue
|
||||
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
const link = this.graph._links.get(input.link)
|
||||
if (link) link.target_slot--
|
||||
// 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--
|
||||
}
|
||||
}
|
||||
this.onInputRemoved?.(slot, slot_info[0])
|
||||
this.setDirtyCanvas(true, true)
|
||||
|
||||
@@ -31,6 +31,8 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -656,4 +656,119 @@ 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "الاتصال بالدعم"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "إظهار منتقي النماذج (للمطورين)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "تكرار سير العمل الحالي"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"Save": "حفظ",
|
||||
"Save As": "حفظ باسم",
|
||||
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
|
||||
"Show Model Selector (Dev)": "إظهار منتقي النماذج (للمطورين)",
|
||||
"Show Settings Dialog": "عرض نافذة الإعدادات",
|
||||
"Sign Out": "تسجيل خروج",
|
||||
"Toggle Essential Bottom Panel": "تبديل اللوحة السفلية الأساسية",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contact Support"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Show Model Selector (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicate Current Workflow"
|
||||
},
|
||||
|
||||
@@ -980,6 +980,7 @@
|
||||
"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)",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contactar soporte"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Mostrar selector de modelo (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicar flujo de trabajo actual"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"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 Essential Bottom Panel": "Alternar panel inferior esencial",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"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 Essential Bottom Panel": "Afficher/Masquer le panneau inférieur essentiel",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "サポートに連絡"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "モデルセレクターを表示(開発用)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "現在のワークフローを複製"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "名前を付けて保存",
|
||||
"Show Keybindings Dialog": "キーバインドダイアログを表示",
|
||||
"Show Model Selector (Dev)": "モデルセレクターを表示(開発用)",
|
||||
"Show Settings Dialog": "設定ダイアログを表示",
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "지원팀에 문의하기"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "모델 선택기 표시 (개발자용)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "현재 워크플로 복제"
|
||||
},
|
||||
|
||||
@@ -839,7 +839,8 @@
|
||||
"Restart": "재시작",
|
||||
"Save": "저장",
|
||||
"Save As": "다른 이름으로 저장",
|
||||
"Show Keybindings Dialog": "키 바인딩 대화상자 표시",
|
||||
"Show Keybindings Dialog": "단축키 대화상자 표시",
|
||||
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Связаться с поддержкой"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Показать выбор модели (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Дублировать текущий рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"Save": "Сохранить",
|
||||
"Save As": "Сохранить как",
|
||||
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
|
||||
"Show Model Selector (Dev)": "Показать выбор модели (Dev)",
|
||||
"Show Settings Dialog": "Показать диалог настроек",
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Essential Bottom Panel": "Показать/скрыть основную нижнюю панель",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "聯絡支援"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "顯示模型選擇器(開發)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "複製目前工作流程"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"Save": "儲存",
|
||||
"Save As": "另存新檔",
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
|
||||
"Show Settings Dialog": "顯示設定對話框",
|
||||
"Sign Out": "登出",
|
||||
"Toggle Essential Bottom Panel": "切換基本下方面板",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "联系支持"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "顯示模型選擇器(開發)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "复制当前工作流"
|
||||
},
|
||||
|
||||
@@ -839,7 +839,8 @@
|
||||
"Restart": "重启",
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Keybindings Dialog": "显示快捷键对话框",
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Essential Bottom Panel": "切换基础底部面板",
|
||||
|
||||
462
src/services/GRAPH_MUTATION_SERVICE_DESIGN.md
Normal file
462
src/services/GRAPH_MUTATION_SERVICE_DESIGN.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# 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
|
||||
161
src/services/IGraphMutationService.ts
Normal file
161
src/services/IGraphMutationService.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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>
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
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'
|
||||
@@ -20,7 +23,11 @@ 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 ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type ShowDialogOptions,
|
||||
useDialogStore
|
||||
} from '@/stores/dialogStore'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -424,6 +431,33 @@ 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,
|
||||
@@ -443,6 +477,7 @@ export const useDialogService = () => {
|
||||
prompt,
|
||||
confirm,
|
||||
toggleManagerDialog,
|
||||
toggleManagerProgressDialog
|
||||
toggleManagerProgressDialog,
|
||||
showLayoutDialog
|
||||
}
|
||||
}
|
||||
|
||||
1152
src/services/graphMutationService.ts
Normal file
1152
src/services/graphMutationService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -28,9 +28,11 @@ interface CustomDialogComponentProps {
|
||||
pt?: DialogPassThroughOptions
|
||||
closeOnEscape?: boolean
|
||||
dismissableMask?: boolean
|
||||
unstyled?: boolean
|
||||
headless?: boolean
|
||||
}
|
||||
|
||||
type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
||||
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
||||
CustomDialogComponentProps
|
||||
|
||||
interface DialogInstance {
|
||||
|
||||
2
src/types/custom_components/index.d.ts
vendored
Normal file
2
src/types/custom_components/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './navTypes'
|
||||
export * from './widgetTypes'
|
||||
9
src/types/custom_components/navTypes.ts
Normal file
9
src/types/custom_components/navTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface NavItemData {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface NavGroupData {
|
||||
title: string
|
||||
items: NavItemData[]
|
||||
}
|
||||
3
src/types/custom_components/widgetTypes.ts
Normal file
3
src/types/custom_components/widgetTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
|
||||
export const OnCloseKey: InjectionKey<() => void> = Symbol()
|
||||
@@ -27,25 +27,41 @@ 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 id = String(inputNode.id).split(':').at(-1)
|
||||
if (id === undefined) throw new Error('Invalid input node id')
|
||||
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 inputNodeDto = this.nodesByExecutionId?.get(id)
|
||||
if (!inputNodeDto) {
|
||||
throw new Error(
|
||||
`Failed to get input node ${id} for group node child ${this.id} with slot ${slot}`
|
||||
`Failed to get input node ${inputNodeId} for group node child ${this.id} with slot ${slot}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
node: inputNodeDto,
|
||||
origin_id: String(inputNode.id),
|
||||
origin_id: inputNodeId,
|
||||
origin_slot: link.origin_slot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +79,14 @@ export default {
|
||||
colors: {
|
||||
zinc: {
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
100: '#8282821a',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
700: '#38393b',
|
||||
800: '#262729',
|
||||
900: '#18181b',
|
||||
950: '#09090b'
|
||||
},
|
||||
|
||||
1049
tests-ui/tests/services/graphMutationService.test.ts
Normal file
1049
tests-ui/tests/services/graphMutationService.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
199
tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts
Normal file
199
tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
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.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user