Compare commits
1 Commits
v1.27.7
...
fix/5545-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db9d76df65 |
@@ -7,21 +7,3 @@ c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
||||
|
||||
# Reorganize renderer components into domain-driven folder structure
|
||||
c8a83a9caede7bdb5f8598c5492b07d08c339d49
|
||||
|
||||
# Domain-driven design (DDD) refactors - September 2025
|
||||
# These commits reorganized the codebase into domain-driven architecture
|
||||
|
||||
# [refactor] Improve renderer domain organization (#5552)
|
||||
6349ceee6c0a57fc7992e85635def9b6e22eaeb2
|
||||
|
||||
# [refactor] Improve settings domain organization (#5550)
|
||||
4c8c4a1ad4f53354f700a33ea1b95262aeda2719
|
||||
|
||||
# [refactor] Improve workflow domain organization (#5584)
|
||||
ca312fd1eab540cc4ddc0e3d244d38b3858574f0
|
||||
|
||||
# [refactor] Move thumbnail functionality to renderer/core domain (#5586)
|
||||
e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
|
||||
|
||||
# [refactor] Improve updates/notifications domain organization (#5590)
|
||||
27ab355f9c73415dc39f4d3f512b02308f847801
|
||||
|
||||
15
.github/workflows/json-validate.yaml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Validate JSON
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
json-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate JSON syntax
|
||||
run: ./scripts/cicd/check-json.sh
|
||||
139
.github/workflows/publish-frontend-types.yaml
vendored
@@ -1,139 +0,0 @@
|
||||
name: Publish Frontend Types
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish (e.g., 1.26.7)'
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
description: 'npm dist-tag to use'
|
||||
required: true
|
||||
default: latest
|
||||
type: string
|
||||
ref:
|
||||
description: 'Git ref to checkout (commit SHA, tag, or branch)'
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
required: false
|
||||
type: string
|
||||
default: latest
|
||||
ref:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-frontend-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish_types_manual:
|
||||
name: Publish @comfyorg/comfyui-frontend-types
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ inputs.version }}"
|
||||
SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
|
||||
if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
|
||||
echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Determine ref to checkout
|
||||
id: resolve_ref
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF="${{ inputs.ref }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
if [ -n "$REF" ]; then
|
||||
if ! git check-ref-format --allow-onelevel "$REF"; then
|
||||
echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.resolve_ref.outputs.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
|
||||
- name: Build types
|
||||
run: pnpm build:types
|
||||
|
||||
- name: Verify version matches input
|
||||
id: verify
|
||||
shell: bash
|
||||
run: |
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
TYPES_PKG_VERSION=$(node -p "require('./dist/package.json').version")
|
||||
if [ "$PKG_VERSION" != "${{ inputs.version }}" ]; then
|
||||
echo "Error: package.json version $PKG_VERSION does not match input ${{ inputs.version }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$TYPES_PKG_VERSION" != "${{ inputs.version }}" ]; then
|
||||
echo "Error: dist/package.json version $TYPES_PKG_VERSION does not match input ${{ inputs.version }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if version already on npm
|
||||
id: check_npm
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NAME=$(node -p "require('./dist/package.json').name")
|
||||
VER="${{ steps.verify.outputs.version }}"
|
||||
STATUS=0
|
||||
OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$?
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish."
|
||||
else
|
||||
if echo "$OUTPUT" | grep -q "E404"; then
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::error title=Registry lookup failed::$OUTPUT" >&2
|
||||
exit "$STATUS"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Publish package
|
||||
if: steps.check_npm.outputs.exists == 'false'
|
||||
run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks
|
||||
working-directory: dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
42
.github/workflows/release.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -126,8 +126,34 @@ jobs:
|
||||
|
||||
publish_types:
|
||||
needs: build
|
||||
uses: ./.github/workflows/publish-frontend-types.yaml
|
||||
with:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
secrets: inherit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
tsconfig.tsbuildinfo
|
||||
dist
|
||||
key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
types-tools-cache-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build:types
|
||||
- name: Publish package
|
||||
run: pnpm publish --access public
|
||||
working-directory: dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
2
.gitignore
vendored
@@ -51,7 +51,7 @@ tests-ui/workflows/examples
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
browser_tests/**/*-win32.png
|
||||
browser_tests/local/
|
||||
browser-tests/local/
|
||||
|
||||
.env
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '../../src/schemas/comfyWorkflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
@@ -145,7 +144,6 @@ export class ComfyPage {
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
public readonly vueNodes: VueNodeHelpers
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -174,7 +172,6 @@ export class ComfyPage {
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page, this)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
this.vueNodes = new VueNodeHelpers(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -1424,7 +1421,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async closeDialog() {
|
||||
await this.page.locator('.p-dialog-close-button').click({ force: true })
|
||||
await this.page.locator('.p-dialog-close-button').click()
|
||||
await expect(this.page.locator('.p-dialog')).toBeHidden()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Vue Node Test Helpers
|
||||
*/
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class VueNodeHelpers {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Get locator for all Vue node components in the DOM
|
||||
*/
|
||||
get nodes(): Locator {
|
||||
return this.page.locator('[data-node-id]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator for selected Vue node components (using visual selection indicators)
|
||||
*/
|
||||
get selectedNodes(): Locator {
|
||||
return this.page.locator(
|
||||
'[data-node-id].outline-black, [data-node-id].outline-white'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total count of Vue nodes in the DOM
|
||||
*/
|
||||
async getNodeCount(): Promise<number> {
|
||||
return await this.nodes.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of selected Vue nodes
|
||||
*/
|
||||
async getSelectedNodeCount(): Promise<number> {
|
||||
return await this.selectedNodes.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Vue node IDs currently in the DOM
|
||||
*/
|
||||
async getNodeIds(): Promise<string[]> {
|
||||
return await this.nodes.evaluateAll((nodes) =>
|
||||
nodes
|
||||
.map((n) => n.getAttribute('data-node-id'))
|
||||
.filter((id): id is string => id !== null)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific Vue node by ID
|
||||
*/
|
||||
async selectNode(nodeId: string): Promise<void> {
|
||||
await this.page.locator(`[data-node-id="${nodeId}"]`).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select multiple Vue nodes by IDs using Ctrl+click
|
||||
*/
|
||||
async selectNodes(nodeIds: string[]): Promise<void> {
|
||||
if (nodeIds.length === 0) return
|
||||
|
||||
// Select first node normally
|
||||
await this.selectNode(nodeIds[0])
|
||||
|
||||
// Add additional nodes with Ctrl+click
|
||||
for (let i = 1; i < nodeIds.length; i++) {
|
||||
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selections by clicking empty space
|
||||
*/
|
||||
async clearSelection(): Promise<void> {
|
||||
await this.page.mouse.click(50, 50)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected Vue nodes using Delete key
|
||||
*/
|
||||
async deleteSelected(): Promise<void> {
|
||||
await this.page.locator('#graph-canvas').focus()
|
||||
await this.page.keyboard.press('Delete')
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected Vue nodes using Backspace key
|
||||
*/
|
||||
async deleteSelectedWithBackspace(): Promise<void> {
|
||||
await this.page.locator('#graph-canvas').focus()
|
||||
await this.page.keyboard.press('Backspace')
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Vue nodes to be rendered
|
||||
*/
|
||||
async waitForNodes(expectedCount?: number): Promise<void> {
|
||||
if (expectedCount !== undefined) {
|
||||
await this.page.waitForFunction(
|
||||
(count) => document.querySelectorAll('[data-node-id]').length >= count,
|
||||
expectedCount
|
||||
)
|
||||
} else {
|
||||
await this.page.waitForSelector('[data-node-id]')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '../../../src/schemas/comfyWorkflowSchema'
|
||||
import { ManageGroupNode } from '../../helpers/manageGroupNode'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
import type { Position, Size } from '../types'
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path'
|
||||
import {
|
||||
TemplateInfo,
|
||||
WorkflowTemplates
|
||||
} from '../../src/platform/workflow/templates/types/template'
|
||||
} from '../../src/types/workflowTemplateTypes'
|
||||
|
||||
export class ComfyTemplates {
|
||||
readonly content: Locator
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { SettingParams } from '../../src/platform/settings/types'
|
||||
import { SettingParams } from '../../src/types/settingTypes'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
@@ -247,7 +247,7 @@ test.describe('Topbar commands', () => {
|
||||
test.describe('Dialog', () => {
|
||||
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
void window['app'].extensionManager.dialog
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: 'Test Prompt',
|
||||
message: 'Test Prompt Message'
|
||||
@@ -267,7 +267,7 @@ test.describe('Topbar commands', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
void window['app'].extensionManager.dialog
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
@@ -284,7 +284,7 @@ test.describe('Topbar commands', () => {
|
||||
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['value'] = 'foo'
|
||||
void window['app'].extensionManager.dialog
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
|
||||
@@ -1012,8 +1012,6 @@ test.describe('Canvas Navigation', () => {
|
||||
test('Shift + mouse wheel should pan canvas horizontally', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning')
|
||||
|
||||
await comfyPage.page.click('canvas')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 98 KiB |
@@ -46,7 +46,7 @@ test.describe('Node Help', () => {
|
||||
|
||||
// Click the help button in the selection toolbox
|
||||
const helpButton = comfyPage.selectionToolbox.locator(
|
||||
'button[data-testid="info-button"]'
|
||||
'button:has(.pi-question-circle)'
|
||||
)
|
||||
await expect(helpButton).toBeVisible()
|
||||
await helpButton.click()
|
||||
@@ -164,7 +164,7 @@ test.describe('Node Help', () => {
|
||||
|
||||
// Click help button
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -194,7 +194,7 @@ test.describe('Node Help', () => {
|
||||
|
||||
// Click help button
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -228,7 +228,7 @@ test.describe('Node Help', () => {
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -276,7 +276,7 @@ test.describe('Node Help', () => {
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -348,7 +348,7 @@ This is documentation for a custom node.
|
||||
}
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
if (await helpButton.isVisible()) {
|
||||
await helpButton.click()
|
||||
@@ -389,7 +389,7 @@ This is documentation for a custom node.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -456,7 +456,7 @@ This is English documentation.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -479,7 +479,7 @@ This is English documentation.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -522,7 +522,7 @@ This is English documentation.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -538,7 +538,7 @@ This is English documentation.
|
||||
|
||||
// Click help button again
|
||||
const helpButton2 = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
)
|
||||
await helpButton2.click()
|
||||
|
||||
|
||||
@@ -190,9 +190,7 @@ test.describe('Remote COMBO Widget', () => {
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="refresh-button"]'
|
||||
)
|
||||
comfyPage.page.locator('.selection-toolbox .pi-refresh')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,177 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Selection Toolbox - More Options Submenus', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
const openMoreOptions = async (comfyPage: any) => {
|
||||
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
throw new Error('No KSampler nodes found')
|
||||
}
|
||||
|
||||
// Drag the KSampler to the center of the screen
|
||||
const nodePos = await ksamplerNodes[0].getPosition()
|
||||
const viewportSize = comfyPage.page.viewportSize()
|
||||
const centerX = viewportSize.width / 3
|
||||
const centerY = viewportSize.height / 2
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: nodePos.x, y: nodePos.y },
|
||||
{ x: centerX, y: centerY }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await ksamplerNodes[0].click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await comfyPage.page.click('[data-testid="more-options-button"]')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisible = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
await moreOptionsBtn.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(2000)
|
||||
|
||||
const menuOptionsVisibleAfterClick = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisibleAfterClick) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Could not open More Options menu - popover not showing')
|
||||
}
|
||||
|
||||
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
|
||||
exact: true
|
||||
})
|
||||
await expect(nodeInfoButton).toBeVisible()
|
||||
await nodeInfoButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialShape = await nodeRef.getProperty<number>('shape')
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Shape', { exact: true }).click()
|
||||
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
await comfyPage.page.getByText('Box', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newShape = await nodeRef.getProperty<number>('shape')
|
||||
expect(newShape).not.toBe(initialShape)
|
||||
expect(newShape).toBe(1)
|
||||
})
|
||||
|
||||
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Color', { exact: true }).click()
|
||||
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
|
||||
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
|
||||
await blueSwatch.first().click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
expect(newColor).toBe('#223')
|
||||
if (initialColor) {
|
||||
expect(newColor).not.toBe(initialColor)
|
||||
}
|
||||
})
|
||||
|
||||
test('renames a node using Rename action', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page
|
||||
.getByText('Rename', { exact: true })
|
||||
.click({ force: true })
|
||||
const input = comfyPage.page.locator(
|
||||
'.group-title-editor.node-title-editor .editable-text input'
|
||||
)
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill('RenamedNode')
|
||||
await input.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
const newTitle = await nodeRef.getProperty<string>('title')
|
||||
expect(newTitle).toBe('RenamedNode')
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking outside', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await comfyPage.page
|
||||
.locator('#graph-canvas')
|
||||
.click({ position: { x: 0, y: 50 }, force: true })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking the button again (toggle)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const btn = document.querySelector('[data-testid="more-options-button"]')
|
||||
if (btn) {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
detail: 1
|
||||
})
|
||||
btn.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,141 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes - Delete Key Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Enable Vue nodes rendering
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Can select all and delete Vue nodes with Delete key', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// Get initial Vue node count
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(initialNodeCount).toBeGreaterThan(0)
|
||||
|
||||
// Select all Vue nodes
|
||||
await comfyPage.ctrlA()
|
||||
|
||||
// Verify all Vue nodes are selected
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(initialNodeCount)
|
||||
|
||||
// Delete with Delete key
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Verify all Vue nodes were deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(0)
|
||||
})
|
||||
|
||||
test('Can select specific Vue node and delete it', async ({ comfyPage }) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// Get initial Vue node count
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(initialNodeCount).toBeGreaterThan(0)
|
||||
|
||||
// Get first Vue node ID and select it
|
||||
const nodeIds = await comfyPage.vueNodes.getNodeIds()
|
||||
await comfyPage.vueNodes.selectNode(nodeIds[0])
|
||||
|
||||
// Verify selection
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(1)
|
||||
|
||||
// Delete with Delete key
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Verify one Vue node was deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount - 1)
|
||||
})
|
||||
|
||||
test('Can select and delete Vue node with Backspace key', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
|
||||
// Select first Vue node
|
||||
const nodeIds = await comfyPage.vueNodes.getNodeIds()
|
||||
await comfyPage.vueNodes.selectNode(nodeIds[0])
|
||||
|
||||
// Delete with Backspace key instead of Delete
|
||||
await comfyPage.vueNodes.deleteSelectedWithBackspace()
|
||||
|
||||
// Verify Vue node was deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount - 1)
|
||||
})
|
||||
|
||||
test('Delete key does not delete node when typing in Vue node widgets', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
||||
|
||||
// Find a text input widget in a Vue node
|
||||
const textWidget = comfyPage.page
|
||||
.locator('input[type="text"], textarea')
|
||||
.first()
|
||||
|
||||
// Click on text widget to focus it
|
||||
await textWidget.click()
|
||||
await textWidget.fill('test text')
|
||||
|
||||
// Press Delete while focused on widget - should delete text, not node
|
||||
await textWidget.press('Delete')
|
||||
|
||||
// Node count should remain the same
|
||||
const finalNodeCount = await comfyPage.getGraphNodesCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount)
|
||||
})
|
||||
|
||||
test('Delete key does not delete node when nothing is selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// Ensure no Vue nodes are selected
|
||||
await comfyPage.vueNodes.clearSelection()
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(0)
|
||||
|
||||
// Press Delete key - should not crash and should handle gracefully
|
||||
await comfyPage.page.keyboard.press('Delete')
|
||||
|
||||
// Vue node count should remain the same
|
||||
const nodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(nodeCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Can multi-select with Ctrl+click and delete multiple Vue nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
|
||||
// Multi-select first two Vue nodes using Ctrl+click
|
||||
const nodeIds = await comfyPage.vueNodes.getNodeIds()
|
||||
const nodesToSelect = nodeIds.slice(0, 2)
|
||||
await comfyPage.vueNodes.selectNodes(nodesToSelect)
|
||||
|
||||
// Verify expected nodes are selected
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(nodesToSelect.length)
|
||||
|
||||
// Delete selected Vue nodes
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Verify expected nodes were deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount - nodesToSelect.length)
|
||||
})
|
||||
})
|
||||
@@ -264,13 +264,7 @@ test.describe('Animated image widget', () => {
|
||||
expect(filename).toContain('animated_webp.webp')
|
||||
})
|
||||
|
||||
// FIXME: This test keeps flip-flopping because it relies on animated webp timing,
|
||||
// which is inherently unreliable in CI environments. The test asset is an animated
|
||||
// webp with 2 frames, and the test depends on animation frame timing to verify that
|
||||
// animated webp images are properly displayed (as opposed to being treated as static webp).
|
||||
// While the underlying functionality works (animated webp are correctly distinguished
|
||||
// from static webp), the test is flaky due to timing dependencies with webp animation frames.
|
||||
test.fixme('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
test('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/save_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -5,14 +5,13 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import vueParser from 'vue-eslint-parser'
|
||||
|
||||
const extraFileExtensions = ['.vue']
|
||||
|
||||
export default defineConfig([
|
||||
export default [
|
||||
{
|
||||
files: ['src/**/*.{js,mjs,cjs,ts,vue}']
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'src/scripts/*',
|
||||
@@ -25,58 +24,35 @@ export default defineConfig([
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ['./**/*.{ts,mts}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||
},
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts',
|
||||
'scripts/collect-i18n-node-defs.ts'
|
||||
]
|
||||
},
|
||||
tsConfigRootDir: import.meta.dirname,
|
||||
project: ['./tsconfig.json', './tsconfig.eslint.json'],
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
extraFileExtensions
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['./**/*.vue'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__COMFYUI_FRONTEND_VERSION__: 'readonly'
|
||||
},
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
projectService: true,
|
||||
tsConfigRootDir: import.meta.dirname,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
extraFileExtensions
|
||||
extraFileExtensions: ['.vue']
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
pluginVue.configs['flat/recommended'],
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
eslintPluginPrettierRecommended,
|
||||
storybook.configs['flat/recommended'],
|
||||
{
|
||||
files: ['src/**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
'@intlify/vue-i18n': pluginI18n
|
||||
},
|
||||
rules: {
|
||||
@@ -91,7 +67,6 @@ export default defineConfig([
|
||||
'vue/multi-word-component-names': 'off', // TODO: fix
|
||||
'vue/no-template-shadow': 'off', // TODO: fix
|
||||
'vue/one-component-per-file': 'off', // TODO: fix
|
||||
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
|
||||
// Restrict deprecated PrimeVue components
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
@@ -160,5 +135,6 @@ export default defineConfig([
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
...storybook.configs['flat/recommended']
|
||||
]
|
||||
@@ -25,9 +25,7 @@ const config: KnipConfig = {
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
'src/types/comfyRegistryTypes.ts',
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Staged for for use with subgraph widget promotion
|
||||
'src/lib/litegraph/src/widgets/DisconnectedWidget.ts'
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -3,13 +3,13 @@ export default {
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
'vue-tsc --noEmit'
|
||||
]
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
return [
|
||||
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
|
||||
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
|
||||
`eslint --fix ${fileNames.join(' ')}`,
|
||||
`prettier --write ${fileNames.join(' ')}`
|
||||
]
|
||||
}
|
||||
|
||||
31
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.27.7",
|
||||
"version": "1.27.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -14,9 +14,9 @@
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:browser": "npx nx e2e",
|
||||
"test:unit": "nx run test tests-ui/tests",
|
||||
@@ -38,10 +38,10 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@iconify-json/lucide": "^1.2.66",
|
||||
"@iconify/tailwind": "^1.2.0",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
|
||||
"@lobehub/i18n-cli": "^1.25.1",
|
||||
"@nx/eslint": "21.4.1",
|
||||
"@nx/playwright": "21.4.1",
|
||||
@@ -64,11 +64,11 @@
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-storybook": "^9.1.6",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-storybook": "^9.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globals": "^15.9.0",
|
||||
"happy-dom": "^15.11.0",
|
||||
@@ -79,30 +79,29 @@
|
||||
"lint-staged": "^15.2.7",
|
||||
"nx": "21.4.1",
|
||||
"prettier": "^3.3.2",
|
||||
"storybook": "^9.1.6",
|
||||
"storybook": "^9.1.1",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"tsx": "^4.15.6",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"unplugin-icons": "^0.22.0",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-vue-devtools": "^7.7.6",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.7",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.72",
|
||||
"@iconify/json": "^2.2.380",
|
||||
"@primeuix/forms": "0.0.2",
|
||||
"@primeuix/styled": "0.3.2",
|
||||
|
||||
@@ -7,7 +7,6 @@ export default defineConfig({
|
||||
headless: true
|
||||
},
|
||||
reporter: 'list',
|
||||
workers: 1,
|
||||
timeout: 60000,
|
||||
testMatch: /collect-i18n-.*\.ts/
|
||||
})
|
||||
|
||||
1058
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--debug]" >&2
|
||||
}
|
||||
|
||||
debug=0
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--debug)
|
||||
debug=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Validate JSON syntax in tracked files using jq
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Error: jq is required but not installed" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
EXCLUDE_PATTERNS=(
|
||||
'**/tsconfig*.json'
|
||||
)
|
||||
|
||||
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then
|
||||
# shellcheck disable=SC2206
|
||||
EXCLUDE_PATTERNS+=( ${JSON_LINT_EXCLUDES} )
|
||||
fi
|
||||
|
||||
pathspecs=(-- '*.json')
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ ${pattern:0:1} == ':' ]]; then
|
||||
pathspecs+=("$pattern")
|
||||
else
|
||||
pathspecs+=(":(glob,exclude)${pattern}")
|
||||
fi
|
||||
done
|
||||
|
||||
mapfile -t json_files < <(git ls-files "${pathspecs[@]}")
|
||||
|
||||
if [ "${#json_files[@]}" -eq 0 ]; then
|
||||
echo 'No JSON files found.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$debug" -eq 1 ]; then
|
||||
echo 'JSON files to validate:'
|
||||
printf ' %s\n' "${json_files[@]}"
|
||||
fi
|
||||
|
||||
failed=0
|
||||
for file in "${json_files[@]}"; do
|
||||
if ! jq -e . "$file" >/dev/null; then
|
||||
echo "Invalid JSON syntax: $file" >&2
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
echo 'JSON validation failed.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo 'All JSON files are valid.'
|
||||
@@ -2,10 +2,9 @@ import * as fs from 'fs'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
|
||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||
import type { FormItem, SettingParams } from '../src/platform/settings/types'
|
||||
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
||||
import type { FormItem, SettingParams } from '../src/types/settingTypes'
|
||||
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
@@ -132,23 +131,6 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
||||
])
|
||||
)
|
||||
|
||||
// Desktop Dialogs
|
||||
const allDesktopDialogsLocale = Object.fromEntries(
|
||||
Object.values(DESKTOP_DIALOGS).map((dialog) => [
|
||||
normalizeI18nKey(dialog.id),
|
||||
{
|
||||
title: dialog.title,
|
||||
message: dialog.message,
|
||||
buttons: Object.fromEntries(
|
||||
dialog.buttons.map((button) => [
|
||||
normalizeI18nKey(button.label),
|
||||
button.label
|
||||
])
|
||||
)
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
localePath,
|
||||
JSON.stringify(
|
||||
@@ -162,8 +144,7 @@ test('collect-i18n-general', async ({ comfyPage }) => {
|
||||
...allSettingCategoriesLocale
|
||||
},
|
||||
serverConfigItems: allServerConfigsLocale,
|
||||
serverConfigCategories: allServerConfigCategoriesLocale,
|
||||
desktopDialogs: allDesktopDialogsLocale
|
||||
serverConfigCategories: allServerConfigCategoriesLocale
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import type { ComfyNodeDef } from '../src/schemas/nodeDefSchema'
|
||||
import type { ComfyApi } from '../src/scripts/api'
|
||||
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import { normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
@@ -11,28 +11,23 @@ const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
||||
|
||||
test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
// Mock view route
|
||||
await comfyPage.page.route('**/view**', async (route) => {
|
||||
comfyPage.page.route('**/view**', async (route) => {
|
||||
await route.fulfill({
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
})
|
||||
|
||||
// Note: Don't mock the object_info API endpoint - let it hit the actual backend
|
||||
|
||||
const nodeDefs: ComfyNodeDefImpl[] = await comfyPage.page.evaluate(
|
||||
async () => {
|
||||
const api = window['app'].api
|
||||
const rawNodeDefs = await api.getNodeDefs()
|
||||
const { ComfyNodeDefImpl } = await import('../src/stores/nodeDefStore')
|
||||
|
||||
return (
|
||||
Object.values(rawNodeDefs)
|
||||
// Ignore DevTools nodes (used for internal testing)
|
||||
.filter((def: ComfyNodeDef) => !def.name.startsWith('DevTools'))
|
||||
.map((def: ComfyNodeDef) => new ComfyNodeDefImpl(def))
|
||||
)
|
||||
}
|
||||
const nodeDefs: ComfyNodeDefImpl[] = (
|
||||
Object.values(
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
const api = window['app'].api as ComfyApi
|
||||
return await api.getNodeDefs()
|
||||
})
|
||||
) as ComfyNodeDef[]
|
||||
)
|
||||
// Ignore DevTools nodes (used for internal testing)
|
||||
.filter((def) => !def.name.startsWith('DevTools'))
|
||||
.map((def) => new ComfyNodeDefImpl(def))
|
||||
|
||||
console.log(`Collected ${nodeDefs.length} node definitions`)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
import {
|
||||
zComfyWorkflow,
|
||||
zComfyWorkflow1
|
||||
} from '../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
} from '../src/schemas/comfyWorkflowSchema'
|
||||
import { zComfyNodeDef as zComfyNodeDefV2 } from '../src/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { zComfyNodeDef as zComfyNodeDefV1 } from '../src/schemas/nodeDefSchema'
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
@layer theme, base, primevue, components, utilities;
|
||||
|
||||
@import './fonts.css';
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
@import 'tw-animate-css';
|
||||
@@ -53,20 +52,15 @@
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
/* Font Families */
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
|
||||
/* Palette Colors */
|
||||
--color-charcoal-100: #55565e;
|
||||
--color-charcoal-200: #494a50;
|
||||
--color-charcoal-300: #3c3d42;
|
||||
--color-charcoal-400: #313235;
|
||||
--color-charcoal-500: #2d2e32;
|
||||
--color-charcoal-600: #262729;
|
||||
--color-charcoal-700: #202121;
|
||||
--color-charcoal-800: #171718;
|
||||
|
||||
--color-neutral-550: #636363;
|
||||
--color-charcoal-100: #171718;
|
||||
--color-charcoal-200: #202121;
|
||||
--color-charcoal-300: #262729;
|
||||
--color-charcoal-400: #2d2e32;
|
||||
--color-charcoal-500: #313235;
|
||||
--color-charcoal-600: #3c3d42;
|
||||
--color-charcoal-700: #494a50;
|
||||
--color-charcoal-800: #55565e;
|
||||
|
||||
--color-stone-100: #444444;
|
||||
--color-stone-200: #828282;
|
||||
@@ -105,16 +99,11 @@
|
||||
--color-danger-100: #c02323;
|
||||
--color-danger-200: #d62952;
|
||||
|
||||
--color-coral-red-600: #973a40;
|
||||
--color-coral-red-500: #c53f49;
|
||||
--color-coral-red-400: #dd424e;
|
||||
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
--color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3);
|
||||
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
|
||||
--color-blue-selection: rgb( from var(--color-blue-100) r g b / 0.3);
|
||||
--color-node-hover-100: rgb( from var(--color-charcoal-800) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1);
|
||||
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
|
||||
|
||||
/* PrimeVue pulled colors */
|
||||
@@ -127,10 +116,10 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-node-component-surface: var(--color-charcoal-600);
|
||||
--color-node-component-surface: var(--color-charcoal-300);
|
||||
--color-node-component-surface-highlight: var(--color-slate-100);
|
||||
--color-node-component-surface-hovered: var(--color-charcoal-400);
|
||||
--color-node-component-surface-selected: var(--color-charcoal-200);
|
||||
--color-node-component-surface-hovered: var(--color-charcoal-500);
|
||||
--color-node-component-surface-selected: var(--color-charcoal-700);
|
||||
--color-node-stroke: var(--color-stone-100);
|
||||
}
|
||||
|
||||
@@ -142,7 +131,7 @@
|
||||
|
||||
@utility scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@@ -972,7 +961,9 @@ audio.comfy-audio.empty-audio-widget {
|
||||
/* Uses default styling - no overrides needed */
|
||||
}
|
||||
|
||||
/* Smooth transitions between LOD levels */
|
||||
.lg-node {
|
||||
transition: min-height 0.2s ease;
|
||||
/* Disable text selection on all nodes */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 16 16" fill="none">
|
||||
<g clip-path="url(#clip0_704_2695)">
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="#9C9EAB" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_704_2695">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 938 B |
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Utility functions for downloading files
|
||||
*/
|
||||
|
||||
// Constants
|
||||
const DEFAULT_DOWNLOAD_FILENAME = 'download.png'
|
||||
|
||||
/**
|
||||
* Download a file from a URL by creating a temporary anchor element
|
||||
* @param url - The URL of the file to download (must be a valid URL string)
|
||||
* @param filename - Optional filename override (will use URL filename or default if not provided)
|
||||
* @throws {Error} If the URL is invalid or empty
|
||||
*/
|
||||
export const downloadFile = (url: string, filename?: string): void => {
|
||||
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
||||
throw new Error('Invalid URL provided for download')
|
||||
}
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download =
|
||||
filename || extractFilenameFromUrl(url) || DEFAULT_DOWNLOAD_FILENAME
|
||||
|
||||
// Trigger download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract filename from a URL's query parameters
|
||||
* @param url - The URL to extract filename from
|
||||
* @returns The extracted filename or null if not found
|
||||
*/
|
||||
const extractFilenameFromUrl = (url: string): string | null => {
|
||||
try {
|
||||
const urlObj = new URL(url, window.location.origin)
|
||||
return urlObj.searchParams.get('filename')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
import Button from 'primevue/button'
|
||||
import { CSSProperties, computed, watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ import { storeToRefs } from 'pinia'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
const { batchCount } = storeToRefs(queueSettingsStore)
|
||||
|
||||
@@ -24,7 +24,7 @@ import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
|
||||
|
||||
@@ -1,42 +1,15 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="relative overflow-hidden h-full w-full bg-neutral-900"
|
||||
>
|
||||
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div ref="terminalEl" class="h-full terminal-host" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip.left="{
|
||||
value: tooltipText,
|
||||
showDelay: 300
|
||||
}"
|
||||
icon="pi pi-copy"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
:class="
|
||||
cn('absolute top-2 right-8 transition-opacity', {
|
||||
'opacity-0 pointer-events-none select-none': !isHovered
|
||||
})
|
||||
"
|
||||
:aria-label="tooltipText"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementHover, useEventListener } from '@vueuse/core'
|
||||
import type { IDisposable } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Ref, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
created: [ReturnType<typeof useTerminal>, Ref<HTMLElement | undefined>]
|
||||
@@ -44,69 +17,18 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
const terminalEl = ref<HTMLElement | undefined>()
|
||||
const rootEl = ref<HTMLElement | undefined>()
|
||||
const hasSelection = ref(false)
|
||||
emit('created', useTerminal(terminalEl), rootEl)
|
||||
|
||||
const isHovered = useElementHover(rootEl)
|
||||
|
||||
const terminalData = useTerminal(terminalEl)
|
||||
emit('created', terminalData, ref(rootEl))
|
||||
|
||||
const { terminal } = terminalData
|
||||
let selectionDisposable: IDisposable | undefined
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
return hasSelection.value
|
||||
? t('serverStart.copySelectionTooltip')
|
||||
: t('serverStart.copyAllTooltip')
|
||||
})
|
||||
|
||||
const handleCopy = async () => {
|
||||
const existingSelection = terminal.getSelection()
|
||||
const shouldSelectAll = !existingSelection
|
||||
if (shouldSelectAll) terminal.selectAll()
|
||||
|
||||
const selectedText = shouldSelectAll
|
||||
? terminal.getSelection()
|
||||
: existingSelection
|
||||
|
||||
if (selectedText) {
|
||||
await navigator.clipboard.writeText(selectedText)
|
||||
|
||||
if (shouldSelectAll) {
|
||||
terminal.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showContextMenu = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
electronAPI()?.showContextMenu({ type: 'text' })
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
useEventListener(terminalEl, 'contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectionDisposable = terminal.onSelectionChange(() => {
|
||||
hasSelection.value = terminal.hasSelection()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
selectionDisposable?.dispose()
|
||||
emit('unmounted')
|
||||
})
|
||||
onUnmounted(() => emit('unmounted'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
:deep(.p-terminal) .xterm {
|
||||
@apply overflow-hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
@apply bg-neutral-900 overflow-hidden;
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,10 +38,10 @@ import { computed, onUpdated, ref, watch } from 'vue'
|
||||
|
||||
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { forEachSubgraphNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
const MIN_WIDTH = 28
|
||||
|
||||
@@ -53,14 +53,11 @@ import Tag from 'primevue/tag'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -36,8 +36,8 @@ import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const modelValue = defineModel<string>()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<InputText
|
||||
v-else
|
||||
ref="inputRef"
|
||||
v-model:model-value="inputValue"
|
||||
v-model:modelValue="inputValue"
|
||||
v-focus
|
||||
type="text"
|
||||
size="small"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<component
|
||||
:is="markRaw(getFormComponent(props.item))"
|
||||
:id="props.id"
|
||||
v-model:model-value="formValue"
|
||||
v-model:modelValue="formValue"
|
||||
:aria-labelledby="`${props.id}-label`"
|
||||
v-bind="getFormAttrs(props.item)"
|
||||
/>
|
||||
@@ -40,11 +40,10 @@ import BackgroundImageUpload from '@/components/common/BackgroundImageUpload.vue
|
||||
import CustomFormValue from '@/components/common/CustomFormValue.vue'
|
||||
import FormColorPicker from '@/components/common/FormColorPicker.vue'
|
||||
import FormImageUpload from '@/components/common/FormImageUpload.vue'
|
||||
import FormRadioGroup from '@/components/common/FormRadioGroup.vue'
|
||||
import InputKnob from '@/components/common/InputKnob.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { FormItem } from '@/platform/settings/types'
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
const props = defineProps<{
|
||||
@@ -67,7 +66,6 @@ function getFormAttrs(item: FormItem) {
|
||||
}
|
||||
switch (item.type) {
|
||||
case 'combo':
|
||||
case 'radio':
|
||||
attrs['options'] =
|
||||
typeof item.options === 'function'
|
||||
? // @ts-expect-error: Audit and deprecate usage of legacy options type:
|
||||
@@ -99,8 +97,6 @@ function getFormComponent(item: FormItem): Component {
|
||||
return InputKnob
|
||||
case 'combo':
|
||||
return Select
|
||||
case 'radio':
|
||||
return FormRadioGroup
|
||||
case 'image':
|
||||
return FormImageUpload
|
||||
case 'color':
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
import FormRadioGroup from './FormRadioGroup.vue'
|
||||
|
||||
describe('FormRadioGroup', () => {
|
||||
beforeAll(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: any, options = {}) => {
|
||||
return mount(FormRadioGroup, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { RadioButton }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
describe('normalizedOptions computed property', () => {
|
||||
it('handles string array options', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'option1',
|
||||
options: ['option1', 'option2', 'option3'],
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('option1')
|
||||
expect(radioButtons[1].props('value')).toBe('option2')
|
||||
expect(radioButtons[2].props('value')).toBe('option3')
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('option1')
|
||||
expect(labels[1].text()).toBe('option2')
|
||||
expect(labels[2].text()).toBe('option3')
|
||||
})
|
||||
|
||||
it('handles SettingOption array', () => {
|
||||
const options: SettingOption[] = [
|
||||
{ text: 'Small', value: 'sm' },
|
||||
{ text: 'Medium', value: 'md' },
|
||||
{ text: 'Large', value: 'lg' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'md',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('sm')
|
||||
expect(radioButtons[1].props('value')).toBe('md')
|
||||
expect(radioButtons[2].props('value')).toBe('lg')
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('Small')
|
||||
expect(labels[1].text()).toBe('Medium')
|
||||
expect(labels[2].text()).toBe('Large')
|
||||
})
|
||||
|
||||
it('handles SettingOption with undefined value (uses text as value)', () => {
|
||||
const options: SettingOption[] = [
|
||||
{ text: 'Option A', value: undefined },
|
||||
{ text: 'Option B' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Option A',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('Option A')
|
||||
expect(radioButtons[1].props('value')).toBe('Option B')
|
||||
})
|
||||
|
||||
it('handles custom object with optionLabel and optionValue', () => {
|
||||
const options = [
|
||||
{ name: 'First Option', id: 1 },
|
||||
{ name: 'Second Option', id: 2 },
|
||||
{ name: 'Third Option', id: 3 }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 2,
|
||||
options,
|
||||
optionLabel: 'name',
|
||||
optionValue: 'id',
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe(1)
|
||||
expect(radioButtons[1].props('value')).toBe(2)
|
||||
expect(radioButtons[2].props('value')).toBe(3)
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('First Option')
|
||||
expect(labels[1].text()).toBe('Second Option')
|
||||
expect(labels[2].text()).toBe('Third Option')
|
||||
})
|
||||
|
||||
it('handles mixed array with strings and SettingOptions', () => {
|
||||
const options: (string | SettingOption)[] = [
|
||||
'Simple String',
|
||||
{ text: 'Complex Option', value: 'complex' },
|
||||
'Another String'
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'complex',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(3)
|
||||
|
||||
expect(radioButtons[0].props('value')).toBe('Simple String')
|
||||
expect(radioButtons[1].props('value')).toBe('complex')
|
||||
expect(radioButtons[2].props('value')).toBe('Another String')
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('Simple String')
|
||||
expect(labels[1].text()).toBe('Complex Option')
|
||||
expect(labels[2].text()).toBe('Another String')
|
||||
})
|
||||
|
||||
it('handles empty options array', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: null,
|
||||
options: [],
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles undefined options gracefully', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: null,
|
||||
options: undefined,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles object with missing properties gracefully', () => {
|
||||
const options = [
|
||||
{ label: 'Option 1', val: 'opt1' },
|
||||
{ text: 'Option 2', value: 'opt2' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'opt1',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
expect(radioButtons).toHaveLength(2)
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
expect(labels[0].text()).toBe('Unknown')
|
||||
expect(labels[1].text()).toBe('Option 2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('component functionality', () => {
|
||||
it('sets correct input-id and name attributes', () => {
|
||||
const options = ['A', 'B']
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'A',
|
||||
options,
|
||||
id: 'my-radio-group'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
|
||||
expect(radioButtons[0].props('inputId')).toBe('my-radio-group-A')
|
||||
expect(radioButtons[0].props('name')).toBe('my-radio-group')
|
||||
expect(radioButtons[1].props('inputId')).toBe('my-radio-group-B')
|
||||
expect(radioButtons[1].props('name')).toBe('my-radio-group')
|
||||
})
|
||||
|
||||
it('associates labels with radio buttons correctly', () => {
|
||||
const options = ['Yes', 'No']
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Yes',
|
||||
options,
|
||||
id: 'confirm-radio'
|
||||
})
|
||||
|
||||
const labels = wrapper.findAll('label')
|
||||
|
||||
expect(labels[0].attributes('for')).toBe('confirm-radio-Yes')
|
||||
expect(labels[1].attributes('for')).toBe('confirm-radio-No')
|
||||
})
|
||||
|
||||
it('sets aria-describedby attribute correctly', () => {
|
||||
const options: SettingOption[] = [
|
||||
{ text: 'Option 1', value: 'opt1' },
|
||||
{ text: 'Option 2', value: 'opt2' }
|
||||
]
|
||||
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'opt1',
|
||||
options,
|
||||
id: 'test-radio'
|
||||
})
|
||||
|
||||
const radioButtons = wrapper.findAllComponents(RadioButton)
|
||||
|
||||
expect(radioButtons[0].attributes('aria-describedby')).toBe(
|
||||
'Option 1-label'
|
||||
)
|
||||
expect(radioButtons[1].attributes('aria-describedby')).toBe(
|
||||
'Option 2-label'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row gap-4">
|
||||
<div
|
||||
v-for="option in normalizedOptions"
|
||||
:key="option.value"
|
||||
class="flex items-center"
|
||||
>
|
||||
<RadioButton
|
||||
:input-id="`${id}-${option.value}`"
|
||||
:name="id"
|
||||
:value="option.value"
|
||||
:model-value="modelValue"
|
||||
:aria-describedby="`${option.text}-label`"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
<label :for="`${id}-${option.value}`" class="ml-2 cursor-pointer">
|
||||
{{ option.text }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
options: (SettingOption | string)[]
|
||||
optionLabel?: string
|
||||
optionValue?: string
|
||||
id?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: any]
|
||||
}>()
|
||||
|
||||
const normalizedOptions = computed<SettingOption[]>(() => {
|
||||
if (!props.options) return []
|
||||
|
||||
return props.options.map((option) => {
|
||||
if (typeof option === 'string') {
|
||||
return { text: option, value: option }
|
||||
}
|
||||
|
||||
if ('text' in option) {
|
||||
return {
|
||||
text: option.text,
|
||||
value: option.value ?? option.text
|
||||
}
|
||||
}
|
||||
// Handle optionLabel/optionValue
|
||||
return {
|
||||
text: option[props.optionLabel || 'text'] || 'Unknown',
|
||||
value: option[props.optionValue || 'value']
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div class="grid grid-rows-2 gap-8">
|
||||
<!-- Top container: Logo -->
|
||||
<div class="flex items-end justify-center">
|
||||
<img
|
||||
src="/assets/images/comfy-brand-mark.svg"
|
||||
:alt="t('g.logoAlt')"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
<!-- Bottom container: Progress and text -->
|
||||
<div class="flex flex-col items-center justify-center gap-4">
|
||||
<ProgressBar
|
||||
v-if="!hideProgress"
|
||||
:mode="progressMode"
|
||||
:value="progressPercentage ?? 0"
|
||||
:show-value="false"
|
||||
class="w-90 h-2 mt-8"
|
||||
:pt="{ value: { class: 'bg-brand-yellow' } }"
|
||||
/>
|
||||
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="statusText" class="text-lg text-neutral-400">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
/** Props for the StartupDisplay component */
|
||||
interface StartupDisplayProps {
|
||||
/** Progress: 0-100 for determinate, undefined for indeterminate */
|
||||
progressPercentage?: number
|
||||
/** Main title text */
|
||||
title?: string
|
||||
/** Status text shown below the title */
|
||||
statusText?: string
|
||||
/** Hide the progress bar */
|
||||
hideProgress?: boolean
|
||||
/** Use full screen wrapper (default: true) */
|
||||
fullScreen?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
progressPercentage,
|
||||
title,
|
||||
statusText,
|
||||
hideProgress = false,
|
||||
fullScreen = true
|
||||
} = defineProps<StartupDisplayProps>()
|
||||
|
||||
const progressMode = computed(() =>
|
||||
progressPercentage === undefined ? 'indeterminate' : 'determinate'
|
||||
)
|
||||
|
||||
const wrapperClass = computed(() =>
|
||||
fullScreen
|
||||
? 'flex items-center justify-center min-h-screen'
|
||||
: 'flex items-center justify-center'
|
||||
)
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Tree
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:selection-keys="selectionKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
class="tree-explorer py-0 px-2 2xl:px-4"
|
||||
:class="props.class"
|
||||
:value="renderedRoot.children"
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
@@ -94,9 +94,9 @@ import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ConfirmationDialogType } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const props = defineProps<{
|
||||
message: string
|
||||
|
||||
@@ -60,9 +60,9 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -138,7 +138,7 @@ const allMissingNodesInstalled = computed(() => {
|
||||
})
|
||||
// Watch for completion and close dialog
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (allInstalled && showInstallAllButton.value) {
|
||||
if (allInstalled) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="settings-container">
|
||||
<ScrollPanel class="settings-sidebar shrink-0 p-2 w-48 2xl:w-64">
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
v-model:modelValue="searchQuery"
|
||||
class="settings-search-box w-full mb-2"
|
||||
:placeholder="$t('g.searchSettings') + '...'"
|
||||
:debounce-time="128"
|
||||
@@ -66,17 +66,18 @@ import Tabs from 'primevue/tabs'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
||||
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import ColorPaletteMessage from '@/platform/settings/components/ColorPaletteMessage.vue'
|
||||
import SettingsPanel from '@/platform/settings/components/SettingsPanel.vue'
|
||||
import { useSettingSearch } from '@/platform/settings/composables/useSettingSearch'
|
||||
import { useSettingUI } from '@/platform/settings/composables/useSettingUI'
|
||||
import { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { useSettingUI } from '@/composables/setting/useSettingUI'
|
||||
import { SettingTreeNode } from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
|
||||
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
|
||||
import PanelTemplate from './setting/PanelTemplate.vue'
|
||||
import SettingsPanel from './setting/SettingsPanel.vue'
|
||||
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
@@ -112,12 +113,7 @@ const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((group) => ({
|
||||
label: group.label,
|
||||
settings: flattenTree<SettingParams>(group).sort((a, b) => {
|
||||
const sortOrderA = a.sortOrder ?? 0
|
||||
const sortOrderB = b.sortOrder ?? 0
|
||||
|
||||
return sortOrderB - sortOrderA
|
||||
})
|
||||
settings: flattenTree<SettingParams>(group)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="flex flex-1 relative overflow-hidden">
|
||||
<ManagerNavSidebar
|
||||
v-if="isSideNavOpen"
|
||||
v-model:selected-tab="selectedTab"
|
||||
v-model:selectedTab="selectedTab"
|
||||
:tabs="tabs"
|
||||
/>
|
||||
<div
|
||||
@@ -57,9 +57,9 @@
|
||||
</IconButton>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:search-mode="searchMode"
|
||||
v-model:sort-field="sortField"
|
||||
v-model:searchQuery="searchQuery"
|
||||
v-model:searchMode="searchMode"
|
||||
v-model:sortField="sortField"
|
||||
:search-results="searchResults"
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { value = 'N/A', label } = defineProps<{
|
||||
const { value = 'N/A', label = 'N/A' } = defineProps<{
|
||||
label: string
|
||||
value?: string | number
|
||||
}>()
|
||||
|
||||
@@ -41,12 +41,12 @@
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
<SearchFilterDropdown
|
||||
v-model:model-value="searchMode"
|
||||
v-model:modelValue="searchMode"
|
||||
:options="filterOptions"
|
||||
:label="$t('g.filter')"
|
||||
/>
|
||||
<SearchFilterDropdown
|
||||
v-model:model-value="sortField"
|
||||
v-model:modelValue="sortField"
|
||||
:options="availableSortOptions"
|
||||
:label="$t('g.sort')"
|
||||
/>
|
||||
|
||||
@@ -43,8 +43,8 @@ import Button from 'primevue/button'
|
||||
import Message from 'primevue/message'
|
||||
import Select from 'primevue/select'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
@@ -92,9 +92,10 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
const filterTypes = ['All', 'Core', 'Custom']
|
||||
const filterType = ref('All')
|
||||
@@ -54,7 +54,7 @@
|
||||
<div v-for="item in items" :key="item.name" class="mb-4">
|
||||
<FormItem
|
||||
:id="item.id"
|
||||
v-model:form-value="item.value"
|
||||
v-model:formValue="item.value"
|
||||
:item="translateItem(item)"
|
||||
:label-class="{
|
||||
'text-highlight': item.initialValue !== item.value
|
||||
@@ -74,14 +74,15 @@ import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import type { ServerConfig } from '@/constants/serverConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { FormItem as FormItemType } from '@/platform/settings/types'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import type { FormItem as FormItemType } from '@/types/settingTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const serverConfigStore = useServerConfigStore()
|
||||
const {
|
||||
@@ -19,8 +19,8 @@
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import SettingItem from '@/platform/settings/components/SettingItem.vue'
|
||||
import { SettingParams } from '@/platform/settings/types'
|
||||
import SettingItem from '@/components/dialog/content/setting/SettingItem.vue'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
@@ -6,7 +6,7 @@ import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SettingItem from '@/platform/settings/components/SettingItem.vue'
|
||||
import SettingItem from './SettingItem.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
|
||||
@@ -29,8 +29,8 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import { st } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingOption, SettingParams } from '@/platform/settings/types'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import type { SettingOption, SettingParams } from '@/types/settingTypes'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import SettingGroup from '@/platform/settings/components/SettingGroup.vue'
|
||||
import { ISettingGroup } from '@/platform/settings/types'
|
||||
import { ISettingGroup } from '@/types/settingTypes'
|
||||
|
||||
import SettingGroup from './SettingGroup.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
settingGroups: ISettingGroup[]
|
||||
@@ -75,16 +75,16 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="px-4 py-2 flex items-center"
|
||||
>
|
||||
<TabMenu
|
||||
v-model:active-index="activeTabIndex"
|
||||
v-model:activeIndex="activeTabIndex"
|
||||
:model="tabs"
|
||||
class="w-full border-none"
|
||||
:pt="{
|
||||
|
||||
@@ -16,8 +16,8 @@ import { computed } from 'vue'
|
||||
|
||||
import DomWidget from '@/components/graph/widgets/DomWidget.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
@@ -34,7 +34,7 @@ const updateWidgets = () => {
|
||||
const widget = widgetState.widget
|
||||
|
||||
// Early exit for non-visible widgets
|
||||
if (!widget.isVisible() || !widgetState.active) {
|
||||
if (!widget.isVisible()) {
|
||||
widgetState.visible = false
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -40,12 +40,13 @@
|
||||
>
|
||||
<!-- Vue nodes rendered based on graph nodes -->
|
||||
<VueGraphNode
|
||||
v-for="nodeData in allNodes"
|
||||
v-for="nodeData in nodesToRender"
|
||||
:key="nodeData.id"
|
||||
:node-data="nodeData"
|
||||
:position="nodePositions.get(nodeData.id)"
|
||||
:size="nodeSizes.get(nodeData.id)"
|
||||
:readonly="false"
|
||||
:executing="executionStore.executingNodeId === nodeData.id"
|
||||
:error="
|
||||
executionStore.lastExecutionError?.node_id === nodeData.id
|
||||
? 'Execution error'
|
||||
@@ -104,34 +105,33 @@ import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import TransformPane from '@/renderer/core/layout/TransformPane.vue'
|
||||
import MiniMap from '@/renderer/extensions/minimap/MiniMap.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 { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { newUserService } from '@/services/newUserService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
@@ -183,12 +183,12 @@ const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
|
||||
|
||||
const nodePositions = vueNodeLifecycle.nodePositions
|
||||
const nodeSizes = vueNodeLifecycle.nodeSizes
|
||||
const allNodes = viewportCulling.allNodes
|
||||
const nodesToRender = viewportCulling.nodesToRender
|
||||
|
||||
const handleTransformUpdate = () => {
|
||||
viewportCulling.handleTransformUpdate()
|
||||
// TODO: Fix paste position sync in separate PR
|
||||
vueNodeLifecycle.detectChangesInRAF.value()
|
||||
viewportCulling.handleTransformUpdate(
|
||||
vueNodeLifecycle.detectChangesInRAF.value
|
||||
)
|
||||
}
|
||||
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
|
||||
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
|
||||
@@ -205,9 +205,6 @@ const selectedNodeIds = computed(
|
||||
)
|
||||
provide(SelectedNodeIdsKey, selectedNodeIds)
|
||||
|
||||
// Provide execution state to all Vue nodes
|
||||
useExecutionStateProvider()
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
})
|
||||
@@ -431,9 +428,7 @@ onMounted(async () => {
|
||||
workflowPersistence.restoreWorkflowTabsState()
|
||||
|
||||
// Initialize release store to fetch releases from comfy-api (fire-and-forget)
|
||||
const { useReleaseStore } = await import(
|
||||
'@/platform/updates/common/releaseStore'
|
||||
)
|
||||
const { useReleaseStore } = await import('@/stores/releaseStore')
|
||||
const releaseStore = useReleaseStore()
|
||||
void releaseStore.initialize()
|
||||
|
||||
|
||||
@@ -127,10 +127,10 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useZoomControls } from '@/composables/useZoomControls'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import ZoomControlsModal from './modals/ZoomControlsModal.vue'
|
||||
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
isOverNodeInput,
|
||||
isOverNodeOutput
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
let idleTimeout: number
|
||||
|
||||
@@ -1,500 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
// Mock the composables and services
|
||||
vi.mock('@/composables/graph/useCanvasInteractions', () => ({
|
||||
useCanvasInteractions: vi.fn(() => ({
|
||||
handleWheel: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/canvas/useSelectionToolboxPosition', () => ({
|
||||
useSelectionToolboxPosition: vi.fn(() => ({
|
||||
visible: { value: true }
|
||||
})),
|
||||
resetMoreOptionsState: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/element/useRetriggerableAnimation', () => ({
|
||||
useRetriggerableAnimation: vi.fn(() => ({
|
||||
shouldAnimate: { value: false }
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/extensions/minimap/composables/useMinimap', () => ({
|
||||
useMinimap: vi.fn(() => ({
|
||||
containerStyles: {
|
||||
value: {
|
||||
backgroundColor: '#ffffff'
|
||||
}
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/services/extensionService', () => ({
|
||||
useExtensionService: vi.fn(() => ({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isLGraphNode: vi.fn(() => true),
|
||||
isImageNode: vi.fn(() => false),
|
||||
isLoad3dNode: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/nodeFilterUtil', () => ({
|
||||
isOutputNode: vi.fn(() => false),
|
||||
filterOutputNodes: vi.fn((nodes) => nodes.filter(() => false))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Load3D.3DViewerEnable') return true
|
||||
return null
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
getCommand: vi.fn(() => ({ id: 'test-command', title: 'Test Command' }))
|
||||
})
|
||||
}))
|
||||
|
||||
let nodeDefMock = {
|
||||
type: 'TestNode',
|
||||
title: 'Test Node'
|
||||
} as unknown
|
||||
|
||||
vi.mock('@/stores/nodeDefStore', () => ({
|
||||
useNodeDefStore: () => ({
|
||||
fromLGraphNode: vi.fn(() => nodeDefMock)
|
||||
})
|
||||
}))
|
||||
|
||||
describe('SelectionToolbox', () => {
|
||||
let canvasStore: ReturnType<typeof useCanvasStore>
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
info: 'Node Info',
|
||||
bookmark: 'Save to Library',
|
||||
frameNodes: 'Frame Nodes',
|
||||
moreOptions: 'More Options',
|
||||
refreshNode: 'Refresh Node'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mockProvide = {
|
||||
isVisible: { value: true },
|
||||
selectedItems: []
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
canvasStore = useCanvasStore()
|
||||
|
||||
// Mock the canvas to avoid "getCanvas: canvas is null" errors
|
||||
canvasStore.canvas = {
|
||||
setDirty: vi.fn(),
|
||||
state: {
|
||||
selectionChanged: false
|
||||
}
|
||||
} as any
|
||||
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
return mount(SelectionToolbox, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [i18n, PrimeVue],
|
||||
provide: {
|
||||
[Symbol.for('SelectionOverlay')]: mockProvide
|
||||
},
|
||||
stubs: {
|
||||
Panel: {
|
||||
template:
|
||||
'<div class="panel selection-toolbox absolute left-1/2 rounded-lg"><slot /></div>',
|
||||
props: ['pt', 'style', 'class']
|
||||
},
|
||||
InfoButton: { template: '<div class="info-button" />' },
|
||||
ColorPickerButton: {
|
||||
template:
|
||||
'<button data-testid="color-picker-button" class="color-picker-button" />'
|
||||
},
|
||||
FrameNodes: { template: '<div class="frame-nodes" />' },
|
||||
PublishButton: {
|
||||
template:
|
||||
'<button data-testid="add-to-library" class="bookmark-button" />'
|
||||
},
|
||||
BypassButton: {
|
||||
template:
|
||||
'<button data-testid="bypass-button" class="bypass-button" />'
|
||||
},
|
||||
PinButton: { template: '<div class="pin-button" />' },
|
||||
Load3DViewerButton: {
|
||||
template: '<div class="load-3d-viewer-button" />'
|
||||
},
|
||||
MaskEditorButton: { template: '<div class="mask-editor-button" />' },
|
||||
DeleteButton: {
|
||||
template:
|
||||
'<button data-testid="delete-button" class="delete-button" />'
|
||||
},
|
||||
RefreshSelectionButton: {
|
||||
template: '<div class="refresh-button" />'
|
||||
},
|
||||
ExecuteButton: { template: '<div class="execute-button" />' },
|
||||
ConvertToSubgraphButton: {
|
||||
template:
|
||||
'<button data-testid="convert-to-subgraph-button" class="convert-to-subgraph-button" />'
|
||||
},
|
||||
ExtensionCommandButton: {
|
||||
template: '<div class="extension-command-button" />'
|
||||
},
|
||||
MoreOptions: {
|
||||
template:
|
||||
'<button data-testid="more-options-button" class="more-options" />'
|
||||
},
|
||||
VerticalDivider: { template: '<div class="vertical-divider" />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Button Visibility Logic', () => {
|
||||
beforeEach(() => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should show info button only for single selections', () => {
|
||||
// Single node selection
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.info-button').exists()).toBe(true)
|
||||
|
||||
// Multiple node selection
|
||||
canvasStore.selectedItems = [
|
||||
{ type: 'TestNode1' },
|
||||
{ type: 'TestNode2' }
|
||||
] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(wrapper2.find('.info-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show info button when node definition is not found', () => {
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
// mock nodedef and return null
|
||||
nodeDefMock = null
|
||||
// remount component
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.info-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should show color picker for all selections', () => {
|
||||
// Single node selection
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('[data-testid="color-picker-button"]').exists()).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
// Multiple node selection
|
||||
canvasStore.selectedItems = [
|
||||
{ type: 'TestNode1' },
|
||||
{ type: 'TestNode2' }
|
||||
] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(
|
||||
wrapper2.find('[data-testid="color-picker-button"]').exists()
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should show frame nodes only for multiple selections', () => {
|
||||
// Single node selection
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.frame-nodes').exists()).toBe(false)
|
||||
|
||||
// Multiple node selection
|
||||
canvasStore.selectedItems = [
|
||||
{ type: 'TestNode1' },
|
||||
{ type: 'TestNode2' }
|
||||
] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(wrapper2.find('.frame-nodes').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should show bypass button for appropriate selections', () => {
|
||||
// Single node selection
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('[data-testid="bypass-button"]').exists()).toBe(true)
|
||||
|
||||
// Multiple node selection
|
||||
canvasStore.selectedItems = [
|
||||
{ type: 'TestNode1' },
|
||||
{ type: 'TestNode2' }
|
||||
] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(wrapper2.find('[data-testid="bypass-button"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should show common buttons for all selections', () => {
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('[data-testid="delete-button"]').exists()).toBe(true)
|
||||
expect(
|
||||
wrapper.find('[data-testid="convert-to-subgraph-button"]').exists()
|
||||
).toBe(true)
|
||||
expect(wrapper.find('[data-testid="more-options-button"]').exists()).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('should show mask editor only for single image nodes', async () => {
|
||||
const mockUtils = await import('@/utils/litegraphUtil')
|
||||
const isImageNodeSpy = vi.spyOn(mockUtils, 'isImageNode')
|
||||
|
||||
// Single image node
|
||||
isImageNodeSpy.mockReturnValue(true)
|
||||
canvasStore.selectedItems = [{ type: 'ImageNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.mask-editor-button').exists()).toBe(true)
|
||||
|
||||
// Single non-image node
|
||||
isImageNodeSpy.mockReturnValue(false)
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(wrapper2.find('.mask-editor-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should show Color picker button only for single Load3D nodes', async () => {
|
||||
const mockUtils = await import('@/utils/litegraphUtil')
|
||||
const isLoad3dNodeSpy = vi.spyOn(mockUtils, 'isLoad3dNode')
|
||||
|
||||
// Single Load3D node
|
||||
isLoad3dNodeSpy.mockReturnValue(true)
|
||||
canvasStore.selectedItems = [{ type: 'Load3DNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.load-3d-viewer-button').exists()).toBe(true)
|
||||
|
||||
// Single non-Load3D node
|
||||
isLoad3dNodeSpy.mockReturnValue(false)
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(wrapper2.find('.load-3d-viewer-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should show ExecuteButton only when output nodes are selected', async () => {
|
||||
const mockNodeFilterUtil = await import('@/utils/nodeFilterUtil')
|
||||
const isOutputNodeSpy = vi.spyOn(mockNodeFilterUtil, 'isOutputNode')
|
||||
const filterOutputNodesSpy = vi.spyOn(
|
||||
mockNodeFilterUtil,
|
||||
'filterOutputNodes'
|
||||
)
|
||||
|
||||
// With output node selected
|
||||
isOutputNodeSpy.mockReturnValue(true)
|
||||
filterOutputNodesSpy.mockReturnValue([{ type: 'SaveImage' }] as any)
|
||||
canvasStore.selectedItems = [
|
||||
{ type: 'SaveImage', constructor: { nodeData: { output_node: true } } }
|
||||
] as any
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.execute-button').exists()).toBe(true)
|
||||
|
||||
// Without output node selected
|
||||
isOutputNodeSpy.mockReturnValue(false)
|
||||
filterOutputNodesSpy.mockReturnValue([])
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
wrapper.unmount()
|
||||
const wrapper2 = mountComponent()
|
||||
expect(wrapper2.find('.execute-button').exists()).toBe(false)
|
||||
|
||||
// No selection at all
|
||||
canvasStore.selectedItems = []
|
||||
wrapper2.unmount()
|
||||
const wrapper3 = mountComponent()
|
||||
expect(wrapper3.find('.execute-button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Divider Visibility Logic', () => {
|
||||
it('should show dividers between button groups when both groups have buttons', () => {
|
||||
// Setup single node to show info + other buttons
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const dividers = wrapper.findAll('.vertical-divider')
|
||||
expect(dividers.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show dividers when adjacent groups are empty', () => {
|
||||
// No selection should show minimal buttons and dividers
|
||||
canvasStore.selectedItems = []
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const buttons = wrapper.find('.panel').element.children
|
||||
expect(buttons.length).toBeGreaterThan(0) // At least MoreOptions should show
|
||||
})
|
||||
})
|
||||
|
||||
describe('Extension Commands', () => {
|
||||
it('should render extension command buttons when available', () => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: {
|
||||
value: new Map([
|
||||
['test-command', { id: 'test-command', title: 'Test Command' }]
|
||||
])
|
||||
},
|
||||
invokeExtensions: vi.fn(() => ['test-command'])
|
||||
} as any)
|
||||
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('.extension-command-button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not render extension commands when none available', () => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('.extension-command-button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Container Styling', () => {
|
||||
it('should apply minimap container styles', () => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const panel = wrapper.find('.panel')
|
||||
expect(panel.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should have correct CSS classes', () => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const panel = wrapper.find('.panel')
|
||||
expect(panel.classes()).toContain('selection-toolbox')
|
||||
expect(panel.classes()).toContain('absolute')
|
||||
expect(panel.classes()).toContain('left-1/2')
|
||||
expect(panel.classes()).toContain('rounded-lg')
|
||||
})
|
||||
|
||||
it('should handle animation class conditionally', () => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const panel = wrapper.find('.panel')
|
||||
expect(panel.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Event Handling', () => {
|
||||
it('should handle wheel events', async () => {
|
||||
const mockCanvasInteractions = vi.mocked(useCanvasInteractions)
|
||||
const handleWheelSpy = vi.fn()
|
||||
mockCanvasInteractions.mockReturnValue({
|
||||
handleWheel: handleWheelSpy
|
||||
} as any)
|
||||
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
|
||||
canvasStore.selectedItems = [{ type: 'TestNode' }] as any
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const panel = wrapper.find('.panel')
|
||||
await panel.trigger('wheel')
|
||||
|
||||
expect(handleWheelSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('No Selection State', () => {
|
||||
beforeEach(() => {
|
||||
const mockExtensionService = vi.mocked(useExtensionService)
|
||||
mockExtensionService.mockReturnValue({
|
||||
extensionCommands: { value: new Map() },
|
||||
invokeExtensions: vi.fn(() => [])
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should still show MoreOptions when no items selected', () => {
|
||||
canvasStore.selectedItems = []
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('.more-options').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should hide most buttons when no items selected', () => {
|
||||
canvasStore.selectedItems = []
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('.info-button').exists()).toBe(false)
|
||||
expect(wrapper.find('.color-picker-button').exists()).toBe(false)
|
||||
expect(wrapper.find('.frame-nodes').exists()).toBe(false)
|
||||
expect(wrapper.find('.bookmark-button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,37 +8,28 @@
|
||||
<Panel
|
||||
v-if="visible"
|
||||
class="rounded-lg selection-toolbox pointer-events-auto"
|
||||
:style="`backgroundColor: ${containerStyles.backgroundColor};`"
|
||||
:pt="{
|
||||
header: 'hidden',
|
||||
content: 'px-1 py-1 h-10 px-1 flex flex-row gap-1'
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<DeleteButton v-if="showDelete" />
|
||||
<VerticalDivider v-if="showInfoButton && showAnyPrimaryActions" />
|
||||
<InfoButton v-if="showInfoButton" />
|
||||
|
||||
<ColorPickerButton v-if="showColorPicker" />
|
||||
<FrameNodes v-if="showFrameNodes" />
|
||||
<ConvertToSubgraphButton v-if="showConvertToSubgraph" />
|
||||
<PublishSubgraphButton v-if="showPublishSubgraph" />
|
||||
<MaskEditorButton v-if="showMaskEditor" />
|
||||
<VerticalDivider
|
||||
v-if="showAnyPrimaryActions && showAnyControlActions"
|
||||
/>
|
||||
|
||||
<BypassButton v-if="showBypass" />
|
||||
<RefreshSelectionButton v-if="showRefresh" />
|
||||
<Load3DViewerButton v-if="showLoad3DViewer" />
|
||||
|
||||
<ExecuteButton />
|
||||
<ColorPickerButton />
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<PublishSubgraphButton />
|
||||
<DeleteButton />
|
||||
<RefreshSelectionButton />
|
||||
<ExtensionCommandButton
|
||||
v-for="command in extensionToolboxCommands"
|
||||
:key="command.id"
|
||||
:command="command"
|
||||
/>
|
||||
<ExecuteButton v-if="showExecute" />
|
||||
<MoreOptions />
|
||||
<HelpButton />
|
||||
</Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -54,29 +45,22 @@ import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/Convert
|
||||
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
|
||||
import InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue'
|
||||
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
||||
import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewerButton.vue'
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue'
|
||||
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
import FrameNodes from './selectionToolbox/FrameNodes.vue'
|
||||
import MoreOptions from './selectionToolbox/MoreOptions.vue'
|
||||
import VerticalDivider from './selectionToolbox/VerticalDivider.vue'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const extensionService = useExtensionService()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
const minimap = useMinimap()
|
||||
const containerStyles = minimap.containerStyles
|
||||
|
||||
const toolboxRef = ref<HTMLElement | undefined>()
|
||||
const { visible } = useSelectionToolboxPosition(toolboxRef)
|
||||
@@ -96,44 +80,6 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
.map((commandId) => commandStore.getCommand(commandId))
|
||||
.filter((command): command is ComfyCommandImpl => command !== undefined)
|
||||
})
|
||||
|
||||
const {
|
||||
hasAnySelection,
|
||||
hasMultipleSelection,
|
||||
isSingleNode,
|
||||
isSingleSubgraph,
|
||||
isSingleImageNode,
|
||||
hasAny3DNodeSelected,
|
||||
hasOutputNodesSelected,
|
||||
nodeDef
|
||||
} = useSelectionState()
|
||||
const showInfoButton = computed(() => !!nodeDef.value)
|
||||
|
||||
const showColorPicker = computed(() => hasAnySelection.value)
|
||||
const showConvertToSubgraph = computed(() => hasAnySelection.value)
|
||||
const showFrameNodes = computed(() => hasMultipleSelection.value)
|
||||
const showPublishSubgraph = computed(() => isSingleSubgraph.value)
|
||||
|
||||
const showBypass = computed(
|
||||
() =>
|
||||
isSingleNode.value || isSingleSubgraph.value || hasMultipleSelection.value
|
||||
)
|
||||
const showLoad3DViewer = computed(() => hasAny3DNodeSelected.value)
|
||||
const showMaskEditor = computed(() => isSingleImageNode.value)
|
||||
|
||||
const showDelete = computed(() => hasAnySelection.value)
|
||||
const showRefresh = computed(() => hasAnySelection.value)
|
||||
const showExecute = computed(() => hasOutputNodesSelected.value)
|
||||
|
||||
const showAnyPrimaryActions = computed(
|
||||
() =>
|
||||
showColorPicker.value ||
|
||||
showConvertToSubgraph.value ||
|
||||
showFrameNodes.value ||
|
||||
showPublishSubgraph.value
|
||||
)
|
||||
|
||||
const showAnyControlActions = computed(() => showBypass.value)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -24,12 +24,9 @@ import {
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import {
|
||||
useCanvasStore,
|
||||
useTitleEditorStore
|
||||
} from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
|
||||
@@ -139,10 +139,10 @@ import { Button, InputNumber, InputNumberInputEvent } from 'primevue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const minimap = useMinimap()
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const mockLGraphNode = {
|
||||
type: 'TestNode',
|
||||
title: 'Test Node',
|
||||
mode: LGraphEventMode.ALWAYS
|
||||
}
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isLGraphNode: vi.fn(() => true)
|
||||
}))
|
||||
|
||||
describe('BypassButton', () => {
|
||||
let canvasStore: ReturnType<typeof useCanvasStore>
|
||||
let commandStore: ReturnType<typeof useCommandStore>
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
selectionToolbox: {
|
||||
bypassButton: {
|
||||
tooltip: 'Toggle bypass mode'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
canvasStore = useCanvasStore()
|
||||
commandStore = useCommandStore()
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(BypassButton, {
|
||||
global: {
|
||||
plugins: [i18n, PrimeVue],
|
||||
directives: { tooltip: Tooltip },
|
||||
stubs: {
|
||||
'i-lucide:ban': true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('should render bypass button', () => {
|
||||
canvasStore.selectedItems = [mockLGraphNode] as any
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should have correct test id', () => {
|
||||
canvasStore.selectedItems = [mockLGraphNode] as any
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('[data-testid="bypass-button"]')
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should execute bypass command when clicked', async () => {
|
||||
canvasStore.selectedItems = [mockLGraphNode] as any
|
||||
const executeSpy = vi.spyOn(commandStore, 'execute').mockResolvedValue()
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(executeSpy).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.ToggleSelectedNodes.Bypass'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show normal styling when node is not bypassed', () => {
|
||||
const normalNode = { ...mockLGraphNode, mode: LGraphEventMode.ALWAYS }
|
||||
canvasStore.selectedItems = [normalNode] as any
|
||||
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.classes()).not.toContain(
|
||||
'dark-theme:[&:not(:active)]:!bg-[#262729]'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show bypassed styling when node is bypassed', async () => {
|
||||
const bypassedNode = { ...mockLGraphNode, mode: LGraphEventMode.BYPASS }
|
||||
canvasStore.selectedItems = [bypassedNode] as any
|
||||
vi.spyOn(commandStore, 'execute').mockResolvedValue()
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click to trigger the reactivity update
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle multiple selected items', () => {
|
||||
vi.spyOn(commandStore, 'execute').mockResolvedValue()
|
||||
canvasStore.selectedItems = [mockLGraphNode, mockLGraphNode] as any
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
showDelay: 1000
|
||||
@@ -7,11 +8,12 @@
|
||||
severity="secondary"
|
||||
text
|
||||
data-testid="bypass-button"
|
||||
class="hover:dark-theme:bg-charcoal-600 hover:bg-[#E7E6E6]"
|
||||
@click="toggleBypass"
|
||||
@click="
|
||||
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:ban class="w-4 h-4" />
|
||||
<i-game-icons:detour />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -21,11 +23,9 @@ import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const toggleBypass = async () => {
|
||||
await commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
||||
}
|
||||
const canvasStore = useCanvasStore()
|
||||
</script>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
// Import after mocks
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
// Mock the litegraph module
|
||||
vi.mock('@/lib/litegraph/src/litegraph', async () => {
|
||||
@@ -95,6 +95,17 @@ describe('ColorPickerButton', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not render when nothing is selected', () => {
|
||||
// Keep selectedItems empty
|
||||
canvasStore.selectedItems = []
|
||||
const wrapper = createWrapper()
|
||||
// The button exists but is hidden with v-show
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
expect(wrapper.find('button').attributes('style')).toContain(
|
||||
'display: none'
|
||||
)
|
||||
})
|
||||
|
||||
it('should toggle color picker visibility on button click', async () => {
|
||||
canvasStore.selectedItems = [{ type: 'LGraphNode' } as any]
|
||||
const wrapper = createWrapper()
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected || canvasStore.groupSelected"
|
||||
v-tooltip.top="{
|
||||
value: localizedCurrentColorName ?? t('color.noColor'),
|
||||
showDelay: 1000
|
||||
showDelay: 512
|
||||
}"
|
||||
data-testid="color-picker-button"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => (showColorPicker = !showColorPicker)"
|
||||
>
|
||||
<div class="flex items-center gap-1 px-0">
|
||||
<i
|
||||
class="w-4 h-4 pi pi-circle-fill"
|
||||
:style="{ color: currentColor ?? '' }"
|
||||
/>
|
||||
<i
|
||||
class="w-4 h-4 pi pi-chevron-down py-1"
|
||||
:style="{ fontSize: '0.5rem' }"
|
||||
/>
|
||||
</div>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="pi pi-circle-fill" :style="{ color: currentColor ?? '' }" />
|
||||
<i class="pi pi-chevron-down" :style="{ fontSize: '0.5rem' }" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
<div
|
||||
v-if="showColorPicker"
|
||||
@@ -50,20 +46,17 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { Raw, computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
ColorOption as CanvasColorOption,
|
||||
Positionable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ColorOption as CanvasColorOption } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LiteGraph,
|
||||
isColorable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { getItemsColorOption } from '@/utils/litegraphUtil'
|
||||
@@ -147,17 +140,13 @@ const localizedCurrentColorName = computed(() => {
|
||||
)
|
||||
return colorOption?.localizedName ?? NO_COLOR_OPTION.localizedName
|
||||
})
|
||||
const updateColorSelectionFromNode = (
|
||||
newSelectedItems: Raw<Positionable[]>
|
||||
) => {
|
||||
showColorPicker.value = false
|
||||
selectedColorOption.value = null
|
||||
currentColorOption.value = getItemsColorOption(newSelectedItems)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.selectedItems,
|
||||
(newSelectedItems) => {
|
||||
updateColorSelectionFromNode(newSelectedItems)
|
||||
showColorPicker.value = false
|
||||
selectedColorOption.value = null
|
||||
currentColorOption.value = getItemsColorOption(newSelectedItems)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
data-testid="convert-to-subgraph-button"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:expand class="w-4 h-4" />
|
||||
<i-lucide:expand />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -21,7 +20,6 @@
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
data-testid="convert-to-subgraph-button"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.Graph.ConvertToSubgraph')"
|
||||
>
|
||||
@@ -36,15 +34,25 @@ import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { isSingleSubgraph, hasAnySelection } = useSelectionState()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isUnpackVisible = isSingleSubgraph
|
||||
const isConvertVisible = computed(
|
||||
() => hasAnySelection.value && !isSingleSubgraph.value
|
||||
)
|
||||
const isUnpackVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.selectedItems?.length === 1 &&
|
||||
canvasStore.selectedItems[0] instanceof SubgraphNode
|
||||
)
|
||||
})
|
||||
const isConvertVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.groupSelected ||
|
||||
canvasStore.rerouteSelected ||
|
||||
canvasStore.nodeSelected
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
severity="danger"
|
||||
text
|
||||
icon-class="w-4 h-4"
|
||||
icon="pi pi-trash"
|
||||
data-testid="delete-button"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
|
||||
/>
|
||||
</template>
|
||||
@@ -19,15 +17,14 @@ import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { selectedItems } = useSelectionState()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isDeletable = computed(() =>
|
||||
selectedItems.value.some((x: Positionable) => x.removable !== false)
|
||||
canvasStore.selectedItems.some((x) => x.removable !== false)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
// Mock the stores
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: vi.fn()
|
||||
}))
|
||||
|
||||
// Mock the utils
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isLGraphNode: vi.fn((node) => !!node?.type)
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/nodeFilterUtil', () => ({
|
||||
isOutputNode: vi.fn((node) => !!node?.constructor?.nodeData?.output_node)
|
||||
}))
|
||||
|
||||
// Mock the composables
|
||||
vi.mock('@/composables/graph/useSelectionState', () => ({
|
||||
useSelectionState: vi.fn(() => ({
|
||||
selectedNodes: {
|
||||
value: []
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('ExecuteButton', () => {
|
||||
let mockCanvas: any
|
||||
let mockCanvasStore: any
|
||||
let mockCommandStore: any
|
||||
let mockSelectedNodes: any[]
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
selectionToolbox: {
|
||||
executeButton: {
|
||||
tooltip: 'Execute selected nodes'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Reset mocks
|
||||
mockCanvas = {
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
|
||||
mockSelectedNodes = []
|
||||
|
||||
mockCanvasStore = {
|
||||
getCanvas: vi.fn(() => mockCanvas),
|
||||
selectedItems: []
|
||||
}
|
||||
|
||||
mockCommandStore = {
|
||||
execute: vi.fn()
|
||||
}
|
||||
|
||||
// Setup store mocks
|
||||
vi.mocked(useCanvasStore).mockReturnValue(mockCanvasStore as any)
|
||||
vi.mocked(useCommandStore).mockReturnValue(mockCommandStore as any)
|
||||
|
||||
// Update the useSelectionState mock
|
||||
const { useSelectionState } = vi.mocked(
|
||||
await import('@/composables/graph/useSelectionState')
|
||||
)
|
||||
useSelectionState.mockReturnValue({
|
||||
selectedNodes: {
|
||||
value: mockSelectedNodes
|
||||
}
|
||||
} as any)
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(ExecuteButton, {
|
||||
global: {
|
||||
plugins: [i18n, PrimeVue],
|
||||
directives: { tooltip: Tooltip },
|
||||
stubs: {
|
||||
'i-lucide:play': { template: '<div class="play-icon" />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should be able to render', () => {
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Click Handler', () => {
|
||||
it('should execute Comfy.QueueSelectedOutputNodes command on click', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
await button.trigger('click')
|
||||
|
||||
expect(mockCommandStore.execute).toHaveBeenCalledWith(
|
||||
'Comfy.QueueSelectedOutputNodes'
|
||||
)
|
||||
expect(mockCommandStore.execute).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,16 +1,20 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('selectionToolbox.executeButton.tooltip'),
|
||||
value: isDisabled
|
||||
? t('selectionToolbox.executeButton.disabledTooltip')
|
||||
: t('selectionToolbox.executeButton.tooltip'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
class="dark-theme:bg-[#0B8CE9] bg-[#31B9F4] size-8 !p-0"
|
||||
:severity="isDisabled ? 'secondary' : 'success'"
|
||||
text
|
||||
:disabled="isDisabled"
|
||||
@mouseenter="() => handleMouseEnter()"
|
||||
@mouseleave="() => handleMouseLeave()"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i-lucide:play class="fill-path-white w-4 h-4" />
|
||||
<i-lucide:play />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@@ -19,24 +23,26 @@ import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { isOutputNode } from '@/utils/nodeFilterUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { selectedNodes } = useSelectionState()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const buttonHovered = ref(false)
|
||||
const selectedOutputNodes = computed(() =>
|
||||
selectedNodes.value.filter(isLGraphNode).filter(isOutputNode)
|
||||
const selectedOutputNodes = computed(
|
||||
() =>
|
||||
canvasStore.selectedItems.filter(
|
||||
(item) => isLGraphNode(item) && item.constructor.nodeData?.output_node
|
||||
) as LGraphNode[]
|
||||
)
|
||||
|
||||
const isDisabled = computed(() => selectedOutputNodes.value.length === 0)
|
||||
|
||||
function outputNodeStokeStyle(this: LGraphNode) {
|
||||
if (
|
||||
this.selected &&
|
||||
@@ -64,9 +70,3 @@ const handleClick = async () => {
|
||||
await commandStore.execute('Comfy.QueueSelectedOutputNodes')
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep.fill-path-white > path {
|
||||
fill: white;
|
||||
stroke: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon-class="w-4 h-4"
|
||||
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
|
||||
@click="() => commandStore.execute(command.id)"
|
||||
/>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: $t('g.frameNodes'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
class="frame-nodes-button"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="frameNodes"
|
||||
>
|
||||
<i-lucide:frame class="w-4 h-4" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useFrameNodes } from '@/composables/graph/useFrameNodes'
|
||||
|
||||
const { frameNodes } = useFrameNodes()
|
||||
</script>
|
||||
49
src/components/graph/selectionToolbox/HelpButton.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="nodeDef"
|
||||
v-tooltip.top="{
|
||||
value: $t('g.help'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
class="help-button"
|
||||
text
|
||||
icon="pi pi-question-circle"
|
||||
severity="secondary"
|
||||
@click="showHelp"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
const nodeHelpStore = useNodeHelpStore()
|
||||
const { id: nodeLibraryTabId } = useNodeLibrarySidebarTab()
|
||||
|
||||
const nodeDef = computed<ComfyNodeDefImpl | null>(() => {
|
||||
if (canvasStore.selectedItems.length !== 1) return null
|
||||
const item = canvasStore.selectedItems[0]
|
||||
if (!isLGraphNode(item)) return null
|
||||
return nodeDefStore.fromLGraphNode(item)
|
||||
})
|
||||
|
||||
const showHelp = () => {
|
||||
const def = nodeDef.value
|
||||
if (!def) return
|
||||
if (sidebarTabStore.activeSidebarTabId !== nodeLibraryTabId) {
|
||||
sidebarTabStore.toggleSidebarTab(nodeLibraryTabId)
|
||||
}
|
||||
nodeHelpStore.openHelp(def)
|
||||
}
|
||||
</script>
|
||||
@@ -1,149 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue'
|
||||
// NOTE: The component import must come after mocks so they take effect.
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
const mockLGraphNode = {
|
||||
type: 'TestNode',
|
||||
title: 'Test Node'
|
||||
}
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isLGraphNode: vi.fn(() => true)
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({
|
||||
useNodeLibrarySidebarTab: () => ({
|
||||
id: 'node-library'
|
||||
})
|
||||
}))
|
||||
|
||||
const openHelpMock = vi.fn()
|
||||
const closeHelpMock = vi.fn()
|
||||
const nodeHelpState: { currentHelpNode: any } = { currentHelpNode: null }
|
||||
vi.mock('@/stores/workspace/nodeHelpStore', () => ({
|
||||
useNodeHelpStore: () => ({
|
||||
openHelp: (def: any) => {
|
||||
nodeHelpState.currentHelpNode = def
|
||||
openHelpMock(def)
|
||||
},
|
||||
closeHelp: () => {
|
||||
nodeHelpState.currentHelpNode = null
|
||||
closeHelpMock()
|
||||
},
|
||||
get currentHelpNode() {
|
||||
return nodeHelpState.currentHelpNode
|
||||
},
|
||||
get isHelpOpen() {
|
||||
return nodeHelpState.currentHelpNode !== null
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
const toggleSidebarTabMock = vi.fn((id: string) => {
|
||||
sidebarState.activeSidebarTabId =
|
||||
sidebarState.activeSidebarTabId === id ? null : id
|
||||
})
|
||||
const sidebarState: { activeSidebarTabId: string | null } = {
|
||||
activeSidebarTabId: 'other-tab'
|
||||
}
|
||||
vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
useSidebarTabStore: () => ({
|
||||
get activeSidebarTabId() {
|
||||
return sidebarState.activeSidebarTabId
|
||||
},
|
||||
toggleSidebarTab: toggleSidebarTabMock
|
||||
})
|
||||
}))
|
||||
|
||||
describe('InfoButton', () => {
|
||||
let canvasStore: ReturnType<typeof useCanvasStore>
|
||||
let nodeDefStore: ReturnType<typeof useNodeDefStore>
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
info: 'Node Info'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
canvasStore = useCanvasStore()
|
||||
nodeDefStore = useNodeDefStore()
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(InfoButton, {
|
||||
global: {
|
||||
plugins: [i18n, PrimeVue],
|
||||
directives: { tooltip: Tooltip },
|
||||
stubs: {
|
||||
'i-lucide:info': true,
|
||||
Button: {
|
||||
template:
|
||||
'<button class="help-button" severity="secondary"><slot /></button>',
|
||||
props: ['severity', 'text', 'class'],
|
||||
emits: ['click']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('should handle click without errors', async () => {
|
||||
const mockNodeDef = {
|
||||
nodePath: 'test/node',
|
||||
display_name: 'Test Node'
|
||||
}
|
||||
canvasStore.selectedItems = [mockLGraphNode] as any
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(mockNodeDef as any)
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
await button.trigger('click')
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should have correct CSS classes', () => {
|
||||
const mockNodeDef = {
|
||||
nodePath: 'test/node',
|
||||
display_name: 'Test Node'
|
||||
}
|
||||
canvasStore.selectedItems = [mockLGraphNode] as any
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(mockNodeDef as any)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.classes()).toContain('help-button')
|
||||
expect(button.attributes('severity')).toBe('secondary')
|
||||
})
|
||||
|
||||
it('should have correct tooltip', () => {
|
||||
const mockNodeDef = {
|
||||
nodePath: 'test/node',
|
||||
display_name: 'Test Node'
|
||||
}
|
||||
canvasStore.selectedItems = [mockLGraphNode] as any
|
||||
vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(mockNodeDef as any)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: $t('g.info'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
data-testid="info-button"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="toggleHelp"
|
||||
>
|
||||
<i-lucide:info class="w-4 h-4" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
|
||||
const { showNodeHelp: toggleHelp } = useSelectionState()
|
||||
</script>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="is3DNode"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
showDelay: 1000
|
||||
@@ -7,18 +8,29 @@
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-pencil"
|
||||
icon-class="w-4 h-4"
|
||||
@click="open3DViewer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const is3DNode = computed(() => {
|
||||
const enable3DViewer = useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
||||
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
|
||||
|
||||
return nodes.length === 1 && nodes.some(isLoad3dNode) && enable3DViewer
|
||||
})
|
||||
|
||||
const open3DViewer = () => {
|
||||
void commandStore.execute('Comfy.3DViewer.Open3DViewer')
|
||||
|
||||
@@ -7,21 +7,28 @@
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-pencil"
|
||||
@click="openMaskEditor"
|
||||
>
|
||||
<i-comfy:mask class="!w-4 !h-4" />
|
||||
</Button>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const { isSingleImageNode } = useSelectionState()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isSingleImageNode = computed(() => {
|
||||
const { selectedItems } = canvasStore
|
||||
const item = selectedItems[0]
|
||||
return selectedItems.length === 1 && isLGraphNode(item) && isImageNode(item)
|
||||
})
|
||||
|
||||
const openMaskEditor = () => {
|
||||
void commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="option.type === 'divider'"
|
||||
class="h-px bg-gray-200 dark-theme:bg-zinc-700 my-1"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
role="button"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-gray-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i v-if="option.icon" :class="[option.icon, 'w-4 h-4']" />
|
||||
<span class="flex-1">{{ option.label }}</span>
|
||||
<span v-if="option.shortcut" class="text-xs opacity-60">
|
||||
{{ option.shortcut }}
|
||||
</span>
|
||||
<i-lucide:chevron-right
|
||||
v-if="option.hasSubmenu"
|
||||
:size="14"
|
||||
class="opacity-60"
|
||||
/>
|
||||
<Badge
|
||||
v-if="option.badge"
|
||||
:severity="option.badge === 'new' ? 'info' : 'secondary'"
|
||||
:value="t(option.badge)"
|
||||
:class="{
|
||||
'bg-[#31B9F4] dark-theme:bg-[#0B8CE9] rounded-4xl':
|
||||
option.badge === 'new',
|
||||
'bg-[#9C9EAB] dark-theme:bg-[#000] rounded-4xl':
|
||||
option.badge === 'deprecated',
|
||||
'text-white uppercase text-[9px] h-4 px-1 gap-2.5': true
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Badge from 'primevue/badge'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { MenuOption } from '@/composables/graph/useMoreOptionsMenu'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
option: MenuOption
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'click', option: MenuOption, event: Event): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const handleClick = (event: Event) => {
|
||||
emit('click', props.option, event)
|
||||
}
|
||||
</script>
|
||||