Compare commits
71 Commits
bl-queue
...
copilot/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bb4c2524 | ||
|
|
c6b528b8be | ||
|
|
37d2f25d2f | ||
|
|
3dcabaeb0a | ||
|
|
eb85f99b10 | ||
|
|
2e99c394a0 | ||
|
|
9ef6f94f92 | ||
|
|
598d170d10 | ||
|
|
2c221bdd7b | ||
|
|
76b44d3de5 | ||
|
|
3fc8b49038 | ||
|
|
10748bdac9 | ||
|
|
bfe083dcba | ||
|
|
917f8ae27d | ||
|
|
8dee207b8c | ||
|
|
a0d9cc7e33 | ||
|
|
d1639c4377 | ||
|
|
9f046a11ea | ||
|
|
e1cd88afe3 | ||
|
|
38f188759d | ||
|
|
ad5be8ec70 | ||
|
|
653cf64e01 | ||
|
|
eae81cf2eb | ||
|
|
626fcff80b | ||
|
|
15b1b91b16 | ||
|
|
e48e11e434 | ||
|
|
09b1e1702c | ||
|
|
64430708ea | ||
|
|
ae70e458ba | ||
|
|
8396c9ae94 | ||
|
|
1234e1c56d | ||
|
|
254a03aba0 | ||
|
|
984ebef416 | ||
|
|
dd0ae9e2ba | ||
|
|
2527b2fe33 | ||
|
|
8cc5b52c64 | ||
|
|
03681a12bd | ||
|
|
9bfc9b740d | ||
|
|
7355a51282 | ||
|
|
7ca8615947 | ||
|
|
4dab27a84e | ||
|
|
97417736be | ||
|
|
86e6e7bf1f | ||
|
|
fb3ab88f04 | ||
|
|
476d6df1ca | ||
|
|
7caad10e93 | ||
|
|
6ea96f071e | ||
|
|
10af2300fa | ||
|
|
2058967761 | ||
|
|
d1af7c8256 | ||
|
|
5bc7c8a5c2 | ||
|
|
094d6e65a2 | ||
|
|
2808e0a437 | ||
|
|
95c2732de4 | ||
|
|
e59d2dd8df | ||
|
|
d54923f766 | ||
|
|
c30f528d11 | ||
|
|
0497421349 | ||
|
|
01b4ad0dbb | ||
|
|
31c85387ba | ||
|
|
8108aaa2d4 | ||
|
|
9c245e9c23 | ||
|
|
cb40da612b | ||
|
|
ddb3a0bfc6 | ||
|
|
5773df6ef7 | ||
|
|
bc281b2513 | ||
|
|
1d06b4d63b | ||
|
|
14c07fd734 | ||
|
|
7cc08e8e35 | ||
|
|
9c0b3c4f7d | ||
|
|
bb83b0107c |
@@ -458,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
|
||||
```bash
|
||||
sleep 10
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
```
|
||||
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
|
||||
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
|
||||
- Create a `core/x.yy` branch for the PREVIOUS minor version
|
||||
- Apply branch protection rules
|
||||
- Document the feature freeze policy
|
||||
```bash
|
||||
# Monitor branch creation (for minor/major releases)
|
||||
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
|
||||
gh run list --workflow=release-branch-create.yaml --limit=1
|
||||
```
|
||||
4. If workflow didn't trigger due to [skip ci]:
|
||||
```bash
|
||||
@@ -477,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
5. If workflow triggered, monitor execution:
|
||||
```bash
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ For each commit:
|
||||
3. Merge the PR: `gh pr merge --merge`
|
||||
4. Monitor release workflow:
|
||||
```bash
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
gh run watch
|
||||
```
|
||||
5. Track progress:
|
||||
|
||||
298
.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
# GitHub Workflows
|
||||
|
||||
This directory contains GitHub Actions workflow files that automate various aspects of the ComfyUI frontend development and release process.
|
||||
|
||||
> **Note:** This documentation is auto-generated from workflow files. Do not edit manually.
|
||||
> Run `pnpm workflow:docs` to regenerate.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Workflow files follow a consistent naming pattern: `<prefix>-<descriptive-name>.yaml`
|
||||
|
||||
### Category Prefixes
|
||||
|
||||
| Prefix | Purpose | Example |
|
||||
| ---------- | ----------------------------------- | ------------------------------------ |
|
||||
| `ci-` | Testing, linting, validation | `ci-json-validation.yaml` |
|
||||
| `pr-` | PR automation (triggered by labels) | `pr-backport.yaml` |
|
||||
| `release-` | Version management, publishing | `release-branch-create.yaml` |
|
||||
| `api-` | External API type generation | `api-update-electron-api-types.yaml` |
|
||||
| `i18n-` | Internationalization updates | `i18n-update-core.yaml` |
|
||||
| `publish-` | Publishing and deployment | `publish-desktop-ui-on-merge.yaml` |
|
||||
| `version-` | Version management | `version-bump-desktop-ui.yaml` |
|
||||
|
||||
|
||||
## Quick Reference
|
||||
|
||||
For label-triggered workflows, add the corresponding label to a PR to trigger the workflow:
|
||||
- `New Browser Test Expectations` - Updates Playwright test snapshots when triggered by label or comment
|
||||
- `Release` - Triggers 3 workflows
|
||||
- `claude-review` - AI-powered code review triggered by adding the 'claude-review' label to a PR
|
||||
- `needs-backport` - Automatically backports merged PRs to release branches when 'needs-backport' label is applied
|
||||
|
||||
For manual workflows, use the "Run workflow" button in the Actions tab.
|
||||
|
||||
|
||||
## Workflow Details
|
||||
|
||||
|
||||
### CI
|
||||
|
||||
#### [`ci-json-validation.yaml`](./ci-json-validation.yaml)
|
||||
|
||||
**Name:** CI: JSON Validation
|
||||
|
||||
**Description:** Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq
|
||||
|
||||
**Triggers:** push
|
||||
|
||||
#### [`ci-lint-format.yaml`](./ci-lint-format.yaml)
|
||||
|
||||
**Name:** CI: Lint Format
|
||||
|
||||
**Description:** Linting and code formatting validation for pull requests
|
||||
|
||||
**Triggers:** pull_request
|
||||
|
||||
#### [`ci-python-validation.yaml`](./ci-python-validation.yaml)
|
||||
|
||||
**Name:** CI: Python Validation
|
||||
|
||||
**Description:** Validates Python code in tools/devtools directory
|
||||
|
||||
**Triggers:** pull_request, push
|
||||
|
||||
#### [`ci-tests-e2e-forks.yaml`](./ci-tests-e2e-forks.yaml)
|
||||
|
||||
**Name:** CI: Tests E2E (Deploy for Forks)
|
||||
|
||||
**Description:** Deploys test results from forked PRs (forks can't access deployment secrets)
|
||||
|
||||
#### [`ci-tests-e2e.yaml`](./ci-tests-e2e.yaml)
|
||||
|
||||
**Name:** CI: Tests E2E
|
||||
|
||||
**Description:** End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
|
||||
|
||||
**Triggers:** pull_request, push
|
||||
|
||||
#### [`ci-tests-storybook-forks.yaml`](./ci-tests-storybook-forks.yaml)
|
||||
|
||||
**Name:** CI: Tests Storybook (Deploy for Forks)
|
||||
|
||||
**Description:** Deploys Storybook previews from forked PRs (forks can't access deployment secrets)
|
||||
|
||||
#### [`ci-tests-storybook.yaml`](./ci-tests-storybook.yaml)
|
||||
|
||||
**Name:** CI: Tests Storybook
|
||||
|
||||
**Description:** Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
|
||||
|
||||
**Triggers:** workflow_dispatch (manual), pull_request
|
||||
|
||||
#### [`ci-tests-unit.yaml`](./ci-tests-unit.yaml)
|
||||
|
||||
**Name:** CI: Tests Unit
|
||||
|
||||
**Description:** Unit and component testing with Vitest
|
||||
|
||||
**Triggers:** pull_request, push
|
||||
|
||||
#### [`ci-workflow-docs.yaml`](./ci-workflow-docs.yaml)
|
||||
|
||||
**Name:** CI: Workflow Documentation
|
||||
|
||||
**Description:** Validates that workflow documentation is up-to-date with workflow files
|
||||
|
||||
**Triggers:** pull_request
|
||||
|
||||
|
||||
### PR
|
||||
|
||||
#### [`pr-backport.yaml`](./pr-backport.yaml)
|
||||
|
||||
**Name:** PR Backport
|
||||
|
||||
**Description:** Automatically backports merged PRs to release branches when 'needs-backport' label is applied
|
||||
|
||||
**Triggers:** workflow_dispatch (manual), pull_request_target (closed, labeled)
|
||||
|
||||
**Label Triggers:** `needs-backport`
|
||||
|
||||
#### [`pr-claude-review.yaml`](./pr-claude-review.yaml)
|
||||
|
||||
**Name:** PR: Claude Review
|
||||
|
||||
**Description:** AI-powered code review triggered by adding the 'claude-review' label to a PR
|
||||
|
||||
**Triggers:** pull_request (labeled)
|
||||
|
||||
**Label Triggers:** `claude-review`
|
||||
|
||||
#### [`pr-update-playwright-expectations.yaml`](./pr-update-playwright-expectations.yaml)
|
||||
|
||||
**Name:** PR: Update Playwright Expectations
|
||||
|
||||
**Description:** Updates Playwright test snapshots when triggered by label or comment
|
||||
|
||||
**Triggers:** pull_request (labeled), issue_comment (created)
|
||||
|
||||
**Label Triggers:** `New Browser Test Expectations`, `/update-playwright`
|
||||
|
||||
|
||||
### RELEASE
|
||||
|
||||
#### [`release-branch-create.yaml`](./release-branch-create.yaml)
|
||||
|
||||
**Name:** Release Branch Create
|
||||
|
||||
**Description:** Creates release branch when version bump PR with 'Release' label is merged
|
||||
|
||||
**Triggers:** pull_request (closed)
|
||||
|
||||
**Label Triggers:** `Release`
|
||||
|
||||
#### [`release-draft-create.yaml`](./release-draft-create.yaml)
|
||||
|
||||
**Name:** Release Draft Create
|
||||
|
||||
**Description:** Creates GitHub release draft when version bump PR with 'Release' label is merged
|
||||
|
||||
**Triggers:** pull_request (closed)
|
||||
|
||||
**Label Triggers:** `Release`
|
||||
|
||||
#### [`release-npm-types.yaml`](./release-npm-types.yaml)
|
||||
|
||||
**Name:** Release NPM Types
|
||||
|
||||
**Description:** Manual workflow to publish TypeScript type definitions to npm
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
#### [`release-pypi-dev.yaml`](./release-pypi-dev.yaml)
|
||||
|
||||
**Name:** Release PyPI Dev
|
||||
|
||||
**Description:** Manual workflow to publish development version to PyPI
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
#### [`release-version-bump.yaml`](./release-version-bump.yaml)
|
||||
|
||||
**Name:** Release: Version Bump
|
||||
|
||||
**Description:** Manual workflow to increment package version with semantic versioning support
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
|
||||
### API
|
||||
|
||||
#### [`api-update-electron-api-types.yaml`](./api-update-electron-api-types.yaml)
|
||||
|
||||
**Name:** Api: Update Electron API Types
|
||||
|
||||
**Description:** When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
#### [`api-update-manager-api-types.yaml`](./api-update-manager-api-types.yaml)
|
||||
|
||||
**Name:** Api: Update Manager API Types
|
||||
|
||||
**Description:** When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
#### [`api-update-registry-api-types.yaml`](./api-update-registry-api-types.yaml)
|
||||
|
||||
**Name:** Api: Update Registry API Types
|
||||
|
||||
**Description:** When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
|
||||
### I18N
|
||||
|
||||
#### [`i18n-update-core.yaml`](./i18n-update-core.yaml)
|
||||
|
||||
**Name:** i18n: Update Core
|
||||
|
||||
**Description:** Generates and updates translations for core ComfyUI components using OpenAI
|
||||
|
||||
**Triggers:** workflow_dispatch (manual), pull_request (opened, synchronize, reopened)
|
||||
|
||||
#### [`i18n-update-custom-nodes.yaml`](./i18n-update-custom-nodes.yaml)
|
||||
|
||||
**Name:** i18n Update Custom Nodes
|
||||
|
||||
**Description:** Updates translations for custom node repositories using OpenAI
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
#### [`i18n-update-nodes.yaml`](./i18n-update-nodes.yaml)
|
||||
|
||||
**Name:** i18n Update Nodes
|
||||
|
||||
**Description:** Updates translations for ComfyUI node definitions
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
|
||||
### PUBLISH
|
||||
|
||||
#### [`publish-desktop-ui-on-merge.yaml`](./publish-desktop-ui-on-merge.yaml)
|
||||
|
||||
**Name:** Publish Desktop UI on PR Merge
|
||||
|
||||
**Description:** Automatically publishes desktop UI package to npm when version bump PR is merged
|
||||
|
||||
**Triggers:** pull_request (closed)
|
||||
|
||||
**Label Triggers:** `Release`
|
||||
|
||||
#### [`publish-desktop-ui.yaml`](./publish-desktop-ui.yaml)
|
||||
|
||||
**Name:** Publish Desktop UI
|
||||
|
||||
**Description:** Manual workflow to publish desktop UI package to npm with specified version
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
|
||||
### VERSION
|
||||
|
||||
#### [`version-bump-desktop-ui.yaml`](./version-bump-desktop-ui.yaml)
|
||||
|
||||
**Name:** Version Bump Desktop UI
|
||||
|
||||
**Description:** Manual workflow to increment desktop UI package version with semantic versioning support
|
||||
|
||||
**Triggers:** workflow_dispatch (manual)
|
||||
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
For more information about GitHub Actions, see:
|
||||
- [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)
|
||||
- [Workflow syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions)
|
||||
|
||||
## Maintaining Workflows
|
||||
|
||||
### Adding a New Workflow
|
||||
|
||||
1. Create a new workflow file following the naming convention
|
||||
2. Include `name` and `description` fields at the top of the workflow
|
||||
3. Run `pnpm workflow:docs` to update this README
|
||||
4. Commit both the workflow file and updated README
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always include a description**: Add a `description` field after the `name` field
|
||||
2. **Use consistent prefixes**: Follow the established prefix conventions
|
||||
3. **Label-triggered workflows**: For PR automation, use the `pr-` prefix
|
||||
4. **Document triggers**: Make trigger conditions clear in the workflow description
|
||||
5. **Keep docs in sync**: Run `pnpm workflow:docs` after any workflow changes
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Electron Types
|
||||
name: 'Api: Update Electron API Types'
|
||||
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update ComfyUI-Manager API Types
|
||||
name: 'Api: Update Manager API Types'
|
||||
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Comfy Registry API Types
|
||||
name: 'Api: Update Registry API Types'
|
||||
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Validate JSON
|
||||
name: "CI: JSON Validation"
|
||||
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Lint and Format
|
||||
name: "CI: Lint Format"
|
||||
description: "Linting and code formatting validation for pull requests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Devtools Python Check
|
||||
name: "CI: Python Validation"
|
||||
description: "Validates Python code in tools/devtools directory"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,8 +1,9 @@
|
||||
name: PR Playwright Deploy (Forks)
|
||||
name: "CI: Tests E2E (Deploy for Forks)"
|
||||
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tests CI"]
|
||||
workflows: ["CI: Tests E2E"]
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Tests CI
|
||||
name: "CI: Tests E2E"
|
||||
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -1,8 +1,9 @@
|
||||
name: PR Storybook Deploy (Forks)
|
||||
name: "CI: Tests Storybook (Deploy for Forks)"
|
||||
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Storybook and Chromatic CI']
|
||||
workflows: ["CI: Tests Storybook"]
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -1,6 +1,5 @@
|
||||
name: Storybook and Chromatic CI
|
||||
|
||||
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
|
||||
name: "CI: Tests Storybook"
|
||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Vitest Tests
|
||||
name: "CI: Tests Unit"
|
||||
description: "Unit and component testing with Vitest"
|
||||
|
||||
on:
|
||||
push:
|
||||
43
.github/workflows/ci-workflow-docs.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: "CI: Workflow Documentation"
|
||||
description: "Validates that workflow documentation is up-to-date with workflow files"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/*.yaml'
|
||||
- '.github/workflows/*.yml'
|
||||
- 'scripts/cicd/generate-workflow-docs.ts'
|
||||
|
||||
jobs:
|
||||
check-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate workflow documentation
|
||||
run: pnpm workflow:docs
|
||||
|
||||
- name: Check if documentation is up-to-date
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain .github/workflows/README.md)" ]; then
|
||||
echo "::error::Workflow documentation is out of date. Please run 'pnpm workflow:docs' and commit the changes."
|
||||
git diff .github/workflows/README.md
|
||||
exit 1
|
||||
else
|
||||
echo "✓ Workflow documentation is up-to-date"
|
||||
fi
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Locales
|
||||
name: "i18n: Update Core"
|
||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
||||
|
||||
on:
|
||||
# Manual dispatch for urgent translation updates
|
||||
@@ -18,14 +19,14 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Setup playwright environment
|
||||
- name: Setup ComfyUI Server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup ComfyUI Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup ComfyUI Server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
@@ -54,5 +55,5 @@ jobs:
|
||||
# Apply the stashed changes if any
|
||||
git stash pop || true
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
|
||||
git diff --staged --quiet || git commit -m "Update locales"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Locales for given custom node repository
|
||||
name: i18n Update Custom Nodes
|
||||
description: "Updates translations for custom node repositories using OpenAI"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Node Definitions Locales
|
||||
name: i18n Update Nodes
|
||||
description: "Updates translations for ComfyUI node definitions"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Auto Backport
|
||||
name: PR Backport
|
||||
description: "Automatically backports merged PRs to release branches when 'needs-backport' label is applied"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -95,41 +96,61 @@ jobs:
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Extract version labels
|
||||
- name: Collect backport targets
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: versions
|
||||
id: targets
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
|
||||
TARGETS=()
|
||||
declare -A SEEN=()
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# For manual triggers, get labels from the PR
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
# For automatic triggers, extract from PR event
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
for label in $LABELS; do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
|
||||
VERSIONS="${VERSIONS}${label} "
|
||||
else
|
||||
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VERSIONS" ]; then
|
||||
echo "::error::No version labels found (e.g., 1.24, 1.22)"
|
||||
add_target() {
|
||||
local label="$1"
|
||||
local target="$2"
|
||||
|
||||
if [ -z "$target" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
target=$(echo "$target" | xargs)
|
||||
|
||||
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
|
||||
TARGETS+=("$target")
|
||||
SEEN["$target"]=1
|
||||
else
|
||||
echo "::warning::Label '${label}' references missing branch '${target}'"
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r label; do
|
||||
[ -z "$label" ] && continue
|
||||
|
||||
if [[ "$label" =~ ^branch:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
add_target "$label" "core/${label}"
|
||||
fi
|
||||
done <<< "$LABELS"
|
||||
|
||||
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
||||
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
|
||||
echo "Found backport targets: ${TARGETS[*]}"
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
@@ -150,16 +171,17 @@ jobs:
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
for target in ${{ steps.targets.outputs.targets }}; do
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
|
||||
TARGET_BRANCH="core/${version}"
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
|
||||
echo "::group::Backporting to ${TARGET_BRANCH}"
|
||||
|
||||
# Fetch target branch (fail if doesn't exist)
|
||||
if ! git fetch origin "${TARGET_BRANCH}"; then
|
||||
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
|
||||
FAILED="${FAILED}${version}:branch-missing "
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
@@ -170,7 +192,7 @@ jobs:
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
git checkout main
|
||||
@@ -180,7 +202,7 @@ jobs:
|
||||
git cherry-pick --abort
|
||||
|
||||
echo "::error::Cherry-pick failed due to conflicts"
|
||||
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
|
||||
|
||||
# Clean up the failed branch
|
||||
git checkout main
|
||||
@@ -214,13 +236,13 @@ jobs:
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
IFS=':' read -r target branch <<< "${backport}"
|
||||
|
||||
if PR_URL=$(gh pr create \
|
||||
--base "core/${version}" \
|
||||
--base "${target}" \
|
||||
--head "${branch}" \
|
||||
--title "[backport ${version}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--title "[backport ${target}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--label "backport" 2>&1); then
|
||||
|
||||
# Extract PR number from URL
|
||||
@@ -230,9 +252,9 @@ jobs:
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
|
||||
fi
|
||||
else
|
||||
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
|
||||
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
|
||||
# Still try to comment on the original PR about the failure
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -253,16 +275,16 @@ jobs:
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
IFS=':' read -r target reason conflicts <<< "${failure}"
|
||||
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
# Convert comma-separated conflicts back to newlines for display
|
||||
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
||||
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
fi
|
||||
done
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Claude PR Review
|
||||
name: "PR: Claude Review"
|
||||
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1,5 +1,6 @@
|
||||
# Setting test expectation screenshots for Playwright
|
||||
name: Update Playwright Expectations
|
||||
name: "PR: Update Playwright Expectations"
|
||||
description: "Updates Playwright test snapshots when triggered by label or comment"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Publish Desktop UI on PR Merge
|
||||
description: "Automatically publishes desktop UI package to npm when version bump PR is merged"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
2
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Publish Desktop UI
|
||||
description: "Manual workflow to publish desktop UI package to npm with specified version"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -44,6 +45,7 @@ jobs:
|
||||
contents: read
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ENABLE_MINIFY: 'true'
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
env:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Create Release Branch
|
||||
name: Release Branch Create
|
||||
description: "Creates release branch when version bump PR with 'Release' label is merged"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Create Release Draft
|
||||
name: Release Draft Create
|
||||
description: "Creates GitHub release draft when version bump PR with 'Release' label is merged"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -55,6 +56,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -126,7 +128,7 @@ jobs:
|
||||
|
||||
publish_types:
|
||||
needs: build
|
||||
uses: ./.github/workflows/publish-frontend-types.yaml
|
||||
uses: ./.github/workflows/release-npm-types.yaml
|
||||
with:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Publish Frontend Types
|
||||
name: Release NPM Types
|
||||
description: "Manual workflow to publish TypeScript type definitions to npm"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Create Dev PyPI Package
|
||||
name: Release PyPI Dev
|
||||
description: "Manual workflow to publish development version to PyPI"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -44,6 +45,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Version Bump
|
||||
name: "Release: Version Bump"
|
||||
description: "Manual workflow to increment package version with semantic versioning support"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,6 +15,11 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
@@ -25,6 +31,24 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -58,7 +82,9 @@ jobs:
|
||||
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
52
.github/workflows/size-data.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: size data
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
collect:
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Collect size data
|
||||
run: node scripts/size-collect.js
|
||||
|
||||
- name: Save PR number & base branch
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
echo ${{ github.event.number }} > ./temp/size/number.txt
|
||||
echo ${{ github.base_ref }} > ./temp/size/base.txt
|
||||
|
||||
- name: Upload size data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: size-data
|
||||
path: temp/size
|
||||
104
.github/workflows/size-report.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: size report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['size data']
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to report on'
|
||||
required: true
|
||||
type: number
|
||||
run_id:
|
||||
description: 'Size data workflow run ID'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size-report:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
(
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
)
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
name: size-data
|
||||
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
|
||||
path: temp/size
|
||||
|
||||
- name: Set PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set base branch
|
||||
id: pr-base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=main" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download previous size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: size-data.yml
|
||||
event: push
|
||||
name: size-data
|
||||
path: temp/size-prev
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Generate size report
|
||||
run: node scripts/size-report.js > size-report.md
|
||||
|
||||
- name: Read size report
|
||||
id: size-report
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./size-report.md
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.pr-number.outputs.content }}
|
||||
body: |
|
||||
${{ steps.size-report.outputs.content }}
|
||||
<!-- COMFYUI_FRONTEND_SIZE -->
|
||||
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'
|
||||
27
.github/workflows/version-bump-desktop-ui.yaml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Version Bump Desktop UI
|
||||
description: "Manual workflow to increment desktop UI package version with semantic versioning support"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,6 +15,11 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-desktop-ui:
|
||||
@@ -26,8 +32,25 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -64,8 +87,10 @@ jobs:
|
||||
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div max-w-48 min-h-52 grid relative"
|
||||
class="task-div relative grid min-h-52 max-w-48"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
>
|
||||
<Card
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
class="relative h-full max-w-48 overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="object-contain w-full h-full opacity-25 pt-4 px-4"
|
||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -27,7 +27,7 @@
|
||||
{{ description }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<div class="mt-1 flex gap-4">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
@@ -73,7 +73,7 @@ defineEmits<{
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
runner.value.state === 'error'
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
? (props.task.errorDescription ?? props.task.shortDescription)
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import { test as base, expect } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
@@ -47,6 +46,10 @@ class ComfyMenu {
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
return this.sideToolbar.locator('.side-bar-button')
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
|
||||
return this._nodeLibraryTab
|
||||
@@ -130,7 +133,8 @@ export class ComfyPage {
|
||||
|
||||
// Buttons
|
||||
public readonly resetViewButton: Locator
|
||||
public readonly queueButton: Locator
|
||||
public readonly queueButton: Locator // Run button in Legacy UI
|
||||
public readonly runButton: Locator // Run button (renamed "Queue" -> "Run")
|
||||
|
||||
// Inputs
|
||||
public readonly workflowUploadInput: Locator
|
||||
@@ -165,6 +169,9 @@ export class ComfyPage {
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.runButton = page
|
||||
.getByTestId('queue-button')
|
||||
.getByRole('button', { name: 'Run' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.visibleToasts = page.locator('.p-toast-message:visible')
|
||||
|
||||
@@ -1086,12 +1093,6 @@ export class ComfyPage {
|
||||
|
||||
const targetPosition = await targetSlot.getPosition()
|
||||
|
||||
// Debug: Log the positions we're trying to use
|
||||
console.log('Drag positions:', {
|
||||
source: sourcePosition,
|
||||
target: targetPosition
|
||||
})
|
||||
|
||||
await this.dragAndDrop(sourcePosition, targetPosition)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*/
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { VueNodeFixture } from './utils/vueNodeFixtures'
|
||||
|
||||
export class VueNodeHelpers {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
@@ -106,6 +108,24 @@ export class VueNodeHelpers {
|
||||
await this.page.keyboard.press('Backspace')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a DOM-focused VueNodeFixture for the first node matching the title.
|
||||
* Resolves the node id up front so subsequent interactions survive title changes.
|
||||
*/
|
||||
async getFixtureByTitle(title: string): Promise<VueNodeFixture> {
|
||||
const node = this.getNodeByTitle(title).first()
|
||||
await node.waitFor({ state: 'visible' })
|
||||
|
||||
const nodeId = await node.evaluate((el) => el.getAttribute('data-node-id'))
|
||||
if (!nodeId) {
|
||||
throw new Error(
|
||||
`Vue node titled "${title}" is missing its data-node-id attribute`
|
||||
)
|
||||
}
|
||||
|
||||
return new VueNodeFixture(this.getNodeLocator(nodeId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Vue nodes to be rendered
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ export class Topbar {
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.menuLocator = page.locator('.comfy-command-menu')
|
||||
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
|
||||
this.menuTrigger = page.locator('.comfy-menu-button-wrapper')
|
||||
}
|
||||
|
||||
async getTabNames(): Promise<string[]> {
|
||||
@@ -105,7 +105,7 @@ export class Topbar {
|
||||
* Close the topbar menu by clicking outside
|
||||
*/
|
||||
async closeTopbarMenu() {
|
||||
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
|
||||
await expect(this.menuLocator).not.toBeVisible()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,131 +1,66 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
|
||||
import type { NodeReference } from './litegraphUtils'
|
||||
|
||||
/**
|
||||
* VueNodeFixture provides Vue-specific testing utilities for interacting with
|
||||
* Vue node components. It bridges the gap between litegraph node references
|
||||
* and Vue UI components.
|
||||
*/
|
||||
/** DOM-centric helper for a single Vue-rendered node on the canvas. */
|
||||
export class VueNodeFixture {
|
||||
constructor(
|
||||
private readonly nodeRef: NodeReference,
|
||||
private readonly page: Page
|
||||
) {}
|
||||
constructor(private readonly locator: Locator) {}
|
||||
|
||||
/**
|
||||
* Get the node's header element using data-testid
|
||||
*/
|
||||
async getHeader(): Promise<Locator> {
|
||||
const nodeId = this.nodeRef.id
|
||||
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
|
||||
get header(): Locator {
|
||||
return this.locator.locator('[data-testid^="node-header-"]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node's title element
|
||||
*/
|
||||
async getTitleElement(): Promise<Locator> {
|
||||
const header = await this.getHeader()
|
||||
return header.locator('[data-testid="node-title"]')
|
||||
get title(): Locator {
|
||||
return this.locator.locator('[data-testid="node-title"]')
|
||||
}
|
||||
|
||||
get titleInput(): Locator {
|
||||
return this.locator.locator('[data-testid="node-title-input"]')
|
||||
}
|
||||
|
||||
get body(): Locator {
|
||||
return this.locator.locator('[data-testid^="node-body-"]')
|
||||
}
|
||||
|
||||
get collapseButton(): Locator {
|
||||
return this.locator.locator('[data-testid="node-collapse-button"]')
|
||||
}
|
||||
|
||||
get collapseIcon(): Locator {
|
||||
return this.collapseButton.locator('i')
|
||||
}
|
||||
|
||||
get root(): Locator {
|
||||
return this.locator
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current title text
|
||||
*/
|
||||
async getTitle(): Promise<string> {
|
||||
const titleElement = await this.getTitleElement()
|
||||
return (await titleElement.textContent()) || ''
|
||||
return (await this.title.textContent()) ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new title by double-clicking and entering text
|
||||
*/
|
||||
async setTitle(newTitle: string): Promise<void> {
|
||||
const titleElement = await this.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
|
||||
const input = (await this.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill(newTitle)
|
||||
async setTitle(value: string): Promise<void> {
|
||||
await this.header.dblclick()
|
||||
const input = this.titleInput
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill(value)
|
||||
await input.press('Enter')
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel title editing
|
||||
*/
|
||||
async cancelTitleEdit(): Promise<void> {
|
||||
const titleElement = await this.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
|
||||
const input = (await this.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await this.header.dblclick()
|
||||
const input = this.titleInput
|
||||
await expect(input).toBeVisible()
|
||||
await input.press('Escape')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the title is currently being edited
|
||||
*/
|
||||
async isEditingTitle(): Promise<boolean> {
|
||||
const header = await this.getHeader()
|
||||
const input = header.locator('[data-testid="node-title-input"]')
|
||||
return await input.isVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collapse/expand button
|
||||
*/
|
||||
async getCollapseButton(): Promise<Locator> {
|
||||
const header = await this.getHeader()
|
||||
return header.locator('[data-testid="node-collapse-button"]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the node's collapsed state
|
||||
*/
|
||||
async toggleCollapse(): Promise<void> {
|
||||
const button = await this.getCollapseButton()
|
||||
await button.click()
|
||||
await this.collapseButton.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collapse icon element
|
||||
*/
|
||||
async getCollapseIcon(): Promise<Locator> {
|
||||
const button = await this.getCollapseButton()
|
||||
return button.locator('i')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collapse icon's CSS classes
|
||||
*/
|
||||
async getCollapseIconClass(): Promise<string> {
|
||||
const icon = await this.getCollapseIcon()
|
||||
return (await icon.getAttribute('class')) || ''
|
||||
return (await this.collapseIcon.getAttribute('class')) ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the collapse button is visible
|
||||
*/
|
||||
async isCollapseButtonVisible(): Promise<boolean> {
|
||||
const button = await this.getCollapseButton()
|
||||
return await button.isVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node's body/content element
|
||||
*/
|
||||
async getBody(): Promise<Locator> {
|
||||
const nodeId = this.nodeRef.id
|
||||
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node body is visible (not collapsed)
|
||||
*/
|
||||
async isBodyVisible(): Promise<boolean> {
|
||||
const body = await this.getBody()
|
||||
return await body.isVisible()
|
||||
boundingBox(): ReturnType<Locator['boundingBox']> {
|
||||
return this.locator.boundingBox()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,9 +116,10 @@ test.describe('Actionbar', () => {
|
||||
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
|
||||
await comfyPage.page.dragAndDrop(
|
||||
'.actionbar .drag-handle',
|
||||
'.comfyui-menu',
|
||||
'.actionbar-container',
|
||||
{
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
targetPosition: { x: 50, y: 20 },
|
||||
force: true
|
||||
}
|
||||
)
|
||||
expect(await comfyPage.actionbar.isDocked()).toBe(true)
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 82 KiB |
@@ -39,15 +39,15 @@ test.describe('Graph Canvas Menu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('Focus mode button is clickable and has correct test id', async ({
|
||||
test('Toggle minimap button is clickable and has correct test id', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
|
||||
await expect(focusButton).toBeVisible()
|
||||
await expect(focusButton).toBeEnabled()
|
||||
const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
await expect(minimapButton).toBeVisible()
|
||||
await expect(minimapButton).toBeEnabled()
|
||||
|
||||
// Test that the button can be clicked without error
|
||||
await focusButton.click()
|
||||
await minimapButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
@@ -233,6 +233,7 @@ test.describe('Group Node', () => {
|
||||
}
|
||||
|
||||
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
|
||||
.getFolder(GROUP_NODE_CATEGORY)
|
||||
.count()
|
||||
@@ -253,8 +254,6 @@ test.describe('Group Node', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.loadWorkflow(WORKFLOW_NAME)
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
|
||||
groupNode = await comfyPage.getFirstNodeRef()
|
||||
if (!groupNode)
|
||||
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
|
||||
|
||||
@@ -3,10 +3,10 @@ import { expect } from '@playwright/test'
|
||||
import type { Position } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -786,24 +786,25 @@ test.describe('Viewport settings', () => {
|
||||
// Screenshot the canvas element
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
|
||||
// Open zoom controls dropdown first
|
||||
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||
'zoom-controls-button'
|
||||
)
|
||||
await zoomControlsButton.click()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
await toggleButton.click()
|
||||
// close zoom menu
|
||||
await zoomControlsButton.click()
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
||||
await comfyPage.nextFrame()
|
||||
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
// Save workflow as a new file, then zoom out before screen shot
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
await changeTab(tabA)
|
||||
|
||||
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
await changeTab(tabB)
|
||||
|
||||
await comfyMouse.move(comfyPage.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
@@ -815,9 +816,6 @@ test.describe('Viewport settings', () => {
|
||||
// Ensure that the screenshots are different due to zoom level
|
||||
expect(screenshotB).not.toBe(screenshotA)
|
||||
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
|
||||
// Go back to Workflow A
|
||||
await changeTab(tabA)
|
||||
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
|
||||
|
||||
@@ -8,9 +8,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Can register sidebar tab', async ({ comfyPage }) => {
|
||||
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
const initialChildrenCount = await comfyPage.menu.buttons.count()
|
||||
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
window['app'].extensionManager.registerSidebarTab({
|
||||
@@ -26,9 +24,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
const newChildrenCount = await comfyPage.menu.buttons.count()
|
||||
expect(newChildrenCount).toBe(initialChildrenCount + 1)
|
||||
})
|
||||
|
||||
|
||||
@@ -35,12 +35,6 @@ test.describe('Minimap', () => {
|
||||
})
|
||||
|
||||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||
// Open zoom controls dropdown first
|
||||
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||
'zoom-controls-button'
|
||||
)
|
||||
await zoomControlsButton.click()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
|
||||
await expect(toggleButton).toBeVisible()
|
||||
@@ -51,13 +45,6 @@ test.describe('Minimap', () => {
|
||||
|
||||
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
|
||||
// Open zoom controls dropdown first
|
||||
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||
'zoom-controls-button'
|
||||
)
|
||||
await zoomControlsButton.click()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
@@ -67,22 +54,10 @@ test.describe('Minimap', () => {
|
||||
|
||||
await expect(minimapContainer).not.toBeVisible()
|
||||
|
||||
// Open zoom controls dropdown again
|
||||
await zoomControlsButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(toggleButton).toContainText('Show Minimap')
|
||||
|
||||
await toggleButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
// Open zoom controls dropdown again to verify button text
|
||||
await zoomControlsButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(toggleButton).toContainText('Hide Minimap')
|
||||
})
|
||||
|
||||
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||||
|
||||
28
browser_tests/tests/recordAudio.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Record Audio Node', () => {
|
||||
test('should add a record audio node and take a screenshot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open the search box by double clicking on the canvas
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
|
||||
// Search for and add the RecordAudio node
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the RecordAudio node was added
|
||||
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
|
||||
expect(recordAudioNodes.length).toBe(1)
|
||||
|
||||
// Take a screenshot of the canvas with the RecordAudio node
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 28 KiB |
@@ -60,7 +60,6 @@ async function getInputLinkDetails(
|
||||
)
|
||||
}
|
||||
|
||||
// Test helpers to reduce repetition across cases
|
||||
function slotLocator(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
@@ -789,6 +788,45 @@ test.describe('Vue Node Link Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('should batch disconnect all links with ctrl+alt+click on slot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 1 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 2 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
const clipOutput = await clipNode.getOutput(0)
|
||||
expect(await clipOutput.getLinkCount()).toBe(2)
|
||||
|
||||
const clipOutputSlot = slotLocator(comfyPage.page, clipNode.id, 0, false)
|
||||
|
||||
await clipOutputSlot.dispatchEvent('pointerdown', {
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
shiftKey: false,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await clipOutput.getLinkCount()).toBe(0)
|
||||
})
|
||||
|
||||
test.describe('Release actions (Shift-drop)', () => {
|
||||
test('Context menu opens and endpoint is pinned on Shift-drop', async ({
|
||||
comfyPage,
|
||||
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 21 KiB |
@@ -2,70 +2,46 @@ import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('Vue Nodes Renaming', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should display node title', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(nodes.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const title = await vueNode.getTitle()
|
||||
expect(title).toBe('KSampler')
|
||||
|
||||
// Verify title is visible in the header
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('KSampler')
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await expect(vueNode.header).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('should allow title renaming by double clicking on the node header', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
// Test renaming with Enter
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
const newTitle = await vueNode.getTitle()
|
||||
expect(newTitle).toBe('My Custom Sampler')
|
||||
|
||||
// Verify the title is displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('My Custom Sampler')
|
||||
await expect(await vueNode.getTitle()).toBe('My Custom Sampler')
|
||||
await expect(vueNode.header).toContainText('My Custom Sampler')
|
||||
|
||||
// Test cancel with Escape
|
||||
const titleElement = await vueNode.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
await vueNode.title.dblclick()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Type a different value but cancel
|
||||
const input = (await vueNode.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill('This Should Be Cancelled')
|
||||
await input.press('Escape')
|
||||
await vueNode.titleInput.fill('This Should Be Cancelled')
|
||||
await vueNode.titleInput.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Title should remain as the previously saved value
|
||||
const titleAfterCancel = await vueNode.getTitle()
|
||||
expect(titleAfterCancel).toBe('My Custom Sampler')
|
||||
await expect(await vueNode.getTitle()).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const loadCheckpointNode = comfyPage.vueNodes
|
||||
.getNodeByTitle('Load Checkpoint')
|
||||
.first()
|
||||
const nodeBbox = await loadCheckpointNode.boundingBox()
|
||||
if (!nodeBbox) throw new Error('Node not found')
|
||||
await loadCheckpointNode.dblclick()
|
||||
|
||||
@@ -50,15 +50,23 @@ test.describe('Vue Node Selection', () => {
|
||||
})
|
||||
}
|
||||
|
||||
test('should select all nodes with ctrl+a', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(initialCount).toBeGreaterThan(0)
|
||||
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(initialCount)
|
||||
})
|
||||
|
||||
test('should select pinned node without dragging', async ({ comfyPage }) => {
|
||||
const PIN_HOTKEY = 'p'
|
||||
const PIN_INDICATOR = '[data-testid="node-pin-indicator"]'
|
||||
|
||||
// Select a node by clicking its title
|
||||
const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint')
|
||||
await checkpointNodeHeader.click()
|
||||
|
||||
// Pin it using the hotkey (as a user would)
|
||||
await comfyPage.page.keyboard.press(PIN_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
|
||||
@@ -12,18 +12,26 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
test.fixme(
|
||||
'should allow toggling bypass on a selected node with hotkey',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
}
|
||||
)
|
||||
|
||||
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
|
||||
|
After Width: | Height: | Size: 96 KiB |
@@ -2,7 +2,6 @@ import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('Vue Node Collapse', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -10,43 +9,50 @@ test.describe('Vue Node Collapse', () => {
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow collapsing node with collapse icon', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await expect(vueNode.root).toBeVisible()
|
||||
|
||||
// Initially should not be collapsed
|
||||
expect(await node.isCollapsed()).toBe(false)
|
||||
const body = await vueNode.getBody()
|
||||
const body = vueNode.body
|
||||
await expect(body).toBeVisible()
|
||||
const expandedBoundingBox = await vueNode.boundingBox()
|
||||
if (!expandedBoundingBox)
|
||||
throw new Error('Failed to get node bounding box before collapse')
|
||||
|
||||
// Collapse the node
|
||||
await vueNode.toggleCollapse()
|
||||
expect(await node.isCollapsed()).toBe(true)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify node content is hidden
|
||||
const collapsedSize = await node.getSize()
|
||||
await expect(body).not.toBeVisible()
|
||||
const collapsedBoundingBox = await vueNode.boundingBox()
|
||||
if (!collapsedBoundingBox)
|
||||
throw new Error('Failed to get node bounding box after collapse')
|
||||
expect(collapsedBoundingBox.height).toBeLessThan(expandedBoundingBox.height)
|
||||
|
||||
// Expand again
|
||||
await vueNode.toggleCollapse()
|
||||
expect(await node.isCollapsed()).toBe(false)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(body).toBeVisible()
|
||||
|
||||
// Size should be restored
|
||||
const expandedSize = await node.getSize()
|
||||
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
|
||||
const expandedBoundingBoxAfter = await vueNode.boundingBox()
|
||||
if (!expandedBoundingBoxAfter)
|
||||
throw new Error('Failed to get node bounding box after expand')
|
||||
expect(expandedBoundingBoxAfter.height).toBeGreaterThanOrEqual(
|
||||
collapsedBoundingBox.height
|
||||
)
|
||||
})
|
||||
|
||||
test('should show collapse/expand icon state', async ({ comfyPage }) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await expect(vueNode.root).toBeVisible()
|
||||
|
||||
// Check initial expanded state icon
|
||||
let iconClass = await vueNode.getCollapseIconClass()
|
||||
@@ -66,9 +72,8 @@ test.describe('Vue Node Collapse', () => {
|
||||
test('should preserve title when collapsing/expanding', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await expect(vueNode.root).toBeVisible()
|
||||
|
||||
// Set custom title
|
||||
await vueNode.setTitle('Test Sampler')
|
||||
@@ -83,7 +88,6 @@ test.describe('Vue Node Collapse', () => {
|
||||
expect(await vueNode.getTitle()).toBe('Test Sampler')
|
||||
|
||||
// Verify title is still displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('Test Sampler')
|
||||
await expect(vueNode.header).toContainText('Test Sampler')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 98 KiB |
@@ -3,7 +3,7 @@ import {
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const ERROR_CLASS = /border-error/
|
||||
const ERROR_CLASS = /border-node-stroke-error/
|
||||
|
||||
test.describe('Vue Node Error', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -17,16 +17,21 @@ test.describe('Vue Node Error', () => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes')
|
||||
|
||||
// Close missing nodes warning dialog
|
||||
await comfyPage.page.getByRole('button', { name: 'Close' }).click()
|
||||
await comfyPage.page.waitForSelector('.comfy-missing-nodes', {
|
||||
state: 'hidden'
|
||||
})
|
||||
|
||||
// Expect error state on missing unknown node
|
||||
const unknownNode = comfyPage.page.locator('[data-node-id]').filter({
|
||||
hasText: 'UNKNOWN NODE'
|
||||
})
|
||||
await expect(unknownNode).toHaveClass(ERROR_CLASS)
|
||||
})
|
||||
|
||||
test('should display error state when node causes execution error', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.runButton.click()
|
||||
|
||||
const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error')
|
||||
await expect(raiseErrorNode).toHaveClass(ERROR_CLASS)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
@@ -4,7 +4,7 @@ import {
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const MUTE_HOTKEY = 'Control+m'
|
||||
const MUTE_CLASS = /opacity-50/
|
||||
const MUTE_OPACITY = '0.5'
|
||||
|
||||
test.describe('Vue Node Mute', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -19,10 +19,11 @@ test.describe('Vue Node Mute', () => {
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(MUTE_CLASS)
|
||||
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png')
|
||||
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(MUTE_CLASS)
|
||||
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
|
||||
})
|
||||
|
||||
test('should allow toggling mute on multiple selected nodes with hotkey', async ({
|
||||
@@ -35,11 +36,11 @@ test.describe('Vue Node Mute', () => {
|
||||
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
await expect(checkpointNode).toHaveClass(MUTE_CLASS)
|
||||
await expect(ksamplerNode).toHaveClass(MUTE_CLASS)
|
||||
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
|
||||
await expect(ksamplerNode).toHaveCSS('opacity', MUTE_OPACITY)
|
||||
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(MUTE_CLASS)
|
||||
await expect(ksamplerNode).not.toHaveClass(MUTE_CLASS)
|
||||
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
|
||||
await expect(ksamplerNode).not.toHaveCSS('opacity', MUTE_OPACITY)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 82 KiB |
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Widget Reactivity', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
test('Should display added widgets', async ({ comfyPage }) => {
|
||||
const loadCheckpointNode = comfyPage.page.locator(
|
||||
'css=[data-testid="node-body-4"] > .lg-node-widgets > div'
|
||||
)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window['graph']._nodes_by_id['4']
|
||||
node.widgets.push(node.widgets[0])
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(2)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window['graph']._nodes_by_id['4']
|
||||
node.widgets[2] = node.widgets[0]
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(3)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window['graph']._nodes_by_id['4']
|
||||
node.widgets.splice(0, 0, node.widgets[0])
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(4)
|
||||
})
|
||||
test('Should hide removed widgets', async ({ comfyPage }) => {
|
||||
const loadCheckpointNode = comfyPage.page.locator(
|
||||
'css=[data-testid="node-body-3"] > .lg-node-widgets > div'
|
||||
)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window['graph']._nodes_by_id['3']
|
||||
node.widgets.pop()
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(5)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window['graph']._nodes_by_id['3']
|
||||
node.widgets.length--
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(4)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window['graph']._nodes_by_id['3']
|
||||
node.widgets.splice(0, 1)
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(3)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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 { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
@@ -23,10 +24,17 @@ const commonGlobals = {
|
||||
} as const
|
||||
|
||||
const settings = {
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
node: true
|
||||
},
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({
|
||||
alwaysTryTypes: true,
|
||||
project: [
|
||||
'./tsconfig.json',
|
||||
'./apps/*/tsconfig.json',
|
||||
'./packages/*/tsconfig.json'
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
],
|
||||
tailwindcss: {
|
||||
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
functions: ['cn', 'clsx', 'tw']
|
||||
@@ -67,11 +75,8 @@ export default defineConfig([
|
||||
...commonParserOptions,
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts'
|
||||
'vite.types.config.mts'
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -249,5 +254,17 @@ export default defineConfig([
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
11
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.29.1",
|
||||
"version": "1.30.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -11,7 +11,10 @@
|
||||
"build:desktop": "nx build @comfyorg/desktop-ui",
|
||||
"build-storybook": "storybook build",
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build",
|
||||
"size:collect": "node scripts/size-collect.js",
|
||||
"size:report": "node scripts/size-report.js",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"dev:desktop": "nx dev @comfyorg/desktop-ui",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
@@ -40,6 +43,7 @@
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
"test:unit": "nx run test",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"workflow:docs": "tsx scripts/cicd/generate-workflow-docs.ts",
|
||||
"zipdist": "node scripts/zipdist.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -85,9 +89,13 @@
|
||||
"jsdom": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-table": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"pretty-bytes": "catalog:",
|
||||
"rollup-plugin-visualizer": "catalog:",
|
||||
"storybook": "catalog:",
|
||||
"stylelint": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
@@ -107,6 +115,7 @@
|
||||
"vue-component-type-helpers": "catalog:",
|
||||
"vue-eslint-parser": "catalog:",
|
||||
"vue-tsc": "catalog:",
|
||||
"yaml": "catalog:",
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "catalog:"
|
||||
},
|
||||
|
||||
@@ -24,11 +24,14 @@
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 8px;
|
||||
|
||||
/* Font Families */
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
|
||||
/* Palette Colors */
|
||||
--color-charcoal-100: #55565e;
|
||||
--color-charcoal-100: #171718;
|
||||
--color-charcoal-200: #494a50;
|
||||
--color-charcoal-300: #3c3d42;
|
||||
--color-charcoal-400: #313235;
|
||||
@@ -60,6 +63,7 @@
|
||||
--color-sand-200: #d6cfc2;
|
||||
--color-sand-300: #888682;
|
||||
|
||||
--color-pure-black: #000000;
|
||||
--color-pure-white: #ffffff;
|
||||
|
||||
--color-slate-100: #9c9eab;
|
||||
@@ -85,10 +89,29 @@
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
--text-xxxs: 0.5625rem;
|
||||
--text-xxxs--line-height: calc(1 / 0.5625);
|
||||
|
||||
--color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3);
|
||||
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
|
||||
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
|
||||
--color-alpha-charcoal-600-30: color-mix(
|
||||
in srgb,
|
||||
var(--color-charcoal-600) 30%,
|
||||
transparent
|
||||
);
|
||||
--color-alpha-stone-100-20: color-mix(
|
||||
in srgb,
|
||||
var(--color-stone-100) 20%,
|
||||
transparent
|
||||
);
|
||||
--color-alpha-gray-500-50: color-mix(
|
||||
in srgb,
|
||||
var(--color-gray-500) 50%,
|
||||
transparent
|
||||
);
|
||||
|
||||
/* PrimeVue pulled colors */
|
||||
--color-muted: var(--p-text-muted-color);
|
||||
@@ -122,6 +145,9 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-pure-white);
|
||||
--button-surface-contrast: var(--color-pure-black);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
@@ -129,9 +155,21 @@
|
||||
|
||||
/* --- */
|
||||
|
||||
--accent-primary: var(--color-charcoal-700);
|
||||
--backdrop: var(--color-white);
|
||||
--button-hover-surface: var(--color-gray-200);
|
||||
--button-active-surface: var(--color-gray-400);
|
||||
--button-icon: var(--color-gray-600);
|
||||
--dialog-surface: var(--color-neutral-200);
|
||||
--interface-menu-component-surface-hovered: var(--color-gray-200);
|
||||
--interface-menu-component-surface-selected: var(--color-gray-400);
|
||||
--interface-menu-keybind-surface-default: var(--color-gray-500);
|
||||
--interface-panel-surface: var(--color-pure-white);
|
||||
--interface-stroke: var(--color-gray-300);
|
||||
--nav-background: var(--color-pure-white);
|
||||
--node-border: var(--color-gray-300);
|
||||
--node-component-border: var(--color-gray-400);
|
||||
--node-component-disabled: var(--color-alpha-stone-100-20);
|
||||
--node-component-executing: var(--color-blue-500);
|
||||
--node-component-header: var(--fg-color);
|
||||
--node-component-header-icon: var(--color-stone-200);
|
||||
@@ -143,7 +181,7 @@
|
||||
--node-component-slot-dot-outline: var(--color-black);
|
||||
--node-component-slot-text: var(--color-stone-200);
|
||||
--node-component-surface-highlight: var(--color-stone-100);
|
||||
--node-component-surface-hovered: var(--color-charcoal-400);
|
||||
--node-component-surface-hovered: var(--color-gray-200);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-white);
|
||||
--node-component-tooltip: var(--color-charcoal-700);
|
||||
@@ -154,13 +192,37 @@
|
||||
from var(--color-zinc-500) r g b / 10%
|
||||
);
|
||||
--node-component-widget-skeleton-surface: var(--color-zinc-300);
|
||||
--node-stroke: var(--color-stone-100);
|
||||
--node-divider: var(--color-sand-100);
|
||||
--node-icon-disabled: var(--color-alpha-gray-500-50);
|
||||
--node-stroke: var(--color-gray-400);
|
||||
--node-stroke-selected: var(--color-accent-primary);
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-blue-100);
|
||||
--text-secondary: var(--color-stone-100);
|
||||
--text-primary: var(--color-charcoal-700);
|
||||
--input-surface: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-pure-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-gray-800);
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
|
||||
--interface-menu-component-surface-selected: var(--color-charcoal-300);
|
||||
--interface-menu-keybind-surface-default: var(--color-charcoal-200);
|
||||
--interface-panel-surface: var(--color-charcoal-100);
|
||||
--interface-stroke: var(--color-charcoal-400);
|
||||
--nav-background: var(--color-charcoal-100);
|
||||
--node-border: var(--color-charcoal-500);
|
||||
--node-component-border: var(--color-stone-200);
|
||||
--node-component-border-error: var(--color-danger-100);
|
||||
--node-component-border-executing: var(--color-blue-500);
|
||||
--node-component-border-selected: var(--color-charcoal-200);
|
||||
--node-component-header-icon: var(--color-slate-300);
|
||||
--node-component-header-surface: var(--color-charcoal-800);
|
||||
--node-component-outline: var(--color-white);
|
||||
@@ -169,19 +231,40 @@
|
||||
--node-component-slot-dot-outline: var(--color-white);
|
||||
--node-component-slot-text: var(--color-slate-200);
|
||||
--node-component-surface-highlight: var(--color-slate-100);
|
||||
--node-component-surface-hovered: var(--color-charcoal-400);
|
||||
--node-component-surface-hovered: var(--color-charcoal-600);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-charcoal-800);
|
||||
--node-component-tooltip: var(--color-white);
|
||||
--node-component-tooltip-border: var(--color-slate-300);
|
||||
--node-component-tooltip-surface: var(--color-charcoal-800);
|
||||
--node-component-widget-skeleton-surface: var(--color-zinc-800);
|
||||
--node-stroke: var(--color-slate-100);
|
||||
--node-component-disabled: var(--color-alpha-charcoal-600-30);
|
||||
--node-divider: var(--color-charcoal-500);
|
||||
--node-icon-disabled: var(--color-alpha-stone-100-20);
|
||||
--node-stroke: var(--color-stone-200);
|
||||
--node-stroke-selected: var(--color-pure-white);
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-blue-100);
|
||||
--text-secondary: var(--color-slate-100);
|
||||
--text-primary: var(--color-pure-white);
|
||||
--input-surface: rgba(130, 130, 130, 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-backdrop: var(--backdrop);
|
||||
--color-button-active-surface: var(--button-active-surface);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered);
|
||||
--color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected);
|
||||
--color-interface-menu-keybind-surface-default: var(--interface-menu-keybind-surface-default);
|
||||
--color-interface-panel-surface: var(--interface-panel-surface);
|
||||
--color-interface-stroke: var(--interface-stroke);
|
||||
--color-nav-background: var(--nav-background);
|
||||
--color-node-border: var(--node-border);
|
||||
--color-node-component-border: var(--node-component-border);
|
||||
--color-node-component-executing: var(--node-component-executing);
|
||||
--color-node-component-header: var(--node-component-header);
|
||||
@@ -213,7 +296,16 @@
|
||||
--color-node-component-widget-skeleton-surface: var(
|
||||
--node-component-widget-skeleton-surface
|
||||
);
|
||||
--color-node-component-disabled: var(--node-component-disabled);
|
||||
--color-node-divider: var(--node-divider);
|
||||
--color-node-icon-disabled: var(--node-icon-disabled);
|
||||
--color-node-stroke: var(--node-stroke);
|
||||
--color-node-stroke-selected: var(--node-stroke-selected);
|
||||
--color-node-stroke-error: var(--node-stroke-error);
|
||||
--color-node-stroke-executing: var(--node-stroke-executing);
|
||||
--color-text-secondary: var(--text-secondary);
|
||||
--color-text-primary: var(--text-primary);
|
||||
--color-input-surface: var(--input-surface);
|
||||
}
|
||||
|
||||
@custom-variant dark-theme {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 16 16" fill="none">
|
||||
<g clip-path="url(#clip0_704_2695)">
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="#9C9EAB" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_704_2695">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 938 B After Width: | Height: | Size: 964 B |
1
packages/design-system/src/icons/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.3" d="m4 2 9.333 6L4 14V2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 221 B |
@@ -82,7 +82,7 @@ export function formatSize(value?: number) {
|
||||
* - filename: 'file'
|
||||
* - suffix: 'txt'
|
||||
*/
|
||||
function getFilenameDetails(fullFilename: string) {
|
||||
export function getFilenameDetails(fullFilename: string) {
|
||||
if (fullFilename.includes('.')) {
|
||||
return {
|
||||
filename: fullFilename.split('.').slice(0, -1).join('.'),
|
||||
@@ -451,3 +451,26 @@ export function stringToLocale(locale: string): SupportedLocale {
|
||||
? (locale as SupportedLocale)
|
||||
: 'en'
|
||||
}
|
||||
|
||||
export function formatDuration(milliseconds: number): string {
|
||||
if (!milliseconds || milliseconds < 0) return '0s'
|
||||
|
||||
const totalSeconds = Math.floor(milliseconds / 1000)
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const remainingSeconds = Math.floor(totalSeconds % 60)
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours}h`)
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes}m`)
|
||||
}
|
||||
if (remainingSeconds > 0 || parts.length === 0) {
|
||||
parts.push(`${remainingSeconds}s`)
|
||||
}
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
191
pnpm-lock.yaml
generated
@@ -183,9 +183,15 @@ catalogs:
|
||||
lint-staged:
|
||||
specifier: ^15.2.7
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
nx:
|
||||
specifier: 21.4.1
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.2.2
|
||||
@@ -193,14 +199,20 @@ catalogs:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
primeicons:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
primevue:
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4
|
||||
storybook:
|
||||
specifier: ^9.1.6
|
||||
version: 9.1.6
|
||||
@@ -251,7 +263,7 @@ catalogs:
|
||||
version: 3.5.13
|
||||
vue-component-type-helpers:
|
||||
specifier: ^3.0.7
|
||||
version: 3.1.0
|
||||
version: 3.1.1
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
@@ -267,6 +279,9 @@ catalogs:
|
||||
vuefire:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1
|
||||
yaml:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
yjs:
|
||||
specifier: ^13.6.27
|
||||
version: 13.6.27
|
||||
@@ -473,7 +488,7 @@ importers:
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/storybook':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
'@nx/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vitest@3.2.4)
|
||||
@@ -485,19 +500,19 @@ importers:
|
||||
version: 1.52.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/vue3':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.12(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.0
|
||||
@@ -545,10 +560,10 @@ importers:
|
||||
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2)
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0-beta.0(tailwindcss@4.1.12)
|
||||
@@ -582,18 +597,30 @@ importers:
|
||||
lint-staged:
|
||||
specifier: 'catalog:'
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
postcss-html:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
rollup-plugin-visualizer:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.4(rollup@4.22.4)
|
||||
storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
stylelint:
|
||||
specifier: 'catalog:'
|
||||
version: 16.24.0(typescript@5.9.2)
|
||||
@@ -641,13 +668,16 @@ importers:
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-type-helpers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
version: 3.1.1
|
||||
vue-eslint-parser:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
|
||||
vue-tsc:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.7(typescript@5.9.2)
|
||||
yaml:
|
||||
specifier: 'catalog:'
|
||||
version: 2.8.1
|
||||
zip-dir:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -4886,9 +4916,6 @@ packages:
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -5993,11 +6020,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.5:
|
||||
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
||||
engines: {node: ^18 || >=20}
|
||||
@@ -6345,10 +6367,6 @@ packages:
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.1:
|
||||
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -6361,11 +6379,15 @@ packages:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
prettier@3.3.2:
|
||||
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-bytes@7.1.0:
|
||||
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -6668,6 +6690,19 @@ packages:
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rollup-plugin-visualizer@6.0.4:
|
||||
resolution: {integrity: sha512-q8Q7J/6YofkmaGW1sH/fPRAz37x/+pd7VBuaUU7lwvOS/YikuiiEU9jeb9PH8XHiq50XFrUsBbOxeAMYQ7KZkg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rolldown: 1.x || ^1.0.0-beta
|
||||
rollup: 2.x || 3.x || 4.x
|
||||
peerDependenciesMeta:
|
||||
rolldown:
|
||||
optional: true
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
rollup@4.22.4:
|
||||
resolution: {integrity: sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -6822,6 +6857,10 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map@0.7.6:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7450,9 +7489,6 @@ packages:
|
||||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.1.0:
|
||||
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
|
||||
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
@@ -9645,7 +9681,7 @@ snapshots:
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@nx/cypress': 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/devkit': 21.4.1(nx@21.4.1)
|
||||
@@ -9653,7 +9689,7 @@ snapshots:
|
||||
'@nx/js': 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)
|
||||
'@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.2)
|
||||
semver: 7.7.2
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
@@ -10004,29 +10040,29 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1)
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
unplugin: 1.16.1
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
@@ -10036,19 +10072,19 @@ snapshots:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
find-package-json: 1.2.0
|
||||
magic-string: 0.30.19
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
typescript: 5.9.2
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-meta: 2.2.12(typescript@5.9.2)
|
||||
@@ -10056,10 +10092,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.1
|
||||
@@ -10324,7 +10360,7 @@ snapshots:
|
||||
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
|
||||
'@tiptap/pm': 2.10.4
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)':
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.3
|
||||
'@babel/parser': 7.28.4
|
||||
@@ -10332,7 +10368,7 @@ snapshots:
|
||||
'@babel/types': 7.28.4
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
transitivePeerDependencies:
|
||||
@@ -12110,20 +12146,20 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2):
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.11.11
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.4.2))
|
||||
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -12562,10 +12598,6 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -13860,8 +13892,6 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
nanoid@5.1.5: {}
|
||||
|
||||
napi-postinstall@0.3.3: {}
|
||||
@@ -14241,14 +14271,14 @@ snapshots:
|
||||
dependencies:
|
||||
htmlparser2: 8.0.2
|
||||
js-tokens: 9.0.1
|
||||
postcss: 8.5.1
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.1)
|
||||
postcss: 8.5.6
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.6)
|
||||
|
||||
postcss-resolve-nested-selector@0.1.6: {}
|
||||
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.1):
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.6):
|
||||
dependencies:
|
||||
postcss: 8.5.1
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.6):
|
||||
dependencies:
|
||||
@@ -14266,12 +14296,6 @@ snapshots:
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.1:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -14284,7 +14308,9 @@ snapshots:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier@3.3.2: {}
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
@@ -14733,6 +14759,15 @@ snapshots:
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rollup-plugin-visualizer@6.0.4(rollup@4.22.4):
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 4.0.3
|
||||
source-map: 0.7.6
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
rollup: 4.22.4
|
||||
|
||||
rollup@4.22.4:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
@@ -14924,6 +14959,8 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
sprintf-js@1.0.3: {}
|
||||
@@ -14950,7 +14987,7 @@ snapshots:
|
||||
internal-slot: 1.1.0
|
||||
optional: true
|
||||
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@testing-library/jest-dom': 6.6.4
|
||||
@@ -14965,7 +15002,7 @@ snapshots:
|
||||
semver: 7.7.2
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
transitivePeerDependencies:
|
||||
- '@testing-library/dom'
|
||||
- bufferutil
|
||||
@@ -15287,7 +15324,7 @@ snapshots:
|
||||
tsx@4.19.4:
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
get-tsconfig: 4.7.5
|
||||
get-tsconfig: 4.10.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
@@ -15639,7 +15676,7 @@ snapshots:
|
||||
vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.1
|
||||
postcss: 8.5.6
|
||||
rollup: 4.22.4
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.10
|
||||
@@ -15704,8 +15741,6 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.1.0: {}
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
|
||||
@@ -62,12 +62,16 @@ catalog:
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
markdown-table: ^3.0.4
|
||||
nx: 21.4.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.3.2
|
||||
prettier: ^3.6.2
|
||||
pretty-bytes: ^7.1.0
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
rollup-plugin-visualizer: ^6.0.4
|
||||
storybook: ^9.1.6
|
||||
stylelint: ^16.24.0
|
||||
tailwindcss: ^4.1.12
|
||||
@@ -90,6 +94,7 @@ catalog:
|
||||
vue-router: ^4.4.3
|
||||
vue-tsc: ^3.0.7
|
||||
vuefire: ^3.2.1
|
||||
yaml: ^2.8.1
|
||||
yjs: ^13.6.27
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
@@ -97,9 +102,6 @@ catalog:
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
@@ -114,3 +116,6 @@ onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- nx
|
||||
- oxc-resolver
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
93
scripts/bundle-categories.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Bundle categorization configuration
|
||||
*
|
||||
* This file defines how bundles are categorized in size reports.
|
||||
* Categories help identify which parts of the application are growing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleCategory
|
||||
* @property {string} name - Display name of the category
|
||||
* @property {string} description - Description of what this category includes
|
||||
* @property {RegExp[]} patterns - Regex patterns to match bundle files
|
||||
* @property {number} order - Sort order for display (lower = first)
|
||||
*/
|
||||
|
||||
/** @type {BundleCategory[]} */
|
||||
export const BUNDLE_CATEGORIES = [
|
||||
{
|
||||
name: 'App Entry Points',
|
||||
description: 'Main application bundles',
|
||||
patterns: [/^index-.*\.js$/],
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
name: 'Core Views',
|
||||
description: 'Major application views and screens',
|
||||
patterns: [/GraphView-.*\.js$/, /UserSelectView-.*\.js$/],
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
name: 'UI Panels',
|
||||
description: 'Settings and configuration panels',
|
||||
patterns: [/.*Panel-.*\.js$/],
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
name: 'UI Components',
|
||||
description: 'Reusable UI components',
|
||||
patterns: [/Avatar-.*\.js$/, /Badge-.*\.js$/],
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
name: 'Services',
|
||||
description: 'Business logic and services',
|
||||
patterns: [/.*Service-.*\.js$/, /.*Store-.*\.js$/],
|
||||
order: 5
|
||||
},
|
||||
{
|
||||
name: 'Utilities',
|
||||
description: 'Helper functions and utilities',
|
||||
patterns: [/.*[Uu]til.*\.js$/],
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
description: 'Uncategorized bundles',
|
||||
patterns: [/.*/], // Catch-all pattern
|
||||
order: 99
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Categorize a bundle file based on its name
|
||||
*
|
||||
* @param {string} fileName - The bundle file name (e.g., "assets/GraphView-BnV6iF9h.js")
|
||||
* @returns {string} - The category name
|
||||
*/
|
||||
export function categorizeBundle(fileName) {
|
||||
// Extract just the file name without path
|
||||
const baseName = fileName.split('/').pop() || fileName
|
||||
|
||||
// Find the first matching category
|
||||
for (const category of BUNDLE_CATEGORIES) {
|
||||
for (const pattern of category.patterns) {
|
||||
if (pattern.test(baseName)) {
|
||||
return category.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category metadata by name
|
||||
*
|
||||
* @param {string} categoryName - The category name
|
||||
* @returns {BundleCategory | undefined} - The category metadata
|
||||
*/
|
||||
export function getCategoryMetadata(categoryName) {
|
||||
return BUNDLE_CATEGORIES.find((cat) => cat.name === categoryName)
|
||||
}
|
||||
450
scripts/cicd/generate-workflow-docs.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Generate workflow documentation from GitHub Actions workflow files
|
||||
*
|
||||
* This script:
|
||||
* 1. Scans all workflow YAML files in .github/workflows
|
||||
* 2. Extracts metadata (name, description, triggers, labels)
|
||||
* 3. Updates the workflows README.md with current information
|
||||
*/
|
||||
import { readFileSync, readdirSync, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
interface WorkflowMetadata {
|
||||
filename: string
|
||||
name: string
|
||||
description?: string
|
||||
prefix: string
|
||||
triggers: string[]
|
||||
labelTriggers: string[]
|
||||
}
|
||||
|
||||
interface WorkflowsByPrefix {
|
||||
[prefix: string]: {
|
||||
description: string
|
||||
workflows: WorkflowMetadata[]
|
||||
}
|
||||
}
|
||||
|
||||
const WORKFLOWS_DIR = join(process.cwd(), '.github/workflows')
|
||||
const README_PATH = join(WORKFLOWS_DIR, 'README.md')
|
||||
|
||||
// Category descriptions for workflow prefixes
|
||||
const PREFIX_DESCRIPTIONS: Record<string, string> = {
|
||||
'ci-': 'Testing, linting, validation',
|
||||
'release-': 'Version management, publishing',
|
||||
'pr-': 'PR automation (triggered by labels)',
|
||||
'api-': 'External API type generation',
|
||||
'i18n-': 'Internationalization updates',
|
||||
'publish-': 'Publishing and deployment',
|
||||
'version-': 'Version management'
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a label to the list if it's not already present
|
||||
*/
|
||||
function addUniqueLabel(labels: string[], label: string): void {
|
||||
if (!labels.includes(label)) {
|
||||
labels.push(label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract label triggers from workflow content
|
||||
*/
|
||||
function extractLabelTriggers(content: string, workflowData: any): string[] {
|
||||
const labels: string[] = []
|
||||
|
||||
// Check for label_trigger in anthropics/claude-code-action
|
||||
const labelTriggerMatch = content.match(/label_trigger:\s*["']([^"']+)["']/i)
|
||||
if (labelTriggerMatch) {
|
||||
labels.push(labelTriggerMatch[1])
|
||||
}
|
||||
|
||||
// Check for github.event.label.name == 'label-name' pattern
|
||||
const labelNameMatches = content.matchAll(
|
||||
/github\.event\.label\.name\s*==\s*['"]([^'"]+)['"]/gi
|
||||
)
|
||||
for (const match of labelNameMatches) {
|
||||
addUniqueLabel(labels, match[1])
|
||||
}
|
||||
|
||||
// Check for contains(github.event.pull_request.labels.*.name, 'label-name') pattern
|
||||
const containsLabelMatches = content.matchAll(
|
||||
/contains\(github\.event\.pull_request\.labels\.\*\.name,\s*['"]([^'"]+)['"]\)/gi
|
||||
)
|
||||
for (const match of containsLabelMatches) {
|
||||
addUniqueLabel(labels, match[1])
|
||||
}
|
||||
|
||||
// Check for startsWith patterns with comment commands (e.g., /update-playwright)
|
||||
// These are included as they can trigger workflows through PR comments
|
||||
const labelCommentMatches = content.matchAll(
|
||||
/startsWith\(github\.event\.comment\.body,\s*['"]([^'"]+)['"]\)/gi
|
||||
)
|
||||
for (const match of labelCommentMatches) {
|
||||
const command = match[1]
|
||||
if (command) {
|
||||
addUniqueLabel(labels, command)
|
||||
}
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract trigger information from workflow
|
||||
*/
|
||||
function extractTriggers(workflowData: any): string[] {
|
||||
const triggers: string[] = []
|
||||
const on = workflowData.on
|
||||
|
||||
if (!on) return triggers
|
||||
|
||||
if (typeof on === 'string') {
|
||||
triggers.push(on)
|
||||
} else if (Array.isArray(on)) {
|
||||
triggers.push(...on)
|
||||
} else if (typeof on === 'object') {
|
||||
// Handle workflow_dispatch
|
||||
if (on.workflow_dispatch !== undefined) {
|
||||
triggers.push('workflow_dispatch (manual)')
|
||||
}
|
||||
|
||||
// Handle pull_request with types
|
||||
if (on.pull_request) {
|
||||
if (typeof on.pull_request === 'object' && on.pull_request.types) {
|
||||
const types = Array.isArray(on.pull_request.types)
|
||||
? on.pull_request.types.join(', ')
|
||||
: on.pull_request.types
|
||||
triggers.push(`pull_request (${types})`)
|
||||
} else {
|
||||
triggers.push('pull_request')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pull_request_target
|
||||
if (on.pull_request_target) {
|
||||
if (
|
||||
typeof on.pull_request_target === 'object' &&
|
||||
on.pull_request_target.types
|
||||
) {
|
||||
const types = Array.isArray(on.pull_request_target.types)
|
||||
? on.pull_request_target.types.join(', ')
|
||||
: on.pull_request_target.types
|
||||
triggers.push(`pull_request_target (${types})`)
|
||||
} else {
|
||||
triggers.push('pull_request_target')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle push
|
||||
if (on.push) {
|
||||
triggers.push('push')
|
||||
}
|
||||
|
||||
// Handle schedule
|
||||
if (on.schedule) {
|
||||
triggers.push('schedule')
|
||||
}
|
||||
|
||||
// Handle issue_comment
|
||||
if (on.issue_comment) {
|
||||
if (typeof on.issue_comment === 'object' && on.issue_comment.types) {
|
||||
const types = Array.isArray(on.issue_comment.types)
|
||||
? on.issue_comment.types.join(', ')
|
||||
: on.issue_comment.types
|
||||
triggers.push(`issue_comment (${types})`)
|
||||
} else {
|
||||
triggers.push('issue_comment')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return triggers
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single workflow file and extract metadata
|
||||
*/
|
||||
function parseWorkflowFile(filename: string): WorkflowMetadata | null {
|
||||
try {
|
||||
const filepath = join(WORKFLOWS_DIR, filename)
|
||||
const content = readFileSync(filepath, 'utf-8')
|
||||
const workflowData = parse(content)
|
||||
|
||||
if (!workflowData || !workflowData.name) {
|
||||
console.warn(`Skipping ${filename}: no name field`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Determine prefix from filename
|
||||
const prefixMatch = filename.match(/^([a-z0-9]+)-/)
|
||||
const prefix = prefixMatch ? prefixMatch[1] + '-' : 'other-'
|
||||
|
||||
const metadata: WorkflowMetadata = {
|
||||
filename,
|
||||
name: workflowData.name,
|
||||
description: workflowData.description,
|
||||
prefix,
|
||||
triggers: extractTriggers(workflowData),
|
||||
labelTriggers: extractLabelTriggers(content, workflowData)
|
||||
}
|
||||
|
||||
return metadata
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filename}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group workflows by prefix
|
||||
*/
|
||||
function groupWorkflowsByPrefix(
|
||||
workflows: WorkflowMetadata[]
|
||||
): WorkflowsByPrefix {
|
||||
const grouped: WorkflowsByPrefix = {}
|
||||
|
||||
for (const workflow of workflows) {
|
||||
if (!grouped[workflow.prefix]) {
|
||||
grouped[workflow.prefix] = {
|
||||
description: PREFIX_DESCRIPTIONS[workflow.prefix] || 'Other workflows',
|
||||
workflows: []
|
||||
}
|
||||
}
|
||||
grouped[workflow.prefix].workflows.push(workflow)
|
||||
}
|
||||
|
||||
// Sort workflows within each group by filename
|
||||
for (const prefix in grouped) {
|
||||
grouped[prefix].workflows.sort((a, b) =>
|
||||
a.filename.localeCompare(b.filename)
|
||||
)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate markdown table for workflow categories
|
||||
*/
|
||||
function generateCategoryTable(grouped: WorkflowsByPrefix): string {
|
||||
const prefixOrder = [
|
||||
'ci-',
|
||||
'pr-',
|
||||
'release-',
|
||||
'api-',
|
||||
'i18n-',
|
||||
'publish-',
|
||||
'version-',
|
||||
'other-'
|
||||
]
|
||||
|
||||
let table =
|
||||
'| Prefix | Purpose | Example |\n'
|
||||
table +=
|
||||
'| ---------- | ----------------------------------- | ------------------------------------ |\n'
|
||||
|
||||
for (const prefix of prefixOrder) {
|
||||
if (grouped[prefix]) {
|
||||
const example = grouped[prefix].workflows[0]?.filename || ''
|
||||
const purpose = grouped[prefix].description
|
||||
table += `| \`${prefix}\` | ${purpose} | \`${example}\` |\n`
|
||||
}
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate detailed workflow list with descriptions
|
||||
*/
|
||||
function generateWorkflowList(grouped: WorkflowsByPrefix): string {
|
||||
const prefixOrder = [
|
||||
'ci-',
|
||||
'pr-',
|
||||
'release-',
|
||||
'api-',
|
||||
'i18n-',
|
||||
'publish-',
|
||||
'version-',
|
||||
'other-'
|
||||
]
|
||||
let markdown = ''
|
||||
|
||||
for (const prefix of prefixOrder) {
|
||||
if (!grouped[prefix]) continue
|
||||
|
||||
const category = grouped[prefix]
|
||||
const prefixName =
|
||||
prefix === 'other-'
|
||||
? 'Other Workflows'
|
||||
: prefix.replace('-', '').toUpperCase()
|
||||
|
||||
markdown += `\n### ${prefixName}\n\n`
|
||||
|
||||
for (const workflow of category.workflows) {
|
||||
markdown += `#### [\`${workflow.filename}\`](./${workflow.filename})\n\n`
|
||||
markdown += `**Name:** ${workflow.name}\n\n`
|
||||
|
||||
if (workflow.description) {
|
||||
markdown += `**Description:** ${workflow.description}\n\n`
|
||||
}
|
||||
|
||||
if (workflow.triggers.length > 0) {
|
||||
markdown += `**Triggers:** ${workflow.triggers.join(', ')}\n\n`
|
||||
}
|
||||
|
||||
if (workflow.labelTriggers.length > 0) {
|
||||
markdown += `**Label Triggers:** \`${workflow.labelTriggers.join('`, `')}\`\n\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return markdown
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate quick reference for label-triggered workflows
|
||||
*/
|
||||
function generateQuickReference(grouped: WorkflowsByPrefix): string {
|
||||
const allWorkflows = Object.values(grouped).flatMap((g) => g.workflows)
|
||||
const labelWorkflows = allWorkflows.filter((w) => w.labelTriggers.length > 0)
|
||||
|
||||
if (labelWorkflows.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Group workflows by label to avoid duplicates
|
||||
const labelMap = new Map<string, string[]>()
|
||||
for (const workflow of labelWorkflows) {
|
||||
for (const label of workflow.labelTriggers) {
|
||||
// Filter out comment-based triggers (commands starting with /) from Quick Reference
|
||||
// These are still shown in detailed workflow sections with full context
|
||||
if (label.startsWith('/')) {
|
||||
continue
|
||||
}
|
||||
const description = workflow.description || workflow.name
|
||||
if (!labelMap.has(label)) {
|
||||
labelMap.set(label, [])
|
||||
}
|
||||
labelMap.get(label)!.push(description)
|
||||
}
|
||||
}
|
||||
|
||||
let markdown = '## Quick Reference\n\n'
|
||||
markdown +=
|
||||
'For label-triggered workflows, add the corresponding label to a PR to trigger the workflow:\n'
|
||||
|
||||
// Sort labels alphabetically for consistency
|
||||
const sortedLabels = Array.from(labelMap.keys()).sort()
|
||||
for (const label of sortedLabels) {
|
||||
const descriptions = labelMap.get(label)!
|
||||
// Use the first description, or note if multiple workflows share the same label
|
||||
const description =
|
||||
descriptions.length === 1
|
||||
? descriptions[0]
|
||||
: `Triggers ${descriptions.length} workflows`
|
||||
markdown += `- \`${label}\` - ${description}\n`
|
||||
}
|
||||
|
||||
markdown +=
|
||||
'\nFor manual workflows, use the "Run workflow" button in the Actions tab.\n'
|
||||
|
||||
return markdown
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the complete README content
|
||||
*/
|
||||
function generateReadme(grouped: WorkflowsByPrefix): string {
|
||||
const categoryTable = generateCategoryTable(grouped)
|
||||
const quickReference = generateQuickReference(grouped)
|
||||
const workflowList = generateWorkflowList(grouped)
|
||||
|
||||
return `# GitHub Workflows
|
||||
|
||||
This directory contains GitHub Actions workflow files that automate various aspects of the ComfyUI frontend development and release process.
|
||||
|
||||
> **Note:** This documentation is auto-generated from workflow files. Do not edit manually.
|
||||
> Run \`pnpm workflow:docs\` to regenerate.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Workflow files follow a consistent naming pattern: \`<prefix>-<descriptive-name>.yaml\`
|
||||
|
||||
### Category Prefixes
|
||||
|
||||
${categoryTable}
|
||||
|
||||
${quickReference}
|
||||
|
||||
## Workflow Details
|
||||
|
||||
${workflowList}
|
||||
|
||||
## Documentation
|
||||
|
||||
For more information about GitHub Actions, see:
|
||||
- [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)
|
||||
- [Workflow syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions)
|
||||
|
||||
## Maintaining Workflows
|
||||
|
||||
### Adding a New Workflow
|
||||
|
||||
1. Create a new workflow file following the naming convention
|
||||
2. Include \`name\` and \`description\` fields at the top of the workflow
|
||||
3. Run \`pnpm workflow:docs\` to update this README
|
||||
4. Commit both the workflow file and updated README
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always include a description**: Add a \`description\` field after the \`name\` field
|
||||
2. **Use consistent prefixes**: Follow the established prefix conventions
|
||||
3. **Label-triggered workflows**: For PR automation, use the \`pr-\` prefix
|
||||
4. **Document triggers**: Make trigger conditions clear in the workflow description
|
||||
5. **Keep docs in sync**: Run \`pnpm workflow:docs\` after any workflow changes
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main() {
|
||||
// Read all workflow files
|
||||
const files = readdirSync(WORKFLOWS_DIR).filter(
|
||||
(f) => f.endsWith('.yaml') || f.endsWith('.yml')
|
||||
)
|
||||
const workflowFiles = files.filter((f) => f !== 'README.md')
|
||||
|
||||
// Parse each workflow
|
||||
const workflows: WorkflowMetadata[] = []
|
||||
for (const filename of workflowFiles) {
|
||||
const metadata = parseWorkflowFile(filename)
|
||||
if (metadata) {
|
||||
workflows.push(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
// Group workflows by prefix
|
||||
const grouped = groupWorkflowsByPrefix(workflows)
|
||||
|
||||
// Generate README
|
||||
const readme = generateReadme(grouped)
|
||||
writeFileSync(README_PATH, readme, 'utf-8')
|
||||
|
||||
// Show label-triggered workflows for validation
|
||||
const labelWorkflows = workflows.filter((w) => w.labelTriggers.length > 0)
|
||||
if (labelWorkflows.length > 0 && process.env.VERBOSE) {
|
||||
for (const workflow of labelWorkflows) {
|
||||
console.warn(
|
||||
`Label-triggered: ${workflow.name}: ${workflow.labelTriggers.join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
90
scripts/size-collect.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// @ts-check
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { brotliCompressSync, gzipSync } from 'node:zlib'
|
||||
import pico from 'picocolors'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { categorizeBundle } from './bundle-categories.js'
|
||||
|
||||
const distDir = path.resolve('dist')
|
||||
const sizeDir = path.resolve('temp/size')
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {string} file
|
||||
* @property {string} category
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to collect bundle size data
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(distDir)) {
|
||||
console.error(pico.red('Error: dist directory does not exist'))
|
||||
console.error(pico.yellow('Please run "pnpm build" first'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(pico.blue('\nCollecting bundle size data...\n'))
|
||||
|
||||
// Collect main bundle files from dist/assets
|
||||
const assetsDir = path.join(distDir, 'assets')
|
||||
const bundles = []
|
||||
|
||||
if (existsSync(assetsDir)) {
|
||||
const files = await readdir(assetsDir)
|
||||
const jsFiles = files.filter(
|
||||
(file) => file.endsWith('.js') && !file.includes('legacy')
|
||||
)
|
||||
|
||||
for (const file of jsFiles) {
|
||||
const filePath = path.join(assetsDir, file)
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
const size = Buffer.byteLength(content)
|
||||
const gzip = gzipSync(content).length
|
||||
const brotli = brotliCompressSync(content).length
|
||||
const fileName = `assets/${file}`
|
||||
const category = categorizeBundle(fileName)
|
||||
|
||||
bundles.push({
|
||||
file: fileName,
|
||||
category,
|
||||
size,
|
||||
gzip,
|
||||
brotli
|
||||
})
|
||||
|
||||
console.log(
|
||||
`${pico.green(file)} ${pico.dim(`[${category}]`)} - ` +
|
||||
`Size: ${prettyBytes(size)} / ` +
|
||||
`Gzip: ${prettyBytes(gzip)} / ` +
|
||||
`Brotli: ${prettyBytes(brotli)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp/size directory
|
||||
await mkdir(sizeDir, { recursive: true })
|
||||
|
||||
// Write individual bundle files
|
||||
for (const bundle of bundles) {
|
||||
const fileName = bundle.file.replace(/[/\\]/g, '_').replace('.js', '.json')
|
||||
await writeFile(
|
||||
path.join(sizeDir, fileName),
|
||||
JSON.stringify(bundle, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
pico.green(`\n✓ Collected size data for ${bundles.length} bundles\n`)
|
||||
)
|
||||
console.log(pico.blue(`Data saved to: ${sizeDir}\n`))
|
||||
}
|
||||
162
scripts/size-report.js
Normal file
@@ -0,0 +1,162 @@
|
||||
// @ts-check
|
||||
import { markdownTable } from 'markdown-table'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { getCategoryMetadata } from './bundle-categories.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SizeResult & { file: string, category?: string }} BundleResult
|
||||
*/
|
||||
|
||||
const currDir = path.resolve('temp/size')
|
||||
const prevDir = path.resolve('temp/size-prev')
|
||||
let output = '## Bundle Size Report\n\n'
|
||||
const sizeHeaders = ['Size', 'Gzip', 'Brotli']
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to generate the size report
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(currDir)) {
|
||||
console.error('Error: temp/size directory does not exist')
|
||||
console.error('Please run "pnpm size:collect" first')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await renderFiles()
|
||||
process.stdout.write(output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders file sizes and diffs between current and previous versions
|
||||
*/
|
||||
async function renderFiles() {
|
||||
/**
|
||||
* @param {string[]} files
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
|
||||
|
||||
const curr = filterFiles(await readdir(currDir))
|
||||
const prev = existsSync(prevDir) ? filterFiles(await readdir(prevDir)) : []
|
||||
const fileList = new Set([...curr, ...prev])
|
||||
|
||||
// Group bundles by category
|
||||
/** @type {Map<string, Array<{fileName: string, curr: BundleResult | undefined, prev: BundleResult | undefined}>>} */
|
||||
const bundlesByCategory = new Map()
|
||||
|
||||
for (const file of fileList) {
|
||||
const currPath = path.resolve(currDir, file)
|
||||
const prevPath = path.resolve(prevDir, file)
|
||||
|
||||
const curr = await importJSON(currPath)
|
||||
const prev = await importJSON(prevPath)
|
||||
const fileName = curr?.file || prev?.file || ''
|
||||
const category = curr?.category || prev?.category || 'Other'
|
||||
|
||||
if (!bundlesByCategory.has(category)) {
|
||||
bundlesByCategory.set(category, [])
|
||||
}
|
||||
|
||||
// @ts-expect-error - get is valid
|
||||
bundlesByCategory.get(category).push({ fileName, curr, prev })
|
||||
}
|
||||
|
||||
// Sort categories by their order
|
||||
const sortedCategories = Array.from(bundlesByCategory.keys()).sort((a, b) => {
|
||||
const metaA = getCategoryMetadata(a)
|
||||
const metaB = getCategoryMetadata(b)
|
||||
return (metaA?.order ?? 99) - (metaB?.order ?? 99)
|
||||
})
|
||||
|
||||
let totalSize = 0
|
||||
let totalCount = 0
|
||||
|
||||
// Render each category
|
||||
for (const category of sortedCategories) {
|
||||
const bundles = bundlesByCategory.get(category) || []
|
||||
if (bundles.length === 0) continue
|
||||
|
||||
const categoryMeta = getCategoryMetadata(category)
|
||||
output += `### ${category}\n\n`
|
||||
if (categoryMeta?.description) {
|
||||
output += `_${categoryMeta.description}_\n\n`
|
||||
}
|
||||
|
||||
const rows = []
|
||||
let categorySize = 0
|
||||
|
||||
for (const { fileName, curr, prev } of bundles) {
|
||||
if (!curr) {
|
||||
// File was deleted
|
||||
rows.push([`~~${fileName}~~`])
|
||||
} else {
|
||||
rows.push([
|
||||
fileName,
|
||||
`${prettyBytes(curr.size)}${getDiff(curr.size, prev?.size)}`,
|
||||
`${prettyBytes(curr.gzip)}${getDiff(curr.gzip, prev?.gzip)}`,
|
||||
`${prettyBytes(curr.brotli)}${getDiff(curr.brotli, prev?.brotli)}`
|
||||
])
|
||||
categorySize += curr.size
|
||||
totalSize += curr.size
|
||||
totalCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Sort rows by file name within category
|
||||
rows.sort((a, b) => {
|
||||
const fileA = a[0].replace(/~~/g, '')
|
||||
const fileB = b[0].replace(/~~/g, '')
|
||||
return fileA.localeCompare(fileB)
|
||||
})
|
||||
|
||||
output += markdownTable([['File', ...sizeHeaders], ...rows])
|
||||
output += `\n\n**Category Total:** ${prettyBytes(categorySize)}\n\n`
|
||||
}
|
||||
|
||||
// Add overall summary
|
||||
if (totalCount > 0) {
|
||||
output += '---\n\n'
|
||||
output += `**Overall Total Size:** ${prettyBytes(totalSize)}\n`
|
||||
output += `**Total Bundle Count:** ${totalCount}\n`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports JSON data from a specified path
|
||||
*
|
||||
* @template T
|
||||
* @param {string} filePath - Path to the JSON file
|
||||
* @returns {Promise<T | undefined>} The JSON content or undefined if the file does not exist
|
||||
*/
|
||||
async function importJSON(filePath) {
|
||||
if (!existsSync(filePath)) return undefined
|
||||
return (await import(filePath, { with: { type: 'json' } })).default
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the difference between the current and previous sizes
|
||||
*
|
||||
* @param {number} curr - The current size
|
||||
* @param {number} [prev] - The previous size
|
||||
* @returns {string} The difference in pretty format
|
||||
*/
|
||||
function getDiff(curr, prev) {
|
||||
if (prev === undefined) return ''
|
||||
const diff = curr - prev
|
||||
if (diff === 0) return ''
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return ` (**${sign}${prettyBytes(diff)}**)`
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
"comfy_base": {
|
||||
"fg-color": "#222",
|
||||
"bg-color": "#DDD",
|
||||
"comfy-menu-bg": "#F5F5F5",
|
||||
"comfy-menu-bg": "#FFFFFF",
|
||||
"comfy-menu-hover-bg": "#ccc",
|
||||
"comfy-menu-secondary-bg": "#EEE",
|
||||
"comfy-input-bg": "#C9C9C9",
|
||||
|
||||
@@ -1,48 +1,88 @@
|
||||
<template>
|
||||
<Splitter
|
||||
:key="sidebarStateKey"
|
||||
class="splitter-overlay-root splitter-overlay"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
<div class="splitter-overlay-root pointer-events-none flex flex-col">
|
||||
<slot name="workflow-tabs" />
|
||||
|
||||
<div
|
||||
class="pointer-events-none flex flex-1 overflow-hidden"
|
||||
:class="{
|
||||
'flex-row': sidebarLocation === 'left',
|
||||
'flex-row-reverse': sidebarLocation === 'right'
|
||||
}"
|
||||
>
|
||||
<div class="side-toolbar-container">
|
||||
<slot name="side-toolbar" />
|
||||
</div>
|
||||
|
||||
<SplitterPanel :size="100">
|
||||
<Splitter
|
||||
class="splitter-overlay max-w-full"
|
||||
layout="vertical"
|
||||
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
||||
state-key="bottom-panel-splitter"
|
||||
key="main-splitter-stable"
|
||||
class="splitter-overlay flex-1 overflow-hidden"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey || 'main-splitter'"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'left'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
|
||||
<slot name="bottom-panel" />
|
||||
|
||||
<SplitterPanel :size="80" class="flex flex-col">
|
||||
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
|
||||
|
||||
<Splitter
|
||||
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
|
||||
layout="vertical"
|
||||
:pt:gutter="
|
||||
'rounded-tl-lg rounded-tr-lg ' +
|
||||
(bottomPanelVisible ? '' : 'hidden')
|
||||
"
|
||||
state-key="bottom-panel-splitter"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
v-show="bottomPanelVisible"
|
||||
class="bottom-panel pointer-events-auto rounded-lg"
|
||||
>
|
||||
<slot name="bottom-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'right'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -74,7 +114,11 @@ const activeSidebarTabId = computed(
|
||||
)
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
|
||||
if (unifiedWidth.value) {
|
||||
return 'unified-sidebar'
|
||||
}
|
||||
// When no tab is active, use a default key to maintain state
|
||||
return activeSidebarTabId.value ?? 'default-sidebar'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -93,12 +137,17 @@ const sidebarStateKey = computed(() => {
|
||||
|
||||
.side-bar-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.splitter-overlay-bottom :deep(.p-splitter-gutter) {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
.splitter-overlay {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="workspaceState.focusMode"
|
||||
class="comfy-menu-hamburger no-drag"
|
||||
:style="positionCSS"
|
||||
class="comfy-menu-hamburger no-drag top-0 right-0"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
@@ -15,14 +14,13 @@
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
<div class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -45,15 +43,6 @@ watchEffect(() => {
|
||||
app.ui.menuContainer.style.display = 'block'
|
||||
}
|
||||
})
|
||||
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const positionCSS = computed<CSSProperties>(() =>
|
||||
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
|
||||
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
|
||||
menuSetting.value === 'Bottom'
|
||||
? { bottom: '0px', right: '0px' }
|
||||
: { top: '0px', right: '0px' }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
51
src/components/TopMenuSection.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto mx-2 flex h-12 items-center rounded-lg px-2 shadow-md"
|
||||
>
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<LoginButton v-if="!isLoggedIn" />
|
||||
<CurrentUserButton v-else class="shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
onMounted(() => {
|
||||
if (legacyCommandsContainerRef.value) {
|
||||
app.menu.element.style.width = 'fit-content'
|
||||
legacyCommandsContainerRef.value.appendChild(app.menu.element)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actionbar-container {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +1,57 @@
|
||||
<template>
|
||||
<Panel
|
||||
class="actionbar w-fit"
|
||||
:style="style"
|
||||
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
||||
>
|
||||
<div ref="panelRef" class="actionbar-content flex items-center select-none">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ComfyQueueButton />
|
||||
<div class="flex h-full items-center">
|
||||
<div
|
||||
v-if="isDragging && !isDocked"
|
||||
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
|
||||
:class="{
|
||||
'drop-zone-active': isMouseOverDropZone
|
||||
}"
|
||||
@mouseenter="onMouseEnterDropZone"
|
||||
@mouseleave="onMouseLeaveDropZone"
|
||||
>
|
||||
{{ t('actionbar.dockToTop') }}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
class="actionbar"
|
||||
:style="style"
|
||||
:class="{
|
||||
fixed: !isDocked,
|
||||
'is-dragging': isDragging,
|
||||
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="panelRef"
|
||||
class="actionbar-content flex items-center select-none"
|
||||
>
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ComfyQueueButton />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
useDraggable,
|
||||
useElementBounding,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -41,10 +60,9 @@ import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
const settingsStore = useSettingStore()
|
||||
|
||||
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
|
||||
const visible = computed(() => position.value !== 'Disabled')
|
||||
|
||||
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
|
||||
const tabContainer = document.querySelector('.workflow-tabs-container')
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
||||
@@ -63,11 +81,9 @@ const {
|
||||
containerElement: document.body,
|
||||
onMove: (event) => {
|
||||
// Prevent dragging the menu over the top of the tabs
|
||||
if (position.value === 'Top') {
|
||||
const minY = topMenuRef?.value?.getBoundingClientRect().top ?? 40
|
||||
if (event.y < minY) {
|
||||
event.y = minY
|
||||
}
|
||||
const minY = tabContainer?.getBoundingClientRect().bottom ?? 40
|
||||
if (event.y < minY) {
|
||||
event.y = minY
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -202,39 +218,38 @@ const adjustMenuPosition = () => {
|
||||
|
||||
useEventListener(window, 'resize', adjustMenuPosition)
|
||||
|
||||
const topMenuBounds = useElementBounding(topMenuRef)
|
||||
const overlapThreshold = 20 // pixels
|
||||
const isOverlappingWithTopMenu = computed(() => {
|
||||
if (!panelRef.value) {
|
||||
return false
|
||||
// Drop zone state
|
||||
const isMouseOverDropZone = ref(false)
|
||||
|
||||
// Mouse event handlers for self-contained drop zone
|
||||
const onMouseEnterDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = true
|
||||
}
|
||||
const { height } = panelRef.value.getBoundingClientRect()
|
||||
const actionbarBottom = y.value + height
|
||||
const topMenuBottom = topMenuBounds.bottom.value
|
||||
}
|
||||
|
||||
const overlapPixels =
|
||||
Math.min(actionbarBottom, topMenuBottom) -
|
||||
Math.max(y.value, topMenuBounds.top.value)
|
||||
return overlapPixels > overlapThreshold
|
||||
})
|
||||
const onMouseLeaveDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDragging, (newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
isDocked.value = isOverlappingWithTopMenu.value
|
||||
// Handle drag state changes
|
||||
watch(isDragging, (dragging) => {
|
||||
if (dragging) {
|
||||
// Starting to drag - undock if docked
|
||||
if (isDocked.value) {
|
||||
isDocked.value = false
|
||||
}
|
||||
} else {
|
||||
// Start dragging
|
||||
isDocked.value = false
|
||||
// Stopped dragging - dock if mouse is over drop zone
|
||||
if (isMouseOverDropZone.value) {
|
||||
isDocked.value = true
|
||||
}
|
||||
// Reset drop zone state
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const eventBus = useEventBus<string>('topMenu')
|
||||
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
eventBus.emit('updateHighlight', {
|
||||
isDragging: dragging,
|
||||
isOverlapping: overlapping
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -242,17 +257,27 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
|
||||
.actionbar {
|
||||
pointer-events: all;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.actionbar.is-docked {
|
||||
position: static;
|
||||
@apply bg-transparent border-none p-0;
|
||||
.actionbar-drop-zone {
|
||||
width: 265px;
|
||||
border: 2px dashed var(--p-primary-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.actionbar-drop-zone.drop-zone-active {
|
||||
background: var(--p-highlight-background-focus);
|
||||
border-color: var(--p-primary-color);
|
||||
border-width: 3px;
|
||||
box-shadow: 0 0 20px var(--p-primary-color);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.actionbar.is-dragging {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
|
||||
@@ -3,17 +3,42 @@
|
||||
<Tabs
|
||||
:key="$i18n.locale"
|
||||
v-model:value="bottomPanelStore.activeBottomPanelTabId"
|
||||
style="--p-tabs-tablist-background: var(--comfy-menu-bg)"
|
||||
>
|
||||
<TabList pt:tab-list="border-none">
|
||||
<TabList
|
||||
pt:tab-list="border-none h-full flex items-center py-2 border-b-1 border-solid"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class="tabs-container">
|
||||
<Tab
|
||||
v-for="tab in bottomPanelStore.bottomPanelTabs"
|
||||
:key="tab.id"
|
||||
:value="tab.id"
|
||||
class="border-none p-3"
|
||||
class="m-1 mx-2 border-none"
|
||||
:class="{
|
||||
'tab-list-single-item':
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
}"
|
||||
:pt:root="
|
||||
(x: TabPassThroughMethodOptions) => ({
|
||||
class: {
|
||||
'p-3 rounded-lg': true,
|
||||
'pointer-events-none':
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
},
|
||||
style: {
|
||||
color: 'var(--fg-color)',
|
||||
backgroundColor:
|
||||
!x.context.active ||
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
? ''
|
||||
: 'var(--bg-color)'
|
||||
}
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="font-bold">
|
||||
<span class="font-normal">
|
||||
{{ getTabDisplayTitle(tab) }}
|
||||
</span>
|
||||
</Tab>
|
||||
@@ -56,6 +81,7 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tab from 'primevue/tab'
|
||||
import type { TabPassThroughMethodOptions } from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed } from 'vue'
|
||||
@@ -95,3 +121,9 @@ const closeBottomPanel = () => {
|
||||
bottomPanelStore.activePanel = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-tablist-active-bar) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||