Compare commits
22 Commits
remove-flo
...
custom-dia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bcdd6131b | ||
|
|
f320b76f0c | ||
|
|
39ff48240a | ||
|
|
c3867a1740 | ||
|
|
519adccbd5 | ||
|
|
a0f67d1185 | ||
|
|
bbc2e27498 | ||
|
|
47012b348b | ||
|
|
7e51f86cb2 | ||
|
|
abd3a83a37 | ||
|
|
948bf3228b | ||
|
|
d854bfad6e | ||
|
|
3cd9c78e34 | ||
|
|
cf030b0ad7 | ||
|
|
3fd922f301 | ||
|
|
26b85fcf22 | ||
|
|
69f5e65f7b | ||
|
|
03e92c5eb7 | ||
|
|
dcfcdc74a2 | ||
|
|
b040bb4aff | ||
|
|
d33da760d0 | ||
|
|
19c85efc25 |
@@ -67,9 +67,9 @@ This is critical for better file inspection:
|
||||
|
||||
Use git locally for much faster analysis:
|
||||
|
||||
1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
|
||||
2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
|
||||
3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
|
||||
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
|
||||
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
|
||||
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
|
||||
|
||||
### Step 1.5: Create Analysis Cache
|
||||
|
||||
|
||||
@@ -7,21 +7,3 @@ c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
||||
|
||||
# Reorganize renderer components into domain-driven folder structure
|
||||
c8a83a9caede7bdb5f8598c5492b07d08c339d49
|
||||
|
||||
# Domain-driven design (DDD) refactors - September 2025
|
||||
# These commits reorganized the codebase into domain-driven architecture
|
||||
|
||||
# [refactor] Improve renderer domain organization (#5552)
|
||||
6349ceee6c0a57fc7992e85635def9b6e22eaeb2
|
||||
|
||||
# [refactor] Improve settings domain organization (#5550)
|
||||
4c8c4a1ad4f53354f700a33ea1b95262aeda2719
|
||||
|
||||
# [refactor] Improve workflow domain organization (#5584)
|
||||
ca312fd1eab540cc4ddc0e3d244d38b3858574f0
|
||||
|
||||
# [refactor] Move thumbnail functionality to renderer/core domain (#5586)
|
||||
e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
|
||||
|
||||
# [refactor] Improve updates/notifications domain organization (#5590)
|
||||
27ab355f9c73415dc39f4d3f512b02308f847801
|
||||
|
||||
2
.gitattributes
vendored
@@ -13,4 +13,4 @@
|
||||
|
||||
# Generated files
|
||||
src/types/comfyRegistryTypes.ts linguist-generated=true
|
||||
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true
|
||||
src/types/generatedManagerTypes.ts linguist-generated=true
|
||||
|
||||
109
.github/workflows/backport.yaml
vendored
@@ -4,25 +4,10 @@ on:
|
||||
pull_request_target:
|
||||
types: [closed, labeled]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to backport'
|
||||
required: true
|
||||
type: string
|
||||
force_rerun:
|
||||
description: 'Force rerun even if backports exist'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: >
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -30,35 +15,6 @@ jobs:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Validate inputs for manual triggers
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
# Validate PR number format
|
||||
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::Invalid PR number format. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PR exists and is merged
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
|
||||
if [ "$MERGED" != "true" ]; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PR has needs-backport label
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -73,7 +29,7 @@ jobs:
|
||||
id: check-existing
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
# Check for existing backport PRs for this PR number
|
||||
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
|
||||
@@ -83,13 +39,6 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For manual triggers with force_rerun, proceed anyway
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Force rerun requested - existing backports will be updated"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found existing backport PRs:"
|
||||
echo "$EXISTING_BACKPORTS"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
@@ -101,17 +50,8 @@ jobs:
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
|
||||
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
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
for label in $(echo "$LABELS" | jq -r '.[].name'); 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
|
||||
@@ -135,20 +75,12 @@ jobs:
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: backport
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
run: |
|
||||
FAILED=""
|
||||
SUCCESS=""
|
||||
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
@@ -201,18 +133,10 @@ jobs:
|
||||
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
run: |
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
|
||||
@@ -241,16 +165,9 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
|
||||
139
.github/workflows/publish-frontend-types.yaml
vendored
@@ -1,139 +0,0 @@
|
||||
name: Publish Frontend Types
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish (e.g., 1.26.7)'
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
description: 'npm dist-tag to use'
|
||||
required: true
|
||||
default: latest
|
||||
type: string
|
||||
ref:
|
||||
description: 'Git ref to checkout (commit SHA, tag, or branch)'
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
required: false
|
||||
type: string
|
||||
default: latest
|
||||
ref:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-frontend-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish_types_manual:
|
||||
name: Publish @comfyorg/comfyui-frontend-types
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ inputs.version }}"
|
||||
SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
|
||||
if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
|
||||
echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Determine ref to checkout
|
||||
id: resolve_ref
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF="${{ inputs.ref }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
if [ -n "$REF" ]; then
|
||||
if ! git check-ref-format --allow-onelevel "$REF"; then
|
||||
echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.resolve_ref.outputs.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
|
||||
- name: Build types
|
||||
run: pnpm build:types
|
||||
|
||||
- name: Verify version matches input
|
||||
id: verify
|
||||
shell: bash
|
||||
run: |
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
TYPES_PKG_VERSION=$(node -p "require('./dist/package.json').version")
|
||||
if [ "$PKG_VERSION" != "${{ inputs.version }}" ]; then
|
||||
echo "Error: package.json version $PKG_VERSION does not match input ${{ inputs.version }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$TYPES_PKG_VERSION" != "${{ inputs.version }}" ]; then
|
||||
echo "Error: dist/package.json version $TYPES_PKG_VERSION does not match input ${{ inputs.version }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if version already on npm
|
||||
id: check_npm
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NAME=$(node -p "require('./dist/package.json').name")
|
||||
VER="${{ steps.verify.outputs.version }}"
|
||||
STATUS=0
|
||||
OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$?
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish."
|
||||
else
|
||||
if echo "$OUTPUT" | grep -q "E404"; then
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::error title=Registry lookup failed::$OUTPUT" >&2
|
||||
exit "$STATUS"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Publish package
|
||||
if: steps.check_npm.outputs.exists == 'false'
|
||||
run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks
|
||||
working-directory: dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
42
.github/workflows/release.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -126,8 +126,34 @@ jobs:
|
||||
|
||||
publish_types:
|
||||
needs: build
|
||||
uses: ./.github/workflows/publish-frontend-types.yaml
|
||||
with:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
secrets: inherit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
tsconfig.tsbuildinfo
|
||||
dist
|
||||
key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
types-tools-cache-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build:types
|
||||
- name: Publish package
|
||||
run: pnpm publish --access public
|
||||
working-directory: dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
6
.gitignore
vendored
@@ -51,7 +51,7 @@ tests-ui/workflows/examples
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
browser_tests/**/*-win32.png
|
||||
browser_tests/local/
|
||||
browser-tests/local/
|
||||
|
||||
.env
|
||||
|
||||
@@ -78,8 +78,8 @@ vite.config.mts.timestamp-*.mjs
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
# MCP Servers
|
||||
.playwright-mcp/*
|
||||
|
||||
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
@@ -15,32 +15,21 @@ const config: StorybookConfig = {
|
||||
async viteFinal(config) {
|
||||
// Use dynamic import to avoid CJS deprecation warning
|
||||
const { mergeConfig } = await import('vite')
|
||||
const { default: tailwindcss } = await import('@tailwindcss/vite')
|
||||
|
||||
// Filter out any plugins that might generate import maps
|
||||
if (config.plugins) {
|
||||
config.plugins = config.plugins
|
||||
// Type guard: ensure we have valid plugin objects with names
|
||||
.filter(
|
||||
(plugin): plugin is NonNullable<typeof plugin> & { name: string } => {
|
||||
return (
|
||||
plugin !== null &&
|
||||
plugin !== undefined &&
|
||||
typeof plugin === 'object' &&
|
||||
'name' in plugin &&
|
||||
typeof plugin.name === 'string'
|
||||
)
|
||||
}
|
||||
)
|
||||
// Business logic: filter out import-map plugins
|
||||
.filter((plugin) => !plugin.name.includes('import-map'))
|
||||
config.plugins = config.plugins.filter((plugin: any) => {
|
||||
if (plugin && plugin.name && plugin.name.includes('import-map')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return mergeConfig(config, {
|
||||
// Replace plugins entirely to avoid inheritance issues
|
||||
plugins: [
|
||||
// Only include plugins we explicitly need for Storybook
|
||||
tailwindcss(),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { setup } from '@storybook/vue3'
|
||||
import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
|
||||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -9,9 +9,11 @@ import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
|
||||
import '@/assets/css/style.css'
|
||||
import { i18n } from '@/i18n'
|
||||
import '@/lib/litegraph/public/css/litegraph.css'
|
||||
import '../src/assets/css/style.css'
|
||||
import { i18n } from '../src/i18n'
|
||||
import '../src/lib/litegraph/public/css/litegraph.css'
|
||||
import { useWidgetStore } from '../src/stores/widgetStore'
|
||||
import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
|
||||
|
||||
const ComfyUIPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
@@ -23,11 +25,13 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
// Setup Vue app for Storybook
|
||||
setup((app) => {
|
||||
app.directive('tooltip', Tooltip)
|
||||
|
||||
// Create Pinia instance
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// Initialize stores
|
||||
useColorPaletteStore(pinia)
|
||||
useWidgetStore(pinia)
|
||||
|
||||
app.use(i18n)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
@@ -46,8 +50,8 @@ setup((app) => {
|
||||
app.use(ToastService)
|
||||
})
|
||||
|
||||
// Theme and dialog decorator
|
||||
export const withTheme = (Story: StoryFn, context: StoryContext) => {
|
||||
// Dark theme decorator
|
||||
export const withTheme = (Story: any, context: any) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
|
||||
// Apply theme class to document root
|
||||
@@ -59,7 +63,7 @@ export const withTheme = (Story: StoryFn, context: StoryContext) => {
|
||||
document.body.classList.remove('dark-theme')
|
||||
}
|
||||
|
||||
return Story(context.args, context)
|
||||
return Story()
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4}
|
||||
@@ -5,7 +5,7 @@ import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '../../src/schemas/comfyWorkflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import { Page, test as base } from '@playwright/test'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ComfyNodeSearchFilterSelectionPanel {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
import { ComfyPage } from '../ComfyPage'
|
||||
|
||||
export class SettingDialog {
|
||||
constructor(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
class SidebarTab {
|
||||
constructor(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
|
||||
export class Topbar {
|
||||
private readonly menuLocator: Locator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '../../../src/schemas/comfyWorkflowSchema'
|
||||
import { ManageGroupNode } from '../../helpers/manageGroupNode'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
import type { Position, Size } from '../types'
|
||||
|
||||
@@ -12,10 +12,9 @@ export const webSocketFixture = base.extend<{
|
||||
// so we can look it up to trigger messages
|
||||
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
|
||||
window.WebSocket = class extends window.WebSocket {
|
||||
constructor(
|
||||
...rest: ConstructorParameters<typeof window.WebSocket>
|
||||
) {
|
||||
super(...rest)
|
||||
constructor() {
|
||||
// @ts-expect-error
|
||||
super(...arguments)
|
||||
store[this.url] = this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
|
||||
interface FitToViewOptions {
|
||||
selectionOnly?: boolean
|
||||
zoom?: number
|
||||
padding?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantly fits the canvas view to graph content without waiting for UI animation.
|
||||
*
|
||||
* Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented.
|
||||
*/
|
||||
export async function fitToViewInstant(
|
||||
comfyPage: ComfyPage,
|
||||
options: FitToViewOptions = {}
|
||||
) {
|
||||
const { selectionOnly = false, zoom = 0.75, padding = 10 } = options
|
||||
|
||||
const rectangles = await comfyPage.page.evaluate<
|
||||
ReadOnlyRect[] | null,
|
||||
{ selectionOnly: boolean }
|
||||
>(
|
||||
({ selectionOnly }) => {
|
||||
const app = window['app']
|
||||
if (!app?.canvas) return null
|
||||
|
||||
const canvas = app.canvas
|
||||
const items = (() => {
|
||||
if (selectionOnly && canvas.selectedItems?.size) {
|
||||
return Array.from(canvas.selectedItems)
|
||||
}
|
||||
try {
|
||||
return Array.from(canvas.positionableItems ?? [])
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})()
|
||||
|
||||
if (!items.length) return null
|
||||
|
||||
const rects: ReadOnlyRect[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const rect = item?.boundingRect
|
||||
if (!rect) continue
|
||||
|
||||
const x = Number(rect[0])
|
||||
const y = Number(rect[1])
|
||||
const width = Number(rect[2])
|
||||
const height = Number(rect[3])
|
||||
|
||||
rects.push([x, y, width, height] as const)
|
||||
}
|
||||
|
||||
return rects.length ? rects : null
|
||||
},
|
||||
{ selectionOnly }
|
||||
)
|
||||
|
||||
if (!rectangles || rectangles.length === 0) return
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const [x, y, width, height] of rectangles) {
|
||||
minX = Math.min(minX, Number(x))
|
||||
minY = Math.min(minY, Number(y))
|
||||
maxX = Math.max(maxX, Number(x) + Number(width))
|
||||
maxY = Math.max(maxY, Number(y) + Number(height))
|
||||
}
|
||||
|
||||
const hasFiniteBounds =
|
||||
Number.isFinite(minX) &&
|
||||
Number.isFinite(minY) &&
|
||||
Number.isFinite(maxX) &&
|
||||
Number.isFinite(maxY)
|
||||
|
||||
if (!hasFiniteBounds) return
|
||||
|
||||
const bounds: ReadOnlyRect = [
|
||||
minX - padding,
|
||||
minY - padding,
|
||||
maxX - minX + 2 * padding,
|
||||
maxY - minY + 2 * padding
|
||||
]
|
||||
|
||||
await comfyPage.page.evaluate(
|
||||
({ bounds, zoom }) => {
|
||||
const app = window['app']
|
||||
if (!app?.canvas) return
|
||||
|
||||
const canvas = app.canvas
|
||||
canvas.ds.fitToBounds(bounds, { zoom })
|
||||
canvas.setDirty(true, true)
|
||||
},
|
||||
{ bounds, zoom }
|
||||
)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import type {
|
||||
import {
|
||||
TemplateInfo,
|
||||
WorkflowTemplates
|
||||
} from '../../src/platform/workflow/templates/types/template'
|
||||
} from '../../src/types/workflowTemplateTypes'
|
||||
|
||||
export class ComfyTemplates {
|
||||
readonly content: Locator
|
||||
|
||||
@@ -29,9 +29,9 @@ test.describe('Actionbar', () => {
|
||||
|
||||
// Intercept the prompt queue endpoint
|
||||
let promptNumber = 0
|
||||
await comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
await route.fulfill({
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
prompt_id: promptNumber,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
|
||||
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { SettingParams } from '../../src/platform/settings/types'
|
||||
import { SettingParams } from '../../src/types/settingTypes'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
@@ -247,7 +247,7 @@ test.describe('Topbar commands', () => {
|
||||
test.describe('Dialog', () => {
|
||||
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
void window['app'].extensionManager.dialog
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: 'Test Prompt',
|
||||
message: 'Test Prompt Message'
|
||||
@@ -267,7 +267,7 @@ test.describe('Topbar commands', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
void window['app'].extensionManager.dialog
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
@@ -284,7 +284,7 @@ test.describe('Topbar commands', () => {
|
||||
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['value'] = 'foo'
|
||||
void window['app'].extensionManager.dialog
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Position } from '@vueuse/core'
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
import { Position } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -1013,8 +1012,6 @@ test.describe('Canvas Navigation', () => {
|
||||
test('Shift + mouse wheel should pan canvas horizontally', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning')
|
||||
|
||||
await comfyPage.page.click('canvas')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 98 KiB |
@@ -1,7 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Remote COMBO Widget', () => {
|
||||
const mockOptions = ['d', 'c', 'b', 'a']
|
||||
|
||||
@@ -160,9 +160,7 @@ test.describe.skip('Queue sidebar', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
@@ -176,9 +174,7 @@ test.describe.skip('Queue sidebar', () => {
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
@@ -200,9 +196,7 @@ test.describe.skip('Queue sidebar', () => {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(end)
|
||||
).toBeVisible()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Version Mismatch Warnings', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('NodeHeader', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
|
||||
import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../fixtures/ComfyPage'
|
||||
import { fitToViewInstant } from '../../helpers/fitToView'
|
||||
|
||||
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
||||
const box = await locator.boundingBox()
|
||||
if (!box) throw new Error('Slot bounding box not available')
|
||||
return {
|
||||
x: box.x + box.width / 2,
|
||||
y: box.y + box.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Vue Node Link Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('vueNodes/simple-triple')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await fitToViewInstant(comfyPage)
|
||||
})
|
||||
|
||||
test('should show a link dragging out from a slot when dragging on a slot', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||
|
||||
const samplerNode = samplerNodes[0]
|
||||
const outputSlot = await samplerNode.getOutput(0)
|
||||
await outputSlot.removeLinks()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const slotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||
const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`)
|
||||
await expect(slotLocator).toBeVisible()
|
||||
|
||||
const start = await getCenter(slotLocator)
|
||||
const canvasBox = await comfyPage.canvas.boundingBox()
|
||||
if (!canvasBox) throw new Error('Canvas bounding box not available')
|
||||
|
||||
// Arbitrary value
|
||||
const dragTarget = {
|
||||
x: start.x + 180,
|
||||
y: start.y - 140
|
||||
}
|
||||
|
||||
await comfyMouse.move(start)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
try {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-dragging-link.png'
|
||||
)
|
||||
} finally {
|
||||
await comfyMouse.drop()
|
||||
}
|
||||
})
|
||||
|
||||
test('should create a link when dropping on a compatible slot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||
const samplerNode = samplerNodes[0]
|
||||
|
||||
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
||||
expect(vaeNodes.length).toBeGreaterThan(0)
|
||||
const vaeNode = vaeNodes[0]
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||
const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true)
|
||||
|
||||
const outputSlot = comfyPage.page.locator(
|
||||
`[data-slot-key="${outputSlotKey}"]`
|
||||
)
|
||||
const inputSlot = comfyPage.page.locator(
|
||||
`[data-slot-key="${inputSlotKey}"]`
|
||||
)
|
||||
|
||||
await expect(outputSlot).toBeVisible()
|
||||
await expect(inputSlot).toBeVisible()
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
||||
|
||||
const linkDetails = await comfyPage.page.evaluate((sourceId) => {
|
||||
const app = window['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) return null
|
||||
|
||||
const source = graph.getNodeById(sourceId)
|
||||
if (!source) return null
|
||||
|
||||
const linkId = source.outputs[0]?.links?.[0]
|
||||
if (linkId == null) return null
|
||||
|
||||
const link = graph.links[linkId]
|
||||
if (!link) return null
|
||||
|
||||
return {
|
||||
originId: link.origin_id,
|
||||
originSlot: link.origin_slot,
|
||||
targetId: link.target_id,
|
||||
targetSlot: link.target_slot
|
||||
}
|
||||
}, samplerNode.id)
|
||||
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails).toMatchObject({
|
||||
originId: samplerNode.id,
|
||||
originSlot: 0,
|
||||
targetId: vaeNode.id,
|
||||
targetSlot: 0
|
||||
})
|
||||
})
|
||||
|
||||
test('should not create a link when slot types are incompatible', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||
const samplerNode = samplerNodes[0]
|
||||
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
expect(clipNodes.length).toBeGreaterThan(0)
|
||||
const clipNode = clipNodes[0]
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const clipInput = await clipNode.getInput(0)
|
||||
|
||||
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||
const inputSlotKey = getSlotKey(String(clipNode.id), 0, true)
|
||||
|
||||
const outputSlot = comfyPage.page.locator(
|
||||
`[data-slot-key="${outputSlotKey}"]`
|
||||
)
|
||||
const inputSlot = comfyPage.page.locator(
|
||||
`[data-slot-key="${inputSlotKey}"]`
|
||||
)
|
||||
|
||||
await expect(outputSlot).toBeVisible()
|
||||
await expect(inputSlot).toBeVisible()
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await clipInput.getLinkCount()).toBe(0)
|
||||
|
||||
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
|
||||
const app = window['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) return 0
|
||||
|
||||
const source = graph.getNodeById(sourceId)
|
||||
if (!source) return 0
|
||||
|
||||
return source.outputs[0]?.links?.length ?? 0
|
||||
}, samplerNode.id)
|
||||
|
||||
expect(graphLinkCount).toBe(0)
|
||||
})
|
||||
|
||||
test('should not create a link when dropping onto a slot on the same node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(samplerNodes.length).toBeGreaterThan(0)
|
||||
const samplerNode = samplerNodes[0]
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const samplerInput = await samplerNode.getInput(3)
|
||||
|
||||
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
||||
const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true)
|
||||
|
||||
const outputSlot = comfyPage.page.locator(
|
||||
`[data-slot-key="${outputSlotKey}"]`
|
||||
)
|
||||
const inputSlot = comfyPage.page.locator(
|
||||
`[data-slot-key="${inputSlotKey}"]`
|
||||
)
|
||||
|
||||
await expect(outputSlot).toBeVisible()
|
||||
await expect(inputSlot).toBeVisible()
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await samplerInput.getLinkCount()).toBe(0)
|
||||
|
||||
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
|
||||
const app = window['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) return 0
|
||||
|
||||
const source = graph.getNodeById(sourceId)
|
||||
if (!source) return 0
|
||||
|
||||
return source.outputs[0]?.links?.length ?? 0
|
||||
}, samplerNode.id)
|
||||
|
||||
expect(graphLinkCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 53 KiB |
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import type { Plugin } from 'vite'
|
||||
import { Plugin } from 'vite'
|
||||
|
||||
interface ShimResult {
|
||||
code: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import glob from 'fast-glob'
|
||||
import fs from 'fs-extra'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
|
||||
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
|
||||
|
||||
interface ImportMapSource {
|
||||
name: string
|
||||
|
||||
@@ -5,14 +5,13 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import vueParser from 'vue-eslint-parser'
|
||||
|
||||
const extraFileExtensions = ['.vue']
|
||||
|
||||
export default defineConfig([
|
||||
export default [
|
||||
{
|
||||
files: ['src/**/*.{js,mjs,cjs,ts,vue}']
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'src/scripts/*',
|
||||
@@ -25,49 +24,35 @@ export default defineConfig([
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ['./**/*.{ts,mts}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||
},
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
projectService: true,
|
||||
tsConfigRootDir: import.meta.dirname,
|
||||
project: ['./tsconfig.json', './tsconfig.eslint.json'],
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
extraFileExtensions
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['./**/*.vue'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||
},
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
projectService: true,
|
||||
tsConfigRootDir: import.meta.dirname,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
extraFileExtensions
|
||||
extraFileExtensions: ['.vue']
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
pluginVue.configs['flat/recommended'],
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
eslintPluginPrettierRecommended,
|
||||
storybook.configs['flat/recommended'],
|
||||
{
|
||||
files: ['src/**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
'@intlify/vue-i18n': pluginI18n
|
||||
},
|
||||
rules: {
|
||||
@@ -75,8 +60,6 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/no-import-type-side-effects': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
@@ -84,7 +67,6 @@ export default defineConfig([
|
||||
'vue/multi-word-component-names': 'off', // TODO: fix
|
||||
'vue/no-template-shadow': 'off', // TODO: fix
|
||||
'vue/one-component-per-file': 'off', // TODO: fix
|
||||
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
|
||||
// Restrict deprecated PrimeVue components
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
@@ -154,13 +136,5 @@ export default defineConfig([
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['tests-ui/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ disallowTypeAnnotations: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
...storybook.configs['flat/recommended']
|
||||
]
|
||||
@@ -22,12 +22,10 @@ const config: KnipConfig = {
|
||||
],
|
||||
ignore: [
|
||||
// Auto generated manager types
|
||||
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
'src/types/comfyRegistryTypes.ts',
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Staged for for use with subgraph widget promotion
|
||||
'src/lib/litegraph/src/widgets/DisconnectedWidget.ts'
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -3,13 +3,13 @@ export default {
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
'vue-tsc --noEmit'
|
||||
]
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
return [
|
||||
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
|
||||
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
|
||||
`eslint --fix ${fileNames.join(' ')}`,
|
||||
`prettier --write ${fileNames.join(' ')}`
|
||||
]
|
||||
}
|
||||
|
||||
31
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.28.0",
|
||||
"version": "1.27.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -14,9 +14,9 @@
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:browser": "npx nx e2e",
|
||||
"test:unit": "nx run test tests-ui/tests",
|
||||
@@ -38,10 +38,10 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@iconify-json/lucide": "^1.2.66",
|
||||
"@iconify/tailwind": "^1.2.0",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
|
||||
"@lobehub/i18n-cli": "^1.25.1",
|
||||
"@nx/eslint": "21.4.1",
|
||||
"@nx/playwright": "21.4.1",
|
||||
@@ -64,11 +64,11 @@
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-storybook": "^9.1.6",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-storybook": "^9.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globals": "^15.9.0",
|
||||
"happy-dom": "^15.11.0",
|
||||
@@ -79,30 +79,29 @@
|
||||
"lint-staged": "^15.2.7",
|
||||
"nx": "21.4.1",
|
||||
"prettier": "^3.3.2",
|
||||
"storybook": "^9.1.6",
|
||||
"storybook": "^9.1.1",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"tsx": "^4.15.6",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"unplugin-icons": "^0.22.0",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-vue-devtools": "^7.7.6",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.7",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.72",
|
||||
"@iconify/json": "^2.2.380",
|
||||
"@primeuix/forms": "0.0.2",
|
||||
"@primeuix/styled": "0.3.2",
|
||||
|
||||
1058
pnpm-lock.yaml
generated
BIN
public/fonts/abc-rom/abc-rom-black-italic.woff2
Normal file
BIN
public/fonts/abc-rom/abc-rom-medium.woff2
Normal file
BIN
public/fonts/abc-rom/abc-rom-regular.woff2
Normal file
@@ -2,10 +2,9 @@ import * as fs from 'fs'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
|
||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
||||
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
||||
import type { FormItem, SettingParams } from '../src/types/settingTypes'
|
||||
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
@@ -132,23 +131,6 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
||||
])
|
||||
)
|
||||
|
||||
// Desktop Dialogs
|
||||
const allDesktopDialogsLocale = Object.fromEntries(
|
||||
Object.values(DESKTOP_DIALOGS).map((dialog) => [
|
||||
normalizeI18nKey(dialog.id),
|
||||
{
|
||||
title: dialog.title,
|
||||
message: dialog.message,
|
||||
buttons: Object.fromEntries(
|
||||
dialog.buttons.map((button) => [
|
||||
normalizeI18nKey(button.label),
|
||||
button.label
|
||||
])
|
||||
)
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
localePath,
|
||||
JSON.stringify(
|
||||
@@ -162,8 +144,7 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
||||
...allSettingCategoriesLocale
|
||||
},
|
||||
serverConfigItems: allServerConfigsLocale,
|
||||
serverConfigCategories: allServerConfigCategoriesLocale,
|
||||
desktopDialogs: allDesktopDialogsLocale
|
||||
serverConfigCategories: allServerConfigCategoriesLocale
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
@@ -5,7 +5,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
import {
|
||||
zComfyWorkflow,
|
||||
zComfyWorkflow1
|
||||
} from '../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
} from '../src/schemas/comfyWorkflowSchema'
|
||||
import { zComfyNodeDef as zComfyNodeDefV2 } from '../src/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { zComfyNodeDef as zComfyNodeDefV1 } from '../src/schemas/nodeDefSchema'
|
||||
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
/* Inter Font Family */
|
||||
/* ABC ROM Font Family */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/fonts/inter-latin-normal.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-family: 'ABC ROM Medium';
|
||||
src: url('/fonts/abc-rom/abc-rom-medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/fonts/inter-latin-italic.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-family: 'ABC ROM Regular';
|
||||
src: url('/fonts/abc-rom/abc-rom-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ABC ROM Black Italic';
|
||||
src: url('/fonts/abc-rom/abc-rom-black-italic.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Simplified font family names for easier use */
|
||||
@font-face {
|
||||
font-family: 'ABC ROM';
|
||||
src: url('/fonts/abc-rom/abc-rom-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ABC ROM';
|
||||
src: url('/fonts/abc-rom/abc-rom-medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ABC ROM';
|
||||
src: url('/fonts/abc-rom/abc-rom-black-italic.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
@layer theme, base, primevue, components, utilities;
|
||||
|
||||
@import './fonts.css';
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
@import 'tw-animate-css';
|
||||
@@ -53,20 +52,15 @@
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
/* Font Families */
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
|
||||
/* Palette Colors */
|
||||
--color-charcoal-100: #55565e;
|
||||
--color-charcoal-200: #494a50;
|
||||
--color-charcoal-300: #3c3d42;
|
||||
--color-charcoal-400: #313235;
|
||||
--color-charcoal-500: #2d2e32;
|
||||
--color-charcoal-600: #262729;
|
||||
--color-charcoal-700: #202121;
|
||||
--color-charcoal-800: #171718;
|
||||
|
||||
--color-neutral-550: #636363;
|
||||
--color-charcoal-100: #171718;
|
||||
--color-charcoal-200: #202121;
|
||||
--color-charcoal-300: #262729;
|
||||
--color-charcoal-400: #2d2e32;
|
||||
--color-charcoal-500: #313235;
|
||||
--color-charcoal-600: #3c3d42;
|
||||
--color-charcoal-700: #494a50;
|
||||
--color-charcoal-800: #55565e;
|
||||
|
||||
--color-stone-100: #444444;
|
||||
--color-stone-200: #828282;
|
||||
@@ -105,16 +99,11 @@
|
||||
--color-danger-100: #c02323;
|
||||
--color-danger-200: #d62952;
|
||||
|
||||
--color-coral-red-600: #973a40;
|
||||
--color-coral-red-500: #c53f49;
|
||||
--color-coral-red-400: #dd424e;
|
||||
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
--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-blue-selection: rgb( from var(--color-blue-100) r g b / 0.3);
|
||||
--color-node-hover-100: rgb( from var(--color-charcoal-800) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1);
|
||||
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
|
||||
|
||||
/* PrimeVue pulled colors */
|
||||
@@ -127,10 +116,10 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-node-component-surface: var(--color-charcoal-600);
|
||||
--color-node-component-surface: var(--color-charcoal-300);
|
||||
--color-node-component-surface-highlight: var(--color-slate-100);
|
||||
--color-node-component-surface-hovered: var(--color-charcoal-400);
|
||||
--color-node-component-surface-selected: var(--color-charcoal-200);
|
||||
--color-node-component-surface-hovered: var(--color-charcoal-500);
|
||||
--color-node-component-surface-selected: var(--color-charcoal-700);
|
||||
--color-node-stroke: var(--color-stone-100);
|
||||
}
|
||||
|
||||
@@ -142,7 +131,7 @@
|
||||
|
||||
@utility scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@@ -972,7 +961,9 @@ audio.comfy-audio.empty-audio-widget {
|
||||
/* Uses default styling - no overrides needed */
|
||||
}
|
||||
|
||||
/* Smooth transitions between LOD levels */
|
||||
.lg-node {
|
||||
transition: min-height 0.2s ease;
|
||||
/* Disable text selection on all nodes */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -50,7 +50,7 @@ import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { CSSProperties, computed, watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ import { storeToRefs } from 'pinia'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
const { batchCount } = storeToRefs(queueSettingsStore)
|
||||
|
||||
@@ -22,10 +22,9 @@ import {
|
||||
} 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 { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
|
||||
|
||||
@@ -3,38 +3,13 @@
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div ref="terminalEl" class="h-full terminal-host" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip.left="{
|
||||
value: tooltipText,
|
||||
showDelay: 300
|
||||
}"
|
||||
icon="pi pi-copy"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
:class="
|
||||
cn('absolute top-2 right-8 transition-opacity', {
|
||||
'opacity-0 pointer-events-none select-none': !isHovered
|
||||
})
|
||||
"
|
||||
:aria-label="tooltipText"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementHover, useEventListener } from '@vueuse/core'
|
||||
import type { IDisposable } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Ref, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
created: [ReturnType<typeof useTerminal>, Ref<HTMLElement | undefined>]
|
||||
@@ -42,59 +17,9 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
const terminalEl = ref<HTMLElement | undefined>()
|
||||
const rootEl = ref<HTMLElement | undefined>()
|
||||
const hasSelection = ref(false)
|
||||
emit('created', useTerminal(terminalEl), rootEl)
|
||||
|
||||
const isHovered = useElementHover(rootEl)
|
||||
|
||||
const terminalData = useTerminal(terminalEl)
|
||||
emit('created', terminalData, ref(rootEl))
|
||||
|
||||
const { terminal } = terminalData
|
||||
let selectionDisposable: IDisposable | undefined
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
return hasSelection.value
|
||||
? t('serverStart.copySelectionTooltip')
|
||||
: t('serverStart.copyAllTooltip')
|
||||
})
|
||||
|
||||
const handleCopy = async () => {
|
||||
const existingSelection = terminal.getSelection()
|
||||
const shouldSelectAll = !existingSelection
|
||||
if (shouldSelectAll) terminal.selectAll()
|
||||
|
||||
const selectedText = shouldSelectAll
|
||||
? terminal.getSelection()
|
||||
: existingSelection
|
||||
|
||||
if (selectedText) {
|
||||
await navigator.clipboard.writeText(selectedText)
|
||||
|
||||
if (shouldSelectAll) {
|
||||
terminal.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showContextMenu = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
electronAPI()?.showContextMenu({ type: 'text' })
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
useEventListener(terminalEl, 'contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectionDisposable = terminal.onSelectionChange(() => {
|
||||
hasSelection.value = terminal.hasSelection()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
selectionDisposable?.dispose()
|
||||
emit('unmounted')
|
||||
})
|
||||
onUnmounted(() => emit('unmounted'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDisposable } from '@xterm/xterm'
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { IDisposable } from '@xterm/xterm'
|
||||
import { Ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
import { until } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Ref, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import type { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
|
||||
import { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ import { computed, onUpdated, ref, watch } from 'vue'
|
||||
|
||||
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { forEachSubgraphNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
const MIN_WIDTH = 28
|
||||
|
||||
@@ -47,21 +47,17 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import type { MenuState } from 'primevue/menu'
|
||||
import Menu from 'primevue/menu'
|
||||
import Menu, { MenuState } from 'primevue/menu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -36,8 +36,8 @@ import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const modelValue = defineModel<string>()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<InputText
|
||||
v-else
|
||||
ref="inputRef"
|
||||
v-model:model-value="inputValue"
|
||||
v-model:modelValue="inputValue"
|
||||
v-focus
|
||||
type="text"
|
||||
size="small"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
|
||||
import type { CustomExtension, VueExtension } from '@/types/extensionTypes'
|
||||
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
extension: VueExtension | CustomExtension
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<component
|
||||
:is="markRaw(getFormComponent(props.item))"
|
||||
:id="props.id"
|
||||
v-model:model-value="formValue"
|
||||
v-model:modelValue="formValue"
|
||||
:aria-labelledby="`${props.id}-label`"
|
||||
v-bind="getFormAttrs(props.item)"
|
||||
/>
|
||||
@@ -40,11 +40,10 @@ import BackgroundImageUpload from '@/components/common/BackgroundImageUpload.vue
|
||||
import CustomFormValue from '@/components/common/CustomFormValue.vue'
|
||||
import FormColorPicker from '@/components/common/FormColorPicker.vue'
|
||||
import FormImageUpload from '@/components/common/FormImageUpload.vue'
|
||||
import FormRadioGroup from '@/components/common/FormRadioGroup.vue'
|
||||
import InputKnob from '@/components/common/InputKnob.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import type { FormItem } from '@/platform/settings/types'
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
const props = defineProps<{
|
||||
@@ -67,7 +66,6 @@ function getFormAttrs(item: FormItem) {
|
||||
}
|
||||
switch (item.type) {
|
||||
case 'combo':
|
||||
case 'radio':
|
||||
attrs['options'] =
|
||||
typeof item.options === 'function'
|
||||
? // @ts-expect-error: Audit and deprecate usage of legacy options type:
|
||||
@@ -99,8 +97,6 @@ function getFormComponent(item: FormItem): Component {
|
||||
return InputKnob
|
||||
case 'combo':
|
||||
return Select
|
||||
case 'radio':
|
||||
return FormRadioGroup
|
||||
case 'image':
|
||||
return FormImageUpload
|
||||
case 'color':
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
import FormRadioGroup from './FormRadioGroup.vue'
|
||||
|
||||
describe('FormRadioGroup', () => {
|
||||
beforeAll(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: any, options = {}) => {
|
||||
return mount(FormRadioGroup, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { RadioButton }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
describe('normalizedOptions computed property', () => {
|
||||
it('handles string array options', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'option1',
|
||||
options: ['option1', 'option2', 'option3'],
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('option1')
|
||||
expect(radioButtons[1].props('value')).toBe('option2')
|
||||
expect(radioButtons[2].props('value')).toBe('option3')
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('option1')
|
||||
expect(labels[1].text()).toBe('option2')
|
||||
expect(labels[2].text()).toBe('option3')
|
||||
})
|
||||
|
||||
it('handles SettingOption array', () => {
|
||||
const options: SettingOption[] = [
|
||||
{ text: 'Small', value: 'sm' },
|
||||
{ text: 'Medium', value: 'md' },
|
||||
{ text: 'Large', value: 'lg' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'md',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('sm')
|
||||
expect(radioButtons[1].props('value')).toBe('md')
|
||||
expect(radioButtons[2].props('value')).toBe('lg')
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('Small')
|
||||
expect(labels[1].text()).toBe('Medium')
|
||||
expect(labels[2].text()).toBe('Large')
|
||||
})
|
||||
|
||||
it('handles SettingOption with undefined value (uses text as value)', () => {
|
||||
const options: SettingOption[] = [
|
||||
{ text: 'Option A', value: undefined },
|
||||
{ text: 'Option B' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Option A',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('Option A')
|
||||
expect(radioButtons[1].props('value')).toBe('Option B')
|
||||
})
|
||||
|
||||
it('handles custom object with optionLabel and optionValue', () => {
|
||||
const options = [
|
||||
{ name: 'First Option', id: 1 },
|
||||
{ name: 'Second Option', id: 2 },
|
||||
{ name: 'Third Option', id: 3 }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 2,
|
||||
options,
|
||||
optionLabel: 'name',
|
||||
optionValue: 'id',
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe(1)
|
||||
expect(radioButtons[1].props('value')).toBe(2)
|
||||
expect(radioButtons[2].props('value')).toBe(3)
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('First Option')
|
||||
expect(labels[1].text()).toBe('Second Option')
|
||||
expect(labels[2].text()).toBe('Third Option')
|
||||
})
|
||||
|
||||
it('handles mixed array with strings and SettingOptions', () => {
|
||||
const options: (string | SettingOption)[] = [
|
||||
'Simple String',
|
||||
{ text: 'Complex Option', value: 'complex' },
|
||||
'Another String'
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'complex',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('Simple String')
|
||||
expect(radioButtons[1].props('value')).toBe('complex')
|
||||
expect(radioButtons[2].props('value')).toBe('Another String')
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('Simple String')
|
||||
expect(labels[1].text()).toBe('Complex Option')
|
||||
expect(labels[2].text()).toBe('Another String')
|
||||
})
|
||||
|
||||
it('handles empty options array', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: null,
|
||||
options: [],
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles undefined options gracefully', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: null,
|
||||
options: undefined,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles object with missing properties gracefully', () => {
|
||||
const options = [
|
||||
{ label: 'Option 1', val: 'opt1' },
|
||||
{ text: 'Option 2', value: 'opt2' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'opt1',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(2)
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('Unknown')
|
||||
expect(labels[1].text()).toBe('Option 2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('component functionality', () => {
|
||||
it('sets correct input-id and name attributes', () => {
|
||||
const options = ['A', 'B']
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'A',
|
||||
options,
|
||||
id: 'my-radio-group'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
|
||||
expect(radioButtons[0].props('inputId')).toBe('my-radio-group-A')
|
||||
expect(radioButtons[0].props('name')).toBe('my-radio-group')
|
||||
expect(radioButtons[1].props('inputId')).toBe('my-radio-group-B')
|
||||
expect(radioButtons[1].props('name')).toBe('my-radio-group')
|
||||
})
|
||||
|
||||
it('associates labels with radio buttons correctly', () => {
|
||||
const options = ['Yes', 'No']
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Yes',
|
||||
options,
|
||||
id: 'confirm-radio'
|
||||
})
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
|
||||
expect(labels[0].attributes('for')).toBe('confirm-radio-Yes')
|
||||
expect(labels[1].attributes('for')).toBe('confirm-radio-No')
|
||||
})
|
||||
|
||||
it('sets aria-describedby attribute correctly', () => {
|
||||
const options: SettingOption[] = [
|
||||
{ text: 'Option 1', value: 'opt1' },
|
||||
{ text: 'Option 2', value: 'opt2' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'opt1',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
|
||||
expect(radioButtons[0].attributes('aria-describedby')).toBe(
|
||||
'Option 1-label'
|
||||
)
|
||||
expect(radioButtons[1].attributes('aria-describedby')).toBe(
|
||||
'Option 2-label'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row gap-4">
|
||||
<div
|
||||
v-for="option in normalizedOptions"
|
||||
:key="option.value"
|
||||
class="flex items-center"
|
||||
>
|
||||
<RadioButton
|
||||
:input-id="`${id}-${option.value}`"
|
||||
:name="id"
|
||||
:value="option.value"
|
||||
:model-value="modelValue"
|
||||
:aria-describedby="`${option.text}-label`"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
<label :for="`${id}-${option.value}`" class="ml-2 cursor-pointer">
|
||||
{{ option.text }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
options: (SettingOption | string)[]
|
||||
optionLabel?: string
|
||||
optionValue?: string
|
||||
id?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: any]
|
||||
}>()
|
||||
|
||||
const normalizedOptions = computed<SettingOption[]>(() => {
|
||||
if (!props.options) return []
|
||||
|
||||
return props.options.map((option) => {
|
||||
if (typeof option === 'string') {
|
||||
return { text: option, value: option }
|
||||
}
|
||||
|
||||
if ('text' in option) {
|
||||
return {
|
||||
text: option.text,
|
||||
value: option.value ?? option.text
|
||||
}
|
||||
}
|
||||
// Handle optionLabel/optionValue
|
||||
return {
|
||||
text: option[props.optionLabel || 'text'] || 'Unknown',
|
||||
value: option[props.optionValue || 'value']
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
class="absolute inset-0"
|
||||
/>
|
||||
<img
|
||||
v-if="cachedSrc"
|
||||
v-show="isImageLoaded"
|
||||
ref="imageRef"
|
||||
:src="cachedSrc"
|
||||
:alt="alt"
|
||||
@@ -77,8 +77,8 @@ const shouldLoad = computed(() => isIntersecting.value)
|
||||
|
||||
watch(
|
||||
shouldLoad,
|
||||
async (shouldLoadVal) => {
|
||||
if (shouldLoadVal && src && !cachedSrc.value && !hasError.value) {
|
||||
async (shouldLoad) => {
|
||||
if (shouldLoad && src && !cachedSrc.value && !hasError.value) {
|
||||
try {
|
||||
const cachedMedia = await getCachedMedia(src)
|
||||
if (cachedMedia.error) {
|
||||
@@ -93,7 +93,7 @@ watch(
|
||||
console.warn('Failed to load cached media:', error)
|
||||
cachedSrc.value = src
|
||||
}
|
||||
} else if (!shouldLoadVal) {
|
||||
} else if (!shouldLoad) {
|
||||
if (cachedSrc.value?.startsWith('blob:')) {
|
||||
releaseUrl(src)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
|
||||
import type { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||
import { PrimeVueSeverity } from '@/types/primeVueTypes'
|
||||
|
||||
const {
|
||||
disabled,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Tree
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:selection-keys="selectionKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
class="tree-explorer py-0 px-2 2xl:px-4"
|
||||
:class="props.class"
|
||||
:value="renderedRoot.children"
|
||||
|
||||
@@ -9,8 +9,10 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { InjectKeyHandleEditLabelFunction } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
RenderedTreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
// Create a mock i18n instance
|
||||
const i18n = createI18n({
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
@@ -94,9 +94,9 @@ import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ConfirmationDialogType } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const props = defineProps<{
|
||||
message: string
|
||||
|
||||
@@ -59,13 +59,14 @@ import { useI18n } from 'vue-i18n'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackInstallButton from './manager/button/PackInstallButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
@@ -137,7 +138,7 @@ const allMissingNodesInstalled = computed(() => {
|
||||
})
|
||||
// Watch for completion and close dialog
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (allInstalled && showInstallAllButton.value) {
|
||||
if (allInstalled) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -29,7 +28,7 @@ const defaultMockTaskLogs = [
|
||||
{ taskName: 'Task 2', logs: ['Log 3', 'Log 4'] }
|
||||
]
|
||||
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
taskLogs: [...defaultMockTaskLogs],
|
||||
succeededTasksLogs: [...defaultMockTaskLogs],
|
||||
@@ -88,7 +88,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { compare } from 'semver'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { compareVersions } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
missingCoreNodes: Record<string, LGraphNode[]>
|
||||
@@ -68,7 +68,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
|
||||
const sortedMissingCoreNodes = computed(() => {
|
||||
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
|
||||
// Sort by version in descending order (newest first)
|
||||
return compare(b, a) // Reversed for descending order
|
||||
return compareVersions(b, a) // Reversed for descending order
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="settings-container">
|
||||
<ScrollPanel class="settings-sidebar shrink-0 p-2 w-48 2xl:w-64">
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
v-model:modelValue="searchQuery"
|
||||
class="settings-search-box w-full mb-2"
|
||||
:placeholder="$t('g.searchSettings') + '...'"
|
||||
:debounce-time="128"
|
||||
@@ -66,17 +66,18 @@ import Tabs from 'primevue/tabs'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
||||
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import ColorPaletteMessage from '@/platform/settings/components/ColorPaletteMessage.vue'
|
||||
import SettingsPanel from '@/platform/settings/components/SettingsPanel.vue'
|
||||
import { useSettingSearch } from '@/platform/settings/composables/useSettingSearch'
|
||||
import { useSettingUI } from '@/platform/settings/composables/useSettingUI'
|
||||
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { useSettingUI } from '@/composables/setting/useSettingUI'
|
||||
import { SettingTreeNode } from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
|
||||
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
|
||||
import PanelTemplate from './setting/PanelTemplate.vue'
|
||||
import SettingsPanel from './setting/SettingsPanel.vue'
|
||||
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
@@ -148,7 +148,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="flex flex-1 relative overflow-hidden">
|
||||
<ManagerNavSidebar
|
||||
v-if="isSideNavOpen"
|
||||
v-model:selected-tab="selectedTab"
|
||||
v-model:selectedTab="selectedTab"
|
||||
:tabs="tabs"
|
||||
/>
|
||||
<div
|
||||
@@ -57,9 +57,9 @@
|
||||
</IconButton>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:search-mode="searchMode"
|
||||
v-model:sort-field="sortField"
|
||||
v-model:searchQuery="searchQuery"
|
||||
v-model:searchMode="searchMode"
|
||||
v-model:sortField="sortField"
|
||||
:search-results="searchResults"
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
@@ -143,24 +143,24 @@ import IconButton from '@/components/button/IconButton.vue'
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import ManagerNavSidebar from '@/components/dialog/content/manager/ManagerNavSidebar.vue'
|
||||
import InfoPanel from '@/components/dialog/content/manager/infoPanel/InfoPanel.vue'
|
||||
import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue'
|
||||
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
|
||||
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
|
||||
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
|
||||
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
||||
import { useManagerStatePersistence } from '@/composables/manager/useManagerStatePersistence'
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import ManagerNavSidebar from '@/workbench/extensions/manager/components/manager/ManagerNavSidebar.vue'
|
||||
import InfoPanel from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue'
|
||||
import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue'
|
||||
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
|
||||
import RegistrySearchBar from '@/workbench/extensions/manager/components/manager/registrySearchBar/RegistrySearchBar.vue'
|
||||
import GridSkeleton from '@/workbench/extensions/manager/components/manager/skeleton/GridSkeleton.vue'
|
||||
import { useManagerStatePersistence } from '@/workbench/extensions/manager/composables/useManagerStatePersistence'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { initialTab } = defineProps<{
|
||||
initialTab?: ManagerTab
|
||||
82
src/components/dialog/content/manager/ManagerHeader.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import ManagerHeader from './ManagerHeader.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMessages
|
||||
}
|
||||
})
|
||||
|
||||
describe('ManagerHeader', () => {
|
||||
const createWrapper = () => {
|
||||
return mount(ManagerHeader, {
|
||||
global: {
|
||||
plugins: [createPinia(), PrimeVue, i18n],
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
components: {
|
||||
Tag
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the component title', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('h2').text()).toBe(
|
||||
enMessages.manager.discoverCommunityContent
|
||||
)
|
||||
})
|
||||
|
||||
it('displays the legacy manager UI tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
|
||||
})
|
||||
|
||||
it('applies info severity to the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('p-tag-info')
|
||||
})
|
||||
|
||||
it('displays info icon in the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const icon = wrapper.find('.pi-info-circle')
|
||||
expect(icon.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has cursor-help class on the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('cursor-help')
|
||||
})
|
||||
|
||||
it('has proper structure with flex container', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
|
||||
expect(flexContainer.exists()).toBe(true)
|
||||
|
||||
const tag = flexContainer.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
25
src/components/dialog/content/manager/ManagerHeader.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
<div class="flex justify-end ml-auto pr-4 pl-2">
|
||||
<Tag
|
||||
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
:value="$t('manager.legacyManagerUI')"
|
||||
class="cursor-help ml-2"
|
||||
:pt="{
|
||||
root: { class: 'text-xs' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
</script>
|
||||
@@ -32,7 +32,7 @@ import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import type { TabItem } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
|
||||
defineProps<{
|
||||
tabs: TabItem[]
|
||||
@@ -169,7 +169,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import type {
|
||||
import {
|
||||
ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
@@ -19,7 +19,7 @@
|
||||
import Message from 'primevue/message'
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
@@ -35,7 +34,7 @@ const mockInstalledPacks = {
|
||||
|
||||
const mockIsPackEnabled = vi.fn(() => true)
|
||||
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
installedPacks: mockInstalledPacks,
|
||||
isPackInstalled: (id: string) =>
|
||||
@@ -43,13 +43,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import { valid as validSemver } from 'semver'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import PackVersionSelectorPopover from '@/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const TRUNCATED_HASH_LENGTH = 7
|
||||
|
||||
@@ -81,9 +81,7 @@ const installedVersion = computed(() => {
|
||||
'nightly'
|
||||
|
||||
// If Git hash, truncate to 7 characters
|
||||
return validSemver(version)
|
||||
? version
|
||||
: version.slice(0, TRUNCATED_HASH_LENGTH)
|
||||
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
|
||||
})
|
||||
|
||||
const toggleVersionSelector = (event: Event) => {
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -64,7 +63,7 @@ vi.mock('@/services/comfyRegistryService', () => ({
|
||||
}))
|
||||
|
||||
// Mock the manager store
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
installPack: {
|
||||
call: mockInstallPack,
|
||||
@@ -84,7 +84,6 @@ import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { valid as validSemver } from 'semver'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -93,10 +92,11 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
|
||||
type ManagerDatabaseSource =
|
||||
@@ -142,7 +142,7 @@ onMounted(() => {
|
||||
getInitialSelectedVersion() ?? SelectedVersionValues.LATEST
|
||||
selectedVersion.value =
|
||||
// Use NIGHTLY when version is a Git hash
|
||||
validSemver(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
|
||||
isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
|
||||
})
|
||||
|
||||
const getInitialSelectedVersion = () => {
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
@@ -8,7 +7,7 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
@@ -33,7 +32,7 @@ const mockNodePack = {
|
||||
const mockIsPackEnabled = vi.fn()
|
||||
const mockEnablePack = { call: vi.fn().mockResolvedValue(undefined) }
|
||||
const mockDisablePack = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
isPackEnabled: mockIsPackEnabled,
|
||||
enablePack: mockEnablePack,
|
||||
@@ -36,10 +36,10 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||