Merge branch 'main' into sno-fix-playwright-remove-declare-2

This commit is contained in:
snomiao
2025-09-23 07:00:23 +09:00
committed by GitHub
508 changed files with 22377 additions and 3775 deletions

View File

@@ -67,9 +67,9 @@ This is critical for better file inspection:
Use git locally for much faster analysis: Use git locally for much faster analysis:
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt` 1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt` 2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt` 3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
### Step 1.5: Create Analysis Cache ### Step 1.5: Create Analysis Cache

2
.gitattributes vendored
View File

@@ -13,4 +13,4 @@
# Generated files # Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true src/types/comfyRegistryTypes.ts linguist-generated=true
src/types/generatedManagerTypes.ts linguist-generated=true src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true

View File

@@ -4,10 +4,25 @@ on:
pull_request_target: pull_request_target:
types: [closed, labeled] types: [closed, labeled]
branches: [main] 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: jobs:
backport: backport:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-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'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -15,6 +30,35 @@ jobs:
issues: write issues: write
steps: 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 - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -29,7 +73,7 @@ jobs:
id: check-existing id: check-existing
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
run: | run: |
# Check for existing backport PRs for this PR number # 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') EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
@@ -39,6 +83,13 @@ jobs:
exit 0 exit 0
fi 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 "Found existing backport PRs:"
echo "$EXISTING_BACKPORTS" echo "$EXISTING_BACKPORTS"
echo "skip=true" >> $GITHUB_OUTPUT echo "skip=true" >> $GITHUB_OUTPUT
@@ -50,8 +101,17 @@ jobs:
run: | run: |
# Extract version labels (e.g., "1.24", "1.22") # Extract version labels (e.g., "1.24", "1.22")
VERSIONS="" VERSIONS=""
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
for label in $(echo "$LABELS" | jq -r '.[].name'); do if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# For manual triggers, get labels from the PR
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
# For automatic triggers, extract from PR event
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi
for label in $LABELS; do
# Match version labels like "1.24" (major.minor only) # Match version labels like "1.24" (major.minor only)
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Validate the branch exists before adding to list # Validate the branch exists before adding to list
@@ -75,12 +135,20 @@ jobs:
if: steps.check-existing.outputs.skip != 'true' if: steps.check-existing.outputs.skip != 'true'
id: backport id: backport
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
run: | run: |
FAILED="" FAILED=""
SUCCESS="" 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 for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}" echo "::group::Backporting to core/${version}"
@@ -133,10 +201,18 @@ jobs:
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
env: env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
PR_TITLE: ${{ github.event.pull_request.title }} PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: | 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 for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r version branch <<< "${backport}" IFS=':' read -r version branch <<< "${backport}"
@@ -165,9 +241,16 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
PR_NUMBER="${{ github.event.pull_request.number }}" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_AUTHOR="${{ github.event.pull_request.user.login }}" PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" 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
for failure in ${{ steps.backport.outputs.failed }}; do for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r version reason conflicts <<< "${failure}" IFS=':' read -r version reason conflicts <<< "${failure}"

View File

@@ -88,6 +88,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Build types - name: Build types
run: pnpm build:types run: pnpm build:types
@@ -131,7 +133,7 @@ jobs:
- name: Publish package - name: Publish package
if: steps.check_npm.outputs.exists == 'false' if: steps.check_npm.outputs.exists == 'false'
run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks
working-directory: dist working-directory: dist
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

5
.gitignore vendored
View File

@@ -44,6 +44,7 @@ components.d.ts
tests-ui/data/* tests-ui/data/*
tests-ui/ComfyUI_examples tests-ui/ComfyUI_examples
tests-ui/workflows/examples tests-ui/workflows/examples
coverage/
# Browser tests # Browser tests
/test-results/ /test-results/
@@ -78,8 +79,8 @@ vite.config.mts.timestamp-*.mjs
*storybook.log *storybook.log
storybook-static storybook-static
# MCP Servers
.playwright-mcp/*
.nx/cache .nx/cache
.nx/workspace-data .nx/workspace-data

View File

@@ -9,7 +9,7 @@ module.exports = defineConfig({
entry: 'src/locales/en', entry: 'src/locales/en',
entryLocale: 'en', entryLocale: 'en',
output: 'src/locales', output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'], outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. 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'. 'latent' is the short form of 'latent space'.
'mask' is in the context of image processing. 'mask' is in the context of image processing.

View File

@@ -15,21 +15,32 @@ const config: StorybookConfig = {
async viteFinal(config) { async viteFinal(config) {
// Use dynamic import to avoid CJS deprecation warning // Use dynamic import to avoid CJS deprecation warning
const { mergeConfig } = await import('vite') const { mergeConfig } = await import('vite')
const { default: tailwindcss } = await import('@tailwindcss/vite')
// Filter out any plugins that might generate import maps // Filter out any plugins that might generate import maps
if (config.plugins) { if (config.plugins) {
config.plugins = config.plugins.filter((plugin: any) => { config.plugins = config.plugins
if (plugin && plugin.name && plugin.name.includes('import-map')) { // Type guard: ensure we have valid plugin objects with names
return false .filter(
} (plugin): plugin is NonNullable<typeof plugin> & { name: string } => {
return true 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'))
} }
return mergeConfig(config, { return mergeConfig(config, {
// Replace plugins entirely to avoid inheritance issues // Replace plugins entirely to avoid inheritance issues
plugins: [ plugins: [
// Only include plugins we explicitly need for Storybook // Only include plugins we explicitly need for Storybook
tailwindcss(),
Icons({ Icons({
compiler: 'vue3', compiler: 'vue3',
customCollections: { customCollections: {

View File

@@ -1,7 +1,7 @@
import { definePreset } from '@primevue/themes' import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura' import Aura from '@primevue/themes/aura'
import { setup } from '@storybook/vue3' import { setup } from '@storybook/vue3'
import type { Preview } from '@storybook/vue3-vite' import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import 'primeicons/primeicons.css' import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
@@ -9,11 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice' import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip' import Tooltip from 'primevue/tooltip'
import '../src/assets/css/style.css' import '@/assets/css/style.css'
import { i18n } from '../src/i18n' import { i18n } from '@/i18n'
import '../src/lib/litegraph/public/css/litegraph.css' import '@/lib/litegraph/public/css/litegraph.css'
import { useWidgetStore } from '../src/stores/widgetStore'
import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
const ComfyUIPreset = definePreset(Aura, { const ComfyUIPreset = definePreset(Aura, {
semantic: { semantic: {
@@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, {
// Setup Vue app for Storybook // Setup Vue app for Storybook
setup((app) => { setup((app) => {
app.directive('tooltip', Tooltip) app.directive('tooltip', Tooltip)
// Create Pinia instance
const pinia = createPinia() const pinia = createPinia()
app.use(pinia) app.use(pinia)
// Initialize stores
useColorPaletteStore(pinia)
useWidgetStore(pinia)
app.use(i18n) app.use(i18n)
app.use(PrimeVue, { app.use(PrimeVue, {
theme: { theme: {
@@ -50,8 +46,8 @@ setup((app) => {
app.use(ToastService) app.use(ToastService)
}) })
// Dark theme decorator // Theme and dialog decorator
export const withTheme = (Story: any, context: any) => { export const withTheme = (Story: StoryFn, context: StoryContext) => {
const theme = context.globals.theme || 'light' const theme = context.globals.theme || 'light'
// Apply theme class to document root // Apply theme class to document root
@@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => {
document.body.classList.remove('dark-theme') document.body.classList.remove('dark-theme')
} }
return Story() return Story(context.args, context)
} }
const preview: Preview = { const preview: Preview = {

View File

@@ -1,17 +1,61 @@
# Admins # Desktop/Electron
* @Comfy-Org/comfy_frontend_devs /src/types/desktop/ @webfiltered
/src/constants/desktopDialogs.ts @webfiltered
/src/constants/desktopMaintenanceTasks.ts @webfiltered
/src/stores/electronDownloadStore.ts @webfiltered
/src/extensions/core/electronAdapter.ts @webfiltered
/src/views/DesktopDialogView.vue @webfiltered
/src/components/install/ @webfiltered
/src/components/maintenance/ @webfiltered
/vite.electron.config.mts @webfiltered
# Maintainers # Common UI Components
*.md @Comfy-Org/comfy_maintainer /src/components/chip/ @viva-jinyi
/tests-ui/ @Comfy-Org/comfy_maintainer /src/components/card/ @viva-jinyi
/browser_tests/ @Comfy-Org/comfy_maintainer /src/components/button/ @viva-jinyi
/.env_example @Comfy-Org/comfy_maintainer /src/components/input/ @viva-jinyi
# Translations (AIGODLIKE team + shinshin86) # Topbar
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer /src/components/topbar/ @pythongosssss
# Load 3D extension # Thumbnail
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs /src/renderer/core/thumbnail/ @pythongosssss
# Mask Editor extension # Legacy UI
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs /scripts/ui/ @pythongosssss
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
/src/services/nodeHelpService.ts @benceruleanlu
# Selection toolbox
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88
# Assets
/src/platform/assets/ @arjansingh
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
# 3D
/src/extensions/core/load3d.ts @jtydhr88
/src/components/load3d/ @jtydhr88
# Manager
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer

View File

@@ -0,0 +1 @@
{"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}

View File

@@ -1,4 +1,5 @@
import { Page, test as base } from '@playwright/test' import type { Page } from '@playwright/test'
import { test as base } from '@playwright/test'
export class UserSelectPage { export class UserSelectPage {
constructor( constructor(

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test' import type { Locator, Page } from '@playwright/test'
export class ComfyNodeSearchFilterSelectionPanel { export class ComfyNodeSearchFilterSelectionPanel {
constructor(public readonly page: Page) {} constructor(public readonly page: Page) {}

View File

@@ -1,6 +1,6 @@
import { Page } from '@playwright/test' import type { Page } from '@playwright/test'
import { ComfyPage } from '../ComfyPage' import type { ComfyPage } from '../ComfyPage'
export class SettingDialog { export class SettingDialog {
constructor( constructor(

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test' import type { Locator, Page } from '@playwright/test'
class SidebarTab { class SidebarTab {
constructor( constructor(

View File

@@ -1,4 +1,5 @@
import { Locator, Page, expect } from '@playwright/test' import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
export class Topbar { export class Topbar {
private readonly menuLocator: Locator private readonly menuLocator: Locator

View File

@@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{
// so we can look it up to trigger messages // so we can look it up to trigger messages
const store: Record<string, WebSocket> = ((window as any).__ws__ = {}) const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
window.WebSocket = class extends window.WebSocket { window.WebSocket = class extends window.WebSocket {
constructor() { constructor(
// @ts-expect-error ...rest: ConstructorParameters<typeof window.WebSocket>
super(...arguments) ) {
super(...rest)
store[this.url] = this store[this.url] = this
} }
} }

View File

@@ -1,4 +1,4 @@
import { FullConfig } from '@playwright/test' import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { backupPath } from './utils/backupUtils' import { backupPath } from './utils/backupUtils'

View File

@@ -1,4 +1,4 @@
import { FullConfig } from '@playwright/test' import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { restorePath } from './utils/backupUtils' import { restorePath } from './utils/backupUtils'

View File

@@ -0,0 +1,104 @@
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()
}

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test' import type { Locator, Page } from '@playwright/test'
export class ManageGroupNode { export class ManageGroupNode {
footer: Locator footer: Locator

View File

@@ -1,7 +1,7 @@
import { Locator, Page } from '@playwright/test' import type { Locator, Page } from '@playwright/test'
import path from 'path' import path from 'path'
import { import type {
TemplateInfo, TemplateInfo,
WorkflowTemplates WorkflowTemplates
} from '../../src/platform/workflow/templates/types/template' } from '../../src/platform/workflow/templates/types/template'

View File

@@ -29,9 +29,9 @@ test.describe('Actionbar', () => {
// Intercept the prompt queue endpoint // Intercept the prompt queue endpoint
let promptNumber = 0 let promptNumber = 0
comfyPage.page.route('**/api/prompt', async (route, req) => { await comfyPage.page.route('**/api/prompt', async (route, req) => {
await new Promise((r) => setTimeout(r, 100)) await new Promise((r) => setTimeout(r, 100))
route.fulfill({ await route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify({
prompt_id: promptNumber, prompt_id: promptNumber,

View File

@@ -1,5 +1,5 @@
import type { ComfyPage } from '../fixtures/ComfyPage'
import { import {
ComfyPage,
comfyExpect as expect, comfyExpect as expect,
comfyPageFixture as test comfyPageFixture as test
} from '../fixtures/ComfyPage' } from '../fixtures/ComfyPage'

View File

@@ -1,4 +1,5 @@
import { Page, expect } from '@playwright/test' import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'

View File

@@ -1,4 +1,5 @@
import { Locator, expect } from '@playwright/test' import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema' import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { SettingParams } from '../../src/platform/settings/types' import type { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Topbar commands', () => { test.describe('Topbar commands', () => {

View File

@@ -1,6 +1,7 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils' import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Group Node', () => { test.describe('Group Node', () => {

View File

@@ -1,12 +1,13 @@
import { Locator, expect } from '@playwright/test' import type { Locator } from '@playwright/test'
import { Position } from '@vueuse/core' import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import { import {
type ComfyPage, type ComfyPage,
comfyPageFixture as test, comfyPageFixture as test,
testComfySnapToGridGridSize testComfySnapToGridGridSize
} from '../fixtures/ComfyPage' } from '../fixtures/ComfyPage'
import { type NodeReference } from '../fixtures/utils/litegraphUtils' import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Item Interaction', () => { test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => { test('Can select/delete all items', async ({ comfyPage }) => {
@@ -1012,6 +1013,8 @@ test.describe('Canvas Navigation', () => {
test('Shift + mouse wheel should pan canvas horizontally', async ({ test('Shift + mouse wheel should pan canvas horizontally', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning')
await comfyPage.page.click('canvas') await comfyPage.page.click('canvas')
await comfyPage.nextFrame() await comfyPage.nextFrame()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -1,6 +1,7 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Remote COMBO Widget', () => { test.describe('Remote COMBO Widget', () => {
const mockOptions = ['d', 'c', 'b', 'a'] const mockOptions = ['d', 'c', 'b', 'a']

View File

@@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => {
comfyPage comfyPage
}) => { }) => {
await comfyPage.nextFrame() await comfyPage.nextFrame()
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() await expect(
comfyPage.menu.queueTab.getGalleryImage(firstImage)
).toBeVisible()
}) })
test('maintains active gallery item when new tasks are added', async ({ test('maintains active gallery item when new tasks are added', async ({
@@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => {
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage) const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
await newTask.waitFor({ state: 'visible' }) await newTask.waitFor({ state: 'visible' })
// The active gallery item should still be the initial image // The active gallery item should still be the initial image
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() await expect(
comfyPage.menu.queueTab.getGalleryImage(firstImage)
).toBeVisible()
}) })
test.describe('Gallery navigation', () => { test.describe('Gallery navigation', () => {
@@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => {
delay: 256 delay: 256
}) })
await comfyPage.nextFrame() await comfyPage.nextFrame()
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible() await expect(
comfyPage.menu.queueTab.getGalleryImage(end)
).toBeVisible()
}) })
}) })
}) })

View File

@@ -1,4 +1,5 @@
import { Page, expect } from '@playwright/test' import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { SystemStats } from '../../src/schemas/apiSchema' import type { SystemStats } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Version Mismatch Warnings', () => { test.describe('Version Mismatch Warnings', () => {

View File

@@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
test.describe('NodeHeader', () => { test.describe('NodeHeader', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -0,0 +1,221 @@
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)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,47 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Selection', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
const modifiers = [
{ key: 'Control', name: 'ctrl' },
{ key: 'Shift', name: 'shift' }
] as const
for (const { key: modifier, name } of modifiers) {
test(`should allow selecting multiple nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Empty Latent Image').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
await comfyPage.page.getByText('KSampler').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3)
})
test(`should allow de-selecting nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Load Checkpoint').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
}
})

View File

@@ -0,0 +1,49 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const BYPASS_HOTKEY = 'Control+b'
const BYPASS_CLASS = /before:bg-bypass\/60/
test.describe('Vue Node Bypass', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
await checkpointNode.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
const ksamplerNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'KSampler'
})
await checkpointNode.getByText('Load Checkpoint').click()
await ksamplerNode.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await expect(ksamplerNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS)
})
})

View File

@@ -1,5 +1,5 @@
{ {
"extends": "./tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
/* Test files should not be compiled */ /* Test files should not be compiled */
"noEmit": true, "noEmit": true,
@@ -9,13 +9,6 @@
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": [ "include": [
"*.ts", "**/*.ts",
"*.mts",
"*.config.js",
"browser_tests/**/*.ts",
"scripts/**/*.js",
"scripts/**/*.ts",
"tests-ui/**/*.ts",
".storybook/**/*.ts"
] ]
} }

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { Plugin } from 'vite' import type { Plugin } from 'vite'
interface ShimResult { interface ShimResult {
code: string code: string

View File

@@ -1,7 +1,7 @@
import glob from 'fast-glob' import glob from 'fast-glob'
import fs from 'fs-extra' import fs from 'fs-extra'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite' import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
interface ImportMapSource { interface ImportMapSource {
name: string name: string

View File

@@ -5,13 +5,14 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook' import storybook from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports' import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue' import pluginVue from 'eslint-plugin-vue'
import { defineConfig } from 'eslint/config'
import globals from 'globals' import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
import vueParser from 'vue-eslint-parser'
export default [ const extraFileExtensions = ['.vue']
{
files: ['src/**/*.{js,mjs,cjs,ts,vue}'] export default defineConfig([
},
{ {
ignores: [ ignores: [
'src/scripts/*', 'src/scripts/*',
@@ -24,35 +25,49 @@ export default [
] ]
}, },
{ {
files: ['./**/*.{ts,mts}'],
languageOptions: { languageOptions: {
globals: { globals: {
...globals.browser, ...globals.browser,
__COMFYUI_FRONTEND_VERSION__: 'readonly' __COMFYUI_FRONTEND_VERSION__: 'readonly'
}, },
parser: tseslint.parser,
parserOptions: { parserOptions: {
project: ['./tsconfig.json', './tsconfig.eslint.json'], parser: tseslint.parser,
projectService: true,
tsConfigRootDir: import.meta.dirname,
ecmaVersion: 2020, ecmaVersion: 2020,
sourceType: 'module', sourceType: 'module',
extraFileExtensions: ['.vue'] 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
} }
} }
}, },
pluginJs.configs.recommended, pluginJs.configs.recommended,
...tseslint.configs.recommended, tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'], pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended, eslintPluginPrettierRecommended,
{ storybook.configs['flat/recommended'],
files: ['src/**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{ {
plugins: { plugins: {
'unused-imports': unusedImports, 'unused-imports': unusedImports,
// @ts-expect-error Bad types in the plugin
'@intlify/vue-i18n': pluginI18n '@intlify/vue-i18n': pluginI18n
}, },
rules: { rules: {
@@ -60,13 +75,29 @@ export default [
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off', '@typescript-eslint/prefer-as-const': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'always'
}
],
'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off', 'vue/no-v-html': 'off',
// Enforce dark-theme: instead of dark: prefix // Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'], 'vue/no-restricted-class': ['error', '/^dark:/'],
'vue/multi-word-component-names': 'off', // TODO: fix 'vue/multi-word-component-names': 'off', // TODO: fix
'vue/no-template-shadow': 'off', // TODO: fix 'vue/no-template-shadow': 'off', // TODO: fix
/* Toggle on to do additional until we can clean up existing violations.
'vue/no-unused-emit-declarations': 'error',
'vue/no-unused-properties': 'error',
'vue/no-unused-refs': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/no-useless-v-bind': 'error',
// */
'vue/one-component-per-file': '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 // Restrict deprecated PrimeVue components
'no-restricted-imports': [ 'no-restricted-imports': [
'error', 'error',
@@ -136,5 +167,13 @@ export default [
] ]
} }
}, },
...storybook.configs['flat/recommended'] {
] files: ['tests-ui/**/*'],
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false }
]
}
}
])

View File

@@ -8,8 +8,8 @@
<link rel="stylesheet" type="text/css" href="user.css" /> <link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" /> <link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
<!-- Fullscreen mode on iOS --> <!-- Fullscreen mode on mobile browsers -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<!-- Status bar style (eg. black or transparent) --> <!-- Status bar style (eg. black or transparent) -->
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">

View File

@@ -22,10 +22,12 @@ const config: KnipConfig = {
], ],
ignore: [ ignore: [
// Auto generated manager types // Auto generated manager types
'src/types/generatedManagerTypes.ts', 'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'src/types/comfyRegistryTypes.ts', 'src/types/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this) // Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts' 'src/scripts/ui/components/splitButton.ts',
// Staged for for use with subgraph widget promotion
'src/lib/litegraph/src/widgets/DisconnectedWidget.ts'
], ],
compilers: { compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -3,13 +3,13 @@ export default {
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [ './**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
...formatAndEslint(stagedFiles), ...formatAndEslint(stagedFiles),
'vue-tsc --noEmit' 'pnpm typecheck'
] ]
} }
function formatAndEslint(fileNames) { function formatAndEslint(fileNames) {
return [ return [
`eslint --fix ${fileNames.join(' ')}`, `pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
`prettier --write ${fileNames.join(' ')}` `pnpm exec prettier --cache --write ${fileNames.join(' ')}`
] ]
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "@comfyorg/comfyui-frontend", "name": "@comfyorg/comfyui-frontend",
"private": true, "private": true,
"version": "1.27.4", "version": "1.28.0",
"type": "module", "type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org", "homepage": "https://comfy.org",
@@ -14,9 +14,9 @@
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
"zipdist": "node scripts/zipdist.js", "zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache", "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
"format:check": "prettier --check './**/*.{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}'", "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'", "format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:browser": "npx nx e2e", "test:browser": "npx nx e2e",
"test:unit": "nx run test tests-ui/tests", "test:unit": "nx run test tests-ui/tests",
@@ -27,6 +27,8 @@
"preview": "nx preview", "preview": "nx preview",
"lint": "eslint src --cache", "lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix", "lint:fix": "eslint src --cache --fix",
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
"lint:no-cache": "eslint src", "lint:no-cache": "eslint src",
"lint:fix:no-cache": "eslint src --fix", "lint:fix:no-cache": "eslint src --fix",
"knip": "knip --cache", "knip": "knip --cache",
@@ -38,10 +40,10 @@
"build-storybook": "storybook build" "build-storybook": "storybook build"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.8.0", "@eslint/js": "^9.35.0",
"@iconify-json/lucide": "^1.2.66", "@iconify-json/lucide": "^1.2.66",
"@iconify/tailwind": "^1.2.0", "@iconify/tailwind": "^1.2.0",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0", "@intlify/eslint-plugin-vue-i18n": "^4.1.0",
"@lobehub/i18n-cli": "^1.25.1", "@lobehub/i18n-cli": "^1.25.1",
"@nx/eslint": "21.4.1", "@nx/eslint": "21.4.1",
"@nx/playwright": "21.4.1", "@nx/playwright": "21.4.1",
@@ -64,11 +66,11 @@
"@vitest/ui": "^3.0.0", "@vitest/ui": "^3.0.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"eslint": "^9.34.0", "eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.2", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.2.6", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-storybook": "^9.1.1", "eslint-plugin-storybook": "^9.1.6",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.2.0",
"eslint-plugin-vue": "^9.27.0", "eslint-plugin-vue": "^10.4.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"globals": "^15.9.0", "globals": "^15.9.0",
"happy-dom": "^15.11.0", "happy-dom": "^15.11.0",
@@ -79,22 +81,24 @@
"lint-staged": "^15.2.7", "lint-staged": "^15.2.7",
"nx": "21.4.1", "nx": "21.4.1",
"prettier": "^3.3.2", "prettier": "^3.3.2",
"storybook": "^9.1.1", "storybook": "^9.1.6",
"tailwindcss": "^4.1.12", "tailwindcss": "^4.1.12",
"tailwindcss-primeui": "^0.6.1", "tailwindcss-primeui": "^0.6.1",
"tsx": "^4.15.6", "tsx": "^4.15.6",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.3.8",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.44.0",
"unplugin-icons": "^0.22.0", "unplugin-icons": "^0.22.0",
"unplugin-vue-components": "^0.28.0", "unplugin-vue-components": "^0.28.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vite": "^5.4.19", "vite": "^5.4.19",
"vite-plugin-dts": "^4.3.0", "vite-plugin-dts": "^4.5.4",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.6", "vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"vue-tsc": "^2.1.10", "vue-component-type-helpers": "^3.0.7",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.7",
"zip-dir": "^2.0.0", "zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1" "zod-to-json-schema": "^3.24.1"
}, },

1051
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -2,6 +2,7 @@ import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage' import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands' import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig' import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
import type { FormItem, SettingParams } from '../src/platform/settings/types' import type { FormItem, SettingParams } from '../src/platform/settings/types'
import type { ComfyCommandImpl } from '../src/stores/commandStore' import type { ComfyCommandImpl } from '../src/stores/commandStore'
@@ -131,6 +132,23 @@ 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( fs.writeFileSync(
localePath, localePath,
JSON.stringify( JSON.stringify(
@@ -144,7 +162,8 @@ test('collect-i18n-general', async ({ comfyPage }) => {
...allSettingCategoriesLocale ...allSettingCategoriesLocale
}, },
serverConfigItems: allServerConfigsLocale, serverConfigItems: allServerConfigsLocale,
serverConfigCategories: allServerConfigCategoriesLocale serverConfigCategories: allServerConfigCategoriesLocale,
desktopDialogs: allDesktopDialogsLocale
}, },
null, null,
2 2

17
src/assets/css/fonts.css Normal file
View File

@@ -0,0 +1,17 @@
/* Inter Font Family */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin-normal.woff2') format('woff2');
font-weight: 100 900;
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-style: italic;
font-display: swap;
}

View File

@@ -1,5 +1,6 @@
@layer theme, base, primevue, components, utilities; @layer theme, base, primevue, components, utilities;
@import './fonts.css';
@import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/theme' layer(theme);
@import 'tailwindcss/utilities' layer(utilities); @import 'tailwindcss/utilities' layer(utilities);
@import 'tw-animate-css'; @import 'tw-animate-css';
@@ -52,15 +53,20 @@
--text-xxs: 0.625rem; --text-xxs: 0.625rem;
--text-xxs--line-height: calc(1 / 0.625); --text-xxs--line-height: calc(1 / 0.625);
/* Font Families */
--font-inter: 'Inter', sans-serif;
/* Palette Colors */ /* Palette Colors */
--color-charcoal-100: #171718; --color-charcoal-100: #55565e;
--color-charcoal-200: #202121; --color-charcoal-200: #494a50;
--color-charcoal-300: #262729; --color-charcoal-300: #3c3d42;
--color-charcoal-400: #2d2e32; --color-charcoal-400: #313235;
--color-charcoal-500: #313235; --color-charcoal-500: #2d2e32;
--color-charcoal-600: #3c3d42; --color-charcoal-600: #262729;
--color-charcoal-700: #494a50; --color-charcoal-700: #202121;
--color-charcoal-800: #55565e; --color-charcoal-800: #171718;
--color-neutral-550: #636363;
--color-stone-100: #444444; --color-stone-100: #444444;
--color-stone-200: #828282; --color-stone-200: #828282;
@@ -99,12 +105,16 @@
--color-danger-100: #c02323; --color-danger-100: #c02323;
--color-danger-200: #d62952; --color-danger-200: #d62952;
--color-bypass: #6A246A; --color-coral-red-600: #973a40;
--color-coral-red-500: #c53f49;
--color-coral-red-400: #dd424e;
--color-bypass: #6a246a;
--color-error: #962a2a; --color-error: #962a2a;
--color-blue-selection: rgb( from var(--color-blue-100) r g b / 0.3); --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-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1); --color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4); --color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
/* PrimeVue pulled colors */ /* PrimeVue pulled colors */
@@ -117,10 +127,10 @@
} }
@theme inline { @theme inline {
--color-node-component-surface: var(--color-charcoal-300); --color-node-component-surface: var(--color-charcoal-600);
--color-node-component-surface-highlight: var(--color-slate-100); --color-node-component-surface-highlight: var(--color-slate-100);
--color-node-component-surface-hovered: var(--color-charcoal-500); --color-node-component-surface-hovered: var(--color-charcoal-400);
--color-node-component-surface-selected: var(--color-charcoal-700); --color-node-component-surface-selected: var(--color-charcoal-200);
--color-node-stroke: var(--color-stone-100); --color-node-stroke: var(--color-stone-100);
} }
@@ -132,7 +142,7 @@
@utility scrollbar-hide { @utility scrollbar-hide {
scrollbar-width: none; scrollbar-width: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 1px; width: 1px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {

98
src/base/common/async.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Cross-browser async utilities for scheduling tasks during browser idle time
* with proper fallbacks for browsers that don't support requestIdleCallback.
*
* Implementation based on:
* https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts
*/
interface IdleDeadline {
didTimeout: boolean
timeRemaining(): number
}
interface IDisposable {
dispose(): void
}
/**
* Internal implementation function that handles the actual scheduling logic.
* Uses feature detection to determine whether to use native requestIdleCallback
* or fall back to setTimeout-based implementation.
*/
let _runWhenIdle: (
targetWindow: any,
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
/**
* Execute the callback during the next browser idle period.
* Falls back to setTimeout-based scheduling in browsers without native support.
*/
export let runWhenGlobalIdle: (
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
// Self-invoking function to set up the idle callback implementation
;(function () {
const safeGlobal: any = globalThis
if (
typeof safeGlobal.requestIdleCallback !== 'function' ||
typeof safeGlobal.cancelIdleCallback !== 'function'
) {
// Fallback implementation for browsers without native support (e.g., Safari)
_runWhenIdle = (_targetWindow, runner, _timeout?) => {
setTimeout(() => {
if (disposed) {
return
}
// Simulate IdleDeadline - give 15ms window (one frame at ~64fps)
const end = Date.now() + 15
const deadline: IdleDeadline = {
didTimeout: true,
timeRemaining() {
return Math.max(0, end - Date.now())
}
}
runner(Object.freeze(deadline))
})
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
}
}
}
} else {
// Native requestIdleCallback implementation
_runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => {
const handle: number = targetWindow.requestIdleCallback(
runner,
typeof timeout === 'number' ? { timeout } : undefined
)
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
targetWindow.cancelIdleCallback(handle)
}
}
}
}
runWhenGlobalIdle = (runner, timeout) =>
_runWhenIdle(globalThis, runner, timeout)
})()

View File

@@ -21,7 +21,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import { CSSProperties, computed, watchEffect } from 'vue' import type { CSSProperties } from 'vue'
import { computed, watchEffect } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'

View File

@@ -22,7 +22,8 @@ import {
} from '@vueuse/core' } from '@vueuse/core'
import { clamp } from 'es-toolkit/compat' import { clamp } from 'es-toolkit/compat'
import Panel from 'primevue/panel' import Panel from 'primevue/panel'
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue' import type { Ref } from 'vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'

View File

@@ -26,7 +26,8 @@
import { useElementHover, useEventListener } from '@vueuse/core' import { useElementHover, useEventListener } from '@vueuse/core'
import type { IDisposable } from '@xterm/xterm' import type { IDisposable } from '@xterm/xterm'
import Button from 'primevue/button' import Button from 'primevue/button'
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue' import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal' import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
@@ -46,7 +47,7 @@ const hasSelection = ref(false)
const isHovered = useElementHover(rootEl) const isHovered = useElementHover(rootEl)
const terminalData = useTerminal(terminalEl) const terminalData = useTerminal(terminalEl)
emit('created', terminalData, rootEl) emit('created', terminalData, ref(rootEl))
const { terminal } = terminalData const { terminal } = terminalData
let selectionDisposable: IDisposable | undefined let selectionDisposable: IDisposable | undefined

View File

@@ -3,8 +3,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { IDisposable } from '@xterm/xterm' import type { IDisposable } from '@xterm/xterm'
import { Ref, onMounted, onUnmounted } from 'vue' import type { Ref } from 'vue'
import { onMounted, onUnmounted } from 'vue'
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal' import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { electronAPI } from '@/utils/envUtil' import { electronAPI } from '@/utils/envUtil'

View File

@@ -15,10 +15,11 @@
import { until } from '@vueuse/core' import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import ProgressSpinner from 'primevue/progressspinner' import ProgressSpinner from 'primevue/progressspinner'
import { Ref, onMounted, onUnmounted, ref } from 'vue' import type { Ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal' import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema' import type { LogEntry, LogsWsMessage, TerminalSize } from '@/schemas/apiSchema'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore' import { useExecutionStore } from '@/stores/executionStore'

View File

@@ -47,7 +47,8 @@
<script setup lang="ts"> <script setup lang="ts">
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'
import Menu, { MenuState } from 'primevue/menu' import type { MenuState } from 'primevue/menu'
import Menu from 'primevue/menu'
import type { MenuItem } from 'primevue/menuitem' import type { MenuItem } from 'primevue/menuitem'
import Tag from 'primevue/tag' import Tag from 'primevue/tag'
import { computed, nextTick, ref } from 'vue' import { computed, nextTick, ref } from 'vue'

View File

@@ -7,7 +7,7 @@
<InputText <InputText
v-else v-else
ref="inputRef" ref="inputRef"
v-model:modelValue="inputValue" v-model:model-value="inputValue"
v-focus v-focus
type="text" type="text"
size="small" size="small"

View File

@@ -17,7 +17,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount } from 'vue' import { onBeforeUnmount } from 'vue'
import { CustomExtension, VueExtension } from '@/types/extensionTypes' import type { CustomExtension, VueExtension } from '@/types/extensionTypes'
const props = defineProps<{ const props = defineProps<{
extension: VueExtension | CustomExtension extension: VueExtension | CustomExtension

View File

@@ -21,7 +21,7 @@
<component <component
:is="markRaw(getFormComponent(props.item))" :is="markRaw(getFormComponent(props.item))"
:id="props.id" :id="props.id"
v-model:modelValue="formValue" v-model:model-value="formValue"
:aria-labelledby="`${props.id}-label`" :aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)" v-bind="getFormAttrs(props.item)"
/> />
@@ -44,7 +44,7 @@ import FormRadioGroup from '@/components/common/FormRadioGroup.vue'
import InputKnob from '@/components/common/InputKnob.vue' import InputKnob from '@/components/common/InputKnob.vue'
import InputSlider from '@/components/common/InputSlider.vue' import InputSlider from '@/components/common/InputSlider.vue'
import UrlInput from '@/components/common/UrlInput.vue' import UrlInput from '@/components/common/UrlInput.vue'
import { FormItem } from '@/platform/settings/types' import type { FormItem } from '@/platform/settings/types'
const formValue = defineModel<any>('formValue') const formValue = defineModel<any>('formValue')
const props = defineProps<{ const props = defineProps<{

View File

@@ -10,7 +10,7 @@
class="absolute inset-0" class="absolute inset-0"
/> />
<img <img
v-show="isImageLoaded" v-if="cachedSrc"
ref="imageRef" ref="imageRef"
:src="cachedSrc" :src="cachedSrc"
:alt="alt" :alt="alt"
@@ -77,8 +77,8 @@ const shouldLoad = computed(() => isIntersecting.value)
watch( watch(
shouldLoad, shouldLoad,
async (shouldLoad) => { async (shouldLoadVal) => {
if (shouldLoad && src && !cachedSrc.value && !hasError.value) { if (shouldLoadVal && src && !cachedSrc.value && !hasError.value) {
try { try {
const cachedMedia = await getCachedMedia(src) const cachedMedia = await getCachedMedia(src)
if (cachedMedia.error) { if (cachedMedia.error) {
@@ -93,7 +93,7 @@ watch(
console.warn('Failed to load cached media:', error) console.warn('Failed to load cached media:', error)
cachedSrc.value = src cachedSrc.value = src
} }
} else if (!shouldLoad) { } else if (!shouldLoadVal) {
if (cachedSrc.value?.startsWith('blob:')) { if (cachedSrc.value?.startsWith('blob:')) {
releaseUrl(src) releaseUrl(src)
} }

View File

@@ -32,7 +32,7 @@
import Button from 'primevue/button' import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner' import ProgressSpinner from 'primevue/progressspinner'
import { PrimeVueSeverity } from '@/types/primeVueTypes' import type { PrimeVueSeverity } from '@/types/primeVueTypes'
const { const {
disabled, disabled,

View File

@@ -1,7 +1,7 @@
<template> <template>
<Tree <Tree
v-model:expandedKeys="expandedKeys" v-model:expanded-keys="expandedKeys"
v-model:selectionKeys="selectionKeys" v-model:selection-keys="selectionKeys"
class="tree-explorer py-0 px-2 2xl:px-4" class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class" :class="props.class"
:value="renderedRoot.children" :value="renderedRoot.children"

View File

@@ -9,10 +9,8 @@ import { createI18n } from 'vue-i18n'
import EditableText from '@/components/common/EditableText.vue' import EditableText from '@/components/common/EditableText.vue'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue' import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import { import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
InjectKeyHandleEditLabelFunction, import { InjectKeyHandleEditLabelFunction } from '@/types/treeExplorerTypes'
RenderedTreeExplorerNode
} from '@/types/treeExplorerTypes'
// Create a mock i18n instance // Create a mock i18n instance
const i18n = createI18n({ const i18n = createI18n({

View File

@@ -59,14 +59,13 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue' import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes' import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useManagerState } from '@/composables/useManagerState'
import { useToastStore } from '@/platform/updates/common/toastStore' import { useToastStore } from '@/platform/updates/common/toastStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useDialogStore } from '@/stores/dialogStore' import { useDialogStore } from '@/stores/dialogStore'
import type { MissingNodeType } from '@/types/comfy' import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes' import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import PackInstallButton from './manager/button/PackInstallButton.vue' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const props = defineProps<{ const props = defineProps<{
missingNodeTypes: MissingNodeType[] missingNodeTypes: MissingNodeType[]
@@ -138,7 +137,7 @@ const allMissingNodesInstalled = computed(() => {
}) })
// Watch for completion and close dialog // Watch for completion and close dialog
watch(allMissingNodesInstalled, async (allInstalled) => { watch(allMissingNodesInstalled, async (allInstalled) => {
if (allInstalled) { if (allInstalled && showInstallAllButton.value) {
// Use nextTick to ensure state updates are complete // Use nextTick to ensure state updates are complete
await nextTick() await nextTick()

View File

@@ -43,11 +43,11 @@
<script setup lang="ts"> <script setup lang="ts">
import Message from 'primevue/message' import Message from 'primevue/message'
import { compare } from 'semver'
import { computed } from 'vue' import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore' import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { compareVersions } from '@/utils/formatUtil'
const props = defineProps<{ const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]> missingCoreNodes: Record<string, LGraphNode[]>
@@ -68,7 +68,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
const sortedMissingCoreNodes = computed(() => { const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => { return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first) // Sort by version in descending order (newest first)
return compareVersions(b, a) // Reversed for descending order return compare(b, a) // Reversed for descending order
}) })
}) })

View File

@@ -148,7 +148,7 @@ import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi' import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import { SignInData, SignUpData } from '@/schemas/signInSchema' import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isInChina } from '@/utils/networkUtil' import { isInChina } from '@/utils/networkUtil'
import ApiKeyForm from './signin/ApiKeyForm.vue' import ApiKeyForm from './signin/ApiKeyForm.vue'

View File

@@ -17,7 +17,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms' import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod' import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button' import Button from 'primevue/button'
import { ref } from 'vue' import { ref } from 'vue'

View File

@@ -1,82 +0,0 @@
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)
})
})

View File

@@ -1,25 +0,0 @@
<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>

View File

@@ -96,8 +96,8 @@ import Message from 'primevue/message'
import ProgressSpinner from 'primevue/progressspinner' import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import type { AuditLog } from '@/services/customerEventsService'
import { import {
AuditLog,
EventType, EventType,
useCustomerEventsService useCustomerEventsService
} from '@/services/customerEventsService' } from '@/services/customerEventsService'

View File

@@ -13,7 +13,7 @@
import Tag from 'primevue/tag' import Tag from 'primevue/tag'
import { computed } from 'vue' import { computed } from 'vue'
import { KeyComboImpl } from '@/stores/keybindingStore' import type { KeyComboImpl } from '@/stores/keybindingStore'
const { keyCombo, isModified = false } = defineProps<{ const { keyCombo, isModified = false } = defineProps<{
keyCombo: KeyComboImpl keyCombo: KeyComboImpl

View File

@@ -79,7 +79,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms' import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod' import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button' import Button from 'primevue/button'
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'

View File

@@ -1,5 +1,6 @@
import { Form } from '@primevue/forms' import { Form } from '@primevue/forms'
import { VueWrapper, mount } from '@vue/test-utils' import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import Button from 'primevue/button' import Button from 'primevue/button'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'

View File

@@ -71,7 +71,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms' import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod' import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button' import Button from 'primevue/button'
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'

View File

@@ -59,7 +59,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Form, FormField, FormSubmitEvent } from '@primevue/forms' import type { FormSubmitEvent } from '@primevue/forms'
import { Form, FormField } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod' import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button' import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox' import Checkbox from 'primevue/checkbox'

View File

@@ -34,7 +34,7 @@ const updateWidgets = () => {
const widget = widgetState.widget const widget = widgetState.widget
// Early exit for non-visible widgets // Early exit for non-visible widgets
if (!widget.isVisible()) { if (!widget.isVisible() || !widgetState.active) {
widgetState.visible = false widgetState.visible = false
continue continue
} }

View File

@@ -33,7 +33,7 @@
<!-- TransformPane for Vue node rendering --> <!-- TransformPane for Vue node rendering -->
<TransformPane <TransformPane
v-if="isVueNodesEnabled && comfyApp.canvas && comfyAppReady" v-if="shouldRenderVueNodes && comfyApp.canvas && comfyAppReady"
:canvas="comfyApp.canvas" :canvas="comfyApp.canvas"
@transform-update="handleTransformUpdate" @transform-update="handleTransformUpdate"
@wheel.capture="canvasInteractions.forwardEventToCanvas" @wheel.capture="canvasInteractions.forwardEventToCanvas"
@@ -43,8 +43,6 @@
v-for="nodeData in allNodes" v-for="nodeData in allNodes"
:key="nodeData.id" :key="nodeData.id"
:node-data="nodeData" :node-data="nodeData"
:position="nodePositions.get(nodeData.id)"
:size="nodeSizes.get(nodeData.id)"
:readonly="false" :readonly="false"
:error=" :error="
executionStore.lastExecutionError?.node_id === nodeData.id executionStore.lastExecutionError?.node_id === nodeData.id
@@ -53,9 +51,6 @@
" "
:zoom-level="canvasStore.canvas?.ds?.scale || 1" :zoom-level="canvasStore.canvas?.ds?.scale || 1"
:data-node-id="nodeData.id" :data-node-id="nodeData.id"
@node-click="handleNodeSelect"
@update:collapsed="handleNodeCollapse"
@update:title="handleNodeTitleUpdate"
/> />
</TransformPane> </TransformPane>
@@ -76,9 +71,9 @@
import { useEventListener, whenever } from '@vueuse/core' import { useEventListener, whenever } from '@vueuse/core'
import { import {
computed, computed,
nextTick,
onMounted, onMounted,
onUnmounted, onUnmounted,
provide,
ref, ref,
shallowRef, shallowRef,
watch, watch,
@@ -96,7 +91,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
import SideToolbar from '@/components/sidebar/SideToolbar.vue' import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue' import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback' import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useViewportCulling } from '@/composables/graph/useViewportCulling' import { useViewportCulling } from '@/composables/graph/useViewportCulling'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useNodeBadge } from '@/composables/node/useNodeBadge' import { useNodeBadge } from '@/composables/node/useNodeBadge'
@@ -117,12 +111,11 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave' import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence' import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue' import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue' import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue' import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useExecutionStateProvider } from '@/renderer/extensions/vueNodes/execution/useExecutionStateProvider'
import { UnauthorizedError, api } from '@/scripts/api' import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app' import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker' import { ChangeTracker } from '@/scripts/changeTracker'
@@ -169,44 +162,33 @@ const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
// Feature flags // Feature flags
const { shouldRenderVueNodes } = useVueFeatureFlags() const { shouldRenderVueNodes } = useVueFeatureFlags()
const isVueNodesEnabled = computed(() => shouldRenderVueNodes.value)
// Vue node system // Vue node system
const vueNodeLifecycle = useVueNodeLifecycle(isVueNodesEnabled) const vueNodeLifecycle = useVueNodeLifecycle()
const viewportCulling = useViewportCulling( const viewportCulling = useViewportCulling()
isVueNodesEnabled,
vueNodeLifecycle.vueNodeData,
vueNodeLifecycle.nodeDataTrigger,
vueNodeLifecycle.nodeManager
)
const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
const nodePositions = vueNodeLifecycle.nodePositions const handleVueNodeLifecycleReset = async () => {
const nodeSizes = vueNodeLifecycle.nodeSizes if (shouldRenderVueNodes.value) {
const allNodes = viewportCulling.allNodes vueNodeLifecycle.disposeNodeManagerAndSyncs()
await nextTick()
const handleTransformUpdate = () => { vueNodeLifecycle.initializeNodeManager()
viewportCulling.handleTransformUpdate() }
// TODO: Fix paste position sync in separate PR
vueNodeLifecycle.detectChangesInRAF.value()
} }
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate
// Provide selection state to all Vue nodes watch(() => canvasStore.currentGraph, handleVueNodeLifecycleReset)
const selectedNodeIds = computed(
() => watch(
new Set( () => canvasStore.isInSubgraph,
canvasStore.selectedItems async (newValue, oldValue) => {
.filter((item) => item.id !== undefined) if (oldValue && !newValue) {
.map((item) => String(item.id)) useWorkflowStore().updateActiveGraph()
) }
await handleVueNodeLifecycleReset()
}
) )
provide(SelectedNodeIdsKey, selectedNodeIds)
// Provide execution state to all Vue nodes const allNodes = viewportCulling.allNodes
useExecutionStateProvider() const handleTransformUpdate = viewportCulling.handleTransformUpdate
watchEffect(() => { watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated') nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
@@ -404,6 +386,7 @@ onMounted(async () => {
// @ts-expect-error fixme ts strict error // @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value) await comfyApp.setup(canvasRef.value)
attachSlotLinkPreviewRenderer(comfyApp.canvas)
canvasStore.canvas = comfyApp.canvas canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false workspaceStore.spinner = false

View File

@@ -124,11 +124,11 @@ import ButtonGroup from 'primevue/buttongroup'
import { computed, onBeforeUnmount, onMounted } from 'vue' import { computed, onBeforeUnmount, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useZoomControls } from '@/composables/useZoomControls' import { useZoomControls } from '@/composables/useZoomControls'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap' import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'

View File

@@ -33,9 +33,11 @@ const tooltipText = ref('')
const left = ref<string>() const left = ref<string>()
const top = ref<string>() const top = ref<string>()
const hideTooltip = () => (tooltipText.value = '') function hideTooltip() {
return (tooltipText.value = '')
}
const showTooltip = async (tooltip: string | null | undefined) => { async function showTooltip(tooltip: string | null | undefined) {
if (!tooltip) return if (!tooltip) return
left.value = comfyApp.canvas.mouse[0] + 'px' left.value = comfyApp.canvas.mouse[0] + 'px'
@@ -56,9 +58,9 @@ const showTooltip = async (tooltip: string | null | undefined) => {
} }
} }
const onIdle = () => { function onIdle() {
const { canvas } = comfyApp const { canvas } = comfyApp
const node = canvas.node_over const node = canvas?.node_over
if (!node) return if (!node) return
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 } const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
@@ -68,7 +70,7 @@ const onIdle = () => {
ctor.title_mode !== LiteGraph.NO_TITLE && ctor.title_mode !== LiteGraph.NO_TITLE &&
canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title
) { ) {
return showTooltip(nodeDef.description) return showTooltip(nodeDef?.description)
} }
if (node.flags?.collapsed) return if (node.flags?.collapsed) return
@@ -83,7 +85,7 @@ const onIdle = () => {
const inputName = node.inputs[inputSlot].name const inputName = node.inputs[inputSlot].name
const translatedTooltip = st( const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(inputName)}.tooltip`, `nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(inputName)}.tooltip`,
nodeDef.inputs[inputName]?.tooltip ?? '' nodeDef?.inputs[inputName]?.tooltip ?? ''
) )
return showTooltip(translatedTooltip) return showTooltip(translatedTooltip)
} }
@@ -97,7 +99,7 @@ const onIdle = () => {
if (outputSlot !== -1) { if (outputSlot !== -1) {
const translatedTooltip = st( const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.outputs.${outputSlot}.tooltip`, `nodeDefs.${normalizeI18nKey(node.type ?? '')}.outputs.${outputSlot}.tooltip`,
nodeDef.outputs[outputSlot]?.tooltip ?? '' nodeDef?.outputs[outputSlot]?.tooltip ?? ''
) )
return showTooltip(translatedTooltip) return showTooltip(translatedTooltip)
} }
@@ -107,7 +109,7 @@ const onIdle = () => {
if (widget && !isDOMWidget(widget)) { if (widget && !isDOMWidget(widget)) {
const translatedTooltip = st( const translatedTooltip = st(
`nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(widget.name)}.tooltip`, `nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(widget.name)}.tooltip`,
nodeDef.inputs[widget.name]?.tooltip ?? '' nodeDef?.inputs[widget.name]?.tooltip ?? ''
) )
// Widget tooltip can be set dynamically, current translation collection does not support this. // Widget tooltip can be set dynamically, current translation collection does not support this.
return showTooltip(widget.tooltip ?? translatedTooltip) return showTooltip(widget.tooltip ?? translatedTooltip)

View File

@@ -5,12 +5,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue' import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useExtensionService } from '@/services/extensionService' import { useExtensionService } from '@/services/extensionService'
// Mock the composables and services // Mock the composables and services
vi.mock('@/composables/graph/useCanvasInteractions', () => ({ vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
useCanvasInteractions: vi.fn(() => ({ useCanvasInteractions: vi.fn(() => ({
handleWheel: vi.fn() handleWheel: vi.fn()
})) }))

View File

@@ -11,7 +11,7 @@
:style="`backgroundColor: ${containerStyles.backgroundColor};`" :style="`backgroundColor: ${containerStyles.backgroundColor};`"
:pt="{ :pt="{
header: 'hidden', header: 'hidden',
content: 'px-1 py-1 h-10 px-1 flex flex-row gap-1' content: 'p-1 h-10 flex flex-row gap-1'
}" }"
@wheel="canvasInteractions.handleWheel" @wheel="canvasInteractions.handleWheel"
> >
@@ -60,9 +60,9 @@ import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButt
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue' import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue' import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue'
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition' import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useSelectionState } from '@/composables/graph/useSelectionState' import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap' import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useExtensionService } from '@/services/extensionService' import { useExtensionService } from '@/services/extensionService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore' import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'

View File

@@ -135,7 +135,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Button, InputNumber, InputNumberInputEvent } from 'primevue' import type { InputNumberInputEvent } from 'primevue'
import { Button, InputNumber } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'

View File

@@ -7,7 +7,7 @@
severity="secondary" severity="secondary"
text text
data-testid="bypass-button" data-testid="bypass-button"
class="hover:dark-theme:bg-charcoal-300 hover:bg-[#E7E6E6]" class="hover:dark-theme:bg-charcoal-600 hover:bg-[#E7E6E6]"
@click="toggleBypass" @click="toggleBypass"
> >
<template #icon> <template #icon>

View File

@@ -50,7 +50,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import SelectButton from 'primevue/selectbutton' import SelectButton from 'primevue/selectbutton'
import { Raw, computed, ref, watch } from 'vue' import type { Raw } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { import type {

View File

@@ -20,7 +20,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useSelectionState } from '@/composables/graph/useSelectionState' import { useSelectionState } from '@/composables/graph/useSelectionState'
import { Positionable } from '@/lib/litegraph/src/interfaces' import type { Positionable } from '@/lib/litegraph/src/interfaces'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n() const { t } = useI18n()

View File

@@ -17,7 +17,8 @@
import Button from 'primevue/button' import Button from 'primevue/button'
import { st } from '@/i18n' import { st } from '@/i18n'
import { ComfyCommand, useCommandStore } from '@/stores/commandStore' import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil' import { normalizeI18nKey } from '@/utils/formatUtil'
defineProps<{ defineProps<{

View File

@@ -48,9 +48,9 @@
import Popover from 'primevue/popover' import Popover from 'primevue/popover'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { import type {
type MenuOption, MenuOption,
type SubMenuOption SubMenuOption
} from '@/composables/graph/useMoreOptionsMenu' } from '@/composables/graph/useMoreOptionsMenu'
import { useNodeCustomization } from '@/composables/graph/useNodeCustomization' import { useNodeCustomization } from '@/composables/graph/useNodeCustomization'

View File

@@ -56,10 +56,10 @@ import { computed, nextTick, ref, watch } from 'vue'
import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue' import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue'
import ResponseBlurb from '@/components/graph/widgets/chatHistory/ResponseBlurb.vue' import ResponseBlurb from '@/components/graph/widgets/chatHistory/ResponseBlurb.vue'
import { ComponentWidget } from '@/scripts/domWidget' import type { ComponentWidget } from '@/scripts/domWidget'
import { linkifyHtml, nl2br } from '@/utils/formatUtil' import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const { widget, history = '[]' } = defineProps<{ const { widget, history } = defineProps<{
widget?: ComponentWidget<string> widget?: ComponentWidget<string>
history: string history: string
}>() }>()

View File

@@ -19,14 +19,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useElementBounding, useEventListener } from '@vueuse/core' import { useElementBounding, useEventListener } from '@vueuse/core'
import { CSSProperties, computed, nextTick, onMounted, ref, watch } from 'vue' import type { CSSProperties } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition' import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
import { useDomClipping } from '@/composables/element/useDomClipping' import { useDomClipping } from '@/composables/element/useDomClipping'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { isComponentWidget, isDOMWidget } from '@/scripts/domWidget' import { isComponentWidget, isDOMWidget } from '@/scripts/domWidget'
import { DomWidgetState } from '@/stores/domWidgetStore' import type { DomWidgetState } from '@/stores/domWidgetStore'
const { widgetState } = defineProps<{ const { widgetState } = defineProps<{
widgetState: DomWidgetState widgetState: DomWidgetState

View File

@@ -15,7 +15,7 @@
import Skeleton from 'primevue/skeleton' import Skeleton from 'primevue/skeleton'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { NodeId } from '@/lib/litegraph/src/litegraph' import type { NodeId } from '@/lib/litegraph/src/litegraph'
import { useExecutionStore } from '@/stores/executionStore' import { useExecutionStore } from '@/stores/executionStore'
import { linkifyHtml, nl2br } from '@/utils/formatUtil' import { linkifyHtml, nl2br } from '@/utils/formatUtil'

View File

@@ -142,14 +142,14 @@ import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue' import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment' import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useManagerState } from '@/composables/useManagerState'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { type ReleaseNote } from '@/platform/updates/common/releaseService' import type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore' import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { electronAPI, isElectron } from '@/utils/envUtil' import { electronAPI, isElectron } from '@/utils/envUtil'
import { formatVersionAnchor } from '@/utils/formatUtil' import { formatVersionAnchor } from '@/utils/formatUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
// Types // Types
interface MenuItem { interface MenuItem {

View File

@@ -3,7 +3,7 @@ import type { MultiSelectProps } from 'primevue/multiselect'
import { ref } from 'vue' import { ref } from 'vue'
import MultiSelect from './MultiSelect.vue' import MultiSelect from './MultiSelect.vue'
import { type SelectOption } from './types' import type { SelectOption } from './types'
// Combine our component props with PrimeVue MultiSelect props // Combine our component props with PrimeVue MultiSelect props
interface ExtendedProps extends Partial<MultiSelectProps> { interface ExtendedProps extends Partial<MultiSelectProps> {

View File

@@ -3,7 +3,7 @@ import type { MultiSelectProps } from 'primevue/multiselect'
import { ref } from 'vue' import { ref } from 'vue'
import MultiSelect from './MultiSelect.vue' import MultiSelect from './MultiSelect.vue'
import { type SelectOption } from './types' import type { SelectOption } from './types'
// Combine our component props with PrimeVue MultiSelect props // Combine our component props with PrimeVue MultiSelect props
// Since we use v-bind="$attrs", all PrimeVue props are available // Since we use v-bind="$attrs", all PrimeVue props are available

View File

@@ -106,9 +106,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import MultiSelect, { import type { MultiSelectPassThroughMethodOptions } from 'primevue/multiselect'
MultiSelectPassThroughMethodOptions import MultiSelect from 'primevue/multiselect'
} from 'primevue/multiselect'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -117,7 +116,7 @@ import { usePopoverSizing } from '@/composables/usePopoverSizing'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
import TextButton from '../button/TextButton.vue' import TextButton from '../button/TextButton.vue'
import { type SelectOption } from './types' import type { SelectOption } from './types'
type Option = SelectOption type Option = SelectOption

View File

@@ -58,13 +58,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Select, { SelectPassThroughMethodOptions } from 'primevue/select' import type { SelectPassThroughMethodOptions } from 'primevue/select'
import Select from 'primevue/select'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
import { type SelectOption } from './types' import type { SelectOption } from './types'
defineOptions({ defineOptions({
inheritAttrs: false inheritAttrs: false

Some files were not shown because too many files have changed in this diff Show More