Compare commits
35 Commits
feat/heade
...
v1.26.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
321fe71497 | ||
|
|
7d7c6a324a | ||
|
|
7d2d006c07 | ||
|
|
30d48f2356 | ||
|
|
451ef24ea6 | ||
|
|
727a3494e0 | ||
|
|
7a1a2dd654 | ||
|
|
75d7a3725b | ||
|
|
ba4c159ce5 | ||
|
|
29ae9b4483 | ||
|
|
a505aec037 | ||
|
|
9e78558849 | ||
|
|
efd9b04a6e | ||
|
|
194201e871 | ||
|
|
0daacfd914 | ||
|
|
5a35562d3d | ||
|
|
ceac8f3741 | ||
|
|
b1057f164b | ||
|
|
4a189bdc93 | ||
|
|
f0adb4c9d3 | ||
|
|
d5d0aa52c2 | ||
|
|
69c660b3b7 | ||
|
|
88579c2a40 | ||
|
|
7ab247aa1d | ||
|
|
c78d03dd2c | ||
|
|
65785af348 | ||
|
|
ec4ad5ea92 | ||
|
|
e9ddf29507 | ||
|
|
fdd8564c07 | ||
|
|
d18081a54e | ||
|
|
45cc6ca2b4 | ||
|
|
c303a3f037 | ||
|
|
c90fd18ade | ||
|
|
2ed1704749 | ||
|
|
7d5a4d423e |
@@ -111,50 +111,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
```
|
||||
7. **HUMAN ANALYSIS**: Review change summary and verify scope
|
||||
|
||||
### Step 3: Version Preview
|
||||
|
||||
**Version Preview:**
|
||||
- Current: `${CURRENT_VERSION}`
|
||||
- Proposed: Show exact version number
|
||||
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
|
||||
|
||||
### Step 4: Security and Dependency Audit
|
||||
|
||||
1. Run security audit:
|
||||
```bash
|
||||
npm audit --audit-level moderate
|
||||
```
|
||||
2. Check for known vulnerabilities in dependencies
|
||||
3. Scan for hardcoded secrets or credentials:
|
||||
```bash
|
||||
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
|
||||
```
|
||||
4. Verify no sensitive data in recent commits
|
||||
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
|
||||
|
||||
### Step 5: Pre-Release Testing
|
||||
|
||||
1. Run complete test suite:
|
||||
```bash
|
||||
npm run test:unit
|
||||
npm run test:component
|
||||
```
|
||||
2. Run type checking:
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
3. Run linting (may have issues with missing packages):
|
||||
```bash
|
||||
npm run lint || echo "Lint issues - verify if critical"
|
||||
```
|
||||
4. Test build process:
|
||||
```bash
|
||||
npm run build
|
||||
npm run build:types
|
||||
```
|
||||
5. **QUALITY GATE**: All tests and builds passing?
|
||||
|
||||
### Step 6: Breaking Change Analysis
|
||||
### Step 3: Breaking Change Analysis
|
||||
|
||||
1. Analyze API changes in:
|
||||
- Public TypeScript interfaces
|
||||
@@ -169,7 +126,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
3. Generate breaking change summary
|
||||
4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified?
|
||||
|
||||
### Step 7: Analyze Dependency Updates
|
||||
### Step 4: Analyze Dependency Updates
|
||||
|
||||
1. **Check significant dependency updates:**
|
||||
```bash
|
||||
@@ -195,7 +152,117 @@ echo "Last stable release: $LAST_STABLE"
|
||||
done
|
||||
```
|
||||
|
||||
### Step 8: Generate Comprehensive Release Notes
|
||||
### Step 5: Generate GTM Feature Summary
|
||||
|
||||
1. **Collect PR data for analysis:**
|
||||
```bash
|
||||
# Get list of PR numbers from commits
|
||||
PR_NUMBERS=$(git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent | \
|
||||
grep -oE "#[0-9]+" | tr -d '#' | sort -u)
|
||||
|
||||
# Save PR data for each PR
|
||||
echo "[" > prs-${NEW_VERSION}.json
|
||||
first=true
|
||||
for PR in $PR_NUMBERS; do
|
||||
[[ "$first" == true ]] && first=false || echo "," >> prs-${NEW_VERSION}.json
|
||||
gh pr view $PR --json number,title,author,body,labels 2>/dev/null >> prs-${NEW_VERSION}.json || echo "{}" >> prs-${NEW_VERSION}.json
|
||||
done
|
||||
echo "]" >> prs-${NEW_VERSION}.json
|
||||
```
|
||||
|
||||
2. **Analyze for GTM-worthy features:**
|
||||
```
|
||||
<task>
|
||||
Review these PRs to identify features worthy of marketing attention.
|
||||
|
||||
A feature is GTM-worthy if it meets ALL of these criteria:
|
||||
- Introduces a NEW capability users didn't have before (not just improvements)
|
||||
- Would be a compelling reason for users to upgrade to this version
|
||||
- Can be demonstrated visually or has clear before/after comparison
|
||||
- Affects a significant portion of the user base
|
||||
|
||||
NOT GTM-worthy:
|
||||
- Bug fixes (even important ones)
|
||||
- Minor UI tweaks or color changes
|
||||
- Performance improvements without user-visible impact
|
||||
- Internal refactoring
|
||||
- Small convenience features
|
||||
- Features that only improve existing functionality marginally
|
||||
|
||||
For each GTM-worthy feature, note:
|
||||
- PR number, title, and author
|
||||
- Media links from the PR description
|
||||
- One compelling sentence on why users should care
|
||||
|
||||
If there are no GTM-worthy features, just say "No marketing-worthy features in this release."
|
||||
</task>
|
||||
|
||||
PR data: [contents of prs-${NEW_VERSION}.json]
|
||||
```
|
||||
|
||||
3. **Generate GTM notification:**
|
||||
```bash
|
||||
# Save to gtm-summary-${NEW_VERSION}.md based on analysis
|
||||
# If GTM-worthy features exist, include them with testing instructions
|
||||
# If not, note that this is a maintenance/bug fix release
|
||||
|
||||
# Check if notification is needed
|
||||
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
|
||||
echo "✅ No GTM notification needed for this release"
|
||||
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
else
|
||||
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
echo "📤 Share this file in #gtm channel to notify the team"
|
||||
fi
|
||||
```
|
||||
|
||||
### Step 6: Version Preview
|
||||
|
||||
**Version Preview:**
|
||||
- Current: `${CURRENT_VERSION}`
|
||||
- Proposed: Show exact version number based on analysis:
|
||||
- Major version if breaking changes detected
|
||||
- Minor version if new features added
|
||||
- Patch version if only bug fixes
|
||||
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
|
||||
|
||||
### Step 7: Security and Dependency Audit
|
||||
|
||||
1. Run security audit:
|
||||
```bash
|
||||
npm audit --audit-level moderate
|
||||
```
|
||||
2. Check for known vulnerabilities in dependencies
|
||||
3. Scan for hardcoded secrets or credentials:
|
||||
```bash
|
||||
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
|
||||
```
|
||||
4. Verify no sensitive data in recent commits
|
||||
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
|
||||
|
||||
### Step 8: Pre-Release Testing
|
||||
|
||||
1. Run complete test suite:
|
||||
```bash
|
||||
npm run test:unit
|
||||
npm run test:component
|
||||
```
|
||||
2. Run type checking:
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
3. Run linting (may have issues with missing packages):
|
||||
```bash
|
||||
npm run lint || echo "Lint issues - verify if critical"
|
||||
```
|
||||
4. Test build process:
|
||||
```bash
|
||||
npm run build
|
||||
npm run build:types
|
||||
```
|
||||
5. **QUALITY GATE**: All tests and builds passing?
|
||||
|
||||
### Step 9: Generate Comprehensive Release Notes
|
||||
|
||||
1. Extract commit messages since base release:
|
||||
```bash
|
||||
@@ -257,7 +324,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
|
||||
5. **CONTENT REVIEW**: Release notes follow standard format?
|
||||
|
||||
### Step 9: Create Version Bump PR
|
||||
### Step 10: Create Version Bump PR
|
||||
|
||||
**For standard version bumps (patch/minor/major):**
|
||||
```bash
|
||||
@@ -303,7 +370,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
4. **PR REVIEW**: Version bump PR created with standardized release notes?
|
||||
|
||||
### Step 10: Critical Release PR Verification
|
||||
### Step 11: Critical Release PR Verification
|
||||
|
||||
1. **CRITICAL**: Verify PR has "Release" label:
|
||||
```bash
|
||||
@@ -325,7 +392,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
|
||||
|
||||
### Step 11: Pre-Merge Validation
|
||||
### Step 12: Pre-Merge Validation
|
||||
|
||||
1. **Review Requirements**: Release PRs require approval
|
||||
2. Monitor CI checks - watch for update-locales
|
||||
@@ -333,7 +400,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
4. Check no new commits to main since PR creation
|
||||
5. **DEPLOYMENT READINESS**: Ready to merge?
|
||||
|
||||
### Step 12: Execute Release
|
||||
### Step 13: Execute Release
|
||||
|
||||
1. **FINAL CONFIRMATION**: Merge PR to trigger release?
|
||||
2. Merge the Release PR:
|
||||
@@ -366,7 +433,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
### Step 13: Enhance GitHub Release
|
||||
### Step 14: Enhance GitHub Release
|
||||
|
||||
1. Wait for automatic release creation:
|
||||
```bash
|
||||
@@ -394,7 +461,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
gh release view v${NEW_VERSION}
|
||||
```
|
||||
|
||||
### Step 14: Verify Multi-Channel Distribution
|
||||
### Step 15: Verify Multi-Channel Distribution
|
||||
|
||||
1. **GitHub Release:**
|
||||
```bash
|
||||
@@ -432,7 +499,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
|
||||
4. **DISTRIBUTION VERIFICATION**: All channels published successfully?
|
||||
|
||||
### Step 15: Post-Release Monitoring Setup
|
||||
### Step 16: Post-Release Monitoring Setup
|
||||
|
||||
1. **Monitor immediate release health:**
|
||||
```bash
|
||||
@@ -508,87 +575,42 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
|
||||
4. **RELEASE COMPLETION**: All post-release setup completed?
|
||||
|
||||
### Step 16: Generate GTM Feature Summary
|
||||
### Step 17: Create Release Summary
|
||||
|
||||
1. **Extract and analyze PR data:**
|
||||
1. **Create comprehensive release summary:**
|
||||
```bash
|
||||
echo "📊 Checking for marketing-worthy features..."
|
||||
|
||||
# Extract all PR data inline
|
||||
PR_DATA=$(
|
||||
PR_LIST=$(git log ${BASE_TAG}..HEAD --grep="Merge pull request" --pretty=format:"%s" | grep -oE "#[0-9]+" | tr -d '#' | sort -u)
|
||||
|
||||
echo "["
|
||||
first=true
|
||||
for PR in $PR_LIST; do
|
||||
[[ "$first" == true ]] && first=false || echo ","
|
||||
gh pr view $PR --json number,title,author,body,files,labels,closedAt 2>/dev/null || continue
|
||||
done
|
||||
echo "]"
|
||||
)
|
||||
|
||||
# Save for analysis
|
||||
echo "$PR_DATA" > prs-${NEW_VERSION}.json
|
||||
cat > release-summary-${NEW_VERSION}.md << EOF
|
||||
# Release Summary: ComfyUI Frontend v${NEW_VERSION}
|
||||
|
||||
**Released:** $(date)
|
||||
**Type:** ${VERSION_TYPE}
|
||||
**Duration:** ~${RELEASE_DURATION} minutes
|
||||
**Release Commit:** ${RELEASE_COMMIT}
|
||||
|
||||
## Metrics
|
||||
- **Commits Included:** ${COMMITS_COUNT}
|
||||
- **Contributors:** ${CONTRIBUTORS_COUNT}
|
||||
- **Files Changed:** ${FILES_CHANGED}
|
||||
- **Lines Added/Removed:** +${LINES_ADDED}/-${LINES_REMOVED}
|
||||
|
||||
## Distribution Status
|
||||
- ✅ GitHub Release: Published
|
||||
- ✅ PyPI Package: Available
|
||||
- ✅ npm Types: Available
|
||||
|
||||
## Next Steps
|
||||
- Monitor for 24-48 hours
|
||||
- Address any critical issues immediately
|
||||
- Plan next release cycle
|
||||
|
||||
## Files Generated
|
||||
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
|
||||
- \`post-release-checklist.md\` - Follow-up tasks
|
||||
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
|
||||
EOF
|
||||
```
|
||||
|
||||
2. **Analyze for GTM-worthy features:**
|
||||
```
|
||||
<task>
|
||||
Review these PRs to identify if ANY would interest a marketing/growth team.
|
||||
|
||||
Consider if a PR:
|
||||
- Changes something users directly interact with or experience
|
||||
- Makes something noticeably better, faster, or easier
|
||||
- Introduces capabilities users have been asking for
|
||||
- Has visual assets (screenshots, GIFs, videos) that could be shared
|
||||
- Tells a compelling story about improvement or innovation
|
||||
- Would make users excited if they heard about it
|
||||
|
||||
Many releases contain only technical improvements, bug fixes, or internal changes -
|
||||
that's perfectly normal. Only flag PRs that would genuinely interest end users.
|
||||
|
||||
If you find marketing-worthy PRs, note:
|
||||
- PR number, title, and author
|
||||
- Any media links from the description
|
||||
- One sentence on why it's worth showcasing
|
||||
|
||||
If nothing is marketing-worthy, just say "No marketing-worthy features in this release."
|
||||
</task>
|
||||
|
||||
PR data: [contents of prs-${NEW_VERSION}.json]
|
||||
```
|
||||
|
||||
3. **Generate GTM notification (only if needed):**
|
||||
```
|
||||
If there are marketing-worthy features, create a message for #gtm with:
|
||||
|
||||
🚀 Frontend Release v${NEW_VERSION}
|
||||
|
||||
Timeline: Available now in nightly, ~2-3 weeks for core
|
||||
|
||||
Features worth showcasing:
|
||||
[List the selected PRs with media links and authors]
|
||||
|
||||
Testing: --front-end-version ${NEW_VERSION}
|
||||
|
||||
If there are NO marketing-worthy features, generate:
|
||||
"No marketing-worthy features in v${NEW_VERSION} - mostly internal improvements and bug fixes."
|
||||
```
|
||||
|
||||
4. **Save the output:**
|
||||
```bash
|
||||
# Claude generates the GTM summary and saves it
|
||||
# Save to gtm-summary-${NEW_VERSION}.md
|
||||
|
||||
# Check if notification is needed
|
||||
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
|
||||
echo "✅ No GTM notification needed for this release"
|
||||
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
else
|
||||
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
echo "📤 Share this file in #gtm channel to notify the team"
|
||||
fi
|
||||
```
|
||||
2. **RELEASE COMPLETION**: All steps completed successfully?
|
||||
|
||||
## Advanced Safety Features
|
||||
|
||||
|
||||
6
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,6 @@
|
||||
# .git-blame-ignore-revs
|
||||
# List of commits to ignore in git blame
|
||||
# Format: <commit hash> # <description>
|
||||
|
||||
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
|
||||
c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
||||
96
.github/workflows/chromatic.yaml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: 'Chromatic'
|
||||
|
||||
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
chromatic-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for Chromatic baseline
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Get current time
|
||||
id: current-time
|
||||
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment PR - Build Started
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: append
|
||||
body: |
|
||||
<!-- STORYBOOK_BUILD_STATUS -->
|
||||
## 🎨 Storybook Build Status
|
||||
|
||||
🔄 **Building Storybook and running visual tests...**
|
||||
|
||||
⏳ Build started at: ${{ steps.current-time.outputs.time }} UTC
|
||||
|
||||
---
|
||||
*This comment will be updated when the build completes*
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Storybook and run Chromatic
|
||||
id: chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
buildScriptName: build-storybook
|
||||
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
||||
exitOnceUploaded: true # Don't wait for UI tests to complete
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
if: always()
|
||||
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment PR - Build Complete
|
||||
if: github.event_name == 'pull_request' && always()
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!-- STORYBOOK_BUILD_STATUS -->
|
||||
## 🎨 Storybook Build Status
|
||||
|
||||
${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
|
||||
|
||||
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
|
||||
|
||||
### 📊 Build Summary
|
||||
- **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }}
|
||||
- **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }}
|
||||
- **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }}
|
||||
- **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }}
|
||||
|
||||
### 🔗 Links
|
||||
${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }}
|
||||
${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }}
|
||||
|
||||
---
|
||||
${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}
|
||||
4
.gitignore
vendored
@@ -72,3 +72,7 @@ vite.config.mts.timestamp-*.mjs
|
||||
|
||||
# Linux core dumps
|
||||
./core
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ module.exports = defineConfig({
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
|
||||
|
||||
IMPORTANT Chinese Translation Guidelines:
|
||||
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
|
||||
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
|
||||
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
|
||||
`
|
||||
});
|
||||
|
||||
197
.storybook/CLAUDE.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Storybook Development Guidelines for Claude
|
||||
|
||||
## Quick Commands
|
||||
|
||||
- `npm run storybook`: Start Storybook development server
|
||||
- `npm run build-storybook`: Build static Storybook
|
||||
- `npm run test:component`: Run component tests (includes Storybook components)
|
||||
|
||||
## Development Workflow for Storybook
|
||||
|
||||
1. **Creating New Stories**:
|
||||
- Place `*.stories.ts` files alongside components
|
||||
- Follow the naming pattern: `ComponentName.stories.ts`
|
||||
- Use realistic mock data that matches ComfyUI schemas
|
||||
|
||||
2. **Testing Stories**:
|
||||
- Verify stories render correctly in Storybook UI
|
||||
- Test different component states and edge cases
|
||||
- Ensure proper theming and styling
|
||||
|
||||
3. **Code Quality**:
|
||||
- Run `npm run typecheck` to verify TypeScript
|
||||
- Run `npm run lint` to check for linting issues
|
||||
- Follow existing story patterns and conventions
|
||||
|
||||
## Story Creation Guidelines
|
||||
|
||||
### Basic Story Structure
|
||||
|
||||
```typescript
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import ComponentName from './ComponentName.vue'
|
||||
|
||||
const meta: Meta<typeof ComponentName> = {
|
||||
title: 'Category/ComponentName',
|
||||
component: ComponentName,
|
||||
parameters: {
|
||||
layout: 'centered' // or 'fullscreen', 'padded'
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Component props
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mock Data Patterns
|
||||
|
||||
For ComfyUI components, use realistic mock data:
|
||||
|
||||
```typescript
|
||||
// Node definition mock
|
||||
const mockNodeDef = {
|
||||
input: {
|
||||
required: {
|
||||
prompt: ["STRING", { multiline: true }]
|
||||
}
|
||||
},
|
||||
output: ["CONDITIONING"],
|
||||
output_is_list: [false],
|
||||
category: "conditioning"
|
||||
}
|
||||
|
||||
// Component instance mock
|
||||
const mockComponent = {
|
||||
id: "1",
|
||||
type: "CLIPTextEncode",
|
||||
// ... other properties
|
||||
}
|
||||
```
|
||||
|
||||
### Common Story Variants
|
||||
|
||||
Always include these story variants when applicable:
|
||||
|
||||
- **Default**: Basic component with minimal props
|
||||
- **WithData**: Component with realistic data
|
||||
- **Loading**: Component in loading state
|
||||
- **Error**: Component with error state
|
||||
- **LongContent**: Component with edge case content
|
||||
- **Empty**: Component with no data
|
||||
|
||||
### Storybook-Specific Code Patterns
|
||||
|
||||
#### Store Access
|
||||
```typescript
|
||||
// In stories, access stores through the setup function
|
||||
export const WithStore: Story = {
|
||||
render: () => ({
|
||||
setup() {
|
||||
const store = useMyStore()
|
||||
return { store }
|
||||
},
|
||||
template: '<MyComponent :data="store.data" />'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### Event Testing
|
||||
```typescript
|
||||
export const WithEvents: Story = {
|
||||
args: {
|
||||
onUpdate: fn() // Use Storybook's fn() for action logging
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
### Vue App Setup
|
||||
The Storybook preview is configured with:
|
||||
- Pinia stores initialized
|
||||
- PrimeVue with ComfyUI theme
|
||||
- i18n internationalization
|
||||
- All necessary CSS imports
|
||||
|
||||
### Build Configuration
|
||||
- Vite integration with proper alias resolution
|
||||
- Manual chunking for better performance
|
||||
- TypeScript support with strict checking
|
||||
- CSS processing for Vue components
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Import Errors**: Verify `@/` alias is working correctly
|
||||
2. **Missing Styles**: Ensure CSS imports are in `preview.ts`
|
||||
3. **Store Errors**: Check store initialization in setup
|
||||
4. **Type Errors**: Use proper TypeScript types for story args
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check TypeScript issues
|
||||
npm run typecheck
|
||||
|
||||
# Lint Storybook files
|
||||
npm run lint .storybook/
|
||||
|
||||
# Build to check for production issues
|
||||
npm run build-storybook
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
.storybook/
|
||||
├── main.ts # Core configuration
|
||||
├── preview.ts # Global setup and decorators
|
||||
├── README.md # User documentation
|
||||
└── CLAUDE.md # This file - Claude guidelines
|
||||
|
||||
src/
|
||||
├── components/
|
||||
│ └── MyComponent/
|
||||
│ ├── MyComponent.vue
|
||||
│ └── MyComponent.stories.ts
|
||||
```
|
||||
|
||||
## Integration with ComfyUI
|
||||
|
||||
### Available Context
|
||||
|
||||
Stories have access to:
|
||||
- All ComfyUI stores (widgetStore, colorPaletteStore, etc.)
|
||||
- PrimeVue components with ComfyUI theming
|
||||
- Internationalization system
|
||||
- ComfyUI CSS variables and styling
|
||||
|
||||
### Testing Components
|
||||
|
||||
When testing ComfyUI-specific components:
|
||||
1. Use realistic node definitions and data structures
|
||||
2. Test with different node types (sampling, conditioning, etc.)
|
||||
3. Verify proper CSS theming and dark/light modes
|
||||
4. Check component behavior with various input combinations
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Use manual chunking for large dependencies
|
||||
- Minimize bundle size by avoiding unnecessary imports
|
||||
- Leverage Storybook's lazy loading capabilities
|
||||
- Profile build times and optimize as needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep Stories Focused**: Each story should demonstrate one specific use case
|
||||
2. **Use Descriptive Names**: Story names should clearly indicate what they show
|
||||
3. **Document Complex Props**: Use JSDoc comments for complex prop types
|
||||
4. **Test Edge Cases**: Create stories for unusual but valid use cases
|
||||
5. **Maintain Consistency**: Follow established patterns in existing stories
|
||||
173
.storybook/README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Storybook Configuration for ComfyUI Frontend
|
||||
|
||||
## What is Storybook?
|
||||
|
||||
Storybook is a frontend workshop for building UI components and pages in isolation. It allows developers to:
|
||||
|
||||
- Build components independently from the main application
|
||||
- Test components with different props and states
|
||||
- Document component APIs and usage patterns
|
||||
- Share components across teams and projects
|
||||
- Catch visual regressions through visual testing
|
||||
|
||||
## Storybook vs Other Testing Tools
|
||||
|
||||
| Tool | Purpose | Use Case |
|
||||
|------|---------|----------|
|
||||
| **Storybook** | Component isolation & documentation | Developing, testing, and showcasing individual UI components |
|
||||
| **Playwright** | End-to-end testing | Full user workflow testing across multiple pages |
|
||||
| **Vitest** | Unit testing | Testing business logic, utilities, and component behavior |
|
||||
| **Vue Testing Library** | Component testing | Testing component interactions and DOM output |
|
||||
|
||||
### When to Use Storybook
|
||||
|
||||
**✅ Use Storybook for:**
|
||||
- Developing new UI components in isolation
|
||||
- Creating component documentation and examples
|
||||
- Testing different component states and props
|
||||
- Sharing components with designers and stakeholders
|
||||
- Visual regression testing
|
||||
- Building a component library or design system
|
||||
|
||||
**❌ Don't use Storybook for:**
|
||||
- Testing complex user workflows (use Playwright)
|
||||
- Testing business logic (use Vitest)
|
||||
- Integration testing between components (use Vue Testing Library)
|
||||
|
||||
## How to Use Storybook
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Start Storybook development server
|
||||
npm run storybook
|
||||
|
||||
# Build static Storybook for deployment
|
||||
npm run build-storybook
|
||||
```
|
||||
|
||||
### Creating Stories
|
||||
|
||||
Stories are located alongside components in `src/` directories with the pattern `*.stories.ts`:
|
||||
|
||||
```typescript
|
||||
// MyComponent.stories.ts
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import MyComponent from './MyComponent.vue'
|
||||
|
||||
const meta: Meta<typeof MyComponent> = {
|
||||
title: 'Components/MyComponent',
|
||||
component: MyComponent,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Hello World'
|
||||
}
|
||||
}
|
||||
|
||||
export const WithVariant: Story = {
|
||||
args: {
|
||||
title: 'Variant Example',
|
||||
variant: 'secondary'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Features
|
||||
|
||||
- **Vue 3 Support**: Full Vue 3 composition API and reactivity
|
||||
- **PrimeVue Integration**: All PrimeVue components and theming
|
||||
- **ComfyUI Theming**: Custom ComfyUI theme preset applied
|
||||
- **Pinia Stores**: Access to application stores for components that need state
|
||||
- **TypeScript**: Full TypeScript support with proper type checking
|
||||
- **CSS/SCSS**: Component styling support
|
||||
- **Auto-documentation**: Automatic prop tables and component documentation
|
||||
- **Chromatic Integration**: Automated visual regression testing for component stories
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Keep Stories Simple**: Each story should demonstrate one specific use case
|
||||
2. **Use Realistic Data**: Use data that resembles real application usage
|
||||
3. **Document Edge Cases**: Create stories for loading states, errors, and edge cases
|
||||
4. **Group Related Stories**: Use consistent naming and grouping for related components
|
||||
|
||||
### Component Testing Strategy
|
||||
|
||||
```typescript
|
||||
// Example: Testing different component states
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true
|
||||
}
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
error: 'Failed to load data'
|
||||
}
|
||||
}
|
||||
|
||||
export const WithLongText: Story = {
|
||||
args: {
|
||||
description: 'Very long description that might cause layout issues...'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
- Use browser DevTools to inspect component behavior
|
||||
- Check the Storybook console for Vue warnings or errors
|
||||
- Use the Controls addon to dynamically change props
|
||||
- Leverage the Actions addon to test event handling
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- **`main.ts`**: Core Storybook configuration and Vite integration
|
||||
- **`preview.ts`**: Global decorators, parameters, and Vue app setup
|
||||
- **`manager.ts`**: Storybook UI customization (if needed)
|
||||
|
||||
## Chromatic Visual Testing
|
||||
|
||||
This project uses [Chromatic](https://chromatic.com) for automated visual regression testing of Storybook components.
|
||||
|
||||
### How It Works
|
||||
|
||||
- **Automated Testing**: Every push to `main` and `sno-storybook` branches triggers Chromatic builds
|
||||
- **Pull Request Reviews**: PRs against `main` branch include visual diffs for component changes
|
||||
- **Baseline Management**: Changes on `main` branch are automatically accepted as new baselines
|
||||
- **Cross-browser Testing**: Tests across multiple browsers and viewports
|
||||
|
||||
### Viewing Results
|
||||
|
||||
1. Check the GitHub Actions tab for Chromatic workflow status
|
||||
2. Click on the Chromatic build link in PR comments to review visual changes
|
||||
3. Accept or reject visual changes directly in the Chromatic UI
|
||||
|
||||
### Best Practices for Visual Testing
|
||||
|
||||
- **Consistent Stories**: Ensure stories render consistently across different environments
|
||||
- **Meaningful Names**: Use descriptive story names that clearly indicate the component state
|
||||
- **Edge Cases**: Include stories for loading, error, and empty states
|
||||
- **Realistic Data**: Use data that closely resembles production usage
|
||||
|
||||
## Integration with ComfyUI
|
||||
|
||||
This Storybook setup includes:
|
||||
|
||||
- ComfyUI-specific theming and styling
|
||||
- Pre-configured Pinia stores for state management
|
||||
- Internationalization (i18n) support
|
||||
- PrimeVue component library integration
|
||||
- Proper alias resolution for `@/` imports
|
||||
|
||||
For component-specific examples, see the NodePreview stories in `src/components/node/`.
|
||||
96
.storybook/main.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite'
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import type { InlineConfig } from 'vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: ['@storybook/addon-docs'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {}
|
||||
},
|
||||
async viteFinal(config) {
|
||||
// Use dynamic import to avoid CJS deprecation warning
|
||||
const { mergeConfig } = await import('vite')
|
||||
|
||||
// Filter out any plugins that might generate import maps
|
||||
if (config.plugins) {
|
||||
config.plugins = config.plugins.filter((plugin: any) => {
|
||||
if (plugin && plugin.name && plugin.name.includes('import-map')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return mergeConfig(config, {
|
||||
// Replace plugins entirely to avoid inheritance issues
|
||||
plugins: [
|
||||
// Only include plugins we explicitly need for Storybook
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
comfy: FileSystemIconLoader(
|
||||
process.cwd() + '/src/assets/icons/custom'
|
||||
)
|
||||
}
|
||||
}),
|
||||
Components({
|
||||
dts: false, // Disable dts generation in Storybook
|
||||
resolvers: [
|
||||
IconsResolver({
|
||||
customCollections: ['comfy']
|
||||
})
|
||||
],
|
||||
dirs: [
|
||||
process.cwd() + '/src/components',
|
||||
process.cwd() + '/src/layout',
|
||||
process.cwd() + '/src/views'
|
||||
],
|
||||
deep: true,
|
||||
extensions: ['vue']
|
||||
})
|
||||
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
|
||||
],
|
||||
server: {
|
||||
allowedHosts: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': process.cwd() + '/src'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: () => {
|
||||
// Don't externalize any modules in Storybook build
|
||||
// This ensures PrimeVue and other dependencies are bundled
|
||||
return false
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
if (
|
||||
warning.code === 'UNUSED_EXTERNAL_IMPORT' &&
|
||||
warning.message?.includes('resolveComponent')
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Suppress Storybook font asset warnings
|
||||
if (
|
||||
warning.code === 'UNRESOLVED_IMPORT' &&
|
||||
warning.message?.includes('nunito-sans')
|
||||
) {
|
||||
return
|
||||
}
|
||||
warn(warning)
|
||||
}
|
||||
},
|
||||
chunkSizeWarningLimit: 1000
|
||||
}
|
||||
} satisfies InlineConfig)
|
||||
}
|
||||
}
|
||||
export default config
|
||||
34
.storybook/preview-head.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<style>
|
||||
body {
|
||||
overflow-y: auto !important;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Light theme default */
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Dark theme styles */
|
||||
body.dark-theme,
|
||||
.dark-theme body {
|
||||
background-color: #0a0a0a;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
/* Ensure Storybook canvas follows theme */
|
||||
.sb-show-main {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-theme .sb-show-main,
|
||||
.dark-theme .docs-story {
|
||||
background-color: #0a0a0a !important;
|
||||
}
|
||||
|
||||
/* Fix for Storybook controls panel in dark mode */
|
||||
.dark-theme .docblock-argstable-body {
|
||||
color: #e5e5e5;
|
||||
}
|
||||
</style>
|
||||
104
.storybook/preview.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { setup } from '@storybook/vue3'
|
||||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
|
||||
import '../src/assets/css/style.css'
|
||||
import { i18n } from '../src/i18n'
|
||||
import '../src/lib/litegraph/public/css/litegraph.css'
|
||||
import { useWidgetStore } from '../src/stores/widgetStore'
|
||||
import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
|
||||
|
||||
const ComfyUIPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
// @ts-expect-error fix me
|
||||
primary: Aura['primitive'].blue
|
||||
}
|
||||
})
|
||||
|
||||
// Setup Vue app for Storybook
|
||||
setup((app) => {
|
||||
app.directive('tooltip', Tooltip)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
// Initialize stores
|
||||
useColorPaletteStore(pinia)
|
||||
useWidgetStore(pinia)
|
||||
|
||||
app.use(i18n)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: ComfyUIPreset,
|
||||
options: {
|
||||
prefix: 'p',
|
||||
cssLayer: {
|
||||
name: 'primevue',
|
||||
order: 'primevue, tailwind-utilities'
|
||||
},
|
||||
darkModeSelector: '.dark-theme, :root:has(.dark-theme)'
|
||||
}
|
||||
}
|
||||
})
|
||||
app.use(ConfirmationService)
|
||||
app.use(ToastService)
|
||||
})
|
||||
|
||||
// Dark theme decorator
|
||||
export const withTheme = (Story: any, context: any) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
|
||||
// Apply theme class to document root
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
document.body.classList.add('dark-theme')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark-theme')
|
||||
document.body.classList.remove('dark-theme')
|
||||
}
|
||||
|
||||
return Story()
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i
|
||||
}
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{ name: 'light', value: '#ffffff' },
|
||||
{ name: 'dark', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
},
|
||||
globalTypes: {
|
||||
theme: {
|
||||
name: 'Theme',
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'light',
|
||||
toolbar: {
|
||||
icon: 'circlehollow',
|
||||
items: [
|
||||
{ value: 'light', icon: 'sun', title: 'Light' },
|
||||
{ value: 'dark', icon: 'moon', title: 'Dark' }
|
||||
],
|
||||
showName: true,
|
||||
dynamicTitle: true
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: [withTheme]
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -61,8 +61,6 @@ Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit
|
||||
|
||||
### Dev Server
|
||||
|
||||
Note: The dev server will NOT load any extension from the ComfyUI server. Only core extensions will be loaded.
|
||||
|
||||
- Start local ComfyUI backend at `localhost:8188`
|
||||
- Run `npm run dev` to start the dev server
|
||||
- Run `npm run dev:electron` to start the dev server with electron API mocked
|
||||
@@ -89,6 +87,10 @@ After you start the dev server, you should see following logs:
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
> ⚠️ IMPORTANT:
|
||||
The dev server will NOT load JavaScript extensions from custom nodes. Only core extensions (built into the frontend) will be loaded. This is because the shim system that allows custom node JavaScript to import frontend modules only works in production builds. Python custom nodes still function normally. See [Extension Development Guide](docs/extensions/development.md) for details and workarounds. And See [Extension Overview](docs/extensions/README.md) for extensions overview.
|
||||
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Architecture Decision Records
|
||||
|
||||
@@ -529,6 +529,10 @@ For detailed development setup, testing procedures, and technical information, p
|
||||
|
||||
See [locales/README.md](src/locales/README.md) for details.
|
||||
|
||||
### Storybook
|
||||
|
||||
See [.storybook/README.md](.storybook/README.md) for component development and visual testing documentation.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For comprehensive troubleshooting and technical support, please refer to our official documentation:
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 152 B After Width: | Height: | Size: 152 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
@@ -154,7 +154,7 @@ test.describe('Color Palette', () => {
|
||||
// doesn't update the store immediately.
|
||||
await comfyPage.setup()
|
||||
|
||||
await comfyPage.loadWorkflow('every_node_color')
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'custom-color-palette-obsidian-dark-all-colors.png'
|
||||
@@ -192,7 +192,7 @@ test.describe('Color Palette', () => {
|
||||
|
||||
test.describe('Node Color Adjustments', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('every_node_color')
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
})
|
||||
|
||||
test('should adjust opacity via node opacity setting', async ({
|
||||
|
||||
@@ -7,7 +7,7 @@ test.describe('Load workflow warning', () => {
|
||||
test('Should display a warning when loading a workflow with missing nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_nodes')
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes')
|
||||
|
||||
// Wait for the element with the .comfy-missing-nodes selector to be visible
|
||||
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
|
||||
@@ -17,7 +17,7 @@ test.describe('Load workflow warning', () => {
|
||||
test('Should display a warning when loading a workflow with missing nodes in subgraphs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_nodes_in_subgraph')
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes_in_subgraph')
|
||||
|
||||
// Wait for the element with the .comfy-missing-nodes selector to be visible
|
||||
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
|
||||
@@ -33,7 +33,7 @@ test.describe('Load workflow warning', () => {
|
||||
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
|
||||
await comfyPage.loadWorkflow('missing_nodes')
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes')
|
||||
await comfyPage.closeDialog()
|
||||
|
||||
// Make a change to the graph
|
||||
@@ -51,7 +51,7 @@ test.describe('Execution error', () => {
|
||||
test('Should display an error message when an execution error occurs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe('Execution error', () => {
|
||||
})
|
||||
|
||||
test('Can display Issue Report form', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -84,7 +84,7 @@ test.describe('Missing models warning', () => {
|
||||
test('Should display a warning when missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
await comfyPage.loadWorkflow('missing/missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
@@ -97,7 +97,7 @@ test.describe('Missing models warning', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
// Load workflow that has a node with models metadata at the node level
|
||||
await comfyPage.loadWorkflow('missing_models_from_node_properties')
|
||||
await comfyPage.loadWorkflow('missing/missing_models_from_node_properties')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
@@ -142,7 +142,7 @@ test.describe('Missing models warning', () => {
|
||||
{ times: 1 }
|
||||
)
|
||||
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
await comfyPage.loadWorkflow('missing/missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).not.toBeVisible()
|
||||
@@ -153,7 +153,7 @@ test.describe('Missing models warning', () => {
|
||||
}) => {
|
||||
// This tests the scenario where outdated model metadata exists in the workflow
|
||||
// but the actual selected models (widget values) have changed
|
||||
await comfyPage.loadWorkflow('model_metadata_widget_mismatch')
|
||||
await comfyPage.loadWorkflow('missing/model_metadata_widget_mismatch')
|
||||
|
||||
// The missing models warning should NOT appear
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
@@ -167,7 +167,7 @@ test.describe('Missing models warning', () => {
|
||||
}) => {
|
||||
// The fake_model.safetensors is served by
|
||||
// https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
await comfyPage.loadWorkflow('missing/missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
@@ -190,7 +190,7 @@ test.describe('Missing models warning', () => {
|
||||
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
true
|
||||
)
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
await comfyPage.loadWorkflow('missing/missing_models')
|
||||
|
||||
checkbox = comfyPage.page.getByLabel("Don't show this again")
|
||||
closeButton = comfyPage.page.getByLabel('Close')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('DOM Widget', () => {
|
||||
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('collapsed_multiline')
|
||||
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
|
||||
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
|
||||
await expect(textareaWidget).not.toBeVisible()
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ test.describe('Graph', () => {
|
||||
// Should be able to fix link input slot index after swap the input order
|
||||
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
|
||||
test('Fix link input slots', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('input_order_swap')
|
||||
await comfyPage.loadWorkflow('inputs/input_order_swap')
|
||||
expect(
|
||||
await comfyPage.page.evaluate(() => {
|
||||
return window['app'].graph.links.get(1)?.target_slot
|
||||
@@ -15,7 +15,7 @@ test.describe('Graph', () => {
|
||||
})
|
||||
|
||||
test('Validate workflow links', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('bad_link')
|
||||
await comfyPage.loadWorkflow('links/bad_link')
|
||||
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,7 +137,9 @@ test.describe('Group Node', () => {
|
||||
test('Preserves hidden input configuration when containing duplicate node types', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('group_node_identical_nodes_hidden_inputs')
|
||||
await comfyPage.loadWorkflow(
|
||||
'groupnodes/group_node_identical_nodes_hidden_inputs'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const groupNodeId = 19
|
||||
@@ -204,7 +206,7 @@ test.describe('Group Node', () => {
|
||||
test('Loads from a workflow using the legacy path separator ("/")', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('legacy_group_node')
|
||||
await comfyPage.loadWorkflow('groupnodes/legacy_group_node')
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
await expect(
|
||||
comfyPage.page.locator('.comfy-missing-nodes')
|
||||
@@ -213,7 +215,7 @@ test.describe('Group Node', () => {
|
||||
|
||||
test.describe('Copy and paste', () => {
|
||||
let groupNode: NodeReference | null
|
||||
const WORKFLOW_NAME = 'group_node_v1.3.3'
|
||||
const WORKFLOW_NAME = 'groupnodes/group_node_v1.3.3'
|
||||
const GROUP_NODE_CATEGORY = 'group nodes>workflow'
|
||||
const GROUP_NODE_PREFIX = 'workflow>'
|
||||
const GROUP_NODE_NAME = 'group_node' // Node name in given workflow
|
||||
|
||||
@@ -10,7 +10,7 @@ import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('mixed_graph_items')
|
||||
await comfyPage.loadWorkflow('groups/mixed_graph_items')
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
|
||||
await comfyPage.canvas.press('Delete')
|
||||
@@ -18,7 +18,7 @@ test.describe('Item Interaction', () => {
|
||||
})
|
||||
|
||||
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('mixed_graph_items')
|
||||
await comfyPage.loadWorkflow('groups/mixed_graph_items')
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
await comfyPage.canvas.press('KeyP')
|
||||
await comfyPage.nextFrame()
|
||||
@@ -223,7 +223,7 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test('Link snap to slot', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('snap_to_slot')
|
||||
await comfyPage.loadWorkflow('links/snap_to_slot')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
|
||||
|
||||
const outputSlotPos = {
|
||||
@@ -240,7 +240,7 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('batch_move_links')
|
||||
await comfyPage.loadWorkflow('links/batch_move_links')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
|
||||
|
||||
const outputSlot1Pos = {
|
||||
@@ -320,7 +320,7 @@ test.describe('Node Interaction', () => {
|
||||
x: 167,
|
||||
y: 143
|
||||
}
|
||||
await comfyPage.loadWorkflow('single_save_image_node')
|
||||
await comfyPage.loadWorkflow('nodes/single_save_image_node')
|
||||
await comfyPage.canvas.click({
|
||||
position: textWidgetPos
|
||||
})
|
||||
@@ -340,7 +340,7 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test('Can double click node title to edit', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: 50,
|
||||
@@ -356,7 +356,7 @@ test.describe('Node Interaction', () => {
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: 50,
|
||||
@@ -381,7 +381,7 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test('Can fit group to contents', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('oversized_group')
|
||||
await comfyPage.loadWorkflow('groups/oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
@@ -414,7 +414,7 @@ test.describe('Node Interaction', () => {
|
||||
|
||||
test.describe('Group Interaction', () => {
|
||||
test('Can double click group title to edit', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('single_group')
|
||||
await comfyPage.loadWorkflow('groups/single_group')
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: 50,
|
||||
@@ -632,21 +632,21 @@ test.describe('Widget Interaction', () => {
|
||||
|
||||
test.describe('Load workflow', () => {
|
||||
test('Can load workflow with string node id', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('string_node_id')
|
||||
await comfyPage.loadWorkflow('nodes/string_node_id')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
|
||||
})
|
||||
|
||||
test('Can load workflow with ("STRING",) input node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('string_input')
|
||||
await comfyPage.loadWorkflow('inputs/string_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
|
||||
})
|
||||
|
||||
test('Restore workflow on reload (switch workflow)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
@@ -655,7 +655,7 @@ test.describe('Load workflow', () => {
|
||||
test('Restore workflow on reload (modify workflow)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('collapse')
|
||||
// Wait 300ms between 2 clicks so that it is not treated as a double click
|
||||
@@ -734,7 +734,7 @@ test.describe('Load workflow', () => {
|
||||
|
||||
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
|
||||
})
|
||||
})
|
||||
@@ -747,10 +747,10 @@ test.describe('Load duplicate workflow', () => {
|
||||
test('A workflow can be loaded multiple times in a row', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ test.describe('Load Workflow in Media', () => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropFile(fileName)
|
||||
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 107 KiB |
@@ -73,6 +73,77 @@ test.describe('Menu', () => {
|
||||
expect(isTextCutoff).toBe(false)
|
||||
})
|
||||
|
||||
test('Clicking on active state items does not close menu', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open the menu
|
||||
await comfyPage.menu.topbar.openTopbarMenu()
|
||||
const menu = comfyPage.page.locator('.comfy-command-menu')
|
||||
|
||||
// Navigate to View menu
|
||||
const viewMenuItem = comfyPage.page.locator(
|
||||
'.p-menubar-item-label:text-is("View")'
|
||||
)
|
||||
await viewMenuItem.hover()
|
||||
|
||||
// Wait for submenu to appear
|
||||
const viewSubmenu = comfyPage.page
|
||||
.locator('.p-tieredmenu-submenu:visible')
|
||||
.first()
|
||||
await viewSubmenu.waitFor({ state: 'visible' })
|
||||
|
||||
// Find Bottom Panel menu item
|
||||
const bottomPanelMenuItem = viewSubmenu
|
||||
.locator('.p-tieredmenu-item:has-text("Bottom Panel")')
|
||||
.first()
|
||||
const bottomPanelItem = bottomPanelMenuItem.locator(
|
||||
'.p-menubar-item-label:text-is("Bottom Panel")'
|
||||
)
|
||||
await bottomPanelItem.waitFor({ state: 'visible' })
|
||||
|
||||
// Get checkmark icon element
|
||||
const checkmark = bottomPanelMenuItem.locator('.pi-check')
|
||||
|
||||
// Check initial state of bottom panel (it's initially hidden)
|
||||
const bottomPanel = comfyPage.page.locator('.bottom-panel')
|
||||
await expect(bottomPanel).not.toBeVisible()
|
||||
|
||||
// Checkmark should be invisible initially (panel is hidden)
|
||||
await expect(checkmark).toHaveClass(/invisible/)
|
||||
|
||||
// Click Bottom Panel to toggle it on
|
||||
await bottomPanelItem.click()
|
||||
|
||||
// Verify menu is still visible after clicking
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(viewSubmenu).toBeVisible()
|
||||
|
||||
// Verify bottom panel is now visible
|
||||
await expect(bottomPanel).toBeVisible()
|
||||
|
||||
// Checkmark should now be visible (panel is shown)
|
||||
await expect(checkmark).not.toHaveClass(/invisible/)
|
||||
|
||||
// Click Bottom Panel again to toggle it off
|
||||
await bottomPanelItem.click()
|
||||
|
||||
// Verify menu is still visible after second click
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(viewSubmenu).toBeVisible()
|
||||
|
||||
// Verify bottom panel is hidden again
|
||||
await expect(bottomPanel).not.toBeVisible()
|
||||
|
||||
// Checkmark should be invisible again (panel is hidden)
|
||||
await expect(checkmark).toHaveClass(/invisible/)
|
||||
|
||||
// Click outside to close menu
|
||||
await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
|
||||
// Verify menu is now closed
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.openTopbarMenu()
|
||||
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('File')
|
||||
|
||||
@@ -66,7 +66,7 @@ test.describe('Node source badge', () => {
|
||||
Object.values(NodeBadgeMode).forEach(async (mode) => {
|
||||
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
|
||||
// Execution error workflow has both custom node and core node.
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -6,32 +6,32 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
// a hollow circle no matter what shape it was defined in the workflow JSON.
|
||||
test.describe('Optional input', () => {
|
||||
test('No shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('optional_input_no_shape')
|
||||
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
})
|
||||
|
||||
test('Wrong shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('optional_input_wrong_shape')
|
||||
await comfyPage.loadWorkflow('inputs/optional_input_wrong_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
})
|
||||
|
||||
test('Correct shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('optional_input_correct_shape')
|
||||
await comfyPage.loadWorkflow('inputs/optional_input_correct_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
})
|
||||
|
||||
test('Force input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('force_input')
|
||||
await comfyPage.loadWorkflow('inputs/force_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
|
||||
})
|
||||
|
||||
test('Default input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default_input')
|
||||
await comfyPage.loadWorkflow('inputs/default_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
|
||||
})
|
||||
|
||||
test('Only optional inputs', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('only_optional_inputs')
|
||||
await comfyPage.loadWorkflow('inputs/only_optional_inputs')
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
await expect(
|
||||
comfyPage.page.locator('.comfy-missing-nodes')
|
||||
@@ -43,7 +43,7 @@ test.describe('Optional input', () => {
|
||||
)
|
||||
})
|
||||
test('Old workflow with converted input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('old_workflow_converted_input')
|
||||
await comfyPage.loadWorkflow('inputs/old_workflow_converted_input')
|
||||
const node = await comfyPage.getNodeRefById('1')
|
||||
const inputs = await node.getProperty('inputs')
|
||||
const vaeInput = inputs.find((w) => w.name === 'vae')
|
||||
@@ -55,25 +55,25 @@ test.describe('Optional input', () => {
|
||||
expect(convertedInput.link).not.toBeNull()
|
||||
})
|
||||
test('Renamed converted input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('renamed_converted_widget')
|
||||
await comfyPage.loadWorkflow('inputs/renamed_converted_widget')
|
||||
const node = await comfyPage.getNodeRefById('3')
|
||||
const inputs = await node.getProperty('inputs')
|
||||
const renamedInput = inputs.find((w) => w.name === 'breadth')
|
||||
expect(renamedInput).toBeUndefined()
|
||||
})
|
||||
test('slider', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('simple_slider')
|
||||
await comfyPage.loadWorkflow('inputs/simple_slider')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
|
||||
})
|
||||
test('unknown converted widget', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
|
||||
await comfyPage.loadWorkflow('missing_nodes_converted_widget')
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes_converted_widget')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'missing_nodes_converted_widget.png'
|
||||
)
|
||||
})
|
||||
test('dynamically added input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('dynamically_added_input')
|
||||
await comfyPage.loadWorkflow('inputs/dynamically_added_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'dynamically_added_input.png'
|
||||
)
|
||||
|
||||
@@ -319,7 +319,7 @@ test.describe('Node Help', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
// First load workflow with custom node
|
||||
await comfyPage.loadWorkflow('group_node_v1.3.3')
|
||||
await comfyPage.loadWorkflow('groupnodes/group_node_v1.3.3')
|
||||
|
||||
// Mock custom node documentation with fallback
|
||||
await comfyPage.page.route(
|
||||
|
||||
@@ -16,7 +16,7 @@ test.describe('Node search box', () => {
|
||||
})
|
||||
|
||||
test(`Can trigger on group body double click`, async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('single_group_only')
|
||||
await comfyPage.loadWorkflow('groups/single_group_only')
|
||||
await comfyPage.page.mouse.dblclick(50, 50, { delay: 5 })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
@@ -59,7 +59,7 @@ test.describe('Node search box', () => {
|
||||
})
|
||||
|
||||
test('Can auto link batch moved node', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('batch_move_links')
|
||||
await comfyPage.loadWorkflow('links/batch_move_links')
|
||||
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
@@ -110,7 +110,7 @@ test.describe('Node search box', () => {
|
||||
|
||||
test('@mobile Can trigger on empty canvas tap', async ({ comfyPage }) => {
|
||||
await comfyPage.closeMenu()
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
const screenCenter = {
|
||||
x: 200,
|
||||
y: 400
|
||||
|
||||
@@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Note Node', () => {
|
||||
test('Can load node nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('note_nodes')
|
||||
await comfyPage.loadWorkflow('nodes/note_nodes')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -89,7 +89,7 @@ test.describe('Remote COMBO Widget', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeName = 'Remote Widget Node'
|
||||
await comfyPage.loadWorkflow('remote_widget')
|
||||
await comfyPage.loadWorkflow('inputs/remote_widget')
|
||||
await comfyPage.page.waitForTimeout(512)
|
||||
|
||||
const node = await comfyPage.page.evaluate((name) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe('Reroute Node', () => {
|
||||
test('loads from inserted workflow', async ({ comfyPage }) => {
|
||||
const workflowName = 'single_connected_reroute_node.json'
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
[workflowName]: workflowName
|
||||
[workflowName]: 'links/single_connected_reroute_node.json'
|
||||
})
|
||||
await comfyPage.setup()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
|
||||
@@ -33,7 +33,7 @@ test.describe('Selection Toolbox', () => {
|
||||
test('shows at correct position when node is pasted', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
@@ -56,7 +56,7 @@ test.describe('Selection Toolbox', () => {
|
||||
test('hide when select and drag happen at the same time', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const nodePos = await node.getPosition()
|
||||
|
||||
@@ -103,7 +103,7 @@ test.describe('Selection Toolbox', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
// A group + a KSampler node
|
||||
await comfyPage.loadWorkflow('single_group')
|
||||
await comfyPage.loadWorkflow('groups/single_group')
|
||||
|
||||
// Select group + node should show bypass button
|
||||
await comfyPage.page.focus('canvas')
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can open workflow after insert', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'single_ksampler.json'
|
||||
'workflow1.json': 'nodes/single_ksampler.json'
|
||||
})
|
||||
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
@@ -241,7 +241,7 @@ test.describe('Workflows sidebar', () => {
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_nodes')
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes')
|
||||
await comfyPage.closeDialog()
|
||||
|
||||
// Load blank workflow
|
||||
|
||||
@@ -20,7 +20,7 @@ test.describe('Subgraph Slot Rename Dialog', () => {
|
||||
test('Shows current slot label (not stale) in rename dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -106,7 +106,7 @@ test.describe('Subgraph Slot Rename Dialog', () => {
|
||||
test('Shows current output slot label in rename dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
@@ -52,7 +52,7 @@ test.describe('Subgraph Operations', () => {
|
||||
|
||||
test.describe('I/O Slot Management', () => {
|
||||
test('Can add input slots to subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -68,7 +68,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can add output slots to subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -84,7 +84,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can remove input slots from subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -104,7 +104,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can remove output slots from subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -124,7 +124,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can rename I/O slots', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -157,7 +157,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can rename input slots via double-click', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -189,7 +189,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can rename output slots via double-click', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -224,7 +224,7 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Right-click context menu still works alongside double-click', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -261,7 +261,7 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Can double-click on slot label text to rename', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -355,7 +355,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can delete subgraph node', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
expect(await subgraphNode.exists()).toBe(true)
|
||||
@@ -377,7 +377,7 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Can copy subgraph node by dragging + alt', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
|
||||
@@ -406,7 +406,7 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Copying subgraph node by dragging + alt creates a new subgraph node with unique type', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
|
||||
@@ -438,7 +438,7 @@ test.describe('Subgraph Operations', () => {
|
||||
|
||||
test.describe('Operations Inside Subgraphs', () => {
|
||||
test('Can copy and paste nodes in subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -469,7 +469,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Can undo and redo operations in subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -506,7 +506,7 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Breadcrumb updates when subgraph node title is changed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('nested-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('10')
|
||||
@@ -558,7 +558,9 @@ test.describe('Subgraph Operations', () => {
|
||||
test('DOM widget visibility persists through subgraph navigation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
await comfyPage.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify promoted widget is visible in parent graph
|
||||
@@ -589,7 +591,9 @@ test.describe('Subgraph Operations', () => {
|
||||
test('DOM widget content is preserved through navigation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
await comfyPage.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
const textarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await textarea.fill(TEST_WIDGET_CONTENT)
|
||||
@@ -610,7 +614,9 @@ test.describe('Subgraph Operations', () => {
|
||||
test('DOM elements are cleaned up when subgraph node is removed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
await comfyPage.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
const initialCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
@@ -635,7 +641,7 @@ test.describe('Subgraph Operations', () => {
|
||||
// Enable new menu for breadcrumb navigation
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
const workflowName = 'subgraph-with-promoted-text-widget'
|
||||
const workflowName = 'subgraphs/subgraph-with-promoted-text-widget'
|
||||
await comfyPage.loadWorkflow(workflowName)
|
||||
|
||||
const textareaCount = await comfyPage.page
|
||||
@@ -661,8 +667,8 @@ test.describe('Subgraph Operations', () => {
|
||||
// Click breadcrumb to navigate back to parent graph
|
||||
const homeBreadcrumb = comfyPage.page.getByRole('link', {
|
||||
// In the subgraph navigation breadcrumbs, the home/top level
|
||||
// breadcrumb is just the workflow name
|
||||
name: workflowName
|
||||
// breadcrumb is just the workflow name without the folder path
|
||||
name: 'subgraph-with-promoted-text-widget'
|
||||
})
|
||||
await homeBreadcrumb.waitFor({ state: 'visible' })
|
||||
await homeBreadcrumb.click()
|
||||
@@ -680,7 +686,9 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Multiple promoted widgets are handled correctly', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-multiple-promoted-widgets')
|
||||
await comfyPage.loadWorkflow(
|
||||
'subgraphs/subgraph-with-multiple-promoted-widgets'
|
||||
)
|
||||
|
||||
const parentCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
@@ -711,7 +719,7 @@ test.describe('Subgraph Operations', () => {
|
||||
})
|
||||
|
||||
test('Navigation hotkey can be customized', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Change the Exit Subgraph keybinding from Escape to Alt+Q
|
||||
@@ -767,7 +775,7 @@ test.describe('Subgraph Operations', () => {
|
||||
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
|
||||
@@ -38,7 +38,7 @@ test.describe('Combo text widget', () => {
|
||||
.options.values
|
||||
})
|
||||
|
||||
await comfyPage.loadWorkflow('optional_combo_input')
|
||||
await comfyPage.loadWorkflow('inputs/optional_combo_input')
|
||||
const initialComboValues = await getComboValues()
|
||||
|
||||
// Focus canvas
|
||||
@@ -57,7 +57,7 @@ test.describe('Combo text widget', () => {
|
||||
test('Should refresh combo values of nodes with v2 combo input spec', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('node_with_v2_combo_input')
|
||||
await comfyPage.loadWorkflow('inputs/node_with_v2_combo_input')
|
||||
// click canvas to focus
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
// press R to trigger refresh
|
||||
@@ -90,7 +90,7 @@ test.describe('Boolean widget', () => {
|
||||
|
||||
test.describe('Slider widget', () => {
|
||||
test('Can drag adjust value', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('simple_slider')
|
||||
await comfyPage.loadWorkflow('inputs/simple_slider')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
const widget = await node.getWidget(0)
|
||||
@@ -136,7 +136,7 @@ test.describe('Dynamic widget manipulation', () => {
|
||||
test('Auto expand node when widget is added dynamically', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
|
||||
45
docs/extensions/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ComfyUI Extensions Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Extensions are the primary way to add functionality to ComfyUI. They can be custom nodes, custom nodes that render widgets (UIs made with javascript), ComfyUI shell UI enhancements, and more. This documentation covers everything you need to know about understanding, using, and developing extensions.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
- **[Development Guide](./development.md)** - How to develop extensions, including:
|
||||
- Extension architecture and terminology
|
||||
- How extensions load (backend vs frontend)
|
||||
- Why extensions don't work in dev server
|
||||
- Development workarounds and best practices
|
||||
|
||||
- **[Core Extensions Reference](./core.md)** - Detailed reference for core extensions:
|
||||
- Complete list of all core extensions
|
||||
- Extension architecture principles
|
||||
- Hook execution sequence
|
||||
- Best practices for extension development
|
||||
|
||||
## Quick Links
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **Extension**: Umbrella term for any code that extends ComfyUI
|
||||
- **Custom Nodes**: Python backend nodes (a type of extension)
|
||||
- **JavaScript Extensions**: Frontend UI enhancements
|
||||
- **Core Extensions**: Built-in extensions bundled with ComfyUI
|
||||
|
||||
### Common Tasks
|
||||
|
||||
- [Developing extensions in dev mode](./development.md#development-workarounds)
|
||||
- [Understanding the shim system](./development.md#how-the-shim-works)
|
||||
- [Extension hooks and lifecycle](./core.md#extension-hooks)
|
||||
|
||||
### External Resources
|
||||
|
||||
- [Official JavaScript Extension Docs](https://docs.comfy.org/custom-nodes/js/javascript_overview)
|
||||
- [ComfyExtension TypeScript Interface](../../src/types/comfy.ts)
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check the [Development Guide](./development.md) for common issues
|
||||
- Review [Core Extensions](./core.md) for examples
|
||||
- Visit the [ComfyUI Discord](https://discord.com/invite/comfyorg) for community support
|
||||
181
docs/extensions/core.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Core Extensions
|
||||
|
||||
This directory contains the core extensions that provide essential functionality to the ComfyUI frontend.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Extension Architecture](#extension-architecture)
|
||||
- [Core Extensions](#core-extensions)
|
||||
- [Extension Development](#extension-development)
|
||||
- [Extension Hooks](#extension-hooks)
|
||||
- [Further Reading](#further-reading)
|
||||
|
||||
## Overview
|
||||
|
||||
Extensions in ComfyUI are modular JavaScript modules that extend and enhance the functionality of the frontend. The extensions in this directory are considered "core" as they provide fundamental features that are built into ComfyUI by default.
|
||||
|
||||
## Extension Architecture
|
||||
|
||||
ComfyUI's extension system follows these key principles:
|
||||
|
||||
1. **Registration-based:** Extensions must register themselves with the application using `app.registerExtension()`
|
||||
2. **Hook-driven:** Extensions interact with the system through predefined hooks
|
||||
3. **Non-intrusive:** Extensions should avoid directly modifying core objects where possible
|
||||
|
||||
## Core Extensions List
|
||||
|
||||
The following table lists ALL core extensions in the system as of 2025-01-30:
|
||||
|
||||
### Main Extensions
|
||||
|
||||
| Extension | Description | Category |
|
||||
|-----------|-------------|----------|
|
||||
| clipspace.ts | Implements the Clipspace feature for temporary image storage | Image |
|
||||
| contextMenuFilter.ts | Provides context menu filtering capabilities | UI |
|
||||
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities | Prompts |
|
||||
| editAttention.ts | Implements attention editing functionality | Text |
|
||||
| electronAdapter.ts | Adapts functionality for Electron environment | Platform |
|
||||
| groupNode.ts | Implements the group node functionality to organize workflows | Graph |
|
||||
| groupNodeManage.ts | Provides group node management operations | Graph |
|
||||
| groupOptions.ts | Handles group node configuration options | Graph |
|
||||
| index.ts | Main extension registration and coordination | Core |
|
||||
| load3d.ts | Supports 3D model loading and visualization | 3D |
|
||||
| maskEditorOld.ts | Legacy mask editor implementation | Image |
|
||||
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
|
||||
| nodeTemplates.ts | Provides node template functionality | Templates |
|
||||
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
|
||||
| previewAny.ts | Universal preview functionality for various data types | Preview |
|
||||
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections | Graph |
|
||||
| saveImageExtraOutput.ts | Handles additional image output saving | Image |
|
||||
| saveMesh.ts | Implements 3D mesh saving functionality | 3D |
|
||||
| simpleTouchSupport.ts | Provides basic touch interaction support | Input |
|
||||
| slotDefaults.ts | Manages default values for node slots | Nodes |
|
||||
| uploadAudio.ts | Handles audio file upload functionality | Audio |
|
||||
| uploadImage.ts | Handles image upload functionality | Image |
|
||||
| webcamCapture.ts | Provides webcam capture capabilities | Media |
|
||||
| widgetInputs.ts | Implements various widget input types | Widgets |
|
||||
|
||||
|
||||
### Conditional Lines Subdirectory
|
||||
Located in `extensions/core/load3d/conditional-lines/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| ColoredShadowMaterial.js | Material for colored shadow rendering |
|
||||
| ConditionalEdgesGeometry.js | Geometry for conditional edge rendering |
|
||||
| ConditionalEdgesShader.js | Shader for conditional edges |
|
||||
| OutsideEdgesGeometry.js | Geometry for outside edge detection |
|
||||
|
||||
### Lines2 Subdirectory
|
||||
Located in `extensions/core/load3d/conditional-lines/Lines2/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| ConditionalLineMaterial.js | Material for conditional line rendering |
|
||||
| ConditionalLineSegmentsGeometry.js | Geometry for conditional line segments |
|
||||
|
||||
### ThreeJS Override Subdirectory
|
||||
Located in `extensions/core/load3d/threejsOverride/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| OverrideMTLLoader.js | Custom MTL loader with enhanced functionality |
|
||||
|
||||
## Extension Development
|
||||
|
||||
When developing or modifying extensions, follow these best practices:
|
||||
|
||||
1. **Use provided hooks** rather than directly modifying core application objects
|
||||
2. **Maintain compatibility** with other extensions
|
||||
3. **Follow naming conventions** for both extension names and settings
|
||||
4. **Properly document** extension hooks and functionality
|
||||
5. **Test with other extensions** to ensure no conflicts
|
||||
|
||||
### Extension Registration
|
||||
|
||||
Extensions are registered using the `app.registerExtension()` method:
|
||||
|
||||
```javascript
|
||||
app.registerExtension({
|
||||
name: "MyExtension",
|
||||
|
||||
// Hook implementations
|
||||
async init() {
|
||||
// Implementation
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Other hooks as needed
|
||||
});
|
||||
```
|
||||
|
||||
## Extension Hooks
|
||||
|
||||
ComfyUI extensions can implement various hooks that are called at specific points in the application lifecycle:
|
||||
|
||||
### Hook Execution Sequence
|
||||
|
||||
#### Web Page Load
|
||||
|
||||
```
|
||||
init
|
||||
addCustomNodeDefs
|
||||
getCustomWidgets
|
||||
beforeRegisterNodeDef [repeated multiple times]
|
||||
registerCustomNodes
|
||||
beforeConfigureGraph
|
||||
nodeCreated
|
||||
loadedGraphNode
|
||||
afterConfigureGraph
|
||||
setup
|
||||
```
|
||||
|
||||
#### Loading Workflow
|
||||
|
||||
```
|
||||
beforeConfigureGraph
|
||||
beforeRegisterNodeDef [zero, one, or multiple times]
|
||||
nodeCreated [repeated multiple times]
|
||||
loadedGraphNode [repeated multiple times]
|
||||
afterConfigureGraph
|
||||
```
|
||||
|
||||
#### Adding New Node
|
||||
|
||||
```
|
||||
nodeCreated
|
||||
```
|
||||
|
||||
### Key Hooks
|
||||
|
||||
| Hook | Description |
|
||||
|------|-------------|
|
||||
| `init` | Called after canvas creation but before nodes are added |
|
||||
| `setup` | Called after the application is fully set up and running |
|
||||
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
|
||||
| `getCustomWidgets` | Allows extensions to add custom widgets |
|
||||
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
|
||||
| `registerCustomNodes` | Allows extensions to register additional nodes |
|
||||
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
|
||||
| `nodeCreated` | Called after a node's constructor |
|
||||
| `beforeConfigureGraph` | Called before a graph is configured |
|
||||
| `afterConfigureGraph` | Called after a graph is configured |
|
||||
| `getSelectionToolboxCommands` | Allows extensions to add commands to the selection toolbox |
|
||||
|
||||
For the complete list of available hooks and detailed descriptions, see the [ComfyExtension interface in comfy.ts](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/types/comfy.ts).
|
||||
|
||||
## Further Reading
|
||||
|
||||
For more detailed information about ComfyUI's extension system, refer to the official documentation:
|
||||
|
||||
- [JavaScript Extension Overview](https://docs.comfy.org/custom-nodes/js/javascript_overview)
|
||||
- [JavaScript Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks)
|
||||
- [JavaScript Objects and Hijacking](https://docs.comfy.org/custom-nodes/js/javascript_objects_and_hijacking)
|
||||
- [JavaScript Settings](https://docs.comfy.org/custom-nodes/js/javascript_settings)
|
||||
- [JavaScript Examples](https://docs.comfy.org/custom-nodes/js/javascript_examples)
|
||||
|
||||
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.
|
||||
136
docs/extensions/development.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Extension Development Guide
|
||||
|
||||
## Understanding Extensions in ComfyUI
|
||||
|
||||
### Terminology Clarification
|
||||
|
||||
**ComfyUI Extension** - The umbrella term for any 3rd party code that extends ComfyUI functionality. This includes:
|
||||
|
||||
1. **Python Custom Nodes** - Backend nodes providing new operations
|
||||
- Located in `/custom_nodes/` directories
|
||||
- Registered via Python module system
|
||||
- Identified by `custom_nodes.<name>` in `python_module` field
|
||||
|
||||
2. **JavaScript Extensions** - Frontend functionality that can be:
|
||||
- Pure JavaScript extensions (implement `ComfyExtension` interface)
|
||||
- JavaScript components of custom nodes (in `/web/` or `/js/` folders, or custom directories specified via `WEB_DIRECTORY` export in `__init__.py` [see docs](https://docs.comfy.org/custom-nodes/backend/lifecycle#web-directory))
|
||||
- Core extensions (built into frontend at `/src/extensions/core/` - see [Core Extensions Documentation](./core.md))
|
||||
|
||||
### How Extensions Load
|
||||
|
||||
**Backend Flow (Python Custom Nodes):**
|
||||
1. ComfyUI server starts → scans `/custom_nodes/` directories
|
||||
2. Loads Python modules (e.g., `/custom_nodes/ComfyUI-Impact-Pack/__init__.py`)
|
||||
3. Python code registers new node types with the server
|
||||
4. Server exposes these via `/object_info` API with metadata like `python_module: "custom_nodes.ComfyUI-Impact-Pack"`
|
||||
5. These nodes execute on the server when workflows run
|
||||
|
||||
**Frontend Flow (JavaScript):**
|
||||
|
||||
*Core Extensions (always available):*
|
||||
1. Built directly into the frontend bundle at `/src/extensions/core/`
|
||||
2. Loaded immediately when the frontend starts
|
||||
3. No network requests needed - they're part of the compiled code
|
||||
|
||||
*Custom Node JavaScript (loaded dynamically):*
|
||||
1. Frontend starts → calls `/extensions` API
|
||||
2. Server responds with list of JavaScript files from:
|
||||
- `/web/extensions/*.js` (legacy location)
|
||||
- `/custom_nodes/*/web/*.js` (node-specific UI code)
|
||||
3. Frontend fetches each JavaScript file (e.g., `/extensions/ComfyUI-Impact-Pack/impact.js`)
|
||||
4. JavaScript executes immediately, calling `app.registerExtension()` to hook into the UI
|
||||
5. These registered hooks enhance the UI for their associated Python nodes
|
||||
|
||||
**The Key Distinction:**
|
||||
- **Python nodes** = Backend processing (what shows in your node menu)
|
||||
- **JavaScript extensions** = Frontend enhancements (how nodes look/behave in the UI)
|
||||
- A custom node package can have both, just Python, or (rarely) just JavaScript
|
||||
|
||||
## Why Extensions Don't Load in Dev Server
|
||||
|
||||
The development server cannot load custom node JavaScript due to architectural constraints from the TypeScript/Vite migration.
|
||||
|
||||
### The Technical Challenge
|
||||
|
||||
ComfyUI migrated to TypeScript and Vite, but thousands of extensions rely on the old unbundled module system. The solution was a **shim system** that maintains backward compatibility - but only in production builds.
|
||||
|
||||
### How the Shim Works
|
||||
|
||||
**Production Build:**
|
||||
During production build, a custom Vite plugin:
|
||||
- Binds all module exports to `window.comfyAPI`
|
||||
- Generates shim files that re-export from this global object
|
||||
|
||||
```javascript
|
||||
// Original source: /scripts/api.ts
|
||||
export const api = { }
|
||||
|
||||
// Generated shim: /scripts/api.js
|
||||
export * from window.comfyAPI.modules['/scripts/api.js']
|
||||
|
||||
// Extension imports work unchanged:
|
||||
import { api } from '/scripts/api.js'
|
||||
```
|
||||
|
||||
**Why Dev Server Can't Support This:**
|
||||
- The dev server serves raw source files without bundling
|
||||
- Vite refuses to transform node_modules in unbundled mode
|
||||
- Creating real-time shims would require intercepting every module request
|
||||
- This would defeat the purpose of a fast dev server
|
||||
|
||||
### The Trade-off
|
||||
|
||||
This was the least friction approach:
|
||||
- ✅ Extensions work in production without changes
|
||||
- ✅ Developers get modern tooling (TypeScript, hot reload)
|
||||
- ❌ Extension testing requires production build or workarounds
|
||||
|
||||
The alternative would have been breaking all existing extensions or staying with the legacy build system.
|
||||
|
||||
## Development Workarounds
|
||||
|
||||
### Option 1: Develop as Core Extension (Recommended)
|
||||
|
||||
1. Copy your extension to `src/extensions/core/` (see [Core Extensions Documentation](./core.md) for existing core extensions and architecture)
|
||||
2. Update imports to relative paths:
|
||||
```javascript
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
```
|
||||
3. Add to `src/extensions/core/index.ts`
|
||||
4. Test with hot reload working
|
||||
5. Move back when complete
|
||||
|
||||
### Option 2: Use Production Build
|
||||
|
||||
Build the frontend for full functionality:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
For faster iteration during development, use watch mode:
|
||||
```bash
|
||||
npx vite build --watch
|
||||
```
|
||||
|
||||
Note: Watch mode provides faster rebuilds than full builds, but still no hot reload
|
||||
|
||||
### Option 3: Test Against Cloud/Staging
|
||||
|
||||
For cloud extensions, modify `.env`:
|
||||
```
|
||||
DEV_SERVER_COMFYUI_URL=http://stagingcloud.comfy.org/
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Python nodes work normally in dev mode
|
||||
- JavaScript extensions require workarounds or production builds
|
||||
- Core extensions provide built-in functionality - see [Core Extensions Documentation](./core.md) for the complete list
|
||||
- The `ComfyExtension` interface defines all available hooks for extending the frontend
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Core Extensions Architecture](./core.md) - Complete list of core extensions and development guidelines
|
||||
- [JavaScript Extension Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks) - Official documentation on extension hooks
|
||||
- [ComfyExtension Interface](../../src/types/comfy.ts) - TypeScript interface defining all extension capabilities
|
||||
@@ -1,6 +1,8 @@
|
||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import pluginJs from '@eslint/js'
|
||||
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import globals from 'globals'
|
||||
@@ -94,5 +96,6 @@ export default [
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
...storybook.configs['flat/recommended']
|
||||
]
|
||||
|
||||
@@ -42,7 +42,14 @@ const config: KnipConfig = {
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
// Auto generated manager types
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
// Design system components (may not be used immediately)
|
||||
'src/components/button/IconGroup.vue',
|
||||
'src/components/button/MoreButton.vue',
|
||||
'src/components/button/TextButton.vue',
|
||||
'src/components/card/CardTitle.vue',
|
||||
'src/components/card/CardDescription.vue',
|
||||
'src/components/input/SingleSelect.vue'
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
// Vue-specific configuration
|
||||
|
||||
2053
package-lock.json
generated
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.5",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -19,7 +19,7 @@
|
||||
"test:browser": "npx playwright test",
|
||||
"test:unit": "vitest run tests-ui/tests",
|
||||
"test:component": "vitest run src/components/",
|
||||
"prepare": "husky || true",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --cache",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
@@ -28,7 +28,9 @@
|
||||
"knip": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts"
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.8.0",
|
||||
@@ -38,6 +40,8 @@
|
||||
"@lobehub/i18n-cli": "^1.20.0",
|
||||
"@pinia/testing": "^0.1.5",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@storybook/addon-docs": "^9.1.1",
|
||||
"@storybook/vue3-vite": "^9.1.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
@@ -51,6 +55,7 @@
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-storybook": "^9.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -62,6 +67,7 @@
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
"storybook": "^9.1.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
|
||||
@@ -16,8 +16,8 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Retry on CI only - increased for better flaky test handling */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
|
||||
<Tabs
|
||||
:key="$i18n.locale"
|
||||
v-model:value="bottomPanelStore.activeBottomPanelTabId"
|
||||
>
|
||||
<TabList pt:tab-list="border-none">
|
||||
<div class="w-full flex justify-between">
|
||||
<div class="tabs-container">
|
||||
@@ -11,11 +14,7 @@
|
||||
class="p-3 border-none"
|
||||
>
|
||||
<span class="font-bold">
|
||||
{{
|
||||
shouldCapitalizeTab(tab.id)
|
||||
? tab.title.toUpperCase()
|
||||
: tab.title
|
||||
}}
|
||||
{{ getTabDisplayTitle(tab) }}
|
||||
</span>
|
||||
</Tab>
|
||||
</div>
|
||||
@@ -60,13 +59,16 @@ import Tab from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
const dialogService = useDialogService()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isShortcutsTabActive = computed(() => {
|
||||
const activeTabId = bottomPanelStore.activeBottomPanelTabId
|
||||
@@ -80,6 +82,11 @@ const shouldCapitalizeTab = (tabId: string): boolean => {
|
||||
return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'
|
||||
}
|
||||
|
||||
const getTabDisplayTitle = (tab: BottomPanelExtension): string => {
|
||||
const title = tab.titleKey ? t(tab.titleKey) : tab.title || ''
|
||||
return shouldCapitalizeTab(tab.id) ? title.toUpperCase() : title
|
||||
}
|
||||
|
||||
const openKeybindingSettings = async () => {
|
||||
dialogService.showSettingsDialog('keybinding')
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
<div class="shortcut-info flex-grow pr-4">
|
||||
<div class="shortcut-name text-sm font-medium">
|
||||
{{ command.label || command.id }}
|
||||
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
:class="{
|
||||
'flex items-center gap-1': isActive,
|
||||
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
|
||||
'p-breadcrumb-item-link-icon-visible': isActive
|
||||
'p-breadcrumb-item-link-icon-visible': isActive,
|
||||
'active-breadcrumb-item': isActive
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
@@ -111,21 +112,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
{
|
||||
label: t('g.rename'),
|
||||
icon: 'pi pi-pencil',
|
||||
command: async () => {
|
||||
let initialName =
|
||||
workflowStore.activeSubgraph?.name ??
|
||||
workflowStore.activeWorkflow?.filename
|
||||
|
||||
if (!initialName) return
|
||||
|
||||
const newName = await dialogService.prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('breadcrumbsMenu.enterNewName'),
|
||||
defaultValue: initialName
|
||||
})
|
||||
|
||||
await rename(newName, initialName)
|
||||
}
|
||||
command: startRename
|
||||
},
|
||||
{
|
||||
label: t('breadcrumbsMenu.duplicate'),
|
||||
@@ -175,20 +162,24 @@ const handleClick = (event: MouseEvent) => {
|
||||
menu.value?.hide()
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
isEditing.value = true
|
||||
itemLabel.value = props.item.label as string
|
||||
void nextTick(() => {
|
||||
if (itemInputRef.value?.$el) {
|
||||
itemInputRef.value.$el.focus()
|
||||
itemInputRef.value.$el.select()
|
||||
if (wrapperRef.value) {
|
||||
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
|
||||
}
|
||||
}
|
||||
})
|
||||
startRename()
|
||||
}
|
||||
}
|
||||
|
||||
const startRename = () => {
|
||||
isEditing.value = true
|
||||
itemLabel.value = props.item.label as string
|
||||
void nextTick(() => {
|
||||
if (itemInputRef.value?.$el) {
|
||||
itemInputRef.value.$el.focus()
|
||||
itemInputRef.value.$el.select()
|
||||
if (wrapperRef.value) {
|
||||
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const inputBlur = async (doRename: boolean) => {
|
||||
if (doRename) {
|
||||
await rename(itemLabel.value, props.item.label as string)
|
||||
@@ -212,4 +203,8 @@ const inputBlur = async (doRename: boolean) => {
|
||||
.p-breadcrumb-item-label {
|
||||
@apply whitespace-nowrap text-ellipsis overflow-hidden;
|
||||
}
|
||||
|
||||
.active-breadcrumb-item {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
38
src/components/button/IconButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getButtonTypeClasses,
|
||||
getIconButtonSizeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
|
||||
interface IconButtonProps extends BaseButtonProps {
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
class: className,
|
||||
onClick
|
||||
} = defineProps<IconButtonProps>()
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} p-0`
|
||||
const sizeClasses = getIconButtonSizeClasses(size)
|
||||
const typeClasses = getButtonTypeClasses(type)
|
||||
|
||||
return [baseClasses, sizeClasses, typeClasses, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
})
|
||||
</script>
|
||||