mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-03 13:48:49 +00:00
Compare commits
1 Commits
v1.24.0
...
feat/impor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97547434b0 |
@@ -1,222 +0,0 @@
|
||||
# Create Hotfix Release
|
||||
|
||||
This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step.
|
||||
|
||||
<task>
|
||||
Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS
|
||||
|
||||
Expected format: Comma-separated list of commits or PR numbers
|
||||
Examples:
|
||||
- `abc123,def456,ghi789` (commits)
|
||||
- `#1234,#5678` (PRs)
|
||||
- `abc123,#1234,def456` (mixed)
|
||||
|
||||
If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs.
|
||||
</task>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure:
|
||||
- You have push access to the repository
|
||||
- GitHub CLI (`gh`) is authenticated
|
||||
- You're on a clean working tree
|
||||
- You understand the commits/PRs you're cherry-picking
|
||||
|
||||
## Hotfix Release Process
|
||||
|
||||
### Step 1: Identify Target Core Branch
|
||||
|
||||
1. Fetch the current ComfyUI requirements.txt from master branch:
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/comfyanonymous/ComfyUI/master/requirements.txt | grep "comfyui-frontend-package"
|
||||
```
|
||||
2. Extract the `comfyui-frontend-package` version (e.g., `comfyui-frontend-package==1.23.4`)
|
||||
3. Parse version to get major.minor (e.g., `1.23.4` → `1.23`)
|
||||
4. Determine core branch: `core/<major>.<minor>` (e.g., `core/1.23`)
|
||||
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
|
||||
6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
|
||||
|
||||
### Step 2: Parse and Validate Arguments
|
||||
|
||||
1. Parse the comma-separated list of commits/PRs
|
||||
2. For each item:
|
||||
- If starts with `#`: Treat as PR number
|
||||
- Otherwise: Treat as commit hash
|
||||
3. For PR numbers:
|
||||
- Fetch PR details using `gh pr view <number>`
|
||||
- Extract the merge commit if PR is merged
|
||||
- If PR has multiple commits, list them all
|
||||
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
|
||||
4. Validate all commit hashes exist in the repository
|
||||
|
||||
### Step 3: Analyze Target Changes
|
||||
|
||||
1. For each commit/PR to cherry-pick:
|
||||
- Display commit hash, author, date
|
||||
- Show PR title and number (if applicable)
|
||||
- Display commit message
|
||||
- Show files changed and diff statistics
|
||||
- Check if already in core branch: `git branch --contains <commit>`
|
||||
2. Identify potential conflicts by checking changed files
|
||||
3. **CONFIRMATION REQUIRED**: Proceed with these commits?
|
||||
|
||||
### Step 4: Create Hotfix Branch
|
||||
|
||||
1. Checkout the core branch (e.g., `core/1.23`)
|
||||
2. Pull latest changes: `git pull origin core/X.Y`
|
||||
3. Display current version from package.json
|
||||
4. Create hotfix branch: `hotfix/<version>-<timestamp>`
|
||||
- Example: `hotfix/1.23.4-20241120`
|
||||
5. **CONFIRMATION REQUIRED**: Created branch correctly?
|
||||
|
||||
### Step 5: Cherry-pick Changes
|
||||
|
||||
For each commit:
|
||||
1. Attempt cherry-pick: `git cherry-pick <commit>`
|
||||
2. If conflicts occur:
|
||||
- Display conflict details
|
||||
- Show conflicting sections
|
||||
- Provide resolution guidance
|
||||
- **CONFIRMATION REQUIRED**: Conflicts resolved correctly?
|
||||
3. After successful cherry-pick:
|
||||
- Show the changes: `git show HEAD`
|
||||
- Run validation: `npm run typecheck && npm run lint`
|
||||
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
|
||||
|
||||
### Step 6: Create PR to Core Branch
|
||||
|
||||
1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>`
|
||||
2. Create PR using gh CLI:
|
||||
```bash
|
||||
gh pr create --base core/X.Y --head hotfix/<version>-<timestamp> \
|
||||
--title "[Hotfix] Cherry-pick fixes to core/X.Y" \
|
||||
--body "Cherry-picked commits: ..."
|
||||
```
|
||||
3. Add appropriate labels (but NOT "Release" yet)
|
||||
4. PR body should include:
|
||||
- List of cherry-picked commits/PRs
|
||||
- Original issue references
|
||||
- Testing instructions
|
||||
- Impact assessment
|
||||
5. **CONFIRMATION REQUIRED**: PR created correctly?
|
||||
|
||||
### Step 7: Wait for Tests
|
||||
|
||||
1. Monitor PR checks: `gh pr checks`
|
||||
2. Display test results as they complete
|
||||
3. If any tests fail:
|
||||
- Show failure details
|
||||
- Analyze if related to cherry-picks
|
||||
- **DECISION REQUIRED**: Fix and continue, or abort?
|
||||
4. Wait for all required checks to pass
|
||||
5. **CONFIRMATION REQUIRED**: All tests passing?
|
||||
|
||||
### Step 8: Merge Hotfix PR
|
||||
|
||||
1. Verify all checks have passed
|
||||
2. Check for required approvals
|
||||
3. Merge the PR: `gh pr merge --merge`
|
||||
4. Delete the hotfix branch
|
||||
5. **CONFIRMATION REQUIRED**: PR merged successfully?
|
||||
|
||||
### Step 9: Create Version Bump
|
||||
|
||||
1. Checkout the core branch: `git checkout core/X.Y`
|
||||
2. Pull latest changes: `git pull origin core/X.Y`
|
||||
3. Read current version from package.json
|
||||
4. Determine patch version increment:
|
||||
- Current: `1.23.4` → New: `1.23.5`
|
||||
5. Create release branch named with new version: `release/1.23.5`
|
||||
6. Update version in package.json to `1.23.5`
|
||||
7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
|
||||
8. **CONFIRMATION REQUIRED**: Version bump correct?
|
||||
|
||||
### Step 10: Create Release PR
|
||||
|
||||
1. Push release branch: `git push origin release/1.23.5`
|
||||
2. Create PR with Release label:
|
||||
```bash
|
||||
gh pr create --base core/X.Y --head release/1.23.5 \
|
||||
--title "[Release] v1.23.5" \
|
||||
--body "..." \
|
||||
--label "Release"
|
||||
```
|
||||
3. **CRITICAL**: Verify "Release" label is added
|
||||
4. PR description should include:
|
||||
- Version: `1.23.4` → `1.23.5`
|
||||
- Included fixes (link to previous PR)
|
||||
- Release notes for users
|
||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||
|
||||
### Step 11: Monitor Release Process
|
||||
|
||||
1. Wait for PR checks to pass
|
||||
2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
|
||||
3. Merge the PR: `gh pr merge --merge`
|
||||
4. Monitor release workflow:
|
||||
```bash
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run watch
|
||||
```
|
||||
5. Track progress:
|
||||
- GitHub release draft/publication
|
||||
- PyPI upload
|
||||
- npm types publication
|
||||
|
||||
### Step 12: Post-Release Verification
|
||||
|
||||
1. Verify GitHub release:
|
||||
```bash
|
||||
gh release view v1.23.5
|
||||
```
|
||||
2. Check PyPI package:
|
||||
```bash
|
||||
pip index versions comfyui-frontend-package | grep 1.23.5
|
||||
```
|
||||
3. Verify npm package:
|
||||
```bash
|
||||
npm view @comfyorg/comfyui-frontend-types@1.23.5
|
||||
```
|
||||
4. Generate release summary with:
|
||||
- Version released
|
||||
- Commits included
|
||||
- Issues fixed
|
||||
- Distribution status
|
||||
5. **CONFIRMATION REQUIRED**: Release completed successfully?
|
||||
|
||||
## Safety Checks
|
||||
|
||||
Throughout the process:
|
||||
- Always verify core branch matches ComfyUI's requirements.txt
|
||||
- For PRs: Ensure using correct commits (merge vs individual)
|
||||
- Check version numbers follow semantic versioning
|
||||
- **Critical**: "Release" label must be on version bump PR
|
||||
- Validate cherry-picks don't break core branch stability
|
||||
- Keep audit trail of all operations
|
||||
|
||||
## Rollback Procedures
|
||||
|
||||
If something goes wrong:
|
||||
- Before push: `git reset --hard origin/core/X.Y`
|
||||
- After PR creation: Close PR and start over
|
||||
- After failed release: Create new patch version with fixes
|
||||
- Document any issues for future reference
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Core branch version will be behind main - this is expected
|
||||
- The "Release" label triggers the PyPI/npm publication
|
||||
- PR numbers must include the `#` prefix
|
||||
- Mixed commits/PRs are supported but review carefully
|
||||
- Always wait for full test suite before proceeding
|
||||
|
||||
## Expected Timeline
|
||||
|
||||
- Step 1-3: ~10 minutes (analysis)
|
||||
- Steps 4-6: ~15-30 minutes (cherry-picking)
|
||||
- Step 7: ~10-20 minutes (tests)
|
||||
- Steps 8-10: ~10 minutes (version bump)
|
||||
- Step 11-12: ~15-20 minutes (release)
|
||||
- Total: ~60-90 minutes
|
||||
|
||||
This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@@ -130,239 +130,4 @@ test.describe('Release Notifications', () => {
|
||||
whatsNewSection.locator('text=No recent releases')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should hide "What\'s New" section when notifications are disabled', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Disable version update notifications
|
||||
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
|
||||
|
||||
// Mock release API with test data
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
if (
|
||||
url.includes('api.comfy.org') ||
|
||||
url.includes('stagingapi.comfy.org')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 1,
|
||||
project: 'comfyui',
|
||||
version: 'v0.3.44',
|
||||
attention: 'high',
|
||||
content: '## New Features\n\n- Added awesome feature',
|
||||
published_at: new Date().toISOString()
|
||||
}
|
||||
])
|
||||
})
|
||||
} else {
|
||||
await route.continue()
|
||||
}
|
||||
})
|
||||
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
|
||||
// Open help center
|
||||
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
|
||||
await helpCenterButton.waitFor({ state: 'visible' })
|
||||
await helpCenterButton.click()
|
||||
|
||||
// Verify help center menu appears
|
||||
const helpMenu = comfyPage.page.locator('.help-center-menu')
|
||||
await expect(helpMenu).toBeVisible()
|
||||
|
||||
// Verify "What's New?" section is hidden
|
||||
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
|
||||
// Should not show any popups or toasts
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.release-notification-toast')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should not make API calls when notifications are disabled', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Disable version update notifications
|
||||
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
|
||||
|
||||
// Track API calls
|
||||
let apiCallCount = 0
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
if (
|
||||
url.includes('api.comfy.org') ||
|
||||
url.includes('stagingapi.comfy.org')
|
||||
) {
|
||||
apiCallCount++
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([])
|
||||
})
|
||||
} else {
|
||||
await route.continue()
|
||||
}
|
||||
})
|
||||
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
|
||||
// Wait a bit to ensure any potential API calls would have been made
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Verify no API calls were made
|
||||
expect(apiCallCount).toBe(0)
|
||||
})
|
||||
|
||||
test('should show "What\'s New" section when notifications are enabled', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Enable version update notifications (default behavior)
|
||||
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true)
|
||||
|
||||
// Mock release API with test data
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
if (
|
||||
url.includes('api.comfy.org') ||
|
||||
url.includes('stagingapi.comfy.org')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 1,
|
||||
project: 'comfyui',
|
||||
version: 'v0.3.44',
|
||||
attention: 'medium',
|
||||
content: '## New Features\n\n- Added awesome feature',
|
||||
published_at: new Date().toISOString()
|
||||
}
|
||||
])
|
||||
})
|
||||
} else {
|
||||
await route.continue()
|
||||
}
|
||||
})
|
||||
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
|
||||
// Open help center
|
||||
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
|
||||
await helpCenterButton.waitFor({ state: 'visible' })
|
||||
await helpCenterButton.click()
|
||||
|
||||
// Verify help center menu appears
|
||||
const helpMenu = comfyPage.page.locator('.help-center-menu')
|
||||
await expect(helpMenu).toBeVisible()
|
||||
|
||||
// Verify "What's New?" section is visible
|
||||
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
|
||||
await expect(whatsNewSection).toBeVisible()
|
||||
|
||||
// Should show the release
|
||||
await expect(
|
||||
whatsNewSection.locator('text=Comfy v0.3.44 Release')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should toggle "What\'s New" section when setting changes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Mock release API with test data
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
if (
|
||||
url.includes('api.comfy.org') ||
|
||||
url.includes('stagingapi.comfy.org')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 1,
|
||||
project: 'comfyui',
|
||||
version: 'v0.3.44',
|
||||
attention: 'low',
|
||||
content: '## Bug Fixes\n\n- Fixed minor issue',
|
||||
published_at: new Date().toISOString()
|
||||
}
|
||||
])
|
||||
})
|
||||
} else {
|
||||
await route.continue()
|
||||
}
|
||||
})
|
||||
|
||||
// Start with notifications enabled
|
||||
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true)
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
|
||||
// Open help center
|
||||
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
|
||||
await helpCenterButton.waitFor({ state: 'visible' })
|
||||
await helpCenterButton.click()
|
||||
|
||||
// Verify "What's New?" section is visible
|
||||
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
|
||||
await expect(whatsNewSection).toBeVisible()
|
||||
|
||||
// Close help center
|
||||
await comfyPage.page.click('.help-center-backdrop')
|
||||
|
||||
// Disable notifications
|
||||
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
|
||||
|
||||
// Reopen help center
|
||||
await helpCenterButton.click()
|
||||
|
||||
// Verify "What's New?" section is now hidden
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should handle edge case with empty releases and disabled notifications', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Disable notifications
|
||||
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
|
||||
|
||||
// Mock empty releases
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
if (
|
||||
url.includes('api.comfy.org') ||
|
||||
url.includes('stagingapi.comfy.org')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([])
|
||||
})
|
||||
} else {
|
||||
await route.continue()
|
||||
}
|
||||
})
|
||||
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
|
||||
// Open help center
|
||||
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
|
||||
await helpCenterButton.waitFor({ state: 'visible' })
|
||||
await helpCenterButton.click()
|
||||
|
||||
// Verify help center still works
|
||||
const helpMenu = comfyPage.page.locator('.help-center-menu')
|
||||
await expect(helpMenu).toBeVisible()
|
||||
|
||||
// Section should be hidden regardless of empty releases
|
||||
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Settings Search functionality', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Register test settings to verify hidden/deprecated filtering
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
name: 'TestSettingsExtension',
|
||||
settings: [
|
||||
{
|
||||
id: 'TestHiddenSetting',
|
||||
name: 'Test Hidden Setting',
|
||||
type: 'hidden',
|
||||
defaultValue: 'hidden_value',
|
||||
category: ['Test', 'Hidden']
|
||||
},
|
||||
{
|
||||
id: 'TestDeprecatedSetting',
|
||||
name: 'Test Deprecated Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'deprecated_value',
|
||||
deprecated: true,
|
||||
category: ['Test', 'Deprecated']
|
||||
},
|
||||
{
|
||||
id: 'TestVisibleSetting',
|
||||
name: 'Test Visible Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'visible_value',
|
||||
category: ['Test', 'Visible']
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('can open settings dialog and use search box', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await expect(searchBox).toBeVisible()
|
||||
|
||||
// Verify search box has the correct placeholder
|
||||
await expect(searchBox).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search')
|
||||
)
|
||||
})
|
||||
|
||||
test('search box is functional and accepts input', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find and interact with the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Comfy')
|
||||
|
||||
// Verify the input was accepted
|
||||
await expect(searchBox).toHaveValue('Comfy')
|
||||
})
|
||||
|
||||
test('search box clears properly', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find and interact with the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('test')
|
||||
await expect(searchBox).toHaveValue('test')
|
||||
|
||||
// Clear the search box
|
||||
await searchBox.clear()
|
||||
await expect(searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('settings categories are visible in sidebar', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Check that the sidebar has categories
|
||||
const categories = comfyPage.page.locator(
|
||||
'.settings-sidebar .p-listbox-option'
|
||||
)
|
||||
expect(await categories.count()).toBeGreaterThan(0)
|
||||
|
||||
// Check that at least one category is visible
|
||||
await expect(categories.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('can select different categories in sidebar', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Get categories and click on different ones
|
||||
const categories = comfyPage.page.locator(
|
||||
'.settings-sidebar .p-listbox-option'
|
||||
)
|
||||
const categoryCount = await categories.count()
|
||||
|
||||
if (categoryCount > 1) {
|
||||
// Click on the second category
|
||||
await categories.nth(1).click()
|
||||
|
||||
// Verify the category is selected
|
||||
await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/)
|
||||
}
|
||||
})
|
||||
|
||||
test('settings content area is visible', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Check that the content area is visible
|
||||
const contentArea = comfyPage.page.locator('.settings-content')
|
||||
await expect(contentArea).toBeVisible()
|
||||
|
||||
// Check that tab panels are visible
|
||||
const tabPanels = comfyPage.page.locator('.settings-tab-panels')
|
||||
await expect(tabPanels).toBeVisible()
|
||||
})
|
||||
|
||||
test('search functionality affects UI state', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
|
||||
// Type in search box
|
||||
await searchBox.fill('graph')
|
||||
await comfyPage.page.waitForTimeout(200) // Wait for debounce
|
||||
|
||||
// Verify that the search input is handled
|
||||
await expect(searchBox).toHaveValue('graph')
|
||||
})
|
||||
|
||||
test('settings dialog can be closed', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Close with escape key
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(settingsDialog).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('search box has proper debouncing behavior', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Type rapidly in search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('a')
|
||||
await searchBox.fill('ab')
|
||||
await searchBox.fill('abc')
|
||||
await searchBox.fill('abcd')
|
||||
|
||||
// Wait for debounce
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
// Verify final value
|
||||
await expect(searchBox).toHaveValue('abcd')
|
||||
})
|
||||
|
||||
test('search excludes hidden settings from results', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Test')
|
||||
await comfyPage.page.waitForTimeout(300) // Wait for debounce
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Should show visible setting but not hidden setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
})
|
||||
|
||||
test('search excludes deprecated settings from results', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Test')
|
||||
await comfyPage.page.waitForTimeout(300) // Wait for debounce
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Should show visible setting but not deprecated setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
})
|
||||
|
||||
test('search shows visible settings but excludes hidden and deprecated', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
await searchBox.fill('Test')
|
||||
await comfyPage.page.waitForTimeout(300) // Wait for debounce
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Should only show the visible setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
|
||||
// Should not show hidden or deprecated settings
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
})
|
||||
|
||||
test('search by setting name excludes hidden and deprecated', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
|
||||
// Search specifically for hidden setting by name
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Hidden')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
// Should not show the hidden setting even when searching by name
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
|
||||
// Search specifically for deprecated setting by name
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Deprecated')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
// Should not show the deprecated setting even when searching by name
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
|
||||
// Search for visible setting by name - should work
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Visible')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
// Should show the visible setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
})
|
||||
})
|
||||
502
frontend-v3-compatibility-plan.md
Normal file
502
frontend-v3-compatibility-plan.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# Frontend V3 Compatibility Layer Implementation Plan (Import-Based API Versioning)
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines an **import-based API versioning** approach for implementing v3 compatibility in the ComfyUI frontend. This approach provides **type-safe API versioning** through import-based version selection, **dual endpoint data fetching**, and **proxy-based data synchronization**. Extensions choose their API version at import time, ensuring compile-time type safety and backward compatibility.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Import-Based Version Selection**: Extensions choose API version through import statements
|
||||
2. **Type-Safe API Surfaces**: Compile-time type checking for each API version
|
||||
3. **Dual Endpoint Fetching**: Simultaneous fetching from current and v3 endpoints
|
||||
4. **Proxy-Based Synchronization**: Bidirectional data sync between API versions
|
||||
5. **Zero Breaking Changes**: Existing extensions continue to work unchanged
|
||||
6. **Gradual Migration**: Developers can adopt new versions incrementally
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### 1. Import-Based API Version Selection
|
||||
|
||||
Extensions import the API version they want to use, getting typed interfaces and guaranteed compatibility:
|
||||
|
||||
```typescript
|
||||
// Legacy extensions (unchanged)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Version-specific imports
|
||||
import { app } from '@/scripts/app/v1' // v1.x API
|
||||
import { app } from '@/scripts/app/v1_2' // v1.2 API
|
||||
import { app } from '@/scripts/app/v2' // v2.x API
|
||||
import { app } from '@/scripts/app/latest' // Latest/bleeding edge
|
||||
import { app } from '@/scripts/app' // Defaults to latest
|
||||
|
||||
// Full version-specific imports
|
||||
import { app, extensionManager, api } from '@/scripts/app/v1_2'
|
||||
|
||||
// Extensions get typed, version-specific interfaces
|
||||
app.registerExtension({
|
||||
name: 'MyExtension',
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app: ComfyAppV1_2) {
|
||||
// nodeData is guaranteed to be in v1.2 format
|
||||
// app methods are v1.2 compatible
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Dual Endpoint Data Fetching
|
||||
|
||||
The system fetches data from both current and future API endpoints simultaneously:
|
||||
|
||||
```typescript
|
||||
// Fetch from multiple endpoints
|
||||
const fetchNodeDefinitions = async () => {
|
||||
const [currentResponse, v3Response] = await Promise.allSettled([
|
||||
api.get('/object_info'), // Current format
|
||||
api.get('/v3/object_info') // V3 format (when available)
|
||||
])
|
||||
|
||||
// Store all formats
|
||||
return {
|
||||
canonical: mergeToCanonical(currentResponse, v3Response),
|
||||
v1: transformToV1(currentResponse),
|
||||
v1_2: transformToV1_2(currentResponse),
|
||||
v3: v3Response || transformToV3(currentResponse)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proxy-Based Data Synchronization
|
||||
|
||||
Proxies ensure that changes made through any API version stay synchronized:
|
||||
|
||||
```typescript
|
||||
// Extension modifies node data through v1.2 API
|
||||
const createV1_2NodeDefProxy = (canonicalNodeDef: ComfyNodeDefLatest) => {
|
||||
return new Proxy({}, {
|
||||
get(target, prop) {
|
||||
// Map v1.2 property access to canonical format
|
||||
if (prop === 'input') {
|
||||
return transformLatestToV1_2Input(canonicalNodeDef.inputs)
|
||||
}
|
||||
return canonicalNodeDef[mapV1_2PropToLatest(prop)]
|
||||
},
|
||||
|
||||
set(target, prop, value) {
|
||||
// Map v1.2 property changes back to canonical format
|
||||
if (prop === 'input') {
|
||||
canonicalNodeDef.inputs = transformV1_2InputToLatest(value)
|
||||
notifyDataChange(canonicalNodeDef.name, prop, value)
|
||||
return true
|
||||
}
|
||||
canonicalNodeDef[mapV1_2PropToLatest(prop)] = value
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Phase 1: API Version Infrastructure
|
||||
|
||||
**1.1 Version-Specific Entry Points**
|
||||
```typescript
|
||||
// src/scripts/app/index.ts (latest/default)
|
||||
export * from './latest'
|
||||
|
||||
// src/scripts/app/v1.ts
|
||||
export { app as default } from './adapters/v1AppAdapter'
|
||||
export { extensionManager } from './adapters/v1ExtensionAdapter'
|
||||
export { api } from './adapters/v1ApiAdapter'
|
||||
|
||||
// src/scripts/app/v1_2.ts
|
||||
export { app as default } from './adapters/v1_2AppAdapter'
|
||||
export { extensionManager } from './adapters/v1_2ExtensionAdapter'
|
||||
export { api } from './adapters/v1_2ApiAdapter'
|
||||
```
|
||||
|
||||
**1.2 Version-Specific TypeScript Interfaces**
|
||||
```typescript
|
||||
// src/types/versions/v1.ts
|
||||
export interface ComfyNodeDefV1 {
|
||||
name: string
|
||||
input?: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
}
|
||||
output?: string[]
|
||||
output_is_list?: boolean[]
|
||||
}
|
||||
|
||||
// src/types/versions/v1_2.ts
|
||||
export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 {
|
||||
inputs?: ComfyInputSpecV1_2[]
|
||||
metadata?: NodeMetadataV1_2
|
||||
}
|
||||
|
||||
// src/types/versions/v3.ts
|
||||
export interface ComfyNodeDefV3 {
|
||||
name: string
|
||||
schema: JsonSchema
|
||||
inputs: InputSpecV3[]
|
||||
outputs: OutputSpecV3[]
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Multi-Version Data Layer
|
||||
|
||||
**2.1 Unified Data Store**
|
||||
```typescript
|
||||
// src/stores/nodeDefStore.ts
|
||||
export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeDefinitions = ref<{
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
v1: Record<string, ComfyNodeDefV1>
|
||||
v1_2: Record<string, ComfyNodeDefV1_2>
|
||||
v3: Record<string, ComfyNodeDefV3>
|
||||
}>({
|
||||
canonical: {},
|
||||
v1: {},
|
||||
v1_2: {},
|
||||
v3: {}
|
||||
})
|
||||
|
||||
const fetchNodeDefinitions = async () => {
|
||||
const [currentData, v3Data] = await Promise.allSettled([
|
||||
api.get('/object_info'),
|
||||
api.get('/v3/object_info')
|
||||
])
|
||||
|
||||
nodeDefinitions.value = transformToAllVersions(currentData, v3Data)
|
||||
}
|
||||
|
||||
// Version-specific getters with reactivity
|
||||
const getNodeDefsV1 = computed(() => nodeDefinitions.value.v1)
|
||||
const getNodeDefsV1_2 = computed(() => nodeDefinitions.value.v1_2)
|
||||
const getNodeDefsV3 = computed(() => nodeDefinitions.value.v3)
|
||||
|
||||
return {
|
||||
nodeDefinitions,
|
||||
fetchNodeDefinitions,
|
||||
getNodeDefsV1,
|
||||
getNodeDefsV1_2,
|
||||
getNodeDefsV3
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**2.2 Data Transformation Pipeline**
|
||||
```typescript
|
||||
// src/utils/versionTransforms.ts
|
||||
export class VersionTransforms {
|
||||
static transformToAllVersions(currentData: any, v3Data: any) {
|
||||
const canonical = this.createCanonicalFormat(currentData, v3Data)
|
||||
|
||||
return {
|
||||
canonical,
|
||||
v1: this.canonicalToV1(canonical),
|
||||
v1_2: this.canonicalToV1_2(canonical),
|
||||
v3: v3Data || this.canonicalToV3(canonical)
|
||||
}
|
||||
}
|
||||
|
||||
static canonicalToV1(canonical: ComfyNodeDefLatest): ComfyNodeDefV1 {
|
||||
return {
|
||||
name: canonical.name,
|
||||
input: {
|
||||
required: canonical.inputs
|
||||
?.filter(i => i.required)
|
||||
.reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
optional: canonical.inputs
|
||||
?.filter(i => !i.required)
|
||||
.reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
},
|
||||
output: canonical.outputs?.map(o => o.type),
|
||||
output_is_list: canonical.outputs?.map(o => o.is_list)
|
||||
}
|
||||
}
|
||||
|
||||
static canonicalToV1_2(canonical: ComfyNodeDefLatest): ComfyNodeDefV1_2 {
|
||||
return {
|
||||
...this.canonicalToV1(canonical),
|
||||
inputs: canonical.inputs?.map(input => ({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
required: input.required,
|
||||
options: input.options
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Proxy-Based Synchronization
|
||||
|
||||
**3.1 Bidirectional Data Proxies**
|
||||
```typescript
|
||||
// src/utils/versionProxies.ts
|
||||
export class VersionProxies {
|
||||
private static canonicalStore = new Map<string, ComfyNodeDefLatest>()
|
||||
private static eventBus = new EventTarget()
|
||||
|
||||
static createV1Proxy(nodeId: string): ComfyNodeDefV1 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) throw new Error(`Node ${nodeId} not found`)
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1, {
|
||||
get(target, prop: keyof ComfyNodeDefV1) {
|
||||
return VersionProxies.transformCanonicalToV1Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1, value) {
|
||||
VersionProxies.transformV1PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createV1_2Proxy(nodeId: string): ComfyNodeDefV1_2 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) throw new Error(`Node ${nodeId} not found`)
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1_2, {
|
||||
get(target, prop: keyof ComfyNodeDefV1_2) {
|
||||
return VersionProxies.transformCanonicalToV1_2Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1_2, value) {
|
||||
VersionProxies.transformV1_2PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static transformCanonicalToV1Property(canonical: ComfyNodeDefLatest, prop: keyof ComfyNodeDefV1) {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
return {
|
||||
required: canonical.inputs?.filter(i => i.required).reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
optional: canonical.inputs?.filter(i => !i.required).reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
}
|
||||
case 'output':
|
||||
return canonical.outputs?.map(o => o.type)
|
||||
case 'output_is_list':
|
||||
return canonical.outputs?.map(o => o.is_list)
|
||||
default:
|
||||
return canonical[prop as keyof ComfyNodeDefLatest]
|
||||
}
|
||||
}
|
||||
|
||||
private static transformV1PropertyToCanonical(canonical: ComfyNodeDefLatest, prop: keyof ComfyNodeDefV1, value: any) {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
canonical.inputs = [
|
||||
...Object.entries(value.required || {}).map(([name, spec]) => ({
|
||||
name,
|
||||
spec,
|
||||
required: true,
|
||||
type: this.inferTypeFromSpec(spec)
|
||||
})),
|
||||
...Object.entries(value.optional || {}).map(([name, spec]) => ({
|
||||
name,
|
||||
spec,
|
||||
required: false,
|
||||
type: this.inferTypeFromSpec(spec)
|
||||
}))
|
||||
]
|
||||
break
|
||||
case 'output':
|
||||
canonical.outputs = value.map((type: string, index: number) => ({
|
||||
type,
|
||||
is_list: canonical.outputs?.[index]?.is_list || false
|
||||
}))
|
||||
break
|
||||
case 'output_is_list':
|
||||
canonical.outputs = canonical.outputs?.map((output, index) => ({
|
||||
...output,
|
||||
is_list: value[index] || false
|
||||
}))
|
||||
break
|
||||
default:
|
||||
(canonical as any)[prop] = value
|
||||
}
|
||||
}
|
||||
|
||||
private static notifyChange(nodeId: string, prop: string, value: any) {
|
||||
this.eventBus.dispatchEvent(new CustomEvent('nodedef-changed', {
|
||||
detail: { nodeId, prop, value }
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Extension System Integration
|
||||
|
||||
**4.1 Version-Aware Extension Service**
|
||||
```typescript
|
||||
// src/services/extensionService.ts
|
||||
export const useExtensionService = () => {
|
||||
const extensionsByVersion = new Map<string, ComfyExtension[]>()
|
||||
|
||||
const registerExtension = (extension: ComfyExtension, apiVersion: string = 'latest') => {
|
||||
extension.apiVersion = apiVersion
|
||||
|
||||
if (!extensionsByVersion.has(apiVersion)) {
|
||||
extensionsByVersion.set(apiVersion, [])
|
||||
}
|
||||
extensionsByVersion.get(apiVersion)!.push(extension)
|
||||
}
|
||||
|
||||
const invokeExtensionsForAllVersions = async (hook: string, canonicalArgs: any[]) => {
|
||||
const promises = []
|
||||
|
||||
for (const [version, extensions] of extensionsByVersion) {
|
||||
const versionPromise = invokeExtensionsForVersion(version, hook, canonicalArgs)
|
||||
promises.push(versionPromise)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
const invokeExtensionsForVersion = async (version: string, hook: string, canonicalArgs: any[]) => {
|
||||
const extensions = extensionsByVersion.get(version) || []
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (extension[hook]) {
|
||||
const transformedArgs = transformArgsForVersion(version, canonicalArgs)
|
||||
await extension[hook](...transformedArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const transformArgsForVersion = (version: string, args: any[]) => {
|
||||
return args.map(arg => {
|
||||
if (arg && typeof arg === 'object' && arg.name) {
|
||||
// This is likely a node definition
|
||||
switch (version) {
|
||||
case 'v1':
|
||||
return VersionProxies.createV1Proxy(arg.name)
|
||||
case 'v1_2':
|
||||
return VersionProxies.createV1_2Proxy(arg.name)
|
||||
case 'v3':
|
||||
return VersionProxies.createV3Proxy(arg.name)
|
||||
default:
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return arg
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
registerExtension,
|
||||
invokeExtensionsForAllVersions,
|
||||
invokeExtensionsForVersion
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4.2 Version-Specific App Adapters**
|
||||
```typescript
|
||||
// src/scripts/app/adapters/v1AppAdapter.ts
|
||||
export class V1AppAdapter {
|
||||
constructor(private canonicalApp: ComfyApp) {}
|
||||
|
||||
registerExtension(extension: ComfyExtensionV1) {
|
||||
const wrappedExtension = {
|
||||
...extension,
|
||||
apiVersion: 'v1',
|
||||
beforeRegisterNodeDef: (nodeType, nodeData, app) => {
|
||||
const v1NodeData = VersionProxies.createV1Proxy(nodeData.name)
|
||||
return extension.beforeRegisterNodeDef?.(nodeType, v1NodeData, this)
|
||||
},
|
||||
nodeCreated: (node, app) => {
|
||||
return extension.nodeCreated?.(node, this)
|
||||
}
|
||||
}
|
||||
|
||||
this.canonicalApp.registerExtension(wrappedExtension)
|
||||
}
|
||||
|
||||
// Implement other ComfyApp methods with v1 compatibility
|
||||
}
|
||||
|
||||
// src/scripts/app/adapters/v1_2AppAdapter.ts
|
||||
export class V1_2AppAdapter {
|
||||
constructor(private canonicalApp: ComfyApp) {}
|
||||
|
||||
registerExtension(extension: ComfyExtensionV1_2) {
|
||||
const wrappedExtension = {
|
||||
...extension,
|
||||
apiVersion: 'v1_2',
|
||||
beforeRegisterNodeDef: (nodeType, nodeData, app) => {
|
||||
const v1_2NodeData = VersionProxies.createV1_2Proxy(nodeData.name)
|
||||
return extension.beforeRegisterNodeDef?.(nodeType, v1_2NodeData, this)
|
||||
}
|
||||
}
|
||||
|
||||
this.canonicalApp.registerExtension(wrappedExtension)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Gradual Migration Path
|
||||
|
||||
Extensions can migrate incrementally:
|
||||
|
||||
```typescript
|
||||
// Phase 1: No changes (works with latest)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Phase 2: Explicit version (better compatibility)
|
||||
import { app } from '@/scripts/app/v1_2'
|
||||
|
||||
// Phase 3: Use newer APIs when ready
|
||||
import { app } from '@/scripts/app/latest'
|
||||
```
|
||||
|
||||
### Development Tools
|
||||
|
||||
```typescript
|
||||
// Enhanced debugging for version compatibility
|
||||
ComfyUI.debugExtensions.showVersionMatrix() // Shows which extensions use which API versions
|
||||
ComfyUI.debugExtensions.testVersionCompatibility() // Tests extension against all API versions
|
||||
ComfyUI.debugExtensions.validateDataSync() // Validates proxy synchronization
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Type Safety**: Compile-time type checking for each API version
|
||||
2. **Zero Breaking Changes**: Existing extensions work unchanged
|
||||
3. **Bidirectional Sync**: Changes through any API version stay synchronized
|
||||
4. **Future Proof**: Easy to add new API versions
|
||||
5. **Performance**: No runtime version detection overhead
|
||||
6. **Developer Experience**: Clear, typed interfaces for each version
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
- **Phase 1**: API Version Infrastructure (3-4 days)
|
||||
- **Phase 2**: Multi-Version Data Layer (2-3 days)
|
||||
- **Phase 3**: Proxy-Based Synchronization (2-3 days)
|
||||
- **Phase 4**: Extension System Integration (2-3 days)
|
||||
- **Phase 5**: Migration Tools & Documentation (1-2 days)
|
||||
|
||||
**Total Estimated Time**: 10-15 days
|
||||
|
||||
This approach provides a solid foundation for API versioning that scales with ComfyUI's growth while maintaining backward compatibility and providing a smooth migration path for extension developers.
|
||||
134
frontend-v3-compatibility-summary.md
Normal file
134
frontend-v3-compatibility-summary.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# ComfyUI Frontend V3 Compatibility - Implementation Summary
|
||||
|
||||
## Core Concept
|
||||
**Import-based API versioning** with proxy-synchronized data layers. Extensions choose their API version through imports, getting typed interfaces and guaranteed backward compatibility.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### 1. Version-Specific Imports
|
||||
```typescript
|
||||
// Legacy (unchanged)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Version-specific
|
||||
import { app } from '@/scripts/app/v1' // v1.x API
|
||||
import { app } from '@/scripts/app/v1_2' // v1.2 API
|
||||
import { app } from '@/scripts/app/v3' // v3.x API
|
||||
import { app } from '@/scripts/app/latest' // Latest
|
||||
|
||||
// Typed interfaces per version
|
||||
app.registerExtension({
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app: ComfyAppV1_2) {
|
||||
// nodeData guaranteed to be v1.2 format
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Dual Endpoint Data Fetching
|
||||
```typescript
|
||||
// Simultaneous fetching from multiple endpoints
|
||||
const [currentData, v3Data] = await Promise.allSettled([
|
||||
api.get('/object_info'), // Current format
|
||||
api.get('/v3/object_info') // V3 format
|
||||
])
|
||||
|
||||
// All versions stored and transformed
|
||||
nodeDefinitions.value = {
|
||||
canonical: mergeToCanonical(currentData, v3Data),
|
||||
v1: transformToV1(currentData),
|
||||
v1_2: transformToV1_2(currentData),
|
||||
v3: v3Data || transformToV3(currentData)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proxy-Based Synchronization
|
||||
```typescript
|
||||
// Bidirectional data sync through proxies
|
||||
const createV1_2Proxy = (canonical: ComfyNodeDefLatest) => {
|
||||
return new Proxy({}, {
|
||||
get(target, prop) {
|
||||
return transformCanonicalToV1_2(canonical, prop)
|
||||
},
|
||||
set(target, prop, value) {
|
||||
transformV1_2ToCanonical(canonical, prop, value)
|
||||
notifyChange(canonical.name, prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Structure
|
||||
|
||||
### Phase 1: API Infrastructure (3-4 days)
|
||||
- **Entry Points**: `src/scripts/app/v1.ts`, `src/scripts/app/v1_2.ts`, etc.
|
||||
- **Type Definitions**: `src/types/versions/` with version-specific interfaces
|
||||
- **Adapters**: `src/scripts/app/adapters/` for API compatibility layers
|
||||
|
||||
### Phase 2: Data Layer (2-3 days)
|
||||
- **Multi-Version Store**: Single store with all format versions
|
||||
- **Transform Pipeline**: `src/utils/versionTransforms.ts` for format conversion
|
||||
- **Reactive Getters**: Version-specific computed properties
|
||||
|
||||
### Phase 3: Proxy System (2-3 days)
|
||||
- **Bidirectional Proxies**: `src/utils/versionProxies.ts`
|
||||
- **Change Notification**: Event system for data sync
|
||||
- **Type Safety**: Proper typing for proxy objects
|
||||
|
||||
### Phase 4: Extension Integration (2-3 days)
|
||||
- **Version-Aware Service**: Extensions grouped by API version
|
||||
- **Hook Invocation**: Transform args per version before calling
|
||||
- **App Adapters**: Version-specific app instances
|
||||
|
||||
## Key Benefits
|
||||
|
||||
- **Type Safety**: Compile-time checking for each API version
|
||||
- **Zero Breaking Changes**: Existing extensions work unchanged
|
||||
- **Bidirectional Sync**: Changes through any API stay synchronized
|
||||
- **Performance**: No runtime version detection overhead
|
||||
- **Future Proof**: Easy to add new API versions
|
||||
- **Developer Experience**: Clear, typed interfaces
|
||||
|
||||
## Migration Path
|
||||
|
||||
```typescript
|
||||
// Step 1: No changes (works with latest)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Step 2: Explicit version (better compatibility)
|
||||
import { app } from '@/scripts/app/v1_2'
|
||||
|
||||
// Step 3: Use newer APIs when ready
|
||||
import { app } from '@/scripts/app/latest'
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
```typescript
|
||||
// v1.2 Extension
|
||||
import { app } from '@/scripts/app/v1_2'
|
||||
|
||||
app.registerExtension({
|
||||
name: 'MyExtension',
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app) {
|
||||
// nodeData.input.required - v1 format
|
||||
// nodeData.inputs - v1.2 format
|
||||
if (nodeData.inputs) {
|
||||
// Use v1.2 features
|
||||
} else {
|
||||
// Fallback to v1 format
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Implementation Timeline
|
||||
- **Phase 1**: API Version Infrastructure (3-4 days)
|
||||
- **Phase 2**: Multi-Version Data Layer (2-3 days)
|
||||
- **Phase 3**: Proxy-Based Synchronization (2-3 days)
|
||||
- **Phase 4**: Extension System Integration (2-3 days)
|
||||
- **Phase 5**: Migration Tools & Documentation (1-2 days)
|
||||
|
||||
**Total**: 10-15 days
|
||||
|
||||
This approach provides type-safe API versioning that scales with ComfyUI's growth while maintaining complete backward compatibility and smooth migration paths for extension developers.
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.24.0",
|
||||
"version": "1.24.0-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.24.0",
|
||||
"version": "1.24.0-1",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.16.6",
|
||||
"@comfyorg/litegraph": "^0.16.5",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -949,9 +949,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.6.tgz",
|
||||
"integrity": "sha512-pRmJYZ39rIpGIaJAaOLicRFe3KyeNTXNAAB0+Thz8cPGpu2dBv8W6PlOu94VYNRc+pBhEwV+jJVlXb5YyAvBXQ==",
|
||||
"version": "0.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.5.tgz",
|
||||
"integrity": "sha512-XFjlMQj6+ONbgl9dzmFP/g8Jfohzbdgxu/j9vld0bE4DDO59v8JCPrmE32ugM/18swxuoEk6iZNWKf2PGs5X2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.24.0",
|
||||
"version": "1.24.0-1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -77,7 +77,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.16.6",
|
||||
"@comfyorg/litegraph": "^0.16.5",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -17,15 +17,7 @@ const IGNORE_PATTERNS = [
|
||||
/^templateWorkflows\./, // Template workflows are loaded dynamically
|
||||
/^dataTypes\./, // Data types might be referenced dynamically
|
||||
/^contextMenu\./, // Context menu items might be dynamic
|
||||
/^color\./, // Color names might be used dynamically
|
||||
// Auto-generated categories from collect-i18n-general.ts
|
||||
/^menuLabels\./, // Menu labels generated from command labels
|
||||
/^settingsCategories\./, // Settings categories generated from setting definitions
|
||||
/^serverConfigItems\./, // Server config items generated from SERVER_CONFIG_ITEMS
|
||||
/^serverConfigCategories\./, // Server config categories generated from config categories
|
||||
/^nodeCategories\./, // Node categories generated from node definitions
|
||||
// Setting option values that are dynamically generated
|
||||
/\.options\./ // All setting options are rendered dynamically
|
||||
/^color\./ // Color names might be used dynamically
|
||||
]
|
||||
|
||||
// Get list of staged locale files
|
||||
@@ -105,21 +97,17 @@ function shouldIgnoreKey(key: string): boolean {
|
||||
|
||||
// Search for key usage in source files
|
||||
function isKeyUsed(key: string, sourceFiles: string[]): boolean {
|
||||
// Escape special regex characters
|
||||
const escapeRegex = (str: string) =>
|
||||
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const escapedKey = escapeRegex(key)
|
||||
const lastPart = key.split('.').pop()
|
||||
const escapedLastPart = lastPart ? escapeRegex(lastPart) : ''
|
||||
|
||||
// Common patterns for i18n key usage
|
||||
const patterns = [
|
||||
// Direct usage: $t('key'), t('key'), i18n.t('key')
|
||||
new RegExp(`[t$]\\s*\\(\\s*['"\`]${escapedKey}['"\`]`, 'g'),
|
||||
new RegExp(`[t$]\\s*\\(\\s*['"\`]${key}['"\`]`, 'g'),
|
||||
// With namespace: $t('g.key'), t('namespace.key')
|
||||
new RegExp(`[t$]\\s*\\(\\s*['"\`][^'"]+\\.${escapedLastPart}['"\`]`, 'g'),
|
||||
new RegExp(
|
||||
`[t$]\\s*\\(\\s*['"\`][^'"]+\\.${key.split('.').pop()}['"\`]`,
|
||||
'g'
|
||||
),
|
||||
// Dynamic keys might reference parts of the key
|
||||
new RegExp(`['"\`]${escapedKey}['"\`]`, 'g')
|
||||
new RegExp(`['"\`]${key}['"\`]`, 'g')
|
||||
]
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</Teleport>
|
||||
|
||||
<!-- What's New Section -->
|
||||
<section v-if="showVersionUpdates" class="whats-new-section">
|
||||
<section class="whats-new-section">
|
||||
<h3 class="section-description">{{ $t('helpCenter.whatsNew') }}</h3>
|
||||
|
||||
<!-- Release Items -->
|
||||
@@ -126,7 +126,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
|
||||
@@ -162,14 +161,13 @@ const TIME_UNITS = {
|
||||
const SUBMENU_CONFIG = {
|
||||
DELAY_MS: 100,
|
||||
OFFSET_PX: 8,
|
||||
Z_INDEX: 10001
|
||||
Z_INDEX: 1002
|
||||
} as const
|
||||
|
||||
// Composables
|
||||
const { t, locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -184,9 +182,6 @@ let hoverTimeout: number | null = null
|
||||
|
||||
// Computed
|
||||
const hasReleases = computed(() => releaseStore.releases.length > 0)
|
||||
const showVersionUpdates = computed(() =>
|
||||
settingStore.get('Comfy.Notification.ShowVersionUpdates')
|
||||
)
|
||||
|
||||
const moreMenuItem = computed(() =>
|
||||
menuItems.value.find((item) => item.key === 'more')
|
||||
|
||||
@@ -32,32 +32,28 @@
|
||||
|
||||
<div class="whats-new-popup" @click.stop>
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
class="close-button"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="closePopup"
|
||||
>
|
||||
<button class="close-button" aria-label="Close" @click="closePopup">
|
||||
<div class="close-icon"></div>
|
||||
</button>
|
||||
|
||||
<!-- Release Content -->
|
||||
<div class="popup-content">
|
||||
<div class="content-text" v-html="formattedContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="popup-actions">
|
||||
<a
|
||||
class="learn-more-link"
|
||||
:href="changelogUrl"
|
||||
target="_blank"
|
||||
rel="noopener,noreferrer"
|
||||
@click="closePopup"
|
||||
>
|
||||
{{ $t('whatsNewPopup.learnMore') }}
|
||||
</a>
|
||||
<!-- TODO: CTA button -->
|
||||
<!-- <button class="cta-button" @click="handleCTA">CTA</button> -->
|
||||
</div>
|
||||
<!-- Actions Section -->
|
||||
<div class="popup-actions">
|
||||
<a
|
||||
class="learn-more-link"
|
||||
:href="changelogUrl"
|
||||
target="_blank"
|
||||
rel="noopener,noreferrer"
|
||||
@click="closePopup"
|
||||
>
|
||||
{{ $t('whatsNewPopup.learnMore') }}
|
||||
</a>
|
||||
<!-- TODO: CTA button -->
|
||||
<!-- <button class="cta-button" @click="handleCTA">CTA</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +68,7 @@ import type { ReleaseNote } from '@/services/releaseService'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
|
||||
const { locale, t } = useI18n()
|
||||
const { locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
|
||||
// Local state for dismissed status
|
||||
@@ -105,12 +101,13 @@ const changelogUrl = computed(() => {
|
||||
// Format release content for display using marked
|
||||
const formattedContent = computed(() => {
|
||||
if (!latestRelease.value?.content) {
|
||||
return `<p>${t('whatsNewPopup.noReleaseNotes')}</p>`
|
||||
return '<p>No release notes available.</p>'
|
||||
}
|
||||
|
||||
try {
|
||||
// Use marked to parse markdown to HTML
|
||||
return marked(latestRelease.value.content, {
|
||||
breaks: true, // Convert line breaks to <br>
|
||||
gfm: true // Enable GitHub Flavored Markdown
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -202,10 +199,14 @@ defineExpose({
|
||||
}
|
||||
|
||||
.whats-new-popup {
|
||||
padding: 32px 32px 24px;
|
||||
background: #353535;
|
||||
border-radius: 12px;
|
||||
max-width: 400px;
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
outline: 1px solid #4e4e4e;
|
||||
outline-offset: -1px;
|
||||
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.3);
|
||||
@@ -216,10 +217,6 @@ defineExpose({
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
overflow: hidden;
|
||||
padding: 32px 32px 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
@@ -227,17 +224,17 @@ defineExpose({
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
padding: 6px 7px;
|
||||
background: #7c7c7c;
|
||||
border-radius: 16px;
|
||||
border-radius: 15.5px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translate(30%, -30%);
|
||||
transform: translate(50%, -50%);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
transform 0.1s ease;
|
||||
@@ -250,7 +247,7 @@ defineExpose({
|
||||
|
||||
.close-button:active {
|
||||
background: #6a6a6a;
|
||||
transform: translate(30%, -30%) scale(0.95);
|
||||
transform: translate(50%, -50%) scale(0.95);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
@@ -291,45 +288,73 @@ defineExpose({
|
||||
.content-text {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Style the markdown content */
|
||||
/* Title */
|
||||
.content-text :deep(*) {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-text :deep(h1) {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Version subtitle - targets the first p tag after h1 */
|
||||
.content-text :deep(h1 + p) {
|
||||
color: #c0c0c0;
|
||||
.content-text :deep(h2) {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 16px 0 12px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.content-text :deep(h2:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-text :deep(h3) {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
font-weight: 600;
|
||||
margin: 12px 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.content-text :deep(h3:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-text :deep(h4) {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 8px 0 6px 0;
|
||||
}
|
||||
|
||||
.content-text :deep(h4:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Regular paragraphs - short description */
|
||||
.content-text :deep(p) {
|
||||
margin-bottom: 16px;
|
||||
color: #e0e0e0;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content-text :deep(p:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-text :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* List */
|
||||
.content-text :deep(ul),
|
||||
.content-text :deep(ol) {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
margin: 0 0 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.content-text :deep(ul:first-child),
|
||||
@@ -342,63 +367,12 @@ defineExpose({
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* List items */
|
||||
.content-text :deep(li) {
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.content-text :deep(li:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Custom bullet points */
|
||||
.content-text :deep(li::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
display: flex;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 100px;
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
/* List item strong text */
|
||||
.content-text :deep(li strong) {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.content-text :deep(li p) {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Code styling */
|
||||
.content-text :deep(code) {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
color: #f8f8f2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Remove top margin for first media element */
|
||||
.content-text :deep(img:first-child),
|
||||
.content-text :deep(video:first-child),
|
||||
.content-text :deep(iframe:first-child) {
|
||||
margin-top: -32px; /* Align with the top edge of the popup content */
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Media elements */
|
||||
@@ -407,7 +381,8 @@ defineExpose({
|
||||
.content-text :deep(iframe) {
|
||||
width: calc(100% + 64px);
|
||||
height: auto;
|
||||
margin: 24px -32px;
|
||||
border-radius: 6px;
|
||||
margin: 12px -32px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -422,6 +397,7 @@ defineExpose({
|
||||
.learn-more-link {
|
||||
color: #60a5fa;
|
||||
font-size: 14px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 500;
|
||||
line-height: 18.2px;
|
||||
text-decoration: none;
|
||||
@@ -441,6 +417,7 @@ defineExpose({
|
||||
border: none;
|
||||
color: #121212;
|
||||
font-size: 14px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</Teleport>
|
||||
|
||||
<!-- Backdrop to close popup when clicking outside -->
|
||||
<Teleport to="body">
|
||||
<Teleport to="#graph-canvas-container">
|
||||
<div
|
||||
v-if="isHelpCenterVisible"
|
||||
class="help-center-backdrop"
|
||||
@@ -101,14 +101,14 @@ onMounted(async () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
z-index: 999;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.help-center-popup {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
z-index: 10000;
|
||||
z-index: 1000;
|
||||
animation: slideInUp 0.2s ease-out;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
|
||||
const renderingSpeed = String(renderingSpeedWidget.value)
|
||||
if (renderingSpeed.toLowerCase().includes('quality')) {
|
||||
basePrice = 0.09
|
||||
basePrice = 0.08
|
||||
} else if (renderingSpeed.toLowerCase().includes('balanced')) {
|
||||
basePrice = 0.06
|
||||
} else if (renderingSpeed.toLowerCase().includes('turbo')) {
|
||||
@@ -322,15 +322,15 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const effectScene = String(effectSceneWidget.value)
|
||||
if (
|
||||
effectScene.includes('fuzzyfuzzy') ||
|
||||
effectScene.includes('squish')
|
||||
effectScene.includes('squish') ||
|
||||
effectScene.includes('expansion')
|
||||
) {
|
||||
return '$0.28/Run'
|
||||
} else if (effectScene.includes('dizzydizzy')) {
|
||||
} else if (
|
||||
effectScene.includes('dizzydizzy') ||
|
||||
effectScene.includes('bloombloom')
|
||||
) {
|
||||
return '$0.49/Run'
|
||||
} else if (effectScene.includes('bloombloom')) {
|
||||
return '$0.49/Run'
|
||||
} else if (effectScene.includes('expansion')) {
|
||||
return '$0.28/Run'
|
||||
}
|
||||
|
||||
return '$0.28/Run'
|
||||
@@ -448,12 +448,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
} else if (model.includes('ray-2')) {
|
||||
if (duration.includes('5s')) {
|
||||
if (resolution.includes('4k')) return '$6.37/Run'
|
||||
if (resolution.includes('1080p')) return '$1.59/Run'
|
||||
if (resolution.includes('1080p')) return '$2.30/Run'
|
||||
if (resolution.includes('720p')) return '$0.71/Run'
|
||||
if (resolution.includes('540p')) return '$0.40/Run'
|
||||
} else if (duration.includes('9s')) {
|
||||
if (resolution.includes('4k')) return '$11.47/Run'
|
||||
if (resolution.includes('1080p')) return '$2.87/Run'
|
||||
if (resolution.includes('1080p')) return '$4.14/Run'
|
||||
if (resolution.includes('720p')) return '$1.28/Run'
|
||||
if (resolution.includes('540p')) return '$0.72/Run'
|
||||
}
|
||||
@@ -499,12 +499,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
} else if (model.includes('ray-2')) {
|
||||
if (duration.includes('5s')) {
|
||||
if (resolution.includes('4k')) return '$6.37/Run'
|
||||
if (resolution.includes('1080p')) return '$1.59/Run'
|
||||
if (resolution.includes('1080p')) return '$2.30/Run'
|
||||
if (resolution.includes('720p')) return '$0.71/Run'
|
||||
if (resolution.includes('540p')) return '$0.40/Run'
|
||||
} else if (duration.includes('9s')) {
|
||||
if (resolution.includes('4k')) return '$11.47/Run'
|
||||
if (resolution.includes('1080p')) return '$2.87/Run'
|
||||
if (resolution.includes('1080p')) return '$4.14/Run'
|
||||
if (resolution.includes('720p')) return '$1.28/Run'
|
||||
if (resolution.includes('540p')) return '$0.72/Run'
|
||||
}
|
||||
@@ -947,63 +947,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
|
||||
return '$0.0172/Run'
|
||||
}
|
||||
},
|
||||
MoonvalleyTxt2VideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const lengthWidget = node.widgets?.find(
|
||||
(w) => w.name === 'length'
|
||||
) as IComboWidget
|
||||
|
||||
// If no length widget exists, default to 5s pricing
|
||||
if (!lengthWidget) return '$1.50/Run'
|
||||
|
||||
const length = String(lengthWidget.value)
|
||||
if (length === '5s') {
|
||||
return '$1.50/Run'
|
||||
} else if (length === '10s') {
|
||||
return '$3.00/Run'
|
||||
}
|
||||
|
||||
return '$1.50/Run'
|
||||
}
|
||||
},
|
||||
MoonvalleyImg2VideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const lengthWidget = node.widgets?.find(
|
||||
(w) => w.name === 'length'
|
||||
) as IComboWidget
|
||||
|
||||
// If no length widget exists, default to 5s pricing
|
||||
if (!lengthWidget) return '$1.50/Run'
|
||||
|
||||
const length = String(lengthWidget.value)
|
||||
if (length === '5s') {
|
||||
return '$1.50/Run'
|
||||
} else if (length === '10s') {
|
||||
return '$3.00/Run'
|
||||
}
|
||||
|
||||
return '$1.50/Run'
|
||||
}
|
||||
},
|
||||
MoonvalleyVideo2VideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const lengthWidget = node.widgets?.find(
|
||||
(w) => w.name === 'length'
|
||||
) as IComboWidget
|
||||
|
||||
// If no length widget exists, default to 5s pricing
|
||||
if (!lengthWidget) return '$2.25/Run'
|
||||
|
||||
const length = String(lengthWidget.value)
|
||||
if (length === '5s') {
|
||||
return '$2.25/Run'
|
||||
} else if (length === '10s') {
|
||||
return '$4.00/Run'
|
||||
}
|
||||
|
||||
return '$2.25/Run'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,10 +1015,7 @@ export const useNodePricing = () => {
|
||||
RecraftVectorizeImageNode: ['n'],
|
||||
RecraftGenerateColorFromImageNode: ['n'],
|
||||
RecraftGenerateImageNode: ['n'],
|
||||
RecraftGenerateVectorImageNode: ['n'],
|
||||
MoonvalleyTxt2VideoNode: ['length'],
|
||||
MoonvalleyImg2VideoNode: ['length'],
|
||||
MoonvalleyVideo2VideoNode: ['length']
|
||||
RecraftGenerateVectorImageNode: ['n']
|
||||
}
|
||||
return widgetMap[nodeType] || []
|
||||
}
|
||||
|
||||
@@ -53,11 +53,6 @@ export function useSettingSearch() {
|
||||
const queryLower = query.toLocaleLowerCase()
|
||||
const allSettings = Object.values(settingStore.settingsById)
|
||||
const filteredSettings = allSettings.filter((setting) => {
|
||||
// Filter out hidden and deprecated settings, just like in normal settings tree
|
||||
if (setting.type === 'hidden' || setting.deprecated) {
|
||||
return false
|
||||
}
|
||||
|
||||
const idLower = setting.id.toLowerCase()
|
||||
const nameLower = setting.name.toLowerCase()
|
||||
const translatedName = st(
|
||||
|
||||
@@ -330,14 +330,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: true,
|
||||
versionAdded: '1.20.3'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Notification.ShowVersionUpdates',
|
||||
category: ['Comfy', 'Notification Preferences'],
|
||||
name: 'Show version updates',
|
||||
tooltip: 'Show updates for new models, and major new features.',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ConfirmClear',
|
||||
category: ['Comfy', 'Workflow', 'ConfirmClear'],
|
||||
|
||||
@@ -949,8 +949,7 @@
|
||||
"Light": "Light",
|
||||
"User": "User",
|
||||
"Credits": "Credits",
|
||||
"API Nodes": "API Nodes",
|
||||
"Notification Preferences": "Notification Preferences"
|
||||
"API Nodes": "API Nodes"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1489,7 +1488,6 @@
|
||||
"loadError": "Failed to load help: {error}"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "Learn more",
|
||||
"noReleaseNotes": "No release notes available."
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
}
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "Number of nodes suggestions",
|
||||
"tooltip": "Only for litegraph searchbox/context menu"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "Show version updates",
|
||||
"tooltip": "Show updates for new models, and major new features."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "Pointer click drift delay",
|
||||
"tooltip": "After pressing a pointer button down, this is the maximum time (in milliseconds) that pointer movement can be ignored for.\n\nHelps prevent objects from being unintentionally nudged if the pointer is moved whilst clicking."
|
||||
|
||||
@@ -785,13 +785,13 @@
|
||||
"Toggle Bottom Panel": "Alternar panel inferior",
|
||||
"Toggle Focus Mode": "Alternar modo de enfoque",
|
||||
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
|
||||
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
|
||||
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
|
||||
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
|
||||
"Toggle Model Library Sidebar": "Alternar barra lateral de biblioteca de modelos",
|
||||
"Toggle Node Library Sidebar": "Alternar barra lateral de biblioteca de nodos",
|
||||
"Toggle Queue Sidebar": "Alternar barra lateral de cola",
|
||||
"Toggle Search Box": "Alternar caja de búsqueda",
|
||||
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
|
||||
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
|
||||
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
|
||||
"Toggle Workflows Sidebar": "Alternar barra lateral de flujos de trabajo",
|
||||
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
@@ -1099,7 +1099,6 @@
|
||||
"Node Search Box": "Caja de Búsqueda de Nodo",
|
||||
"Node Widget": "Widget de Nodo",
|
||||
"NodeLibrary": "Biblioteca de Nodos",
|
||||
"Notification Preferences": "Preferencias de notificación",
|
||||
"Pointer": "Puntero",
|
||||
"Queue": "Cola",
|
||||
"QueueButton": "Botón de Cola",
|
||||
@@ -1484,8 +1483,7 @@
|
||||
"title": "Bienvenido a ComfyUI"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "Aprende más",
|
||||
"noReleaseNotes": "No hay notas de la versión disponibles."
|
||||
"learnMore": "Aprende más"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "Introduzca el nombre del archivo",
|
||||
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "Destacar nodo de ajuste",
|
||||
"tooltip": "Al arrastrar un enlace sobre un nodo con ranura de entrada viable, resalta el nodo"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "Mostrar actualizaciones de versión",
|
||||
"tooltip": "Mostrar actualizaciones para nuevos modelos y funciones principales nuevas."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "Retraso de deriva del clic del puntero",
|
||||
"tooltip": "Después de presionar un botón del puntero, este es el tiempo máximo (en milisegundos) que se puede ignorar el movimiento del puntero.\n\nAyuda a prevenir que los objetos sean movidos involuntariamente si el puntero se mueve al hacer clic."
|
||||
|
||||
@@ -785,13 +785,13 @@
|
||||
"Toggle Bottom Panel": "Basculer le panneau inférieur",
|
||||
"Toggle Focus Mode": "Basculer le mode focus",
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file d’attente",
|
||||
"Toggle Model Library Sidebar": "Basculer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Basculer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Queue Sidebar": "Basculer la barre latérale de la file d'attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
|
||||
"Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows",
|
||||
"Toggle Workflows Sidebar": "Basculer la barre latérale des flux de travail",
|
||||
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
@@ -1099,7 +1099,6 @@
|
||||
"Node Search Box": "Boîte de Recherche de Nœud",
|
||||
"Node Widget": "Widget de Nœud",
|
||||
"NodeLibrary": "Bibliothèque de Nœuds",
|
||||
"Notification Preferences": "Préférences de notification",
|
||||
"Pointer": "Pointeur",
|
||||
"Queue": "File d'Attente",
|
||||
"QueueButton": "Bouton de File d'Attente",
|
||||
@@ -1484,8 +1483,7 @@
|
||||
"title": "Bienvenue sur ComfyUI"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "En savoir plus",
|
||||
"noReleaseNotes": "Aucune note de version disponible."
|
||||
"learnMore": "En savoir plus"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "Entrez le nom du fichier",
|
||||
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "Le snap met en évidence le nœud",
|
||||
"tooltip": "Lorsque vous faites glisser un lien sur un nœud avec une fente d'entrée viable, mettez en évidence le nœud"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "Afficher les mises à jour de version",
|
||||
"tooltip": "Afficher les mises à jour pour les nouveaux modèles et les nouvelles fonctionnalités majeures."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "Délai de dérive du clic du pointeur",
|
||||
"tooltip": "Après avoir appuyé sur un bouton de pointeur, c'est le temps maximum (en millisecondes) que le mouvement du pointeur peut être ignoré.\n\nAide à prévenir que les objets soient déplacés involontairement si le pointeur est déplacé lors du clic."
|
||||
|
||||
@@ -1099,7 +1099,6 @@
|
||||
"Node Search Box": "ノード検索ボックス",
|
||||
"Node Widget": "ノードウィジェット",
|
||||
"NodeLibrary": "ノードライブラリ",
|
||||
"Notification Preferences": "通知設定",
|
||||
"Pointer": "ポインタ",
|
||||
"Queue": "キュー",
|
||||
"QueueButton": "キューボタン",
|
||||
@@ -1484,8 +1483,7 @@
|
||||
"title": "ComfyUIへようこそ"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "詳細はこちら",
|
||||
"noReleaseNotes": "リリースノートはありません。"
|
||||
"learnMore": "詳細はこちら"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "ファイル名を入力",
|
||||
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "スナップハイライトノード",
|
||||
"tooltip": "有効な入力スロットを持つノードの上にリンクをドラッグすると、ノードがハイライトされます"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "バージョン更新を表示",
|
||||
"tooltip": "新しいモデルや主要な新機能のアップデートを表示します。"
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "ポインタークリックドリフト遅延",
|
||||
"tooltip": "ポインターボタンを押した後、ポインタの動きが無視される最大時間(ミリ秒単位)です。\n\nクリック中にポインタが移動した場合、オブジェクトが意図せず動かされるのを防ぎます。"
|
||||
|
||||
@@ -787,11 +787,11 @@
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "대기열 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
"Toggle Workflows Sidebar": "워크플로우 사이드바 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
|
||||
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
@@ -1099,7 +1099,6 @@
|
||||
"Node Search Box": "노드 검색 상자",
|
||||
"Node Widget": "노드 위젯",
|
||||
"NodeLibrary": "노드 라이브러리",
|
||||
"Notification Preferences": "알림 환경설정",
|
||||
"Pointer": "포인터",
|
||||
"Queue": "실행 대기열",
|
||||
"QueueButton": "실행 대기열 버튼",
|
||||
@@ -1484,8 +1483,7 @@
|
||||
"title": "ComfyUI에 오신 것을 환영합니다"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "자세히 알아보기",
|
||||
"noReleaseNotes": "릴리스 노트가 없습니다."
|
||||
"learnMore": "자세히 알아보기"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "파일 이름 입력",
|
||||
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "스냅 하이라이트 노드",
|
||||
"tooltip": "링크를 유효한 입력 슬롯이 있는 노드 위로 드래그할 때 노드를 강조 표시합니다."
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "버전 업데이트 표시",
|
||||
"tooltip": "새 모델과 주요 신규 기능에 대한 업데이트를 표시합니다."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "포인터 클릭 드리프트 지연",
|
||||
"tooltip": "포인터 버튼을 누른 후, 포인터 움직임을 무시할 수 있는 최대 시간(밀리초)입니다.\n\n클릭하는 동안 포인터가 움직여 의도치 않게 객체가 밀리는 것을 방지합니다."
|
||||
|
||||
@@ -785,13 +785,13 @@
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
"Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов",
|
||||
"Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди",
|
||||
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
|
||||
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
|
||||
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
|
||||
"Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов",
|
||||
"Toggle Workflows Sidebar": "Переключение боковой панели рабочих процессов",
|
||||
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
@@ -1099,7 +1099,6 @@
|
||||
"Node Search Box": "Поисковая строка нод",
|
||||
"Node Widget": "Виджет ноды",
|
||||
"NodeLibrary": "Библиотека нод",
|
||||
"Notification Preferences": "Настройки уведомлений",
|
||||
"Pointer": "Указатель",
|
||||
"Queue": "Очередь",
|
||||
"QueueButton": "Кнопка очереди",
|
||||
@@ -1484,8 +1483,7 @@
|
||||
"title": "Добро пожаловать в ComfyUI"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "Узнать больше",
|
||||
"noReleaseNotes": "Нет доступных примечаний к выпуску."
|
||||
"learnMore": "Узнать больше"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "Введите название файла",
|
||||
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "Подсветка ноды при привязке",
|
||||
"tooltip": "При перетаскивании ссылки над нодой с подходящим входным слотом, нода подсвечивается"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "Показывать обновления версий",
|
||||
"tooltip": "Показывать обновления новых моделей и основные новые функции."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "Задержка дрейфа щелчка указателя",
|
||||
"tooltip": "После нажатия кнопки указателя, это максимальное время (в миллисекундах), в течение которого движение указателя может быть проигнорировано.\n\nПомогает предотвратить непреднамеренное смещение объектов, если указатель перемещается во время щелчка."
|
||||
|
||||
@@ -1099,7 +1099,6 @@
|
||||
"Node Search Box": "节点搜索框",
|
||||
"Node Widget": "节点组件",
|
||||
"NodeLibrary": "节点库",
|
||||
"Notification Preferences": "通知偏好",
|
||||
"Pointer": "指针",
|
||||
"Queue": "队列",
|
||||
"QueueButton": "执行按钮",
|
||||
@@ -1484,8 +1483,7 @@
|
||||
"title": "欢迎使用 ComfyUI"
|
||||
},
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "了解更多",
|
||||
"noReleaseNotes": "暂无更新说明。"
|
||||
"learnMore": "了解更多"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "输入文件名",
|
||||
|
||||
@@ -259,10 +259,6 @@
|
||||
"name": "吸附高亮节点",
|
||||
"tooltip": "在拖动连线经过具有可用输入接口的节点时,高亮显示该节点。"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "显示版本更新",
|
||||
"tooltip": "显示新模型和主要新功能的更新。"
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "指针点击漂移延迟",
|
||||
"tooltip": "按下指针按钮后,忽略指针移动的最大时间(毫秒)。\n\n有助于防止在点击时意外移动鼠标。"
|
||||
|
||||
@@ -426,7 +426,6 @@ const zSettings = z.object({
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': zNodeBadgeMode,
|
||||
'Comfy.NodeBadge.NodeLifeCycleBadgeMode': zNodeBadgeMode,
|
||||
'Comfy.NodeBadge.ShowApiPricing': z.boolean(),
|
||||
'Comfy.Notification.ShowVersionUpdates': z.boolean(),
|
||||
'Comfy.QueueButton.BatchCountLimit': z.number(),
|
||||
'Comfy.Queue.MaxHistoryItems': z.number(),
|
||||
'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding),
|
||||
|
||||
@@ -65,9 +65,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
/**
|
||||
* @deprecated Use `settingStore.getDefaultValue` instead.
|
||||
*/
|
||||
getSettingDefaultValue<K extends keyof Settings>(
|
||||
id: K
|
||||
): Settings[K] | undefined {
|
||||
getSettingDefaultValue<K extends keyof Settings>(id: K): Settings[K] {
|
||||
return useSettingStore().getDefaultValue(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import { VersionProxies } from '@/utils/versionProxies'
|
||||
|
||||
export const useExtensionService = () => {
|
||||
const extensionStore = useExtensionStore()
|
||||
@@ -128,10 +129,132 @@ export const useExtensionService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register extension with API version tracking
|
||||
*/
|
||||
const registerExtensionWithVersion = (
|
||||
extension: ComfyExtension,
|
||||
apiVersion: string = 'latest'
|
||||
) => {
|
||||
extension.apiVersion = apiVersion
|
||||
registerExtension(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke extensions for a specific API version with transformed args
|
||||
*/
|
||||
const invokeExtensionsForVersion = async <T extends any[]>(
|
||||
hook: keyof ComfyExtension,
|
||||
apiVersion: string,
|
||||
...args: T
|
||||
) => {
|
||||
const extensions = extensionStore.extensions.filter(
|
||||
(ext) =>
|
||||
ext.apiVersion === apiVersion ||
|
||||
(!ext.apiVersion && apiVersion === 'latest')
|
||||
)
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (extension[hook] && typeof extension[hook] === 'function') {
|
||||
try {
|
||||
const transformedArgs = transformArgsForVersion(apiVersion, args)
|
||||
await (extension[hook] as (...args: T) => void | Promise<void>)(
|
||||
...transformedArgs
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error in extension ${extension.name} hook ${String(hook)}:`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke extensions for all API versions with appropriate data transformation
|
||||
*/
|
||||
const invokeExtensionsForAllVersions = async <T extends any[]>(
|
||||
hook: keyof ComfyExtension,
|
||||
...args: T
|
||||
) => {
|
||||
const apiVersions = new Set(['latest', 'v1', 'v1_2', 'v3'])
|
||||
const promises = []
|
||||
|
||||
for (const version of apiVersions) {
|
||||
promises.push(invokeExtensionsForVersion(hook, version, ...args))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform arguments for specific API version
|
||||
*/
|
||||
const transformArgsForVersion = (version: string, args: any[]) => {
|
||||
return args.map((arg) => {
|
||||
// If argument looks like a node definition, create appropriate proxy
|
||||
if (
|
||||
arg &&
|
||||
typeof arg === 'object' &&
|
||||
arg.name &&
|
||||
typeof arg.name === 'string'
|
||||
) {
|
||||
try {
|
||||
switch (version) {
|
||||
case 'v1':
|
||||
return VersionProxies.createV1Proxy(arg.name)
|
||||
case 'v1_2':
|
||||
return VersionProxies.createV1_2Proxy(arg.name)
|
||||
case 'v3':
|
||||
return VersionProxies.createV3Proxy(arg.name)
|
||||
default:
|
||||
return arg
|
||||
}
|
||||
} catch (error) {
|
||||
// If proxy creation fails, return original arg
|
||||
console.warn(`Failed to create proxy for node ${arg.name}:`, error)
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return arg
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extension compatibility report by API version
|
||||
*/
|
||||
const getExtensionVersionReport = () => {
|
||||
const extensions = extensionStore.extensions
|
||||
const versionGroups = extensions.reduce(
|
||||
(acc, ext) => {
|
||||
const version = ext.apiVersion || 'latest'
|
||||
if (!acc[version]) acc[version] = []
|
||||
acc[version].push(ext)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ComfyExtension[]>
|
||||
)
|
||||
|
||||
return {
|
||||
total: extensions.length,
|
||||
versionGroups,
|
||||
details: Object.entries(versionGroups).map(([version, exts]) => ({
|
||||
version,
|
||||
count: exts.length,
|
||||
extensions: exts.map((ext) => ext.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadExtensions,
|
||||
registerExtension,
|
||||
registerExtensionWithVersion,
|
||||
invokeExtensions,
|
||||
invokeExtensionsAsync
|
||||
invokeExtensionsAsync,
|
||||
invokeExtensionsForVersion,
|
||||
invokeExtensionsForAllVersions,
|
||||
getExtensionVersionReport
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,6 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
const releaseTimestamp = computed(() =>
|
||||
settingStore.get('Comfy.Release.Timestamp')
|
||||
)
|
||||
const showVersionUpdates = computed(() =>
|
||||
settingStore.get('Comfy.Notification.ShowVersionUpdates')
|
||||
)
|
||||
|
||||
// Most recent release
|
||||
const recentRelease = computed(() => {
|
||||
@@ -76,11 +73,6 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Show toast if needed
|
||||
const shouldShowToast = computed(() => {
|
||||
// Skip if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isNewVersionAvailable.value) {
|
||||
return false
|
||||
}
|
||||
@@ -93,7 +85,7 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
// Skip if user already skipped or changelog seen
|
||||
if (
|
||||
releaseVersion.value === recentRelease.value?.version &&
|
||||
['skipped', 'changelog seen'].includes(releaseStatus.value)
|
||||
!['skipped', 'changelog seen'].includes(releaseStatus.value)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@@ -103,11 +95,6 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Show red-dot indicator
|
||||
const shouldShowRedDot = computed(() => {
|
||||
// Skip if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Already latest → no dot
|
||||
if (!isNewVersionAvailable.value) {
|
||||
return false
|
||||
@@ -145,11 +132,6 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Show "What's New" popup
|
||||
const shouldShowPopup = computed(() => {
|
||||
// Skip if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isLatestVersion.value) {
|
||||
return false
|
||||
}
|
||||
@@ -201,14 +183,7 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Fetch releases from API
|
||||
async function fetchReleases(): Promise<void> {
|
||||
if (isLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip fetching if notifications are disabled
|
||||
if (!showVersionUpdates.value) {
|
||||
return
|
||||
}
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
@@ -7,7 +7,7 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
import { compareVersions } from '@/utils/formatUtil'
|
||||
|
||||
export const getSettingInfo = (setting: SettingParams) => {
|
||||
const parts = setting.category || setting.id.split('.')
|
||||
@@ -21,24 +21,16 @@ export interface SettingTreeNode extends TreeNode {
|
||||
data?: SettingParams
|
||||
}
|
||||
|
||||
function tryMigrateDeprecatedValue(
|
||||
setting: SettingParams | undefined,
|
||||
value: unknown
|
||||
) {
|
||||
function tryMigrateDeprecatedValue(setting: SettingParams, value: any) {
|
||||
return setting?.migrateDeprecatedValue?.(value) ?? value
|
||||
}
|
||||
|
||||
function onChange(
|
||||
setting: SettingParams | undefined,
|
||||
newValue: unknown,
|
||||
oldValue: unknown
|
||||
) {
|
||||
function onChange(setting: SettingParams, newValue: any, oldValue: any) {
|
||||
if (setting?.onChange) {
|
||||
setting.onChange(newValue, oldValue)
|
||||
}
|
||||
// Backward compatibility with old settings dialog.
|
||||
// Some extensions still listens event emitted by the old settings dialog.
|
||||
// @ts-expect-error 'setting' is possibly 'undefined'.ts(18048)
|
||||
app.ui.settings.dispatchChange(setting.id, newValue, oldValue)
|
||||
}
|
||||
|
||||
@@ -85,30 +77,13 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the setting params, asserting the type that is intentionally left off
|
||||
* of {@link settingsById}.
|
||||
* @param key The key of the setting to get.
|
||||
* @returns The setting.
|
||||
*/
|
||||
function getSettingById<K extends keyof Settings>(
|
||||
key: K
|
||||
): SettingParams<Settings[K]> | undefined {
|
||||
return settingsById.value[key] as SettingParams<Settings[K]> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value of a setting.
|
||||
* @param key - The key of the setting to get.
|
||||
* @returns The default value of the setting.
|
||||
*/
|
||||
function getDefaultValue<K extends keyof Settings>(
|
||||
key: K
|
||||
): Settings[K] | undefined {
|
||||
// Assertion: settingsById is not typed.
|
||||
const param = getSettingById(key)
|
||||
|
||||
if (param === undefined) return
|
||||
function getDefaultValue<K extends keyof Settings>(key: K): Settings[K] {
|
||||
const param = settingsById.value[key]
|
||||
|
||||
const versionedDefault = getVersionedDefaultValue(key, param)
|
||||
|
||||
@@ -116,33 +91,32 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
return versionedDefault
|
||||
}
|
||||
|
||||
return typeof param.defaultValue === 'function'
|
||||
return typeof param?.defaultValue === 'function'
|
||||
? param.defaultValue()
|
||||
: param.defaultValue
|
||||
: param?.defaultValue
|
||||
}
|
||||
|
||||
function getVersionedDefaultValue<
|
||||
K extends keyof Settings,
|
||||
TValue = Settings[K]
|
||||
>(key: K, param: SettingParams<TValue> | undefined): TValue | null {
|
||||
function getVersionedDefaultValue<K extends keyof Settings>(
|
||||
key: K,
|
||||
param: SettingParams
|
||||
): Settings[K] | null {
|
||||
// get default versioned value, skipping if the key is 'Comfy.InstalledVersion' to prevent infinite loop
|
||||
const defaultsByInstallVersion = param?.defaultsByInstallVersion
|
||||
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
|
||||
if (param?.defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
|
||||
const installedVersion = get('Comfy.InstalledVersion')
|
||||
|
||||
if (installedVersion) {
|
||||
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
|
||||
(a, b) => compareVersions(b, a)
|
||||
const sortedVersions = Object.keys(param.defaultsByInstallVersion).sort(
|
||||
(a, b) => compareVersions(a, b)
|
||||
)
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
for (const version of sortedVersions.reverse()) {
|
||||
// Ensure the version is in a valid format before comparing
|
||||
if (!isSemVer(version)) {
|
||||
if (!isValidVersionFormat(version)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (compareVersions(installedVersion, version) >= 0) {
|
||||
const versionedDefault = defaultsByInstallVersion[version]
|
||||
const versionedDefault = param.defaultsByInstallVersion[version]
|
||||
return typeof versionedDefault === 'function'
|
||||
? versionedDefault()
|
||||
: versionedDefault
|
||||
@@ -154,6 +128,12 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
return null
|
||||
}
|
||||
|
||||
function isValidVersionFormat(
|
||||
version: string
|
||||
): version is `${number}.${number}.${number}` {
|
||||
return /^\d+\.\d+\.\d+$/.test(version)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a setting.
|
||||
* @param setting - The setting to register.
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useModelLibrarySidebarTab } from '@/composables/sidebarTabs/useModelLib
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import { useQueueSidebarTab } from '@/composables/sidebarTabs/useQueueSidebarTab'
|
||||
import { useWorkflowsSidebarTab } from '@/composables/sidebarTabs/useWorkflowsSidebarTab'
|
||||
import { t, te } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
@@ -26,23 +25,11 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
|
||||
const registerSidebarTab = (tab: SidebarTabExtension) => {
|
||||
sidebarTabs.value = [...sidebarTabs.value, tab]
|
||||
|
||||
// Generate label in format "Toggle X Sidebar"
|
||||
const labelFunction = () => {
|
||||
const tabTitle = te(tab.title) ? t(tab.title) : tab.title
|
||||
return `Toggle ${tabTitle} Sidebar`
|
||||
}
|
||||
const tooltipFunction = tab.tooltip
|
||||
? te(String(tab.tooltip))
|
||||
? () => t(String(tab.tooltip))
|
||||
: String(tab.tooltip)
|
||||
: undefined
|
||||
|
||||
useCommandStore().registerCommand({
|
||||
id: `Workspace.ToggleSidebarTab.${tab.id}`,
|
||||
icon: tab.icon,
|
||||
label: labelFunction,
|
||||
tooltip: tooltipFunction,
|
||||
label: tab.title,
|
||||
tooltip: tab.tooltip,
|
||||
versionAdded: '1.3.9',
|
||||
function: () => {
|
||||
toggleSidebarTab(tab.id)
|
||||
|
||||
@@ -47,6 +47,10 @@ export interface ComfyExtension {
|
||||
* The name of the extension
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* API version this extension is using (set automatically by import)
|
||||
*/
|
||||
apiVersion?: string
|
||||
/**
|
||||
* The commands defined by the extension
|
||||
*/
|
||||
|
||||
@@ -32,10 +32,13 @@ export interface Setting {
|
||||
render: () => HTMLElement
|
||||
}
|
||||
|
||||
export interface SettingParams<TValue = unknown> extends FormItem {
|
||||
export interface SettingParams extends FormItem {
|
||||
id: keyof Settings
|
||||
defaultValue: any | (() => any)
|
||||
defaultsByInstallVersion?: Record<`${number}.${number}.${number}`, TValue>
|
||||
defaultsByInstallVersion?: Record<
|
||||
`${number}.${number}.${number}`,
|
||||
any | (() => any)
|
||||
>
|
||||
onChange?: (newValue: any, oldValue?: any) => void
|
||||
// By default category is id.split('.'). However, changing id to assign
|
||||
// new category has poor backward compatibility. Use this field to overwrite
|
||||
|
||||
4
src/types/versions/index.ts
Normal file
4
src/types/versions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Re-export all version-specific types
|
||||
export * from './v1'
|
||||
export * from './v1_2'
|
||||
export * from './v3'
|
||||
101
src/types/versions/v1.ts
Normal file
101
src/types/versions/v1.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* V1 API node definition interface
|
||||
* This represents the current/legacy node definition format
|
||||
*/
|
||||
export interface ComfyNodeDefV1 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
input?: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
hidden?: Record<string, any>
|
||||
}
|
||||
output?: string[]
|
||||
output_is_list?: boolean[]
|
||||
output_name?: string[]
|
||||
output_tooltips?: string[]
|
||||
python_module?: string
|
||||
deprecated?: boolean
|
||||
experimental?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API extension interface
|
||||
*/
|
||||
export interface ComfyExtensionV1 {
|
||||
name: string
|
||||
apiVersion?: 'v1'
|
||||
|
||||
// Lifecycle hooks
|
||||
init?(): void | Promise<void>
|
||||
setup?(): void | Promise<void>
|
||||
|
||||
// Node lifecycle hooks
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: typeof LGraphNode,
|
||||
nodeData: ComfyNodeDefV1,
|
||||
app: ComfyAppV1
|
||||
): void
|
||||
nodeCreated?(node: LGraphNode, app: ComfyAppV1): void
|
||||
|
||||
// Graph hooks
|
||||
beforeConfigureGraph?(graphData: any, missingNodeTypes: any[]): void
|
||||
afterConfigureGraph?(missingNodeTypes: any[]): void
|
||||
|
||||
// Canvas hooks
|
||||
getCustomWidgets?(): Record<string, any>
|
||||
|
||||
// Menu hooks
|
||||
addCustomNodeDefs?(defs: ComfyNodeDefV1[]): ComfyNodeDefV1[]
|
||||
|
||||
// Settings
|
||||
settings?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
defaultValue: any
|
||||
tooltip?: string
|
||||
}>
|
||||
|
||||
// Commands
|
||||
commands?: Array<{
|
||||
id: string
|
||||
function: () => void | Promise<void>
|
||||
}>
|
||||
|
||||
// Keybindings
|
||||
keybindings?: Array<{
|
||||
combo: { key: string; ctrl?: boolean; shift?: boolean; alt?: boolean }
|
||||
commandId: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API ComfyApp interface
|
||||
* Provides the same interface as the current app
|
||||
*/
|
||||
export interface ComfyAppV1 {
|
||||
registerExtension(extension: ComfyExtensionV1): void
|
||||
|
||||
// Graph management
|
||||
loadGraphData(graphData: any): Promise<void>
|
||||
clean(): void
|
||||
|
||||
// Node management
|
||||
getNodeById(id: number): LGraphNode | null
|
||||
|
||||
// Workflow operations
|
||||
queuePrompt(number: number, batchCount?: number): Promise<void>
|
||||
|
||||
// Canvas operations
|
||||
canvas: any
|
||||
graph: any
|
||||
|
||||
// UI state
|
||||
ui: any
|
||||
}
|
||||
111
src/types/versions/v1_2.ts
Normal file
111
src/types/versions/v1_2.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ComfyAppV1, ComfyExtensionV1, ComfyNodeDefV1 } from './v1'
|
||||
|
||||
/**
|
||||
* V1.2 API node definition interface
|
||||
* Extends V1 with additional structured input information
|
||||
*/
|
||||
export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 {
|
||||
inputs?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options?: any
|
||||
spec?: any
|
||||
tooltip?: string
|
||||
default?: any
|
||||
}>
|
||||
|
||||
metadata?: {
|
||||
version?: string
|
||||
author?: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
documentation?: string
|
||||
}
|
||||
|
||||
// Enhanced output information
|
||||
outputs?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
is_list: boolean
|
||||
tooltip?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V1.2 API extension interface
|
||||
* Extends V1 with additional capabilities
|
||||
*/
|
||||
export interface ComfyExtensionV1_2 extends ComfyExtensionV1 {
|
||||
apiVersion?: 'v1_2'
|
||||
|
||||
// V1.2 specific hooks
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: any,
|
||||
nodeData: ComfyNodeDefV1_2,
|
||||
app: ComfyAppV1_2
|
||||
): void
|
||||
nodeCreated?(node: any, app: ComfyAppV1_2): void
|
||||
|
||||
// Enhanced settings with validation
|
||||
settings?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
defaultValue: any
|
||||
tooltip?: string
|
||||
validation?: (value: any) => boolean | string
|
||||
category?: string
|
||||
options?: any[]
|
||||
}>
|
||||
|
||||
// Enhanced commands with descriptions
|
||||
commands?: Array<{
|
||||
id: string
|
||||
function: () => void | Promise<void>
|
||||
label?: string
|
||||
tooltip?: string
|
||||
category?: string
|
||||
icon?: string
|
||||
}>
|
||||
|
||||
// Bottom panel tabs
|
||||
bottomPanelTabs?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
type: 'vue' | 'custom'
|
||||
component?: any
|
||||
tooltip?: string
|
||||
}>
|
||||
|
||||
// Menu items
|
||||
menuItems?: Array<{
|
||||
path: string[]
|
||||
commands: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V1.2 API ComfyApp interface
|
||||
* Extends V1 with additional functionality
|
||||
*/
|
||||
export interface ComfyAppV1_2 extends ComfyAppV1 {
|
||||
registerExtension(extension: ComfyExtensionV1_2): void
|
||||
|
||||
// Enhanced node operations
|
||||
createNode(type: string, title?: string, options?: any): any
|
||||
removeNode(node: any): void
|
||||
|
||||
// Settings operations
|
||||
getSetting(id: string): any
|
||||
setSetting(id: string, value: any): void
|
||||
|
||||
// Command operations
|
||||
executeCommand(id: string): Promise<void>
|
||||
|
||||
// Enhanced UI access
|
||||
bottomPanel: any
|
||||
sidebar: any
|
||||
menu: any
|
||||
}
|
||||
288
src/types/versions/v3.ts
Normal file
288
src/types/versions/v3.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* V3 API node definition interface
|
||||
* Future schema-based node definition format
|
||||
*/
|
||||
export interface ComfyNodeDefV3 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
|
||||
// JSON Schema-based definition
|
||||
schema: {
|
||||
type: 'object'
|
||||
properties: Record<string, any>
|
||||
required?: string[]
|
||||
additionalProperties?: boolean
|
||||
}
|
||||
|
||||
// Structured input/output definitions
|
||||
inputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
schema: any
|
||||
validation?: any
|
||||
tooltip?: string
|
||||
default?: any
|
||||
ui?: {
|
||||
widget?: string
|
||||
options?: any
|
||||
}
|
||||
}>
|
||||
|
||||
outputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
is_list: boolean
|
||||
schema?: any
|
||||
tooltip?: string
|
||||
}>
|
||||
|
||||
// Enhanced metadata
|
||||
metadata: {
|
||||
version: string
|
||||
author: string
|
||||
description: string
|
||||
tags: string[]
|
||||
documentation?: string
|
||||
repository?: string
|
||||
license?: string
|
||||
dependencies?: Array<{
|
||||
name: string
|
||||
version?: string
|
||||
optional?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// Execution information
|
||||
execution?: {
|
||||
async?: boolean
|
||||
gpu_memory?: number
|
||||
cpu_cores?: number
|
||||
timeout?: number
|
||||
retries?: number
|
||||
}
|
||||
|
||||
python_module?: string
|
||||
deprecated?: boolean
|
||||
experimental?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* V3 API extension interface
|
||||
* Future extension format with enhanced capabilities
|
||||
*/
|
||||
export interface ComfyExtensionV3 {
|
||||
name: string
|
||||
apiVersion: 'v3'
|
||||
|
||||
// Required metadata
|
||||
metadata: {
|
||||
version: string
|
||||
author: string
|
||||
description: string
|
||||
repository?: string
|
||||
license?: string
|
||||
dependencies?: Array<{
|
||||
name: string
|
||||
version?: string
|
||||
optional?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// Lifecycle hooks with enhanced data
|
||||
init?(): void | Promise<void>
|
||||
setup?(): void | Promise<void>
|
||||
beforeShutdown?(): void | Promise<void>
|
||||
|
||||
// Node lifecycle hooks
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: any,
|
||||
nodeData: ComfyNodeDefV3,
|
||||
app: ComfyAppV3
|
||||
): void | Promise<void>
|
||||
afterRegisterNodeDef?(
|
||||
nodeType: any,
|
||||
nodeData: ComfyNodeDefV3,
|
||||
app: ComfyAppV3
|
||||
): void | Promise<void>
|
||||
nodeCreated?(node: any, app: ComfyAppV3): void | Promise<void>
|
||||
nodeRemoved?(node: any, app: ComfyAppV3): void | Promise<void>
|
||||
|
||||
// Graph hooks
|
||||
beforeConfigureGraph?(
|
||||
graphData: any,
|
||||
missingNodeTypes: any[]
|
||||
): void | Promise<void>
|
||||
afterConfigureGraph?(missingNodeTypes: any[]): void | Promise<void>
|
||||
graphChanged?(graph: any): void | Promise<void>
|
||||
|
||||
// Enhanced settings with full schema validation
|
||||
settings?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
schema: any
|
||||
defaultValue: any
|
||||
tooltip?: string
|
||||
category?: string
|
||||
validation?: (value: any) => boolean | string
|
||||
ui?: {
|
||||
widget?: string
|
||||
options?: any
|
||||
}
|
||||
}>
|
||||
|
||||
// Enhanced commands with full metadata
|
||||
commands?: Array<{
|
||||
id: string
|
||||
function: () => void | Promise<void>
|
||||
metadata: {
|
||||
label: string
|
||||
tooltip?: string
|
||||
category?: string
|
||||
icon?: string
|
||||
shortcut?: string
|
||||
}
|
||||
validation?: () => boolean
|
||||
}>
|
||||
|
||||
// Keybindings with enhanced options
|
||||
keybindings?: Array<{
|
||||
combo: {
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
alt?: boolean
|
||||
meta?: boolean
|
||||
}
|
||||
commandId: string
|
||||
when?: string // Context condition
|
||||
priority?: number
|
||||
}>
|
||||
|
||||
// UI extensions
|
||||
uiExtensions?: {
|
||||
bottomPanelTabs?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
component: any
|
||||
tooltip?: string
|
||||
when?: string
|
||||
}>
|
||||
|
||||
contextMenus?: Array<{
|
||||
id: string
|
||||
items: Array<{
|
||||
id: string
|
||||
label: string
|
||||
commandId: string
|
||||
when?: string
|
||||
separator?: boolean
|
||||
}>
|
||||
}>
|
||||
|
||||
toolbars?: Array<{
|
||||
id: string
|
||||
location: 'top' | 'bottom' | 'left' | 'right'
|
||||
items: Array<{
|
||||
id: string
|
||||
type: 'button' | 'separator' | 'dropdown'
|
||||
commandId?: string
|
||||
label?: string
|
||||
icon?: string
|
||||
tooltip?: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
// API endpoints (for extensions that provide their own APIs)
|
||||
apiEndpoints?: Array<{
|
||||
path: string
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
handler: (req: any, res: any) => void | Promise<void>
|
||||
schema?: any
|
||||
auth?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V3 API ComfyApp interface
|
||||
* Future app interface with enhanced capabilities
|
||||
*/
|
||||
export interface ComfyAppV3 {
|
||||
registerExtension(extension: ComfyExtensionV3): Promise<void>
|
||||
|
||||
// Enhanced graph management
|
||||
graph: {
|
||||
load(data: any): Promise<void>
|
||||
save(): any
|
||||
clear(): void
|
||||
validate(): Promise<boolean>
|
||||
getNodes(): any[]
|
||||
getNodeById(id: string): any | null
|
||||
addNode(type: string, options?: any): Promise<any>
|
||||
removeNode(node: any): Promise<void>
|
||||
connectNodes(source: any, target: any, options?: any): Promise<void>
|
||||
disconnectNodes(source: any, target: any): Promise<void>
|
||||
}
|
||||
|
||||
// Enhanced workflow operations
|
||||
workflow: {
|
||||
queue(batchCount?: number): Promise<string>
|
||||
cancel(executionId?: string): Promise<void>
|
||||
pause(): Promise<void>
|
||||
resume(): Promise<void>
|
||||
getStatus(): any
|
||||
getHistory(): any[]
|
||||
getQueue(): any[]
|
||||
}
|
||||
|
||||
// Settings management
|
||||
settings: {
|
||||
get<T = any>(id: string): T
|
||||
set(id: string, value: any): Promise<void>
|
||||
getAll(): Record<string, any>
|
||||
reset(id?: string): Promise<void>
|
||||
export(): any
|
||||
import(data: any): Promise<void>
|
||||
}
|
||||
|
||||
// Command system
|
||||
commands: {
|
||||
execute(id: string, args?: any): Promise<any>
|
||||
register(command: any): void
|
||||
unregister(id: string): void
|
||||
getAll(): any[]
|
||||
isAvailable(id: string): boolean
|
||||
}
|
||||
|
||||
// UI management
|
||||
ui: {
|
||||
bottomPanel: any
|
||||
sidebar: any
|
||||
menu: any
|
||||
canvas: any
|
||||
dialogs: any
|
||||
notifications: any
|
||||
}
|
||||
|
||||
// Event system
|
||||
events: {
|
||||
on<T = unknown>(event: string, handler: (data: T) => void): void
|
||||
off<T = unknown>(event: string, handler?: (data: T) => void): void
|
||||
emit<T = unknown>(event: string, data?: T): void
|
||||
once<T = unknown>(event: string, handler: (data: T) => void): void
|
||||
}
|
||||
|
||||
// API access
|
||||
api: {
|
||||
get(path: string, options?: any): Promise<any>
|
||||
post(path: string, data?: any, options?: any): Promise<any>
|
||||
put(path: string, data?: any, options?: any): Promise<any>
|
||||
delete(path: string, options?: any): Promise<any>
|
||||
patch(path: string, data?: any, options?: any): Promise<any>
|
||||
}
|
||||
}
|
||||
@@ -386,10 +386,8 @@ export const downloadUrlToHfRepoUrl = (url: string): string => {
|
||||
}
|
||||
}
|
||||
|
||||
export const isSemVer = (
|
||||
version: string
|
||||
): version is `${number}.${number}.${number}` => {
|
||||
const regex = /^\d+\.\d+\.\d+$/
|
||||
export const isSemVer = (version: string) => {
|
||||
const regex = /^(\d+)\.(\d+)\.(\d+)$/
|
||||
return regex.test(version)
|
||||
}
|
||||
|
||||
|
||||
519
src/utils/versionProxies.ts
Normal file
519
src/utils/versionProxies.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
import type {
|
||||
ComfyNodeDefLatest,
|
||||
ComfyNodeDefV1,
|
||||
ComfyNodeDefV1_2,
|
||||
ComfyNodeDefV3
|
||||
} from './versionTransforms'
|
||||
|
||||
/**
|
||||
* Proxy-based system for synchronizing data between different API versions
|
||||
*/
|
||||
export class VersionProxies {
|
||||
private static canonicalStore = new Map<string, ComfyNodeDefLatest>()
|
||||
private static eventBus = new EventTarget()
|
||||
|
||||
/**
|
||||
* Register a canonical node definition
|
||||
*/
|
||||
static registerCanonicalNode(nodeId: string, nodeData: ComfyNodeDefLatest) {
|
||||
this.canonicalStore.set(nodeId, nodeData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canonical node data
|
||||
*/
|
||||
static getCanonicalNode(nodeId: string): ComfyNodeDefLatest | undefined {
|
||||
return this.canonicalStore.get(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a V1 proxy for a node
|
||||
*/
|
||||
static createV1Proxy(nodeId: string): ComfyNodeDefV1 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) {
|
||||
throw new Error(`Node ${nodeId} not found in canonical store`)
|
||||
}
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1, {
|
||||
get(target, prop: keyof ComfyNodeDefV1) {
|
||||
return VersionProxies.transformCanonicalToV1Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1, value) {
|
||||
VersionProxies.transformV1PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
},
|
||||
|
||||
has(target, prop: keyof ComfyNodeDefV1) {
|
||||
return (
|
||||
prop in canonical ||
|
||||
prop === 'input' ||
|
||||
prop === 'output' ||
|
||||
prop === 'output_is_list'
|
||||
)
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
return [
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'category',
|
||||
'output_node',
|
||||
'input',
|
||||
'output',
|
||||
'output_is_list',
|
||||
'python_module'
|
||||
]
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: this.get!(target, prop, target)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a V1.2 proxy for a node
|
||||
*/
|
||||
static createV1_2Proxy(nodeId: string): ComfyNodeDefV1_2 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) {
|
||||
throw new Error(`Node ${nodeId} not found in canonical store`)
|
||||
}
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1_2, {
|
||||
get(target, prop: keyof ComfyNodeDefV1_2) {
|
||||
return VersionProxies.transformCanonicalToV1_2Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1_2, value) {
|
||||
VersionProxies.transformV1_2PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
},
|
||||
|
||||
has(target, prop: keyof ComfyNodeDefV1_2) {
|
||||
return (
|
||||
prop in canonical ||
|
||||
prop === 'input' ||
|
||||
prop === 'output' ||
|
||||
prop === 'output_is_list' ||
|
||||
prop === 'inputs' ||
|
||||
prop === 'metadata'
|
||||
)
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
return [
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'category',
|
||||
'output_node',
|
||||
'input',
|
||||
'output',
|
||||
'output_is_list',
|
||||
'inputs',
|
||||
'metadata',
|
||||
'python_module'
|
||||
]
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: this.get!(target, prop, target)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a V3 proxy for a node
|
||||
*/
|
||||
static createV3Proxy(nodeId: string): ComfyNodeDefV3 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) {
|
||||
throw new Error(`Node ${nodeId} not found in canonical store`)
|
||||
}
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV3, {
|
||||
get(target, prop: keyof ComfyNodeDefV3) {
|
||||
return VersionProxies.transformCanonicalToV3Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV3, value) {
|
||||
VersionProxies.transformV3PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
},
|
||||
|
||||
has(target, prop: keyof ComfyNodeDefV3) {
|
||||
return (
|
||||
prop in canonical ||
|
||||
prop === 'schema' ||
|
||||
prop === 'inputs' ||
|
||||
prop === 'outputs'
|
||||
)
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
return [
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'category',
|
||||
'output_node',
|
||||
'schema',
|
||||
'inputs',
|
||||
'outputs',
|
||||
'python_module'
|
||||
]
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: this.get!(target, prop, target)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical property to V1 format
|
||||
*/
|
||||
private static transformCanonicalToV1Property(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1
|
||||
): any {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
return canonical.input
|
||||
? {
|
||||
required: canonical.input.required,
|
||||
optional: canonical.input.optional
|
||||
}
|
||||
: undefined
|
||||
case 'output':
|
||||
return canonical.output
|
||||
case 'output_is_list':
|
||||
return canonical.output_is_list
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
return canonical[prop]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical property to V1.2 format
|
||||
*/
|
||||
private static transformCanonicalToV1_2Property(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1_2
|
||||
): any {
|
||||
switch (prop) {
|
||||
case 'inputs':
|
||||
if (!canonical.input) return undefined
|
||||
return [
|
||||
...Object.entries(canonical.input.required || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: true,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
),
|
||||
...Object.entries(canonical.input.optional || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: false,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
)
|
||||
]
|
||||
case 'metadata':
|
||||
return {
|
||||
version: canonical.api_version,
|
||||
author: 'Unknown',
|
||||
description: canonical.description
|
||||
}
|
||||
default:
|
||||
return this.transformCanonicalToV1Property(
|
||||
canonical,
|
||||
prop as keyof ComfyNodeDefV1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical property to V3 format
|
||||
*/
|
||||
private static transformCanonicalToV3Property(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV3
|
||||
): any {
|
||||
switch (prop) {
|
||||
case 'schema': {
|
||||
const requiredInputs = Object.keys(canonical.input?.required || {})
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(canonical.input?.required || {}),
|
||||
...(canonical.input?.optional || {})
|
||||
},
|
||||
required: requiredInputs
|
||||
}
|
||||
}
|
||||
case 'inputs': {
|
||||
if (!canonical.input) return []
|
||||
const requiredInputs2 = Object.entries(canonical.input.required || {})
|
||||
const optionalInputs = Object.entries(canonical.input.optional || {})
|
||||
return [
|
||||
...requiredInputs2.map(([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: true,
|
||||
schema: spec
|
||||
})),
|
||||
...optionalInputs.map(([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: false,
|
||||
schema: spec
|
||||
}))
|
||||
]
|
||||
}
|
||||
case 'outputs':
|
||||
return (
|
||||
canonical.output?.map((type, index) => ({
|
||||
name: `output_${index}`,
|
||||
type,
|
||||
is_list: canonical.output_is_list?.[index] || false
|
||||
})) || []
|
||||
)
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
return canonical[prop]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform V1 property changes back to canonical format
|
||||
*/
|
||||
private static transformV1PropertyToCanonical(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1,
|
||||
value: any
|
||||
): void {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
canonical.input = value
|
||||
? {
|
||||
required: value.required,
|
||||
optional: value.optional
|
||||
}
|
||||
: undefined
|
||||
break
|
||||
case 'output':
|
||||
canonical.output = value
|
||||
break
|
||||
case 'output_is_list':
|
||||
canonical.output_is_list = value
|
||||
break
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
;(canonical as any)[prop] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform V1.2 property changes back to canonical format
|
||||
*/
|
||||
private static transformV1_2PropertyToCanonical(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1_2,
|
||||
value: any
|
||||
): void {
|
||||
switch (prop) {
|
||||
case 'inputs':
|
||||
if (Array.isArray(value)) {
|
||||
const required: Record<string, any> = {}
|
||||
const optional: Record<string, any> = {}
|
||||
|
||||
value.forEach((input) => {
|
||||
if (input.required) {
|
||||
required[input.name] = input.spec
|
||||
} else {
|
||||
optional[input.name] = input.spec
|
||||
}
|
||||
})
|
||||
|
||||
canonical.input = {
|
||||
required: Object.keys(required).length > 0 ? required : undefined,
|
||||
optional: Object.keys(optional).length > 0 ? optional : undefined
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'metadata':
|
||||
if (value && typeof value === 'object') {
|
||||
canonical.api_version = value.version
|
||||
canonical.description = value.description || canonical.description
|
||||
}
|
||||
break
|
||||
default:
|
||||
this.transformV1PropertyToCanonical(
|
||||
canonical,
|
||||
prop as keyof ComfyNodeDefV1,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform V3 property changes back to canonical format
|
||||
*/
|
||||
private static transformV3PropertyToCanonical(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV3,
|
||||
value: any
|
||||
): void {
|
||||
switch (prop) {
|
||||
case 'schema':
|
||||
if (value && typeof value === 'object') {
|
||||
const required: Record<string, any> = {}
|
||||
const optional: Record<string, any> = {}
|
||||
|
||||
Object.entries(value.properties || {}).forEach(([name, spec]) => {
|
||||
if (value.required?.includes(name)) {
|
||||
required[name] = spec
|
||||
} else {
|
||||
optional[name] = spec
|
||||
}
|
||||
})
|
||||
|
||||
canonical.input = {
|
||||
required: Object.keys(required).length > 0 ? required : undefined,
|
||||
optional: Object.keys(optional).length > 0 ? optional : undefined
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'inputs':
|
||||
if (Array.isArray(value)) {
|
||||
const required: Record<string, any> = {}
|
||||
const optional: Record<string, any> = {}
|
||||
|
||||
value.forEach((input) => {
|
||||
if (input.required) {
|
||||
required[input.name] = input.schema
|
||||
} else {
|
||||
optional[input.name] = input.schema
|
||||
}
|
||||
})
|
||||
|
||||
canonical.input = {
|
||||
required: Object.keys(required).length > 0 ? required : undefined,
|
||||
optional: Object.keys(optional).length > 0 ? optional : undefined
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'outputs':
|
||||
if (Array.isArray(value)) {
|
||||
canonical.output = value.map((output) => output.type)
|
||||
canonical.output_is_list = value.map((output) => output.is_list)
|
||||
}
|
||||
break
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
;(canonical as any)[prop] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify about data changes
|
||||
*/
|
||||
private static notifyChange(nodeId: string, prop: string, value: any): void {
|
||||
this.eventBus.dispatchEvent(
|
||||
new CustomEvent('nodedef-changed', {
|
||||
detail: { nodeId, prop, value }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener for data changes
|
||||
*/
|
||||
static addEventListener(type: string, listener: EventListener): void {
|
||||
this.eventBus.addEventListener(type, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event listener
|
||||
*/
|
||||
static removeEventListener(type: string, listener: EventListener): void {
|
||||
this.eventBus.removeEventListener(type, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer type from input specification
|
||||
*/
|
||||
private static inferTypeFromSpec(spec: any): string {
|
||||
if (Array.isArray(spec)) {
|
||||
return 'combo'
|
||||
}
|
||||
|
||||
if (typeof spec === 'object' && spec !== null) {
|
||||
if (spec.type) {
|
||||
return spec.type
|
||||
}
|
||||
if (spec[0] === 'INT') {
|
||||
return 'int'
|
||||
}
|
||||
if (spec[0] === 'FLOAT') {
|
||||
return 'float'
|
||||
}
|
||||
if (spec[0] === 'STRING') {
|
||||
return 'string'
|
||||
}
|
||||
if (spec[0] === 'BOOLEAN') {
|
||||
return 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
250
src/utils/versionTransforms.ts
Normal file
250
src/utils/versionTransforms.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
// Type definitions for different API versions
|
||||
export interface ComfyNodeDefV1 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
input?: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
}
|
||||
output?: string[]
|
||||
output_is_list?: boolean[]
|
||||
python_module?: string
|
||||
}
|
||||
|
||||
export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 {
|
||||
inputs?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options?: any
|
||||
spec?: any
|
||||
}>
|
||||
metadata?: {
|
||||
version?: string
|
||||
author?: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComfyNodeDefV3 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
schema: {
|
||||
type: 'object'
|
||||
properties: Record<string, any>
|
||||
required?: string[]
|
||||
}
|
||||
inputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
schema: any
|
||||
}>
|
||||
outputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
is_list: boolean
|
||||
}>
|
||||
python_module?: string
|
||||
}
|
||||
|
||||
// Use current ComfyNodeDef as the canonical format
|
||||
export type ComfyNodeDefLatest = ComfyNodeDef
|
||||
|
||||
/**
|
||||
* Transforms node definitions between different API versions
|
||||
*/
|
||||
export class VersionTransforms {
|
||||
/**
|
||||
* Transform API responses to all supported versions
|
||||
*/
|
||||
static transformToAllVersions(currentData: any, v3Data: any = null) {
|
||||
// Use current data as canonical since it's our primary source
|
||||
const canonical = currentData || {}
|
||||
|
||||
return {
|
||||
canonical,
|
||||
v1: this.canonicalToV1(canonical),
|
||||
v1_2: this.canonicalToV1_2(canonical),
|
||||
v3: v3Data || this.canonicalToV3(canonical)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical format to v1 format
|
||||
*/
|
||||
static canonicalToV1(
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
): Record<string, ComfyNodeDefV1> {
|
||||
const v1Nodes: Record<string, ComfyNodeDefV1> = {}
|
||||
|
||||
for (const [nodeName, nodeData] of Object.entries(canonical)) {
|
||||
v1Nodes[nodeName] = {
|
||||
name: nodeData.name,
|
||||
display_name: nodeData.display_name,
|
||||
description: nodeData.description,
|
||||
category: nodeData.category,
|
||||
output_node: nodeData.output_node,
|
||||
python_module: nodeData.python_module,
|
||||
input: nodeData.input
|
||||
? {
|
||||
required: nodeData.input.required,
|
||||
optional: nodeData.input.optional
|
||||
}
|
||||
: undefined,
|
||||
output: nodeData.output,
|
||||
output_is_list: nodeData.output_is_list
|
||||
}
|
||||
}
|
||||
|
||||
return v1Nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical format to v1.2 format
|
||||
*/
|
||||
static canonicalToV1_2(
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
): Record<string, ComfyNodeDefV1_2> {
|
||||
const v1_2Nodes: Record<string, ComfyNodeDefV1_2> = {}
|
||||
|
||||
for (const [nodeName, nodeData] of Object.entries(canonical)) {
|
||||
const v1Node = this.canonicalToV1({ [nodeName]: nodeData })[nodeName]
|
||||
|
||||
v1_2Nodes[nodeName] = {
|
||||
...v1Node,
|
||||
inputs: nodeData.input
|
||||
? [
|
||||
...Object.entries(nodeData.input.required || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: true,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
),
|
||||
...Object.entries(nodeData.input.optional || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: false,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
)
|
||||
]
|
||||
: undefined,
|
||||
metadata: {
|
||||
version: nodeData.api_version,
|
||||
author: 'Unknown',
|
||||
description: nodeData.description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v1_2Nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical format to v3 format
|
||||
*/
|
||||
static canonicalToV3(
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
): Record<string, ComfyNodeDefV3> {
|
||||
const v3Nodes: Record<string, ComfyNodeDefV3> = {}
|
||||
|
||||
for (const [nodeName, nodeData] of Object.entries(canonical)) {
|
||||
const requiredInputs = Object.keys(nodeData.input?.required || {})
|
||||
const optionalInputs = Object.keys(nodeData.input?.optional || {})
|
||||
|
||||
v3Nodes[nodeName] = {
|
||||
name: nodeData.name,
|
||||
display_name: nodeData.display_name,
|
||||
description: nodeData.description,
|
||||
category: nodeData.category,
|
||||
output_node: nodeData.output_node,
|
||||
python_module: nodeData.python_module,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(nodeData.input?.required || {}),
|
||||
...(nodeData.input?.optional || {})
|
||||
},
|
||||
required: requiredInputs
|
||||
},
|
||||
inputs: [
|
||||
...requiredInputs.map((name) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(nodeData.input!.required![name]),
|
||||
required: true,
|
||||
schema: nodeData.input!.required![name]
|
||||
})),
|
||||
...optionalInputs.map((name) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(nodeData.input!.optional![name]),
|
||||
required: false,
|
||||
schema: nodeData.input!.optional![name]
|
||||
}))
|
||||
],
|
||||
outputs:
|
||||
nodeData.output?.map((type, index) => ({
|
||||
name: `output_${index}`,
|
||||
type,
|
||||
is_list: nodeData.output_is_list?.[index] || false
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
|
||||
return v3Nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer type from input specification
|
||||
*/
|
||||
private static inferTypeFromSpec(spec: any): string {
|
||||
if (Array.isArray(spec)) {
|
||||
return 'combo'
|
||||
}
|
||||
|
||||
if (typeof spec === 'object' && spec !== null) {
|
||||
if (spec.type) {
|
||||
return spec.type
|
||||
}
|
||||
if (spec[0] === 'INT') {
|
||||
return 'int'
|
||||
}
|
||||
if (spec[0] === 'FLOAT') {
|
||||
return 'float'
|
||||
}
|
||||
if (spec[0] === 'STRING') {
|
||||
return 'string'
|
||||
}
|
||||
if (spec[0] === 'BOOLEAN') {
|
||||
return 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge current and v3 data to create canonical format
|
||||
*/
|
||||
private static createCanonicalFormat(
|
||||
currentData: any,
|
||||
v3Data: any
|
||||
): Record<string, ComfyNodeDefLatest> {
|
||||
// For now, use current data as canonical
|
||||
// In the future, we might merge v3 data for enhanced information
|
||||
return currentData || {}
|
||||
}
|
||||
}
|
||||
@@ -300,14 +300,14 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
|
||||
describe('dynamic pricing - IdeogramV3', () => {
|
||||
it('should return $0.09 for Quality rendering speed', () => {
|
||||
it('should return $0.08 for Quality rendering speed', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('IdeogramV3', [
|
||||
{ name: 'rendering_speed', value: 'Quality' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.09/Run')
|
||||
expect(price).toBe('$0.08/Run')
|
||||
})
|
||||
|
||||
it('should return $0.06 for Balanced rendering speed', () => {
|
||||
@@ -348,7 +348,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.27/Run') // 0.09 * 3
|
||||
expect(price).toBe('$0.24/Run') // 0.08 * 3
|
||||
})
|
||||
|
||||
it('should multiply price by num_images for Turbo rendering speed', () => {
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { st } from '@/i18n'
|
||||
import { getSettingInfo, useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: vi.fn((_: string, fallback: string) => fallback)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: vi.fn(),
|
||||
getSettingInfo: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useSettingSearch', () => {
|
||||
let mockSettingStore: any
|
||||
let mockSettings: any
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock settings data
|
||||
mockSettings = {
|
||||
'Category.Setting1': {
|
||||
id: 'Category.Setting1',
|
||||
name: 'Setting One',
|
||||
type: 'text',
|
||||
defaultValue: 'default',
|
||||
category: ['Category', 'Basic']
|
||||
},
|
||||
'Category.Setting2': {
|
||||
id: 'Category.Setting2',
|
||||
name: 'Setting Two',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
category: ['Category', 'Advanced']
|
||||
},
|
||||
'Category.HiddenSetting': {
|
||||
id: 'Category.HiddenSetting',
|
||||
name: 'Hidden Setting',
|
||||
type: 'hidden',
|
||||
defaultValue: 'hidden',
|
||||
category: ['Category', 'Basic']
|
||||
},
|
||||
'Category.DeprecatedSetting': {
|
||||
id: 'Category.DeprecatedSetting',
|
||||
name: 'Deprecated Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'deprecated',
|
||||
deprecated: true,
|
||||
category: ['Category', 'Advanced']
|
||||
},
|
||||
'Other.Setting3': {
|
||||
id: 'Other.Setting3',
|
||||
name: 'Other Setting',
|
||||
type: 'select',
|
||||
defaultValue: 'option1',
|
||||
category: ['Other', 'SubCategory']
|
||||
}
|
||||
}
|
||||
|
||||
// Mock setting store
|
||||
mockSettingStore = {
|
||||
settingsById: mockSettings
|
||||
}
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
|
||||
|
||||
// Mock getSettingInfo function
|
||||
vi.mocked(getSettingInfo).mockImplementation((setting: any) => {
|
||||
const parts = setting.category || setting.id.split('.')
|
||||
return {
|
||||
category: parts[0] ?? 'Other',
|
||||
subCategory: parts[1] ?? 'Other'
|
||||
}
|
||||
})
|
||||
|
||||
// Mock st function to return fallback value
|
||||
vi.mocked(st).mockImplementation((_: string, fallback: string) => fallback)
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('initializes with default state', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
expect(search.searchQuery.value).toBe('')
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
expect(search.searchInProgress.value).toBe(false)
|
||||
expect(search.queryIsEmpty.value).toBe(true)
|
||||
expect(search.inSearch.value).toBe(false)
|
||||
expect(search.searchResultsCategories.value).toEqual(new Set())
|
||||
})
|
||||
})
|
||||
|
||||
describe('reactive properties', () => {
|
||||
it('queryIsEmpty computed property works correctly', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
expect(search.queryIsEmpty.value).toBe(true)
|
||||
|
||||
search.searchQuery.value = 'test'
|
||||
expect(search.queryIsEmpty.value).toBe(false)
|
||||
|
||||
search.searchQuery.value = ''
|
||||
expect(search.queryIsEmpty.value).toBe(true)
|
||||
})
|
||||
|
||||
it('inSearch computed property works correctly', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Empty query, not in search
|
||||
expect(search.inSearch.value).toBe(false)
|
||||
|
||||
// Has query but search in progress
|
||||
search.searchQuery.value = 'test'
|
||||
search.searchInProgress.value = true
|
||||
expect(search.inSearch.value).toBe(false)
|
||||
|
||||
// Has query and search complete
|
||||
search.searchInProgress.value = false
|
||||
expect(search.inSearch.value).toBe(true)
|
||||
})
|
||||
|
||||
it('searchResultsCategories computed property works correctly', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// No results
|
||||
expect(search.searchResultsCategories.value).toEqual(new Set())
|
||||
|
||||
// Add some filtered results
|
||||
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
|
||||
expect(search.searchResultsCategories.value).toEqual(
|
||||
new Set(['Category', 'Other'])
|
||||
)
|
||||
})
|
||||
|
||||
it('watches searchQuery and sets searchInProgress to true', async () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
expect(search.searchInProgress.value).toBe(false)
|
||||
|
||||
search.searchQuery.value = 'test'
|
||||
await nextTick()
|
||||
|
||||
expect(search.searchInProgress.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleSearch', () => {
|
||||
it('clears results when query is empty', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = ['Category.Setting1']
|
||||
|
||||
search.handleSearch('')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('filters settings by ID (case insensitive)', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('category.setting1')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Category.Setting1')
|
||||
expect(search.filteredSettingIds.value).not.toContain('Other.Setting3')
|
||||
})
|
||||
|
||||
it('filters settings by name (case insensitive)', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('setting one')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Category.Setting1')
|
||||
expect(search.filteredSettingIds.value).not.toContain('Category.Setting2')
|
||||
})
|
||||
|
||||
it('filters settings by category', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('other')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Other.Setting3')
|
||||
expect(search.filteredSettingIds.value).not.toContain('Category.Setting1')
|
||||
})
|
||||
|
||||
it('excludes hidden settings from results', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('hidden')
|
||||
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.HiddenSetting'
|
||||
)
|
||||
})
|
||||
|
||||
it('excludes deprecated settings from results', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('deprecated')
|
||||
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.DeprecatedSetting'
|
||||
)
|
||||
})
|
||||
|
||||
it('sets searchInProgress to false after search', () => {
|
||||
const search = useSettingSearch()
|
||||
search.searchInProgress.value = true
|
||||
|
||||
search.handleSearch('test')
|
||||
|
||||
expect(search.searchInProgress.value).toBe(false)
|
||||
})
|
||||
|
||||
it('includes visible settings in results', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('setting')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Category.Setting1',
|
||||
'Category.Setting2',
|
||||
'Other.Setting3'
|
||||
])
|
||||
)
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.HiddenSetting'
|
||||
)
|
||||
expect(search.filteredSettingIds.value).not.toContain(
|
||||
'Category.DeprecatedSetting'
|
||||
)
|
||||
})
|
||||
|
||||
it('includes all visible settings in comprehensive search', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Search for a partial match that should include multiple settings
|
||||
search.handleSearch('setting')
|
||||
|
||||
// Should find all visible settings (not hidden/deprecated)
|
||||
expect(search.filteredSettingIds.value.length).toBeGreaterThan(0)
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Category.Setting1',
|
||||
'Category.Setting2',
|
||||
'Other.Setting3'
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('uses translated categories for search', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Mock st to return translated category names
|
||||
vi.mocked(st).mockImplementation((key: string, fallback: string) => {
|
||||
if (key === 'settingsCategories.Category') {
|
||||
return 'Translated Category'
|
||||
}
|
||||
return fallback
|
||||
})
|
||||
|
||||
search.handleSearch('translated category')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining(['Category.Setting1', 'Category.Setting2'])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSearchResults', () => {
|
||||
it('groups results by subcategory', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = [
|
||||
'Category.Setting1',
|
||||
'Category.Setting2'
|
||||
]
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
settings: [mockSettings['Category.Setting2']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('filters results by active category', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
|
||||
|
||||
const activeCategory = { label: 'Category' } as any
|
||||
const results = search.getSearchResults(activeCategory)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns all results when no active category', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [mockSettings['Category.Setting1']]
|
||||
},
|
||||
{
|
||||
label: 'SubCategory',
|
||||
settings: [mockSettings['Other.Setting3']]
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns empty array when no filtered results', () => {
|
||||
const search = useSettingSearch()
|
||||
search.filteredSettingIds.value = []
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
it('handles multiple settings in same subcategory', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Add another setting to Basic subcategory
|
||||
mockSettings['Category.Setting4'] = {
|
||||
id: 'Category.Setting4',
|
||||
name: 'Setting Four',
|
||||
type: 'text',
|
||||
defaultValue: 'default',
|
||||
category: ['Category', 'Basic']
|
||||
}
|
||||
|
||||
search.filteredSettingIds.value = [
|
||||
'Category.Setting1',
|
||||
'Category.Setting4'
|
||||
]
|
||||
|
||||
const results = search.getSearchResults(null)
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
label: 'Basic',
|
||||
settings: [
|
||||
mockSettings['Category.Setting1'],
|
||||
mockSettings['Category.Setting4']
|
||||
]
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty settings store', () => {
|
||||
mockSettingStore.settingsById = {}
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('test')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('handles settings with undefined category', () => {
|
||||
mockSettings['NoCategorySetting'] = {
|
||||
id: 'NoCategorySetting',
|
||||
name: 'No Category',
|
||||
type: 'text',
|
||||
defaultValue: 'default'
|
||||
}
|
||||
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('category')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('NoCategorySetting')
|
||||
})
|
||||
|
||||
it('handles special characters in search query', () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
// Search for part of the ID that contains a dot
|
||||
search.handleSearch('category.setting')
|
||||
|
||||
expect(search.filteredSettingIds.value).toContain('Category.Setting1')
|
||||
})
|
||||
|
||||
it('handles very long search queries', () => {
|
||||
const search = useSettingSearch()
|
||||
const longQuery = 'a'.repeat(1000)
|
||||
|
||||
search.handleSearch(longQuery)
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('handles rapid consecutive searches', async () => {
|
||||
const search = useSettingSearch()
|
||||
|
||||
search.handleSearch('setting')
|
||||
search.handleSearch('other')
|
||||
search.handleSearch('category')
|
||||
|
||||
expect(search.filteredSettingIds.value).toEqual(
|
||||
expect.arrayContaining(['Category.Setting1', 'Category.Setting2'])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -61,12 +61,6 @@ describe('useReleaseStore', () => {
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
|
||||
|
||||
// Default showVersionUpdates to true
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
store = useReleaseStore()
|
||||
})
|
||||
|
||||
@@ -120,107 +114,6 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('showVersionUpdates setting', () => {
|
||||
beforeEach(() => {
|
||||
store.releases = [mockRelease]
|
||||
})
|
||||
|
||||
describe('when notifications are enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
it('should show toast for medium/high attention releases', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
|
||||
// Need multiple releases for hasMediumOrHighAttention to work
|
||||
const mediumRelease = {
|
||||
...mockRelease,
|
||||
id: 2,
|
||||
attention: 'medium' as const
|
||||
}
|
||||
store.releases = [mockRelease, mediumRelease]
|
||||
|
||||
expect(store.shouldShowToast).toBe(true)
|
||||
})
|
||||
|
||||
it('should show red dot for new versions', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(true)
|
||||
})
|
||||
|
||||
it('should show popup for latest version', async () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
|
||||
it('should fetch releases during initialization', async () => {
|
||||
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
|
||||
project: 'comfyui',
|
||||
current_version: '1.0.0',
|
||||
form_factor: 'git-windows'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when notifications are disabled', () => {
|
||||
beforeEach(() => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
it('should not show toast even with new version available', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show red dot even with new version available', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show popup even for latest version', async () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(false)
|
||||
})
|
||||
|
||||
it('should skip fetching releases during initialization', async () => {
|
||||
await store.initialize()
|
||||
|
||||
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch releases when calling fetchReleases directly', async () => {
|
||||
await store.fetchReleases()
|
||||
|
||||
expect(mockReleaseService.getReleases).not.toHaveBeenCalled()
|
||||
expect(store.isLoading).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('release initialization', () => {
|
||||
it('should fetch releases successfully', async () => {
|
||||
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
|
||||
@@ -291,17 +184,6 @@ describe('useReleaseStore', () => {
|
||||
|
||||
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not set loading state when notifications disabled', async () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
|
||||
return null
|
||||
})
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(store.isLoading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('action handlers', () => {
|
||||
@@ -366,7 +248,6 @@ describe('useReleaseStore', () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Release.Version') return null
|
||||
if (key === 'Comfy.Release.Status') return null
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -386,10 +267,7 @@ describe('useReleaseStore', () => {
|
||||
it('should show red dot for new versions', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
mockSettingStore.get.mockReturnValue(null)
|
||||
|
||||
store.releases = [mockRelease]
|
||||
|
||||
@@ -398,10 +276,7 @@ describe('useReleaseStore', () => {
|
||||
|
||||
it('should show popup for latest version', async () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
mockSettingStore.get.mockReturnValue(null)
|
||||
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0) // versions are equal (latest version)
|
||||
@@ -411,37 +286,4 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing system stats gracefully', async () => {
|
||||
mockSystemStatsStore.systemStats = null
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
|
||||
return null
|
||||
})
|
||||
|
||||
await store.initialize()
|
||||
|
||||
// Should not fetch system stats when notifications disabled
|
||||
expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle concurrent fetchReleases calls', async () => {
|
||||
mockReleaseService.getReleases.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve([mockRelease]), 100)
|
||||
)
|
||||
)
|
||||
|
||||
// Start two concurrent calls
|
||||
const promise1 = store.fetchReleases()
|
||||
const promise2 = store.fetchReleases()
|
||||
|
||||
await Promise.all([promise1, promise2])
|
||||
|
||||
// Should only call API once due to loading check
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user