Compare commits

...

27 Commits

Author SHA1 Message Date
Jin Yi
d9936fc8d5 refactor: Fix SearchBox v-model binding and rename ModelSelector to SampleModelSelector
SearchBox:
- Fix defineModel usage to use default 'modelValue' prop name
- Remove incorrect empty string parameter that caused Vue warning
- Add comment explaining defineModel behavior

SampleModelSelector (formerly ModelSelector):
- Rename ModelSelector.vue to SampleModelSelector.vue to indicate it's a sample component
- Fix v-model syntax error in SearchBox binding (remove extra '=' character)
- Add v-model:search-query binding to MultiSelect for search functionality
- Add watch handlers for searchText and searchQuery debugging
- Import watch from Vue and add searchText ref for MultiSelect search

useModelSelectorDialog:
- Update import to use renamed SampleModelSelector component

These changes fix the v-model binding issues preventing search functionality
from working properly in both the header SearchBox and MultiSelect components.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 14:06:37 +09:00
Jin Yi
55408ca9b1 refactor: improve props handling for MultiSelect and SingleSelect components
MultiSelect:
- Remove explicit options prop to pass all PrimeVue props via v-bind="$attrs"
- Add defineOptions({ inheritAttrs: false }) for controlled attribute inheritance
- Extend PrimeVue MultiSelectProps type in Storybook for type safety
- Add component comments explaining differences from SingleSelect and prop requirements

SingleSelect:
- Keep options prop (required for getLabel function to map values to labels)
- Add v-bind="$attrs" to maximize PrimeVue API compatibility
- Add defineOptions({ inheritAttrs: false }) for controlled attribute inheritance
- Add component and function comments explaining why options prop is necessary

Common changes:
- Keep option-label="name" as custom option template directly references it
- Use label prop instead of placeholder
- Improve Storybook type definitions to resolve TypeScript compilation errors

This refactor allows MultiSelect to flexibly utilize PrimeVue APIs while
SingleSelect maintains necessary customizations with type safety.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 14:06:37 +09:00
Jin Yi
953cf56c4f fix: props type fix 2025-08-31 14:06:37 +09:00
Jin Yi
feaabd7c2a fix: type any to MultiSelectPassThroughMethodOptions 2025-08-31 14:06:37 +09:00
Jin Yi
e83335dd9f fix: unused border style fix 2025-08-31 14:06:37 +09:00
Jin Yi
2945adb513 fix: prefix 2025-08-31 14:06:37 +09:00
Jin Yi
0ab9af78d1 fix: wrapper deleted 2025-08-31 14:06:37 +09:00
Jin Yi
3de318f3df fix: deleted unnecessary style 2025-08-31 14:06:37 +09:00
Jin Yi
6513d39651 fix: Add keyboard navigation focus styles to Select components
- Add focused state styles to MultiSelect option passthrough
- Add focused state styles to SingleSelect option passthrough
- Ensures keyboard navigation highlights options like hover state

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 14:06:37 +09:00
Jin Yi
355efe2200 fix: storybook dark-theme variables 2025-08-31 14:06:37 +09:00
Jin Yi
79d6572bc0 fix: Storybook's problematic & selector styles 2025-08-31 14:06:37 +09:00
Jin Yi
deec7dc22b fix: option added & show prefix 2025-08-31 14:06:37 +09:00
Jin Yi
2aa9bb8284 fix: has -> show prefix standardize 2025-08-31 14:06:37 +09:00
Jin Yi
2ec38694fc [feat] Enhance MultiSelect component and update Storybook stories
- Improved MultiSelect component layout with proper spacing and conditional rendering
- Added default args to MultiSelect and SearchBox stories for better control
- Added new story variants: WithSearchBox, WithSelectedCount, WithClearButton, AllHeaderFeatures, CustomSearchPlaceholder
- Added WithBorder and NoBorder variants to SearchBox story
- Updated BaseWidget story to showcase MultiSelect with all header features enabled
- Fixed conditional rendering of header section in MultiSelect based on feature flags

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 14:06:37 +09:00
Jin Yi
c4e42e5125 [feat] Enhance Storybook stories for UI components
- Update MoreButton story with transparent button styling and icon sizes
- Add new controls to MultiSelect story for searchBar, selectedCount, and clearButton
- Add hasBorder control to SearchBox story for border customization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 14:06:37 +09:00
snomiao
2358a97fe9 Fix Chromatic build failure by updating build-storybook script (#5276)
* [release] Increment version to 1.26.8

* [fix] Fix Chromatic build failure by updating build-storybook script

Replace nx build-storybook with direct storybook build command to properly
forward --output-dir argument from Chromatic deployment.

Fixes invalid Storybook build issue where static files were empty due to
Nx not passing through command-line arguments to the underlying Storybook
build command.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Change version to 0.7 for testing purposes

* chore(package.json): update version from 0.7 to 1.26.7 to reflect new release

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-30 22:01:12 -07:00
Christian Byrne
571043fa58 [fix] Bypass Nx for i18n collection to fix TypeScript compilation (#5278)
Change collect-i18n command from 'nx e2e' to 'npx playwright test' to use
Playwright's native TypeScript compilation instead of Nx's pipeline.

Nx's compilation pipeline doesn't properly handle TypeScript 'declare' fields
in LiteGraph source files, causing babel transform errors. Playwright's native
compilation handles these correctly.

This addresses the TypeScript compilation error:
'declare fields must first be transformed by @babel/plugin-transform-typescript'

Fixes remaining issue after previous workflow fixes.
2025-08-30 21:03:35 -07:00
Christian Byrne
186b1888a3 [feat] Re-add ComfyUI loading GIF to workflow status comments (#5277)
Restore the ComfyUI animated favicon loading indicator to PR status
comments that was removed in PR #5209 when workflow architecture was
restructured. The loading GIF now appears in:

- Playwright test status comments (pr-playwright-comment.yaml)
- Storybook build status comments (pr-storybook-comment.yaml)

This provides visual continuity and branding consistency during
CI operations, replacing generic hourglass emoji with ComfyUI's
animated logo.
2025-08-31 03:24:18 +00:00
Benjamin Lu
68c41839ec fix: Ensure logout clears both Firebase auth and API key (#5274)
* fix: Ensure logout clears both Firebase auth and API key

When logging out via the avatar dropdown, the logout function was only
clearing Firebase authentication but not the stored API key. This could
leave users partially authenticated with their API key still active.

Updated CurrentUserPopover to use handleSignOut from useCurrentUser
composable, which properly handles both authentication methods:
- Clears API key if logged in with API key
- Signs out Firebase if logged in with Firebase

This ensures complete logout regardless of authentication method.

Fixes #5261

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test: Update CurrentUserPopover tests to match new logout implementation

Updated test mocks to include handleSignOut from useCurrentUser composable
and adjusted test expectations to verify handleSignOut is called instead
of the direct logout method.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-30 19:46:38 -07:00
Christian Byrne
2e72988ef8 [fix] Use defineConfig in playwright.i18n.config.ts for TypeScript compilation (#5270)
Update playwright.i18n.config.ts to use defineConfig() instead of PlaywrightTestConfig
interface to fix TypeScript compilation issues with 'declare' fields.

The defineConfig API provides proper Vite integration for TypeScript/Babel
compilation, fixing the error: 'TypeScript declare fields must first be
transformed by @babel/plugin-transform-typescript'

Fixes compilation error in i18n collection workflow.
2025-08-30 15:33:30 -07:00
brucew4yn3rp
176ff73788 Added brucew4yn3rp as codeowner for Mask Editor (#5267)
* Added brucew4yn3rp as codeowner for maskeditor.ts

* Added brucew4yn3rp as codeowner for maskeditor.ts
2025-08-30 14:46:14 -07:00
Christian Byrne
843ea39954 [fix] Update i18n workflow for PNPM/Nx compatibility (#5266)
Remove explicit file arguments from collect-i18n command as the new Nx e2e setup
uses playwright config with testMatch pattern to automatically find test files.

The command 'pnpm collect-i18n -- scripts/collect-i18n-general.ts' was failing
because Nx e2e doesn't accept file arguments in the same way.

Fixes workflow failure in release PR #5263
2025-08-30 14:05:32 -07:00
snomiao
c3c2681819 [feat] Implement pull_request_target deployment for forked PRs - solves secret access issue (#5209)
* [feat] Fix CI workflow issues for forked PRs and improve test diagnostics

This commit addresses two critical blockers in the CI workflow:

1. **Cloudflare Token Access Issue**:
   - Added conditional deployment that skips Cloudflare Pages for forked PRs
   - Forked PRs now get artifact-based report access instead of live URLs
   - Maintains security by preventing secret access from external repos

2. **Test Startup Issues**:
   - Enhanced ComfyUI server startup with better diagnostics
   - Added server PID tracking and process status verification
   - Improved error messages and timeout handling

Additional improvements:
- Updated PR comment logic to handle both deployment scenarios
- Added FORK_TESTING.md documentation for contributors
- Enhanced deployment info handling for summary generation

Fixes #5207

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Implement pull_request_target deployment for forked PRs

Add secure deployment solution for Playwright reports from forked PRs using pull_request_target event.

Key Changes:
- Add deploy-playwright-reports.yaml workflow using pull_request_target
- Update test-ui.yaml to work with new deployment approach
- Add comprehensive security documentation

Security Features:
- No untrusted code execution (artifacts only)
- Follows GitHub security best practices
- Maintains full secret access for deployment
- Clear audit trail and logging

Fixes #5207

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Implement cost-optimized deployment with webhook triggers

Replace expensive polling mechanism with repository_dispatch webhooks to reduce GitHub Actions costs by 85%.

Key improvements:
- Remove 30-minute polling wait in deploy-playwright-reports.yaml
- Add repository_dispatch trigger for instant deployment activation
- Implement concurrency controls to prevent redundant deployments
- Add webhook trigger from test completion in test-ui.yaml
- Maintain security and forked PR support

Cost benefits:
- Eliminates 4 Ubuntu runners waiting up to 30min each
- Reduces API calls from 240+ to 1 per PR
- Event-driven architecture for better reliability
- No timeout risks or polling overhead

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [cleanup] Simplify PR testing approach per review feedback

- Revert enhanced ComfyUI server startup logging
- Remove complex fork handling and webhook triggers
- Simplify Cloudflare deployment to original approach
- Remove FORK_TESTING.md and PULL_REQUEST_TARGET_DEPLOYMENT.md files
- Remove deploy-playwright-reports.yaml workflow
- Documentation moved to PR comments for better visibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Implement workflow_run architecture for CI comment/deploy separation

Restructures CI workflows to use workflow_run triggers, improving forked PR support and simplifying core testing workflows.

- pr-playwright-comment.yaml - Comments Playwright test results after Tests CI completion
- pr-storybook-comment.yaml - Comments Storybook build status after Chromatic completion
- pr-playwright-deploy.yaml - Deploys Playwright reports with secret access after Tests CI completion

- chromatic.yaml - Removed all commenting logic, focused on Chromatic testing only
- test-ui.yaml - Removed deployment, commenting, and comment-summary job; focused on Playwright testing only

-  Better forked PR support - workflow_run has access to secrets for deployment
-  Cleaner separation of concerns - testing vs commenting/deployment
-  Reduced complexity in core testing workflows
-  Improved reliability for external contributors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Implement workflow_run for both start and completion events

- Updated pr-playwright-comment.yaml to trigger on both 'requested' and 'completed' events
- Updated pr-storybook-comment.yaml to trigger on both 'requested' and 'completed' events
- Added conditional logic to post different messages for workflow start vs completion
- Added "Tests are starting..." message when workflows begin
- Added "Build is starting..." message for Storybook builds
- Maintained existing completion logic with full test results and reports

This allows users to see immediate feedback when their workflows start running,
improving the user experience by providing real-time status updates.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [cleanup] Remove continue-on-error from comment workflows

Comment workflow failures should be visible rather than silently ignored.
This allows better debugging when PR comments fail to post.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Add logging when no PR found in comment workflows

- Add explicit logging step when steps.pr.outputs.result == 'null'
- Shows branch name, workflow run ID, repository, and event details
- Improves debugging when workflow_run triggers but finds no open PR
- Helps identify issues with branch name matching or PR state

Previously these workflows would silently skip all steps when no PR was found,
making it difficult to debug why comments weren't being posted.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update workflow formatting

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [security] Implement security hardening for workflow_run workflows

- Add URL sanitization for deployment report links to prevent malicious URL injection
- Pin third-party GitHub Actions to commit hashes for supply chain security
- Add repository validation checks to prevent workflow misconfiguration
- Validate deployment URLs against pages.dev pattern before using in comments

Following security recommendations from code review to implement defense-in-depth.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [security] Pin only third-party actions to commit hashes

Keep official GitHub actions (actions/github-script, actions/download-artifact) pinned to version tags as they are trusted first-party actions, while only pinning third-party edumserrano/find-create-or-update-comment to commit hash for supply chain security.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-30 11:04:01 -07:00
AustinMroz
b515ef0a5b Fix broken links on bypass prior to reroute (#5237) 2025-08-29 21:57:48 -07:00
Alexander Brown
8eef1b727e devex: Ignore playwright-report and coverage in the vite dev server (#5258) 2025-08-29 16:28:12 -07:00
Yoland Yan
23d0362267 [feat] Add account deletion functionality to UserPanel component (#5216) 2025-08-29 22:29:20 +00:00
Alexander Brown
2bf92a0e57 devex: Add Suggested local Playwright config modifications from \rowser_tests/README.md\ as a toggle block (#5256) 2025-08-29 13:27:21 -07:00
38 changed files with 989 additions and 541 deletions

View File

@@ -12,9 +12,6 @@ jobs:
runs-on: ubuntu-latest
# Only run for PRs from version-bump-* branches or manual triggers
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
permissions:
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -32,29 +29,6 @@ jobs:
node-version: '20'
cache: 'pnpm'
- 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'
continue-on-error: true
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: Cache tool outputs
uses: actions/cache@v4
@@ -81,37 +55,3 @@ jobs:
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()
continue-on-error: true
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.' }}

View File

@@ -34,7 +34,7 @@ jobs:
run: pnpm dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-general.ts
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend

View File

@@ -0,0 +1,163 @@
name: PR Playwright Comment
on:
workflow_run:
workflows: ['Tests CI']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
comment-summary:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request'
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Log when no PR found
if: steps.pr.outputs.result == 'null'
run: |
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
echo "Event: ${{ github.event.workflow_run.event }}"
- name: Generate comment body for start
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
id: comment-body-start
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
echo "<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Tests are starting...** " >> comment.md
echo "" >> comment.md
echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 🚀 Running Tests" >> comment.md
echo "- 🧪 **chromium**: Running tests..." >> comment.md
echo "- 🧪 **chromium-0.5x**: Running tests..." >> comment.md
echo "- 🧪 **chromium-2x**: Running tests..." >> comment.md
echo "- 🧪 **mobile-chrome**: Running tests..." >> comment.md
echo "" >> comment.md
echo "---" >> comment.md
echo "⏱️ Please wait while tests are running across all browsers..." >> comment.md
- name: Download all deployment info
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body for completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
id: comment-body-completed
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
# Validate URLs before using them in comments
sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
if [ "$sanitized_url" = "INVALID_URL" ]; then
echo "Invalid deployment URL detected: $url"
url="#" # Use safe fallback
fi
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Started
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
- name: Comment PR - Tests Complete
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md

View File

@@ -0,0 +1,101 @@
name: PR Playwright Deploy
on:
workflow_run:
workflows: ["Tests CI"]
types: [completed]
jobs:
deploy-reports:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request'
permissions:
actions: read
strategy:
fail-fast: false
matrix:
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Get PR info
id: pr-info
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return { number: null, sanitized_branch: null };
}
const pr = pullRequests[0];
const branchName = context.payload.workflow_run.head_branch;
const sanitizedBranch = branchName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/^-|-$/g, '');
return {
number: pr.number,
sanitized_branch: sanitizedBranch
};
- name: Set project name
if: fromJSON(steps.pr-info.outputs.result).number != null
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ fromJSON(steps.pr-info.outputs.result).sanitized_branch }}" >> $GITHUB_OUTPUT
- name: Download playwright report
if: fromJSON(steps.pr-info.outputs.result).number != null
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: playwright-report-${{ matrix.browser }}
path: playwright-report
- name: Install Wrangler
if: fromJSON(steps.pr-info.outputs.result).number != null
run: npm install -g wrangler
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
if: fromJSON(steps.pr-info.outputs.result).number != null
id: cloudflare-deploy
continue-on-error: true
run: |
# Retry logic for wrangler deploy (3 attempts)
RETRY_COUNT=0
MAX_RETRIES=3
SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
if npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ $SUCCESS = false ]; then
echo "All deployment attempts failed"
exit 1
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

View File

@@ -0,0 +1,108 @@
name: PR Storybook Comment
on:
workflow_run:
workflows: ['Chromatic']
types: [requested, completed]
jobs:
comment-storybook:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request'
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Log when no PR found
if: steps.pr.outputs.result == 'null'
run: |
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
echo "Event: ${{ github.event.workflow_run.event }}"
- name: Get workflow run details
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
id: workflow-run
uses: actions/github-script@v7
with:
script: |
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
return {
conclusion: run.data.conclusion,
html_url: run.data.html_url
};
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Storybook Started
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Build is starting...**
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🎨 Running Chromatic visual tests...
---
⏱️ Please wait while the Storybook build is in progress...
- name: Comment PR - Storybook Complete
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{ fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅' || '❌' }} **${{ fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 🔗 Links
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
---
${{ fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}

View File

@@ -7,15 +7,11 @@ on:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
@@ -48,29 +44,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Tests Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- PLAYWRIGHT_TEST_STATUS -->
---
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
---
*This comment will be updated when tests complete*
- name: Cache tool outputs
uses: actions/cache@v4
@@ -94,14 +67,6 @@ jobs:
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Generate sanitized branch name
id: branch-info
run: |
# Get branch name and sanitize it for Cloudflare branch names
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
@@ -114,8 +79,6 @@ jobs:
needs: setup
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
contents: read
strategy:
fail-fast: false
@@ -144,32 +107,6 @@ jobs:
python-version: '3.10'
cache: 'pip'
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Set project name
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>${{ matrix.browser }}</bold>: Running tests...
- name: Install requirements
run: |
@@ -198,9 +135,6 @@ jobs:
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Wrangler
run: pnpm install -g wrangler
- name: Run Playwright tests (${{ matrix.browser }})
id: playwright
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
@@ -213,181 +147,3 @@ jobs:
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
id: cloudflare-deploy
if: always()
continue-on-error: true
run: |
# Retry logic for wrangler deploy (3 attempts)
RETRY_COUNT=0
MAX_RETRIES=3
SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
if npx wrangler pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ $SUCCESS = false ]; then
echo "All deployment attempts failed"
exit 1
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Save deployment info for summary
if: always()
run: |
mkdir -p deployment-info
# Use step conclusion to determine test result
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
EXIT_CODE="0"
else
EXIT_CODE="1"
fi
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
- name: Upload deployment info
if: always()
uses: actions/upload-artifact@v4
with:
name: deployment-info-${{ matrix.browser }}
path: deployment-info/
retention-days: 1
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Complete
if: always() && github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
comment-summary:
needs: playwright-tests
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Download all deployment info
uses: actions/download-artifact@v4
with:
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body
id: comment-body
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
- name: Check test results and fail if needed
run: |
# Check if all tests passed and fail the job if not
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "false" ]; then
echo "❌ Tests failed in one or more browsers. Failing the CI job."
exit 1
else
echo "✅ All tests passed across all browsers!"
fi

View File

@@ -4,17 +4,26 @@
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Light theme default */
body {
background-color: #ffffff;
color: #1a1a1a;
/* Light theme default - with explicit color to override media queries */
body:not(.dark-theme) {
background-color: #fff !important;
color: #000 !important;
}
/* Override browser dark mode preference for light theme */
@media (prefers-color-scheme: dark) {
body:not(.dark-theme) {
color: #000 !important;
--fg-color: #000 !important;
--bg-color: #fff !important;
}
}
/* Dark theme styles */
body.dark-theme,
.dark-theme body {
background-color: #0a0a0a;
color: #e5e5e5;
background-color: #202020;
color: #fff;
}
/* Ensure Storybook canvas follows theme */
@@ -24,11 +33,33 @@
.dark-theme .sb-show-main,
.dark-theme .docs-story {
background-color: #0a0a0a !important;
background-color: #202020 !important;
}
/* Fix for Storybook controls panel in dark mode */
.dark-theme .docblock-argstable-body {
color: #e5e5e5;
/* CSS Variables for theme consistency */
body:not(.dark-theme) {
--fg-color: #000;
--bg-color: #fff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
}
body.dark-theme {
--fg-color: #fff;
--bg-color: #202020;
--content-bg: #4e4e4e;
--content-fg: #fff;
--content-hover-bg: #222;
--content-hover-fg: #fff;
}
/* Override Storybook's problematic & selector styles */
/* Reset only the specific properties that Storybook injects */
#storybook-root li+li,
#storybook-docs li+li {
margin: inherit;
padding: inherit;
}
</style>

View File

@@ -14,4 +14,4 @@
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Mask Editor extension
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs

View File

@@ -31,10 +31,10 @@
"knip": "knip --cache",
"knip:no-cache": "knip",
"locale": "lobe-i18n locale",
"collect-i18n": "nx e2e --config=playwright.i18n.config.ts",
"collect-i18n": "npx playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"storybook": "nx storybook -p 6006",
"build-storybook": "nx build-storybook"
"build-storybook": "storybook build"
},
"devDependencies": {
"@eslint/js": "^9.8.0",

View File

@@ -1,39 +1,31 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './browser_tests',
/* Run tests in files in parallel */
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 - 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. */
// /* // Toggle for [LOCAL] testing.
retries: process.env.CI ? 3 : 0,
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/* Path to global setup file. Exported function runs once before all the tests */
/*/ // [LOCAL]
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
},
//*/
globalSetup: './browser_tests/globalSetup.ts',
/* Path to global teardown file. Exported function runs once after all the tests */
globalTeardown: './browser_tests/globalTeardown.ts',
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',

View File

@@ -1,6 +1,6 @@
import { PlaywrightTestConfig } from '@playwright/test'
import { defineConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
export default defineConfig({
testDir: './scripts',
use: {
baseURL: 'http://localhost:5173',
@@ -9,6 +9,4 @@ const config: PlaywrightTestConfig = {
reporter: 'list',
timeout: 60000,
testMatch: /collect-i18n-.*\.ts/
}
export default config
})

View File

@@ -24,22 +24,22 @@ export const Basic: Story = {
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="secondary"
type="transparent"
label="Settings"
@click="() => { close() }"
>
<template #icon>
<Download />
<Download :size="16" />
</template>
</IconTextButton>
<IconTextButton
type="primary"
type="transparent"
label="Profile"
@click="() => { close() }"
>
<template #icon>
<ScrollText />
<ScrollText :size="16" />
</template>
</IconTextButton>
</template>

View File

@@ -57,14 +57,23 @@
class="w-8 h-8 mt-4"
style="--pc-spinner-color: #000"
/>
<Button
v-else
class="mt-4 w-32"
severity="secondary"
:label="$t('auth.signOut.signOut')"
icon="pi pi-sign-out"
@click="handleSignOut"
/>
<div v-else class="mt-4 flex flex-col gap-2">
<Button
class="w-32"
severity="secondary"
:label="$t('auth.signOut.signOut')"
icon="pi pi-sign-out"
@click="handleSignOut"
/>
<Button
v-if="!isApiKeyLogin"
class="w-32"
severity="danger"
:label="$t('auth.deleteAccount.deleteAccount')"
icon="pi pi-trash"
@click="handleDeleteAccount"
/>
</div>
</div>
<!-- Login Section -->
@@ -100,6 +109,7 @@ const dialogService = useDialogService()
const {
loading,
isLoggedIn,
isApiKeyLogin,
isEmailProvider,
userDisplayName,
userEmail,
@@ -107,6 +117,7 @@ const {
providerName,
providerIcon,
handleSignOut,
handleSignIn
handleSignIn,
handleDeleteAccount
} = useCurrentUser()
</script>

View File

@@ -1,9 +1,23 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { MultiSelectProps } from 'primevue/multiselect'
import { ref } from 'vue'
import MultiSelect from './MultiSelect.vue'
const meta: Meta<typeof MultiSelect> = {
// Combine our component props with PrimeVue MultiSelect props
// Since we use v-bind="$attrs", all PrimeVue props are available
interface ExtendedProps extends Partial<MultiSelectProps> {
// Our custom props
label?: string
showSearchBox?: boolean
showSelectedCount?: boolean
showClearButton?: boolean
searchPlaceholder?: string
// Override modelValue type to match our Option type
modelValue?: Array<{ name: string; value: string }>
}
const meta: Meta<ExtendedProps> = {
title: 'Components/Input/MultiSelect',
component: MultiSelect,
tags: ['autodocs'],
@@ -13,7 +27,35 @@ const meta: Meta<typeof MultiSelect> = {
},
options: {
control: 'object'
},
showSearchBox: {
control: 'boolean',
description: 'Toggle searchBar visibility'
},
showSelectedCount: {
control: 'boolean',
description: 'Toggle selected count visibility'
},
showClearButton: {
control: 'boolean',
description: 'Toggle clear button visibility'
},
searchPlaceholder: {
control: 'text'
}
},
args: {
label: 'Select',
options: [
{ name: 'Vue', value: 'vue' },
{ name: 'React', value: 'react' },
{ name: 'Angular', value: 'angular' },
{ name: 'Svelte', value: 'svelte' }
],
showSearchBox: false,
showSelectedCount: false,
showClearButton: false,
searchPlaceholder: 'Search...'
}
}
@@ -25,7 +67,7 @@ export const Default: Story = {
components: { MultiSelect },
setup() {
const selected = ref([])
const options = [
const options = args.options || [
{ name: 'Vue', value: 'vue' },
{ name: 'React', value: 'react' },
{ name: 'Angular', value: 'angular' },
@@ -38,8 +80,11 @@ export const Default: Story = {
<MultiSelect
v-model="selected"
:options="options"
label="Select Frameworks"
v-bind="args"
:label="args.label"
:showSearchBox="args.showSearchBox"
:showSelectedCount="args.showSelectedCount"
:showClearButton="args.showClearButton"
:searchPlaceholder="args.searchPlaceholder"
/>
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
<p class="text-sm">Selected: {{ selected.length > 0 ? selected.map(s => s.name).join(', ') : 'None' }}</p>
@@ -50,10 +95,10 @@ export const Default: Story = {
}
export const WithPreselectedValues: Story = {
render: () => ({
render: (args) => ({
components: { MultiSelect },
setup() {
const options = [
const options = args.options || [
{ name: 'JavaScript', value: 'js' },
{ name: 'TypeScript', value: 'ts' },
{ name: 'Python', value: 'python' },
@@ -61,25 +106,43 @@ export const WithPreselectedValues: Story = {
{ name: 'Rust', value: 'rust' }
]
const selected = ref([options[0], options[1]])
return { selected, options }
return { selected, options, args }
},
template: `
<div>
<MultiSelect
v-model="selected"
:options="options"
label="Select Languages"
:label="args.label"
:showSearchBox="args.showSearchBox"
:showSelectedCount="args.showSelectedCount"
:showClearButton="args.showClearButton"
:searchPlaceholder="args.searchPlaceholder"
/>
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
<p class="text-sm">Selected: {{ selected.map(s => s.name).join(', ') }}</p>
</div>
</div>
`
})
}),
args: {
label: 'Select Languages',
options: [
{ name: 'JavaScript', value: 'js' },
{ name: 'TypeScript', value: 'ts' },
{ name: 'Python', value: 'python' },
{ name: 'Go', value: 'go' },
{ name: 'Rust', value: 'rust' }
],
showSearchBox: false,
showSelectedCount: false,
showClearButton: false,
searchPlaceholder: 'Search...'
}
}
export const MultipleSelectors: Story = {
render: () => ({
render: (args) => ({
components: { MultiSelect },
setup() {
const frameworkOptions = ref([
@@ -114,7 +177,8 @@ export const MultipleSelectors: Story = {
tagOptions,
selectedFrameworks,
selectedProjects,
selectedTags
selectedTags,
args
}
},
template: `
@@ -124,22 +188,34 @@ export const MultipleSelectors: Story = {
v-model="selectedFrameworks"
:options="frameworkOptions"
label="Select Frameworks"
:showSearchBox="args.showSearchBox"
:showSelectedCount="args.showSelectedCount"
:showClearButton="args.showClearButton"
:searchPlaceholder="args.searchPlaceholder"
/>
<MultiSelect
v-model="selectedProjects"
:options="projectOptions"
label="Select Projects"
:showSearchBox="args.showSearchBox"
:showSelectedCount="args.showSelectedCount"
:showClearButton="args.showClearButton"
:searchPlaceholder="args.searchPlaceholder"
/>
<MultiSelect
v-model="selectedTags"
:options="tagOptions"
label="Select Tags"
:showSearchBox="args.showSearchBox"
:showSelectedCount="args.showSelectedCount"
:showClearButton="args.showClearButton"
:searchPlaceholder="args.searchPlaceholder"
/>
</div>
<div class="p-4 bg-gray-50 dark-theme:bg-zinc-800 rounded">
<h4 class="font-medium mb-2">Current Selection:</h4>
<div class="space-y-1 text-sm">
<h4 class="font-medium mt-0">Current Selection:</h4>
<div class="flex flex-col text-sm">
<p>Frameworks: {{ selectedFrameworks.length > 0 ? selectedFrameworks.map(s => s.name).join(', ') : 'None' }}</p>
<p>Projects: {{ selectedProjects.length > 0 ? selectedProjects.map(s => s.name).join(', ') : 'None' }}</p>
<p>Tags: {{ selectedTags.length > 0 ? selectedTags.map(s => s.name).join(', ') : 'None' }}</p>
@@ -147,5 +223,54 @@ export const MultipleSelectors: Story = {
</div>
</div>
`
})
}),
args: {
showSearchBox: false,
showSelectedCount: false,
showClearButton: false,
searchPlaceholder: 'Search...'
}
}
export const WithSearchBox: Story = {
...Default,
args: {
...Default.args,
showSearchBox: true
}
}
export const WithSelectedCount: Story = {
...Default,
args: {
...Default.args,
showSelectedCount: true
}
}
export const WithClearButton: Story = {
...Default,
args: {
...Default.args,
showClearButton: true
}
}
export const AllHeaderFeatures: Story = {
...Default,
args: {
...Default.args,
showSearchBox: true,
showSelectedCount: true,
showClearButton: true
}
}
export const CustomSearchPlaceholder: Story = {
...Default,
args: {
...Default.args,
showSearchBox: true,
searchPlaceholder: 'Filter packages...'
}
}

View File

@@ -1,93 +1,104 @@
<template>
<div class="relative inline-block">
<MultiSelect
v-model="selectedItems"
:options="options"
option-label="name"
unstyled
:placeholder="label"
:max-selected-labels="0"
:pt="pt"
<!--
Note: Unlike SingleSelect, we don't need an explicit options prop because:
1. Our value template only shows a static label (not dynamic based on selection)
2. We display a count badge instead of actual selected labels
3. All PrimeVue props (including options) are passed via v-bind="$attrs"
option-label="name" is required because our option template directly accesses option.name
max-selected-labels="0" is required to show count badge instead of selected item labels
-->
<MultiSelect
v-model="selectedItems"
v-bind="$attrs"
option-label="name"
unstyled
:max-selected-labels="0"
:pt="pt"
>
<template
v-if="showSearchBox || showSelectedCount || showClearButton"
#header
>
<template
v-if="hasSearchBox || showSelectedCount || hasClearButton"
#header
>
<div class="p-2 flex flex-col gap-y-4 pb-0">
<SearchBox
v-if="hasSearchBox"
v-model="searchQuery"
:has-border="true"
:place-holder="searchPlaceholder"
/>
<div class="flex items-center justify-between">
<span
v-if="showSelectedCount"
class="text-sm text-neutral-400 dark-theme:text-zinc-500 px-1"
>
{{
selectedCount > 0
? $t('g.itemsSelected', { selectedCount })
: $t('g.itemSelected', { selectedCount })
}}
</span>
<TextButton
v-if="hasClearButton"
:label="$t('g.clearAll')"
type="transparent"
size="fit-content"
class="text-sm !text-blue-500 !dark-theme:text-blue-600"
@click.stop="selectedItems = []"
/>
</div>
<div class="h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
</div>
</template>
<!-- Trigger value (keep text scale identical) -->
<template #value>
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
</template>
<!-- Chevron size identical to current -->
<template #dropdownicon>
<i-lucide:chevron-down class="text-lg text-neutral-400" />
</template>
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
<template #option="slotProps">
<div class="flex items-center gap-2">
<div
class="flex h-4 w-4 p-0.5 flex-shrink-0 items-center justify-center rounded border-[3px] transition-all duration-200"
:class="
slotProps.selected
? 'border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
: 'border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
"
<div class="p-2 flex flex-col pb-0">
<SearchBox
v-if="showSearchBox"
v-model="searchQuery"
:class="showSelectedCount || showClearButton ? 'mb-2' : ''"
:show-order="true"
:place-holder="searchPlaceholder"
/>
<div
v-if="showSelectedCount || showClearButton"
class="mt-2 flex items-center justify-between"
>
<span
v-if="showSelectedCount"
class="text-sm text-neutral-400 dark-theme:text-zinc-500 px-1"
>
<i-lucide:check
v-if="slotProps.selected"
class="text-xs text-bold text-white"
/>
</div>
<span>{{ slotProps.option.name }}</span>
{{
selectedCount > 0
? $t('g.itemsSelected', { selectedCount })
: $t('g.itemSelected', { selectedCount })
}}
</span>
<TextButton
v-if="showClearButton"
:label="$t('g.clearAll')"
type="transparent"
size="fit-content"
class="text-sm !text-blue-500 !dark-theme:text-blue-600"
@click.stop="selectedItems = []"
/>
</div>
</template>
</MultiSelect>
<div class="mt-4 h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
</div>
</template>
<!-- Selected count badge -->
<div
v-if="selectedCount > 0"
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
>
{{ selectedCount }}
</div>
</div>
<!-- Trigger value (keep text scale identical) -->
<template #value>
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
<span
v-if="selectedCount > 0"
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
>
{{ selectedCount }}
</span>
</template>
<!-- Chevron size identical to current -->
<template #dropdownicon>
<i-lucide:chevron-down class="text-lg text-neutral-400" />
</template>
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
<template #option="slotProps">
<div class="flex items-center gap-2">
<div
class="flex h-4 w-4 p-0.5 flex-shrink-0 items-center justify-center rounded transition-all duration-200"
:class="
slotProps.selected
? 'border-[3px] border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
: 'border-[1px] border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
"
>
<i-lucide:check
v-if="slotProps.selected"
class="text-xs text-bold text-white"
/>
</div>
<Button class="border-none outline-none bg-transparent" unstyled>{{
slotProps.option.name
}}</Button>
</div>
</template>
</MultiSelect>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import MultiSelect, {
MultiSelectPassThroughMethodOptions
} from 'primevue/multiselect'
@@ -99,26 +110,29 @@ import TextButton from '../button/TextButton.vue'
type Option = { name: string; value: string }
defineOptions({
inheritAttrs: false
})
interface Props {
/** Input label shown on the trigger button */
label?: string
/** Static options for the multiselect (when not using async search) */
options: Option[]
/** Show search box in the panel header */
hasSearchBox?: boolean
showSearchBox?: boolean
/** Show selected count text in the panel header */
showSelectedCount?: boolean
/** Show "Clear all" action in the panel header */
hasClearButton?: boolean
showClearButton?: boolean
/** Placeholder for the search input */
searchPlaceholder?: string
// Note: options prop is intentionally omitted.
// It's passed via $attrs to maximize PrimeVue API compatibility
}
const {
label,
options,
hasSearchBox = false,
showSearchBox = false,
showSelectedCount = false,
hasClearButton = false,
showClearButton = false,
searchPlaceholder = 'Search...'
} = defineProps<Props>()
@@ -131,7 +145,7 @@ const selectedCount = computed(() => selectedItems.value.length)
const pt = computed(() => ({
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
class: [
'relative inline-flex cursor-pointer select-none w-full',
'relative inline-flex cursor-pointer select-none',
'rounded-lg bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
'transition-all duration-200 ease-in-out',
'border-[2.5px] border-solid',
@@ -153,7 +167,7 @@ const pt = computed(() => ({
},
header: () => ({
class:
hasSearchBox || showSelectedCount || hasClearButton ? 'block' : 'hidden'
showSearchBox || showSelectedCount || showClearButton ? 'block' : 'hidden'
}),
// Overlay & list visuals unchanged
overlay:
@@ -161,9 +175,17 @@ const pt = computed(() => ({
list: {
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
},
// Option row hover tone identical
option:
'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
// Option row hover and focus tone
option: ({ context }: MultiSelectPassThroughMethodOptions) => ({
class: [
'flex gap-1 items-center p-2',
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
// Add focus/highlight state for keyboard navigation
{
'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context?.focused
}
]
}),
// Hide built-in checkboxes entirely via PT (no :deep)
pcHeaderCheckbox: {
root: { class: 'hidden' },

View File

@@ -10,7 +10,15 @@ const meta: Meta<typeof SearchBox> = {
argTypes: {
placeHolder: {
control: 'text'
},
showBorder: {
control: 'boolean',
description: 'Toggle border prop'
}
},
args: {
placeHolder: 'Search...',
showBorder: false
}
}
@@ -25,9 +33,23 @@ export const Default: Story = {
return { searchText, args }
},
template: `
<div>
<SearchBox v-model:="searchQuery" />
<div style="max-width: 320px;">
<SearchBox v-bind="args" v-model="searchText" />
</div>
`
})
}
export const WithBorder: Story = {
...Default,
args: {
showBorder: true
}
}
export const NoBorder: Story = {
...Default,
args: {
showBorder: false
}
}

View File

@@ -15,19 +15,20 @@
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
const { placeHolder, hasBorder = false } = defineProps<{
const { placeHolder, showBorder = false } = defineProps<{
placeHolder?: string
hasBorder?: boolean
showBorder?: boolean
}>()
const searchQuery = defineModel<string>('')
// defineModel without arguments uses 'modelValue' as the prop name
const searchQuery = defineModel<string>()
const wrapperStyle = computed(() => {
return hasBorder
return showBorder
? 'flex w-full items-center rounded gap-2 bg-white dark-theme:bg-zinc-800 p-1 border border-solid border-zinc-200 dark-theme:border-zinc-700'
: 'flex w-full items-center rounded px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800'
})
const iconColorStyle = computed(() => {
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
return !showBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
})
</script>

View File

@@ -4,12 +4,24 @@ import { ref } from 'vue'
import SingleSelect from './SingleSelect.vue'
// SingleSelect already includes options prop, so no need to extend
const meta: Meta<typeof SingleSelect> = {
title: 'Components/Input/SingleSelect',
component: SingleSelect,
tags: ['autodocs'],
argTypes: {
label: { control: 'text' }
label: { control: 'text' },
options: { control: 'object' }
},
args: {
label: 'Sorting Type',
options: [
{ name: 'Popular', value: 'popular' },
{ name: 'Newest', value: 'newest' },
{ name: 'Oldest', value: 'oldest' },
{ name: 'A → Z', value: 'az' },
{ name: 'Z → A', value: 'za' }
]
}
}
@@ -29,19 +41,18 @@ export const Default: Story = {
components: { SingleSelect },
setup() {
const selected = ref<string | null>(null)
const options = sampleOptions
const options = args.options || sampleOptions
return { selected, options, args }
},
template: `
<div>
<SingleSelect v-model="selected" :options="options" :label="args.label || 'Sorting Type'" />
<SingleSelect v-model="selected" :options="options" :label="args.label" />
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
<p class="text-sm">Selected: {{ selected ?? 'None' }}</p>
</div>
</div>
`
}),
args: { label: 'Sorting Type' }
})
}
export const WithIcon: Story = {

View File

@@ -1,58 +1,73 @@
<template>
<div class="relative inline-flex items-center">
<Select
v-model="selectedItem"
:options="options"
option-label="name"
option-value="value"
unstyled
:placeholder="label"
:pt="pt"
>
<!-- Trigger value -->
<template #value="slotProps">
<div class="flex items-center gap-2 text-sm">
<slot name="icon" />
<span
v-if="slotProps.value !== null && slotProps.value !== undefined"
class="text-zinc-700 dark-theme:text-gray-200"
>
{{ getLabel(slotProps.value) }}
</span>
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
</div>
</template>
<!--
Note: We explicitly pass options here (not just via $attrs) because:
1. Our custom value template needs options to look up labels from values
2. PrimeVue's value slot only provides 'value' and 'placeholder', not the selected item's label
3. We need to maintain the icon slot functionality in the value template
option-label="name" is required because our option template directly accesses option.name
-->
<Select
v-model="selectedItem"
v-bind="$attrs"
:options="options"
option-label="name"
option-value="value"
unstyled
:pt="pt"
>
<!-- Trigger value -->
<template #value="slotProps">
<div class="flex items-center gap-2 text-sm">
<slot name="icon" />
<span
v-if="slotProps.value !== null && slotProps.value !== undefined"
class="text-zinc-700 dark-theme:text-gray-200"
>
{{ getLabel(slotProps.value) }}
</span>
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
</div>
</template>
<!-- Trigger caret -->
<template #dropdownicon>
<i-lucide:chevron-down
class="text-base text-neutral-400 dark-theme:text-gray-300"
<!-- Trigger caret -->
<template #dropdownicon>
<i-lucide:chevron-down
class="text-base text-neutral-400 dark-theme:text-gray-300"
/>
</template>
<!-- Option row -->
<template #option="{ option, selected }">
<div class="flex items-center justify-between gap-3 w-full">
<span class="truncate">{{ option.name }}</span>
<i-lucide:check
v-if="selected"
class="text-neutral-900 dark-theme:text-white"
/>
</template>
<!-- Option row -->
<template #option="{ option, selected }">
<div class="flex items-center justify-between gap-3 w-full">
<span class="truncate">{{ option.name }}</span>
<i-lucide:check
v-if="selected"
class="text-neutral-900 dark-theme:text-white"
/>
</div>
</template>
</Select>
</div>
</div>
</template>
</Select>
</template>
<script setup lang="ts">
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
import { computed } from 'vue'
defineOptions({
inheritAttrs: false
})
const { label, options } = defineProps<{
label?: string
options: {
/**
* Required for displaying the selected item's label.
* Cannot rely on $attrs alone because we need to access options
* in getLabel() to map values to their display names.
*/
options?: {
name: string
value: string
}[]
@@ -60,8 +75,14 @@ const { label, options } = defineProps<{
const selectedItem = defineModel<string | null>({ required: true })
/**
* Maps a value to its display label.
* Necessary because PrimeVue's value slot doesn't provide the selected item's label,
* only the raw value. We need this to show the correct text when an item is selected.
*/
const getLabel = (val: string | null | undefined) => {
if (val == null) return label ?? ''
if (!options) return label ?? ''
const found = options.find((o) => o.value === val)
return found ? found.name : label ?? ''
}
@@ -77,7 +98,7 @@ const pt = computed(() => ({
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
class: [
// container
'relative inline-flex w-full cursor-pointer select-none items-center',
'relative inline-flex cursor-pointer select-none items-center',
// trigger surface
'rounded-md',
'bg-transparent text-neutral dark-theme:text-white',
@@ -115,7 +136,9 @@ const pt = computed(() => ({
'flex items-center justify-between gap-3 px-3 py-2',
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
// Selected state + check icon
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected }
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected },
// Add focus state for keyboard navigation
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.focused }
]
}),
optionLabel: {

View File

@@ -41,11 +41,13 @@ afterAll(() => {
})
// Mock the useCurrentUser composable
const mockHandleSignOut = vi.fn()
vi.mock('@/composables/auth/useCurrentUser', () => ({
useCurrentUser: vi.fn(() => ({
userPhotoUrl: 'https://example.com/avatar.jpg',
userDisplayName: 'Test User',
userEmail: 'test@example.com'
userEmail: 'test@example.com',
handleSignOut: mockHandleSignOut
}))
}))
@@ -155,8 +157,8 @@ describe('CurrentUserPopover', () => {
// Click the logout button
await logoutButton.trigger('click')
// Verify logout was called
expect(mockLogout).toHaveBeenCalled()
// Verify handleSignOut was called
expect(mockHandleSignOut).toHaveBeenCalled()
// Verify close event was emitted
expect(wrapper.emitted('close')).toBeTruthy()

View File

@@ -88,7 +88,8 @@ const emit = defineEmits<{
close: []
}>()
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
useCurrentUser()
const authActions = useFirebaseAuthActions()
const dialogService = useDialogService()
@@ -103,7 +104,7 @@ const handleTopUp = () => {
}
const handleLogout = async () => {
await authActions.logout()
await handleSignOut()
emit('close')
}

View File

@@ -12,7 +12,7 @@
</template>
<template #header>
<SearchBox v-model:="searchQuery" class="max-w-[384px]" />
<SearchBox v-model="searchQuery" class="max-w-[384px]" />
</template>
<template #header-right-area>
@@ -59,12 +59,13 @@
<div class="relative px-6 pt-2 pb-4 flex gap-2">
<MultiSelect
v-model="selectedFrameworks"
v-model:search-query="searchText"
class="w-[250px]"
:has-search-box="true"
:show-selected-count="true"
:has-clear-button="true"
label="Select Frameworks"
:options="frameworkOptions"
:show-search-box="true"
:show-selected-count="true"
:show-clear-button="true"
/>
<MultiSelect
v-model="selectedProjects"
@@ -135,7 +136,7 @@
</template>
<script setup lang="ts">
import { provide, ref } from 'vue'
import { provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
@@ -201,9 +202,18 @@ const { onClose } = defineProps<{
provide(OnCloseKey, onClose)
const searchQuery = ref<string>('')
const searchText = ref<string>('')
const selectedFrameworks = ref([])
const selectedProjects = ref([])
const selectedSort = ref<string>('popular')
const selectedNavItem = ref<string | null>('installed')
watch(searchText, (newQuery) => {
console.log('searchText:', searchText.value, newQuery)
})
watch(searchQuery, (newQuery) => {
console.log('searchQuery:', searchQuery.value, newQuery)
})
</script>

View File

@@ -240,6 +240,9 @@ const createStoryTemplate = (args: StoryArgs) => ({
v-model="selectedFrameworks"
label="Select Frameworks"
:options="frameworkOptions"
:has-search-box="true"
:show-selected-count="true"
:has-clear-button="true"
/>
<MultiSelect
v-model="selectedProjects"

View File

@@ -1,5 +1,8 @@
import { computed } from 'vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -8,6 +11,8 @@ export const useCurrentUser = () => {
const authStore = useFirebaseAuthStore()
const commandStore = useCommandStore()
const apiKeyStore = useApiKeyAuthStore()
const dialogService = useDialogService()
const { deleteAccount } = useFirebaseAuthActions()
const firebaseUser = computed(() => authStore.currentUser)
const isApiKeyLogin = computed(() => apiKeyStore.isAuthenticated)
@@ -85,6 +90,18 @@ export const useCurrentUser = () => {
await commandStore.execute('Comfy.User.OpenSignInDialog')
}
const handleDeleteAccount = async () => {
const confirmed = await dialogService.confirm({
title: t('auth.deleteAccount.confirmTitle'),
message: t('auth.deleteAccount.confirmMessage'),
type: 'delete'
})
if (confirmed) {
await deleteAccount()
}
}
return {
loading: authStore.loading,
isLoggedIn,
@@ -96,6 +113,7 @@ export const useCurrentUser = () => {
providerName,
providerIcon,
handleSignOut,
handleSignIn
handleSignIn,
handleDeleteAccount
}
}

View File

@@ -135,6 +135,16 @@ export const useFirebaseAuthActions = () => {
reportError
)
const deleteAccount = wrapWithErrorHandlingAsync(async () => {
await authStore.deleteAccount()
toastStore.add({
severity: 'success',
summary: t('auth.deleteAccount.success'),
detail: t('auth.deleteAccount.successDetail'),
life: 5000
})
}, reportError)
return {
logout,
sendPasswordReset,
@@ -146,6 +156,7 @@ export const useFirebaseAuthActions = () => {
signInWithEmail,
signUpWithEmail,
updatePassword,
deleteAccount,
accessError
}
}

View File

@@ -1,4 +1,4 @@
import ModelSelector from '@/components/widget/ModelSelector.vue'
import SampleModelSelector from '@/components/widget/SampleModelSelector.vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
@@ -15,7 +15,7 @@ export const useModelSelectorDialog = () => {
function show() {
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: ModelSelector,
component: SampleModelSelector,
props: {
onClose: hide
}

View File

@@ -141,7 +141,8 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
*/
resolveInput(
slot: number,
visited = new Set<string>()
visited = new Set<string>(),
type?: ISlotType
): ResolvedInput | undefined {
const uniqueId = `${this.subgraphNode?.subgraph.id}:${this.node.id}[I]${slot}`
if (visited.has(uniqueId)) {
@@ -232,7 +233,11 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
`No output node DTO found for id [${outputNodeExecutionId}]`
)
return outputNodeDto.resolveOutput(link.origin_slot, input.type, visited)
return outputNodeDto.resolveOutput(
link.origin_slot,
type ?? input.type,
visited
)
}
/**
@@ -284,7 +289,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
// Upstreamed: Other virtual nodes are bypassed using the same input/output index (slots must match)
if (node.isVirtualNode) {
if (this.inputs.at(slot)) return this.resolveInput(slot, visited)
if (this.inputs.at(slot)) return this.resolveInput(slot, visited, type)
// Fallback check for nodes performing link redirection
const virtualLink = this.node.getInputLink(slot)

View File

@@ -27,6 +27,15 @@
"title": "مفتاح API",
"whitelistInfo": "حول المواقع غير المدرجة في القائمة البيضاء"
},
"deleteAccount": {
"cancel": "إلغاء",
"confirm": "حذف الحساب",
"confirmMessage": "هل أنت متأكد أنك تريد حذف حسابك؟ لا يمكن التراجع عن هذا الإجراء وسيتم حذف جميع بياناتك نهائيًا.",
"confirmTitle": "حذف الحساب",
"deleteAccount": "حذف الحساب",
"success": "تم حذف الحساب",
"successDetail": "تم حذف حسابك بنجاح."
},
"login": {
"andText": "و",
"confirmPasswordLabel": "تأكيد كلمة المرور",

View File

@@ -1601,6 +1601,15 @@
"passwordUpdate": {
"success": "Password Updated",
"successDetail": "Your password has been updated successfully"
},
"deleteAccount": {
"deleteAccount": "Delete Account",
"confirmTitle": "Delete Account",
"confirmMessage": "Are you sure you want to delete your account? This action cannot be undone and will permanently remove all your data.",
"confirm": "Delete Account",
"cancel": "Cancel",
"success": "Account Deleted",
"successDetail": "Your account has been successfully deleted."
}
},
"validation": {

View File

@@ -27,6 +27,15 @@
"title": "Clave API",
"whitelistInfo": "Acerca de los sitios no incluidos en la lista blanca"
},
"deleteAccount": {
"cancel": "Cancelar",
"confirm": "Eliminar cuenta",
"confirmMessage": "¿Estás seguro de que deseas eliminar tu cuenta? Esta acción no se puede deshacer y eliminará permanentemente todos tus datos.",
"confirmTitle": "Eliminar cuenta",
"deleteAccount": "Eliminar cuenta",
"success": "Cuenta eliminada",
"successDetail": "Tu cuenta ha sido eliminada exitosamente."
},
"login": {
"andText": "y",
"confirmPasswordLabel": "Confirmar contraseña",

View File

@@ -27,6 +27,15 @@
"title": "Clé API",
"whitelistInfo": "À propos des sites non autorisés"
},
"deleteAccount": {
"cancel": "Annuler",
"confirm": "Supprimer le compte",
"confirmMessage": "Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible et supprimera définitivement toutes vos données.",
"confirmTitle": "Supprimer le compte",
"deleteAccount": "Supprimer le compte",
"success": "Compte supprimé",
"successDetail": "Votre compte a été supprimé avec succès."
},
"login": {
"andText": "et",
"confirmPasswordLabel": "Confirmer le mot de passe",

View File

@@ -27,6 +27,15 @@
"title": "APIキー",
"whitelistInfo": "ホワイトリストに登録されていないサイトについて"
},
"deleteAccount": {
"cancel": "キャンセル",
"confirm": "アカウントを削除",
"confirmMessage": "本当にアカウントを削除しますか?この操作は元に戻せず、すべてのデータが完全に削除されます。",
"confirmTitle": "アカウントを削除",
"deleteAccount": "アカウントを削除",
"success": "アカウントが削除されました",
"successDetail": "アカウントは正常に削除されました。"
},
"login": {
"andText": "および",
"confirmPasswordLabel": "パスワードの確認",

View File

@@ -27,6 +27,15 @@
"title": "API 키",
"whitelistInfo": "비허용 사이트에 대하여"
},
"deleteAccount": {
"cancel": "취소",
"confirm": "계정 삭제",
"confirmMessage": "정말로 계정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며 모든 데이터가 영구적으로 삭제됩니다.",
"confirmTitle": "계정 삭제",
"deleteAccount": "계정 삭제",
"success": "계정이 삭제되었습니다",
"successDetail": "계정이 성공적으로 삭제되었습니다."
},
"login": {
"andText": "및",
"confirmPasswordLabel": "비밀번호 확인",

View File

@@ -27,6 +27,15 @@
"title": "API-ключ",
"whitelistInfo": "О не включённых в белый список сайтах"
},
"deleteAccount": {
"cancel": "Отмена",
"confirm": "Удалить аккаунт",
"confirmMessage": "Вы уверены, что хотите удалить свой аккаунт? Это действие необратимо и приведёт к безвозвратному удалению всех ваших данных.",
"confirmTitle": "Удалить аккаунт",
"deleteAccount": "Удалить аккаунт",
"success": "Аккаунт удалён",
"successDetail": "Ваш аккаунт был успешно удалён."
},
"login": {
"andText": "и",
"confirmPasswordLabel": "Подтвердите пароль",

View File

@@ -27,6 +27,15 @@
"title": "API 金鑰",
"whitelistInfo": "關於未列入白名單的網站"
},
"deleteAccount": {
"cancel": "取消",
"confirm": "刪除帳號",
"confirmMessage": "您確定要刪除您的帳號嗎?此操作無法復原,且將永久移除您所有的資料。",
"confirmTitle": "刪除帳號",
"deleteAccount": "刪除帳號",
"success": "帳號已刪除",
"successDetail": "您的帳號已成功刪除。"
},
"login": {
"andText": "以及",
"confirmPasswordLabel": "確認密碼",

View File

@@ -27,6 +27,15 @@
"title": "API 密钥",
"whitelistInfo": "关于非白名单网站"
},
"deleteAccount": {
"cancel": "取消",
"confirm": "删除账户",
"confirmMessage": "您确定要删除您的账户吗?此操作无法撤销,并且会永久删除您的所有数据。",
"confirmTitle": "删除账户",
"deleteAccount": "删除账户",
"success": "账户已删除",
"successDetail": "您的账户已成功删除。"
},
"login": {
"andText": "和",
"confirmPasswordLabel": "确认密码",

View File

@@ -8,6 +8,7 @@ import {
type UserCredential,
browserLocalPersistence,
createUserWithEmailAndPassword,
deleteUser,
onAuthStateChanged,
sendPasswordResetEmail,
setPersistence,
@@ -287,6 +288,14 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
await updatePassword(currentUser.value, newPassword)
}
/** Delete the current user account */
const _deleteAccount = async (): Promise<void> => {
if (!currentUser.value) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}
await deleteUser(currentUser.value)
}
const addCredits = async (
requestBodyContent: CreditPurchasePayload
): Promise<CreditPurchaseResponse> => {
@@ -385,6 +394,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
accessBillingPortal,
sendPasswordReset,
updatePassword: _updatePassword,
deleteAccount: _deleteAccount,
getAuthHeader
}
})

View File

@@ -26,6 +26,9 @@ export default defineConfig({
base: '',
server: {
host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined,
watch: {
ignored: ['**/coverage/**', '**/playwright-report/**']
},
proxy: {
'/internal': {
target: DEV_SERVER_COMFYUI_URL