Compare commits
3 Commits
feature/sh
...
annotated-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72535eb88d | ||
|
|
330633355a | ||
|
|
9c3cf86381 |
@@ -12,10 +12,6 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
|
||||
# to ComfyUI launch script to serve the custom web version.
|
||||
DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
|
||||
|
||||
# The directory containing the ComfyUI installation used to run Playwright tests.
|
||||
# If you aren't using a separate install for testing, point this to your regular install.
|
||||
TEST_COMFYUI_DIR=/home/ComfyUI
|
||||
|
||||
# The directory containing the ComfyUI_examples repo used to extract test workflows.
|
||||
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
|
||||
|
||||
|
||||
53
.github/workflows/update-main.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Update Main Repo from PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
update-main-repo:
|
||||
if: github.event.label.name == 'Update Main Repo'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout frontend repo PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: "comfyanonymous/ComfyUI"
|
||||
path: ComfyUI
|
||||
ref: master
|
||||
|
||||
- name: Copy compiled assets
|
||||
run: |
|
||||
rm -rf ./ComfyUI/web/*
|
||||
cp -R dist/* ./ComfyUI/web/
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: 'Update frontend assets from PR #${{ github.event.pull_request.number }}'
|
||||
title: 'Update frontend assets from PR #${{ github.event.pull_request.number }}'
|
||||
body: |
|
||||
This PR updates the compiled frontend assets from PR #${{ github.event.pull_request.number }} in the frontend repo.
|
||||
|
||||
Frontend PR: ${{ github.event.pull_request.html_url }}
|
||||
branch: update-frontend-assets-pr-${{ github.event.pull_request.number }}
|
||||
base: main
|
||||
path: ComfyUI
|
||||
@@ -2,6 +2,5 @@
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80
|
||||
"trailingComma": "none"
|
||||
}
|
||||
134
README.md
@@ -33,11 +33,11 @@
|
||||
|
||||
### Nightly Release
|
||||
|
||||
Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
|
||||
Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
|
||||
|
||||
To use the latest nightly release, add the following command line argument to your ComfyUI launch script:
|
||||
|
||||
```bat
|
||||
```
|
||||
--front-end-version Comfy-Org/ComfyUI_frontend@latest
|
||||
```
|
||||
|
||||
@@ -62,23 +62,6 @@ There will be a 2-day feature freeze before each stable release. During this per
|
||||
|
||||
### Major features
|
||||
|
||||
<details>
|
||||
<summary>v1.3.7: Keybinding customization</summary>
|
||||
|
||||
## Basic UI
|
||||

|
||||
|
||||
## Reset button
|
||||

|
||||
|
||||
## Edit Keybinding
|
||||

|
||||

|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.4: Node library sidebar tab</summary>
|
||||
|
||||
@@ -107,46 +90,6 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details>
|
||||
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
|
||||
|
||||
Dropping a link of correct type on node widget will automatically convert the widget to input.
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/15cea0b0-b225-4bec-af50-2cdb16dc46bf)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
|
||||
|
||||
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
|
||||
or by holding the space key.
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/c7872532-a2ac-44c1-9e7d-9e03b5d1a80b)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
|
||||
|
||||
@@ -195,59 +138,13 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
### Node developers API
|
||||
|
||||
<details>
|
||||
<summary>v1.3.7: Register commands and keybindings</summary>
|
||||
|
||||
Extensions can call the following API to register commands and keybindings. Do
|
||||
note that keybindings defined in core cannot be overwritten, and some keybindings
|
||||
are reserved by the browser.
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
{
|
||||
id: 'TestCommand',
|
||||
function: () => {
|
||||
alert('TestCommand')
|
||||
}
|
||||
}
|
||||
],
|
||||
keybindings: [
|
||||
{
|
||||
combo: { key: 'k' },
|
||||
commandId: 'TestCommand'
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
|
||||
|
||||
Extensions can call the following API to register custom topbar menu items.
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
{
|
||||
id: 'foo-id',
|
||||
label: 'foo',
|
||||
function: () => {
|
||||
alert(1)
|
||||
}
|
||||
}
|
||||
],
|
||||
menuCommands: [
|
||||
{
|
||||
path: ['ext', 'ext2'],
|
||||
commands: ['foo-id']
|
||||
}
|
||||
]
|
||||
})
|
||||
app.extensionManager.menu.registerTopbarCommands(["ext", "ext2"], [{id:"foo", label: "foo", function: () => alert(1)}])
|
||||
```
|
||||
|
||||

|
||||
@@ -296,6 +193,31 @@ We will support custom icons later.
|
||||

|
||||
</details>
|
||||
|
||||
## Road Map
|
||||
|
||||
### What has been done
|
||||
|
||||
- Migrate all code to TypeScript with minimal change modification to the original logic.
|
||||
- Bundle all code with Vite's rollup build.
|
||||
- Added a shim layer to be backward compatible with the existing extension system. <https://github.com/huchenlei/ComfyUI_frontend/pull/15>
|
||||
- Front-end dev server.
|
||||
- Zod schema for input validation on ComfyUI workflow.
|
||||
- Make litegraph a npm dependency. <https://github.com/Comfy-Org/ComfyUI_frontend/pull/89>
|
||||
- Introduce Vue to start managing part of the UI.
|
||||
- Easy install and version management (<https://github.com/comfyanonymous/ComfyUI/pull/3897>).
|
||||
- Better node management. Sherlock <https://github.com/Nuked88/ComfyUI-N-Sidebar>.
|
||||
- Replace the existing ComfyUI front-end implementation. <https://github.com/comfyanonymous/ComfyUI/pull/4379>
|
||||
|
||||
|
||||
### What to be done
|
||||
|
||||
- Remove `@ts-ignore`s.
|
||||
- Turn on `strict` on `tsconfig.json`.
|
||||
- Add more widget types for node developers.
|
||||
- LLM streaming node.
|
||||
- Linear mode (Similar to InvokeAI's linear mode).
|
||||
- Keybinding settings management. Register keybindings API for custom nodes.
|
||||
|
||||
## Development
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { Page, Locator, APIRequestContext } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import { ComfyActionbar } from './helpers/actionbar'
|
||||
import { ComfyAppMenu } from './helpers/appMenu'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import * as fs from 'fs'
|
||||
import { NodeBadgeMode } from '../src/types/nodeSource'
|
||||
import type { NodeId } from '../src/types/comfyWorkflow'
|
||||
import type { KeyCombo } from '../src/types/keyBindingTypes'
|
||||
import { NodeId } from '../src/types/comfyWorkflow'
|
||||
import { ManageGroupNode } from './helpers/manageGroupNode'
|
||||
import { ComfyTemplates } from './helpers/templates'
|
||||
|
||||
@@ -192,8 +191,12 @@ class WorkflowsSidebarTab extends SidebarTab {
|
||||
return this.page.locator('.new-blank-workflow-button')
|
||||
}
|
||||
|
||||
get openWorkflowButton() {
|
||||
return this.page.locator('.open-workflow-button')
|
||||
get browseWorkflowsButton() {
|
||||
return this.page.locator('.browse-workflows-button')
|
||||
}
|
||||
|
||||
get newDefaultWorkflowButton() {
|
||||
return this.page.locator('.new-default-workflow-button')
|
||||
}
|
||||
|
||||
async getOpenedWorkflowNames() {
|
||||
@@ -227,33 +230,6 @@ class Topbar {
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async openSubmenuMobile() {
|
||||
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
||||
}
|
||||
|
||||
async getMenuItem(itemLabel: string): Promise<Locator> {
|
||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||
}
|
||||
|
||||
async getWorkflowTab(tabName: string): Promise<Locator> {
|
||||
return this.page
|
||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||
.locator('..')
|
||||
}
|
||||
|
||||
async closeWorkflowTab(tabName: string) {
|
||||
const tab = await this.getWorkflowTab(tabName)
|
||||
await tab.locator('.close-button').click({ force: true })
|
||||
}
|
||||
|
||||
async saveWorkflow(workflowName: string) {
|
||||
await this.triggerTopbarCommand(['Workflow', 'Save'])
|
||||
await this.page.locator('.p-dialog-content input').fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
// Wait for the dialog to close.
|
||||
await this.page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
if (path.length < 2) {
|
||||
throw new Error('Path is too short')
|
||||
@@ -261,18 +237,16 @@ class Topbar {
|
||||
|
||||
const tabName = path[0]
|
||||
const topLevelMenu = this.page.locator(
|
||||
`.top-menubar .p-menubar-item-label:text-is("${tabName}")`
|
||||
`.top-menubar .p-menubar-item:has-text("${tabName}")`
|
||||
)
|
||||
await topLevelMenu.waitFor({ state: 'visible' })
|
||||
await topLevelMenu.click()
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const commandName = path[i]
|
||||
const menuItem = this.page
|
||||
.locator(
|
||||
`.top-menubar .p-menubar-submenu .p-menubar-item:has-text("${commandName}")`
|
||||
)
|
||||
.first()
|
||||
const menuItem = this.page.locator(
|
||||
`.top-menubar .p-menubar-submenu .p-menubar-item:has-text("${commandName}")`
|
||||
)
|
||||
await menuItem.waitFor({ state: 'visible' })
|
||||
await menuItem.hover()
|
||||
|
||||
@@ -296,6 +270,21 @@ class ComfyMenu {
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
async saveWorkflow(name: string) {
|
||||
const acceptDialog = async (dialog) => {
|
||||
await dialog.accept(name)
|
||||
}
|
||||
this.page.on('dialog', acceptDialog)
|
||||
|
||||
await this.saveButton.click()
|
||||
|
||||
// Wait a moment to ensure the dialog has been handled
|
||||
await this.page.waitForTimeout(300)
|
||||
|
||||
// Remove the dialog listener
|
||||
this.page.off('dialog', acceptDialog)
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
return new NodeLibrarySidebarTab(this.page)
|
||||
}
|
||||
@@ -352,7 +341,7 @@ export class ComfyPage {
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly menu: ComfyMenu
|
||||
public readonly actionbar: ComfyActionbar
|
||||
public readonly appMenu: ComfyAppMenu
|
||||
public readonly templates: ComfyTemplates
|
||||
|
||||
constructor(
|
||||
@@ -367,7 +356,7 @@ export class ComfyPage {
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.appMenu = new ComfyAppMenu(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
}
|
||||
|
||||
@@ -392,16 +381,6 @@ export class ComfyPage {
|
||||
})
|
||||
}
|
||||
|
||||
async getSelectedGraphNodesCount(): Promise<number> {
|
||||
return await this.page.evaluate(() => {
|
||||
return (
|
||||
window['app']?.graph?.nodes?.filter(
|
||||
(node: any) => node.is_selected === true
|
||||
).length || 0
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async setupWorkflowsDirectory(structure: FolderStructure) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/setup_folder_structure`,
|
||||
@@ -459,72 +438,12 @@ export class ComfyPage {
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode',
|
||||
NodeBadgeMode.None
|
||||
)
|
||||
// Hide canvas menu by default.
|
||||
await this.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
}
|
||||
|
||||
public assetPath(fileName: string) {
|
||||
return `./browser_tests/assets/${fileName}`
|
||||
}
|
||||
|
||||
async executeCommand(commandId: string) {
|
||||
await this.page.evaluate((id: string) => {
|
||||
return window['app'].extensionManager.command.execute(id)
|
||||
}, commandId)
|
||||
}
|
||||
|
||||
async registerCommand(
|
||||
commandId: string,
|
||||
command: (() => void) | (() => Promise<void>)
|
||||
) {
|
||||
await this.page.evaluate(
|
||||
({ commandId, commandStr }) => {
|
||||
const app = window['app']
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 8)
|
||||
const extensionName = `TestExtension_${randomSuffix}`
|
||||
|
||||
app.registerExtension({
|
||||
name: extensionName,
|
||||
commands: [
|
||||
{
|
||||
id: commandId,
|
||||
function: eval(commandStr)
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
{ commandId, commandStr: command.toString() }
|
||||
)
|
||||
}
|
||||
|
||||
async registerKeybinding(keyCombo: KeyCombo, command: () => void) {
|
||||
await this.page.evaluate(
|
||||
({ keyCombo, commandStr }) => {
|
||||
const app = window['app']
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 8)
|
||||
const extensionName = `TestExtension_${randomSuffix}`
|
||||
const commandId = `TestCommand_${randomSuffix}`
|
||||
|
||||
app.registerExtension({
|
||||
name: extensionName,
|
||||
keybindings: [
|
||||
{
|
||||
combo: keyCombo,
|
||||
commandId: commandId
|
||||
}
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
id: commandId,
|
||||
function: eval(commandStr)
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
{ keyCombo, commandStr: command.toString() }
|
||||
)
|
||||
}
|
||||
|
||||
async setSetting(settingId: string, settingValue: any) {
|
||||
return await this.page.evaluate(
|
||||
async ({ id, value }) => {
|
||||
@@ -575,10 +494,6 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async getVisibleToastCount() {
|
||||
return await this.page.locator('.p-toast:visible').count()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode1() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
@@ -725,10 +640,9 @@ export class ComfyPage {
|
||||
y: 645
|
||||
}
|
||||
})
|
||||
const dialogInput = page.locator('.graphdialog input[type="text"]')
|
||||
await dialogInput.click()
|
||||
await dialogInput.fill('128')
|
||||
await dialogInput.press('Enter')
|
||||
await page.locator('input[type="text"]').click()
|
||||
await page.locator('input[type="text"]').fill('128')
|
||||
await page.locator('input[type="text"]').press('Enter')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -812,43 +726,46 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlSend(keyToPress: string) {
|
||||
async ctrlC() {
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press(keyToPress)
|
||||
await this.page.keyboard.press('KeyC')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlA() {
|
||||
await this.ctrlSend('KeyA')
|
||||
}
|
||||
|
||||
async ctrlB() {
|
||||
await this.ctrlSend('KeyB')
|
||||
}
|
||||
|
||||
async ctrlC() {
|
||||
await this.ctrlSend('KeyC')
|
||||
}
|
||||
|
||||
async ctrlV() {
|
||||
await this.ctrlSend('KeyV')
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('KeyV')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlZ() {
|
||||
await this.ctrlSend('KeyZ')
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('KeyZ')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlY() {
|
||||
await this.ctrlSend('KeyY')
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('KeyY')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlArrowUp() {
|
||||
await this.ctrlSend('ArrowUp')
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('ArrowUp')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async ctrlArrowDown() {
|
||||
await this.ctrlSend('ArrowDown')
|
||||
await this.page.keyboard.down('Control')
|
||||
await this.page.keyboard.press('ArrowDown')
|
||||
await this.page.keyboard.up('Control')
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async closeMenu() {
|
||||
@@ -962,15 +879,13 @@ export class ComfyPage {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
|
||||
return Promise.all(
|
||||
(
|
||||
await this.page.evaluate((type) => {
|
||||
return window['app'].graph.nodes
|
||||
.filter((n) => n.type === type)
|
||||
.map((n) => n.id)
|
||||
}, type)
|
||||
).map((id: NodeId) => this.getNodeRefById(id))
|
||||
)
|
||||
return (
|
||||
await this.page.evaluate((type) => {
|
||||
return window['app'].graph.nodes
|
||||
.filter((n) => n.type === type)
|
||||
.map((n) => n.id)
|
||||
}, type)
|
||||
).map((id: NodeId) => this.getNodeRefById(id))
|
||||
}
|
||||
async getFirstNodeRef(): Promise<NodeReference | null> {
|
||||
const id = await this.page.evaluate(() => {
|
||||
@@ -979,12 +894,8 @@ export class ComfyPage {
|
||||
if (!id) return null
|
||||
return this.getNodeRefById(id)
|
||||
}
|
||||
async moveMouseToEmptyArea() {
|
||||
await this.page.mouse.move(10, 10)
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeSlotReference {
|
||||
class NodeSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
readonly index: number,
|
||||
@@ -1034,37 +945,7 @@ export class NodeSlotReference {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeWidgetReference {
|
||||
constructor(
|
||||
readonly index: number,
|
||||
readonly node: NodeReference
|
||||
) {}
|
||||
|
||||
async getPosition(): Promise<Position> {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
const widget = node.widgets[index]
|
||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||
|
||||
const [x, y, w, h] = node.getBounding()
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
x + w / 2,
|
||||
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
||||
])
|
||||
},
|
||||
[this.node.id, this.index] as const
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeReference {
|
||||
class NodeReference {
|
||||
constructor(
|
||||
readonly id: NodeId,
|
||||
readonly comfyPage: ComfyPage
|
||||
@@ -1087,20 +968,6 @@ export class NodeReference {
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
async getBounding(): Promise<Position & Size> {
|
||||
const [x, y, width, height]: [number, number, number, number] =
|
||||
await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error('Node not found')
|
||||
return node.getBounding()
|
||||
}, this.id)
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
async getSize(): Promise<Size> {
|
||||
const size = await this.getProperty<[number, number]>('size')
|
||||
return {
|
||||
@@ -1108,18 +975,6 @@ export class NodeReference {
|
||||
height: size[1]
|
||||
}
|
||||
}
|
||||
async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> {
|
||||
return await this.getProperty('flags')
|
||||
}
|
||||
async isPinned() {
|
||||
return !!(await this.getFlags()).pinned
|
||||
}
|
||||
async isCollapsed() {
|
||||
return !!(await this.getFlags()).collapsed
|
||||
}
|
||||
async isBypassed() {
|
||||
return (await this.getProperty<number | null | undefined>('mode')) === 4
|
||||
}
|
||||
async getProperty<T>(prop: string): Promise<T> {
|
||||
return await this.comfyPage.page.evaluate(
|
||||
([id, prop]) => {
|
||||
@@ -1136,58 +991,22 @@ export class NodeReference {
|
||||
async getInput(index: number) {
|
||||
return new NodeSlotReference('input', index, this)
|
||||
}
|
||||
async getWidget(index: number) {
|
||||
return new NodeWidgetReference(index, this)
|
||||
}
|
||||
async click(
|
||||
position: 'title' | 'collapse',
|
||||
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
|
||||
) {
|
||||
async click(position: 'title', options?: Parameters<Page['click']>[1]) {
|
||||
const nodePos = await this.getPosition()
|
||||
const nodeSize = await this.getSize()
|
||||
let clickPos: Position
|
||||
switch (position) {
|
||||
case 'title':
|
||||
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 }
|
||||
break
|
||||
case 'collapse':
|
||||
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
|
||||
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y + 15 }
|
||||
break
|
||||
default:
|
||||
throw new Error(`Invalid click position ${position}`)
|
||||
}
|
||||
|
||||
const moveMouseToEmptyArea = options?.moveMouseToEmptyArea
|
||||
if (options) {
|
||||
delete options.moveMouseToEmptyArea
|
||||
}
|
||||
|
||||
await this.comfyPage.canvas.click({
|
||||
...options,
|
||||
position: clickPos
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
if (moveMouseToEmptyArea) {
|
||||
await this.comfyPage.moveMouseToEmptyArea()
|
||||
}
|
||||
}
|
||||
async copy() {
|
||||
await this.click('title')
|
||||
await this.comfyPage.ctrlC()
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
async connectWidget(
|
||||
originSlotIndex: number,
|
||||
targetNode: NodeReference,
|
||||
targetWidgetIndex: number
|
||||
) {
|
||||
const originSlot = await this.getOutput(originSlotIndex)
|
||||
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
||||
await this.comfyPage.dragAndDrop(
|
||||
await originSlot.getPosition(),
|
||||
await targetWidget.getPosition()
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
async connectOutput(
|
||||
originSlotIndex: number,
|
||||
@@ -1202,11 +1021,6 @@ export class NodeReference {
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
async getContextMenuOptionNames() {
|
||||
await this.click('title', { button: 'right' })
|
||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||
return await ctx.locator('.litemenu-entry').allInnerTexts()
|
||||
}
|
||||
async clickContextMenuOption(optionText: string) {
|
||||
await this.click('title', { button: 'right' })
|
||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||
@@ -1243,35 +1057,3 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
await use(comfyPage)
|
||||
}
|
||||
})
|
||||
|
||||
const makeMatcher = function <T>(
|
||||
getValue: (node: NodeReference) => Promise<T> | T,
|
||||
type: string
|
||||
) {
|
||||
return async function (
|
||||
node: NodeReference,
|
||||
options?: { timeout?: number; intervals?: number[] }
|
||||
) {
|
||||
const value = await getValue(node)
|
||||
let assertion = expect(
|
||||
value,
|
||||
'Node is ' + (this.isNot ? '' : 'not ') + type
|
||||
)
|
||||
if (this.isNot) {
|
||||
assertion = assertion.not
|
||||
}
|
||||
await expect(async () => {
|
||||
assertion.toBeTruthy()
|
||||
}).toPass({ timeout: 250, ...options })
|
||||
return {
|
||||
pass: !this.isNot,
|
||||
message: () => 'Node is ' + (this.isNot ? 'not ' : '') + type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyExpect = expect.extend({
|
||||
toBePinned: makeMatcher((n) => n.isPinned(), 'pinned'),
|
||||
toBeBypassed: makeMatcher((n) => n.isBypassed(), 'bypassed'),
|
||||
toBeCollapsed: makeMatcher((n) => n.isCollapsed(), 'collapsed')
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ This document outlines the setup and usage of Playwright for testing the ComfyUI
|
||||
## WARNING
|
||||
|
||||
The browser tests will change the ComfyUI backend state, such as user settings and saved workflows.
|
||||
If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directory, these changes won't be automatically restored.
|
||||
Please backup your ComfyUI data before running the tests locally.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import { webSocketFixture } from './fixtures/ws.ts'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Actionbar', () => {
|
||||
test.describe('AppMenu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -23,12 +23,12 @@ test.describe('Actionbar', () => {
|
||||
ws
|
||||
}) => {
|
||||
// Enable change auto-queue mode
|
||||
const queueOpts = await comfyPage.actionbar.queueButton.toggleOptions()
|
||||
const queueOpts = await comfyPage.appMenu.queueButton.toggleOptions()
|
||||
expect(await queueOpts.getMode()).toBe('disabled')
|
||||
await queueOpts.setMode('change')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await queueOpts.getMode()).toBe('change')
|
||||
await comfyPage.actionbar.queueButton.toggleOptions()
|
||||
await comfyPage.appMenu.queueButton.toggleOptions()
|
||||
|
||||
// Intercept the prompt queue endpoint
|
||||
let promptNumber = 0
|
||||
@@ -113,15 +113,4 @@ test.describe('Actionbar', () => {
|
||||
).toBe(END)
|
||||
expect(promptNumber, 'queued prompt count should be 2').toBe(2)
|
||||
})
|
||||
|
||||
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
|
||||
await comfyPage.page.dragAndDrop(
|
||||
'.actionbar .drag-handle',
|
||||
'.comfyui-menu',
|
||||
{
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
}
|
||||
)
|
||||
expect(await comfyPage.actionbar.isDocked()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,404 +0,0 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "workflow>group_node",
|
||||
"pos": {
|
||||
"0": 26,
|
||||
"1": 186
|
||||
},
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 390
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "workflow>group_node"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1,
|
||||
"v1-5-pruned-emaonly.ckpt",
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
|
||||
"text, watermark",
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1,
|
||||
"ComfyUI"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": -1,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": {
|
||||
"0": 473,
|
||||
"1": 609
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1
|
||||
],
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": {
|
||||
"0": 26,
|
||||
"1": 474
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.ckpt"
|
||||
],
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": {
|
||||
"0": 415,
|
||||
"1": 186
|
||||
},
|
||||
"size": {
|
||||
"0": 422.84503173828125,
|
||||
"1": 164.31304931640625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
],
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": {
|
||||
"0": 413,
|
||||
"1": 389
|
||||
},
|
||||
"size": {
|
||||
"0": 425.27801513671875,
|
||||
"1": 180.6060791015625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
],
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 863,
|
||||
"1": 186
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
],
|
||||
"index": 4
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "VAEDecode",
|
||||
"pos": {
|
||||
"0": 1209,
|
||||
"1": 188
|
||||
},
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"index": 5
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "SaveImage",
|
||||
"pos": {
|
||||
"0": 1451,
|
||||
"1": 189
|
||||
},
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
],
|
||||
"index": 6
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
1,
|
||||
6,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
2,
|
||||
7,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
5,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
1,
|
||||
4,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
8,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"external": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ControlNetApplyAdvanced",
|
||||
"pos": {
|
||||
"0": 449,
|
||||
"1": 204
|
||||
},
|
||||
"size": [
|
||||
340.20001220703125,
|
||||
166
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "control_net",
|
||||
"type": "CONTROL_NET",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "strength",
|
||||
"type": "FLOAT",
|
||||
"link": 1,
|
||||
"widget": {
|
||||
"name": "strength"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetApplyAdvanced"
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": {
|
||||
"0": 177,
|
||||
"1": 265
|
||||
},
|
||||
"size": [
|
||||
210,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"widget": {
|
||||
"name": "strength"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
"fixed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
4,
|
||||
"FLOAT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 47.541666666666515,
|
||||
"1": 186.9375
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 304.3653259277344,
|
||||
"1": 42.15586471557617
|
||||
},
|
||||
"size": [
|
||||
315,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": {
|
||||
"0": 14,
|
||||
"1": 43
|
||||
},
|
||||
"size": [
|
||||
203.1999969482422,
|
||||
40.368401303242536
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "connect to widget input",
|
||||
"type": "*",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { comfyPageFixture as test } from './ComfyPage'
|
||||
test.describe('Browser tab title', () => {
|
||||
test.describe('Beta Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -16,7 +16,7 @@ test.describe('Browser tab title', () => {
|
||||
return window['app'].workflowManager.activeWorkflow.name
|
||||
})
|
||||
// Note: unsaved workflow name is always prepended with "*".
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName}`)
|
||||
})
|
||||
|
||||
// Broken by https://github.com/Comfy-Org/ComfyUI_frontend/pull/893
|
||||
@@ -28,15 +28,15 @@ test.describe('Browser tab title', () => {
|
||||
return window['app'].workflowManager.activeWorkflow.name
|
||||
})
|
||||
// Note: unsaved workflow name is always prepended with "*".
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName}`)
|
||||
|
||||
await comfyPage.menu.saveWorkflow('test')
|
||||
expect(await comfyPage.page.title()).toBe('test - ComfyUI')
|
||||
expect(await comfyPage.page.title()).toBe('test')
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.fill('Hello World')
|
||||
await comfyPage.clickEmptySpace()
|
||||
expect(await comfyPage.page.title()).toBe(`*test - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`*test`)
|
||||
|
||||
// Delete the saved workflow for cleanup.
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './ComfyPage'
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].canvas.emitBeforeChange()
|
||||
})
|
||||
}
|
||||
async function afterChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].canvas.emitAfterChange()
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Change Tracker', () => {
|
||||
test('Can group multiple change actions into a single transaction', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
expect(node).toBeTruthy()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
await expect(node).not.toBeBypassed()
|
||||
|
||||
// Make changes outside set
|
||||
// Bypass + collapse node
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeCollapsed()
|
||||
await expect(node).toBeBypassed()
|
||||
|
||||
// Undo, undo, ensure both changes undone
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
await expect(node).toBeCollapsed()
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
|
||||
// Run again, but within a change transaction
|
||||
beforeChange(comfyPage)
|
||||
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeCollapsed()
|
||||
await expect(node).toBeBypassed()
|
||||
|
||||
// End transaction
|
||||
afterChange(comfyPage)
|
||||
|
||||
// Ensure undo reverts both changes
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
})
|
||||
|
||||
test('Can group multiple transaction calls into a single one', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
const bypassAndPin = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
await comfyPage.page.keyboard.press('KeyP')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(node).toBePinned()
|
||||
await afterChange(comfyPage)
|
||||
}
|
||||
|
||||
const collapse = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
await node.click('collapse', { moveMouseToEmptyArea: true })
|
||||
await expect(node).toBeCollapsed()
|
||||
await afterChange(comfyPage)
|
||||
}
|
||||
|
||||
const multipleChanges = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
// Call other actions that uses begin/endChange
|
||||
await collapse()
|
||||
await bypassAndPin()
|
||||
await afterChange(comfyPage)
|
||||
}
|
||||
|
||||
await multipleChanges()
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
await expect(node).not.toBePinned()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
|
||||
await comfyPage.ctrlY()
|
||||
await expect(node).toBeBypassed()
|
||||
await expect(node).toBePinned()
|
||||
await expect(node).toBeCollapsed()
|
||||
})
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Should execute command', async ({ comfyPage }) => {
|
||||
await comfyPage.registerCommand('TestCommand', () => {
|
||||
window['foo'] = true
|
||||
})
|
||||
|
||||
await comfyPage.executeCommand('TestCommand')
|
||||
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
|
||||
})
|
||||
|
||||
test('Should execute async command', async ({ comfyPage }) => {
|
||||
await comfyPage.registerCommand('TestCommand', async () => {
|
||||
await new Promise<void>((resolve) =>
|
||||
setTimeout(() => {
|
||||
window['foo'] = true
|
||||
resolve()
|
||||
}, 5)
|
||||
)
|
||||
})
|
||||
|
||||
await comfyPage.executeCommand('TestCommand')
|
||||
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
|
||||
})
|
||||
|
||||
test('Should handle command errors', async ({ comfyPage }) => {
|
||||
await comfyPage.registerCommand('TestCommand', () => {
|
||||
throw new Error('Test error')
|
||||
})
|
||||
|
||||
await comfyPage.executeCommand('TestCommand')
|
||||
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should handle async command errors', async ({ comfyPage }) => {
|
||||
await comfyPage.registerCommand('TestCommand', async () => {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('Test error'))
|
||||
}, 5)
|
||||
)
|
||||
})
|
||||
|
||||
await comfyPage.executeCommand('TestCommand')
|
||||
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -86,23 +86,4 @@ test.describe('Copy Paste', () => {
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('drag-copy-copied-node.png')
|
||||
})
|
||||
|
||||
test('Can undo paste multiple nodes as single action', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialCount = await comfyPage.getGraphNodesCount()
|
||||
expect(initialCount).toBeGreaterThan(1)
|
||||
await comfyPage.canvas.click()
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.ctrlC()
|
||||
await comfyPage.ctrlV()
|
||||
|
||||
const pasteCount = await comfyPage.getGraphNodesCount()
|
||||
expect(pasteCount).toBe(initialCount * 2)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
const undoCount = await comfyPage.getGraphNodesCount()
|
||||
expect(undoCount).toBe(initialCount)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,34 +95,3 @@ test.describe('Missing models warning', () => {
|
||||
await expect(folderSelect).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
// Restore default setting value
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
||||
})
|
||||
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const searchBox = comfyPage.page.locator('.settings-content')
|
||||
await expect(searchBox).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can open settings with hotkey', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.locator('.settings-container')
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const maxSpeed = 2.5
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
test.step('Setting should persist', async () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expect, Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -12,75 +12,21 @@ test.describe('Topbar commands', () => {
|
||||
|
||||
test('Should allow registering topbar commands', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
window['app'].extensionManager.menu.registerTopbarCommands(
|
||||
['ext'],
|
||||
[
|
||||
{
|
||||
id: 'foo',
|
||||
label: 'foo-command',
|
||||
label: 'foo',
|
||||
function: () => {
|
||||
window['foo'] = true
|
||||
}
|
||||
}
|
||||
],
|
||||
menuCommands: [
|
||||
{
|
||||
path: ['ext'],
|
||||
commands: ['foo']
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo'])
|
||||
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
|
||||
})
|
||||
|
||||
test('Should not allow register command defined in other extension', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.registerCommand('foo', () => alert(1))
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].registerExtension({
|
||||
name: 'TestExtension1',
|
||||
menuCommands: [
|
||||
{
|
||||
path: ['ext'],
|
||||
commands: ['foo']
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
const menuItem: Locator = await comfyPage.menu.topbar.getMenuItem('ext')
|
||||
expect(await menuItem.count()).toBe(0)
|
||||
})
|
||||
|
||||
test('Should allow registering keybindings', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const app = window['app']
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
{
|
||||
id: 'TestCommand',
|
||||
function: () => {
|
||||
window['TestCommand'] = true
|
||||
}
|
||||
}
|
||||
],
|
||||
keybindings: [
|
||||
{
|
||||
combo: { key: 'k' },
|
||||
commandId: 'TestCommand'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.page.keyboard.press('k')
|
||||
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export default function globalSetup(config: FullConfig) {
|
||||
if (!process.env.CI) {
|
||||
if (process.env.TEST_COMFYUI_DIR) {
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
|
||||
renameAndReplaceWithScaffolding: true
|
||||
})
|
||||
} else {
|
||||
console.warn(
|
||||
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export default function globalTeardown(config: FullConfig) {
|
||||
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
|
||||
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
|
||||
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Graph Canvas Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Set link render mode to spline to make sure it's not affected by other tests'
|
||||
// side effects.
|
||||
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
|
||||
})
|
||||
|
||||
test('Can toggle link visibility', async ({ comfyPage }) => {
|
||||
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
|
||||
// so no cleanup is needed.
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
|
||||
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
|
||||
await button.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'canvas-with-hidden-links.png'
|
||||
)
|
||||
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
|
||||
return window['LiteGraph'].HIDDEN_LINK
|
||||
})
|
||||
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
|
||||
hiddenLinkRenderMode
|
||||
)
|
||||
|
||||
await button.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'canvas-with-visible-links.png'
|
||||
)
|
||||
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
|
||||
hiddenLinkRenderMode
|
||||
)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB |
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { ComfyPage, NodeReference, comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -13,7 +13,7 @@ test.describe('Group Node', () => {
|
||||
let libraryTab
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
libraryTab = comfyPage.menu.nodeLibraryTab
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await libraryTab.open()
|
||||
@@ -148,124 +148,4 @@ test.describe('Group Node', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Copy and paste', () => {
|
||||
let groupNode: NodeReference | null
|
||||
const WORKFLOW_NAME = 'group_node_v1.3.3'
|
||||
const GROUP_NODE_CATEGORY = 'group nodes>workflow'
|
||||
const GROUP_NODE_PREFIX = 'workflow>'
|
||||
const GROUP_NODE_NAME = 'group_node' // Node name in given workflow
|
||||
const GROUP_NODE_TYPE = `${GROUP_NODE_PREFIX}${GROUP_NODE_NAME}`
|
||||
|
||||
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
|
||||
return await comfyPage.page.evaluate((nodeType: string) => {
|
||||
return !!window['LiteGraph'].registered_node_types[nodeType]
|
||||
}, GROUP_NODE_TYPE)
|
||||
}
|
||||
|
||||
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
|
||||
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
|
||||
.getFolder(GROUP_NODE_CATEGORY)
|
||||
.count()
|
||||
return groupNodesFolderCt === 1
|
||||
}
|
||||
|
||||
const verifyNodeLoaded = async (
|
||||
comfyPage: ComfyPage,
|
||||
expectedCount: number
|
||||
) => {
|
||||
expect(await comfyPage.getNodeRefsByType(GROUP_NODE_TYPE)).toHaveLength(
|
||||
expectedCount
|
||||
)
|
||||
expect(await isRegisteredLitegraph(comfyPage)).toBe(true)
|
||||
expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true)
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.loadWorkflow(WORKFLOW_NAME)
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
|
||||
groupNode = await comfyPage.getFirstNodeRef()
|
||||
if (!groupNode)
|
||||
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
|
||||
await groupNode.copy()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.page.evaluate((groupNodeName) => {
|
||||
window['LiteGraph'].unregisterNodeType(groupNodeName)
|
||||
}, GROUP_NODE_TYPE)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node within the same workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 2)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node after clearing workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand([
|
||||
'Edit',
|
||||
'Clear Workflow'
|
||||
])
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node into a newly created blank workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node across different workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
|
||||
test('Serializes group node after copy and paste across workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.ctrlV()
|
||||
const currentGraphState = await comfyPage.page.evaluate(() =>
|
||||
window['app'].graph.serialize()
|
||||
)
|
||||
|
||||
await test.step('Load workflow containing a group node pasted from a different workflow', async () => {
|
||||
await comfyPage.page.evaluate(
|
||||
(workflow) => window['app'].loadGraphData(workflow),
|
||||
currentGraphState
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Convert to group node, no selection', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import type { AutoQueueMode } from '../../src/stores/queueStore'
|
||||
|
||||
export class ComfyActionbar {
|
||||
export class ComfyAppMenu {
|
||||
public readonly root: Locator
|
||||
public readonly queueButton: ComfyQueueButton
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.root = page.locator('.actionbar')
|
||||
this.root = page.locator('.app-menu')
|
||||
this.queueButton = new ComfyQueueButton(this)
|
||||
}
|
||||
|
||||
async isDocked() {
|
||||
const className = await this.root.getAttribute('class')
|
||||
return className?.includes('is-docked') ?? false
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyQueueButton {
|
||||
public readonly root: Locator
|
||||
public readonly primaryButton: Locator
|
||||
public readonly dropdownButton: Locator
|
||||
constructor(public readonly actionbar: ComfyActionbar) {
|
||||
this.root = actionbar.root.getByTestId('queue-button')
|
||||
constructor(public readonly appMenu: ComfyAppMenu) {
|
||||
this.root = appMenu.root.getByTestId('queue-button')
|
||||
this.primaryButton = this.root.locator('.p-splitbutton-button')
|
||||
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
|
||||
}
|
||||
|
||||
public async toggleOptions() {
|
||||
await this.dropdownButton.click()
|
||||
return new ComfyQueueButtonOptions(this.actionbar.page)
|
||||
return new ComfyQueueButtonOptions(this.appMenu.page)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,51 +11,12 @@ test.describe('Node Interaction', () => {
|
||||
await expect(textBox).toHaveValue('Hello World 2')
|
||||
})
|
||||
|
||||
test.describe('Node Selection', () => {
|
||||
const multiSelectModifiers = ['Control', 'Shift', 'Meta']
|
||||
|
||||
multiSelectModifiers.forEach((modifier) => {
|
||||
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of clipNodes) {
|
||||
await node.click('title', { modifiers: [modifier] })
|
||||
}
|
||||
const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount()
|
||||
expect(selectedNodeCount).toBe(clipNodes.length)
|
||||
})
|
||||
})
|
||||
|
||||
test('Can highlight selected', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
|
||||
await comfyPage.clickTextEncodeNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
||||
})
|
||||
|
||||
test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
|
||||
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
||||
const clipNode1Pos = await clipNodes[0].getPosition()
|
||||
const clipNode2Pos = await clipNodes[1].getPosition()
|
||||
const offset = 64
|
||||
await comfyPage.page.keyboard.down('Meta')
|
||||
await comfyPage.dragAndDrop(
|
||||
{
|
||||
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
|
||||
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
|
||||
},
|
||||
{
|
||||
x: Math.max(clipNode1Pos.x, clipNode2Pos.x) + offset,
|
||||
y: Math.max(clipNode1Pos.y, clipNode2Pos.y) + offset
|
||||
}
|
||||
)
|
||||
await comfyPage.page.keyboard.up('Meta')
|
||||
expect(await comfyPage.getSelectedGraphNodesCount()).toBe(
|
||||
clipNodes.length
|
||||
)
|
||||
})
|
||||
test('Can highlight selected', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
|
||||
await comfyPage.clickTextEncodeNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
||||
})
|
||||
|
||||
test('Can drag node', async ({ comfyPage }) => {
|
||||
@@ -386,52 +347,6 @@ test.describe('Canvas Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
|
||||
})
|
||||
|
||||
test('Cursor style changes when panning', async ({ comfyPage }) => {
|
||||
const getCursorStyle = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return (
|
||||
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
await comfyPage.page.mouse.down()
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
await comfyPage.page.mouse.up()
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
|
||||
await comfyPage.page.keyboard.down('Space')
|
||||
expect(await getCursorStyle()).toBe('grab')
|
||||
await comfyPage.page.mouse.down()
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
await comfyPage.page.mouse.up()
|
||||
expect(await getCursorStyle()).toBe('grab')
|
||||
await comfyPage.page.keyboard.up('Space')
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
})
|
||||
|
||||
test('Can pan when dragging a link', async ({ comfyPage }) => {
|
||||
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
|
||||
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
|
||||
await comfyPage.page.mouse.down()
|
||||
const posEmpty = comfyPage.emptySpace
|
||||
await comfyPage.page.mouse.move(posEmpty.x, posEmpty.y)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')
|
||||
await comfyPage.page.keyboard.down('Space')
|
||||
await comfyPage.page.mouse.move(posEmpty.x + 100, posEmpty.y + 100)
|
||||
// Canvas should be panned.
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'panning-when-dragging-link.png'
|
||||
)
|
||||
await comfyPage.page.keyboard.up('Space')
|
||||
await comfyPage.page.mouse.move(posEmpty.x, posEmpty.y)
|
||||
// Should be back to dragging link mode when space is released.
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link2.png')
|
||||
await comfyPage.page.mouse.up()
|
||||
})
|
||||
|
||||
test('Can pan very far and back', async ({ comfyPage }) => {
|
||||
// intentionally slice the edge of where the clip text encode dom widgets are
|
||||
await comfyPage.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
|
||||
@@ -490,7 +405,7 @@ test.describe('Load workflow', () => {
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 90 KiB |
@@ -1,37 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Should not trigger non-modifier keybinding when typing in input fields', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.registerKeybinding({ key: 'k' }, () => {
|
||||
window['TestCommand'] = true
|
||||
})
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.fill('k')
|
||||
await expect(textBox).toHaveValue('k')
|
||||
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
test('Should not trigger modifier keybinding when typing in input fields', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => {
|
||||
window['TestCommand'] = true
|
||||
})
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.fill('q')
|
||||
await textBox.press('Control+k')
|
||||
await expect(textBox).toHaveValue('q')
|
||||
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -389,8 +389,6 @@ test.describe('Menu', () => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Can create new blank workflow', async ({ comfyPage }) => {
|
||||
@@ -416,9 +414,10 @@ test.describe('Menu', () => {
|
||||
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
|
||||
)
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual([
|
||||
'workflow1.json',
|
||||
'workflow2.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
@@ -427,9 +426,9 @@ test.describe('Menu', () => {
|
||||
await comfyPage.loadWorkflow('missing_nodes')
|
||||
await comfyPage.closeDialog()
|
||||
|
||||
// Load blank workflow
|
||||
// Load default workflow
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.menu.workflowsTab.newDefaultWorkflowButton.click()
|
||||
|
||||
// Switch back to the missing_nodes workflow
|
||||
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
|
||||
@@ -438,21 +437,6 @@ test.describe('Menu', () => {
|
||||
comfyPage.page.locator('.comfy-missing-nodes')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Can close saved-workflows from the open workflows section', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow(
|
||||
`tempWorkflow-${test.info().title}`
|
||||
)
|
||||
const closeButton = comfyPage.page.locator(
|
||||
'.comfyui-workflows-open .p-button-icon.pi-times'
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['*Unsaved Workflow (2).json'])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows topbar tabs', () => {
|
||||
@@ -461,7 +445,6 @@ test.describe('Menu', () => {
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Topbar'
|
||||
)
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Can show opened workflows', async ({ comfyPage }) => {
|
||||
@@ -469,41 +452,6 @@ test.describe('Menu', () => {
|
||||
'Unsaved Workflow'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can close saved-workflow tabs', async ({ comfyPage }) => {
|
||||
const workflowName = `tempWorkflow-${test.info().title}`
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowName)
|
||||
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([workflowName])
|
||||
await comfyPage.menu.topbar.closeWorkflowTab(workflowName)
|
||||
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([
|
||||
'Unsaved Workflow (2)'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Topbar submmenus', () => {
|
||||
test('@mobile Items fully visible on mobile screen width', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.openSubmenuMobile()
|
||||
const topLevelMenuItem = comfyPage.page
|
||||
.locator('a.p-menubar-item-link')
|
||||
.first()
|
||||
const isTextCutoff = await topLevelMenuItem.evaluate((el) => {
|
||||
return el.scrollWidth > el.clientWidth
|
||||
})
|
||||
expect(isTextCutoff).toBe(false)
|
||||
})
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
const workflowMenuItem =
|
||||
await comfyPage.menu.topbar.getMenuItem('Workflow')
|
||||
await workflowMenuItem.click()
|
||||
const exportTag = comfyPage.page.locator('.keybinding-tag', {
|
||||
hasText: 'Ctrl + s'
|
||||
})
|
||||
expect(await exportTag.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Only test 'Top' to reduce test time.
|
||||
@@ -513,7 +461,7 @@ test.describe('Menu', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', position)
|
||||
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top')
|
||||
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Floating')
|
||||
})
|
||||
|
||||
test(`Can migrate deprecated menu positions on initial load (${position})`, async ({
|
||||
@@ -521,7 +469,20 @@ test.describe('Menu', () => {
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', position)
|
||||
await comfyPage.setup()
|
||||
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top')
|
||||
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Floating')
|
||||
})
|
||||
})
|
||||
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const [defaultSpeed, maxSpeed] = [1.1, 2.5]
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
|
||||
defaultSpeed
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.setup()
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', defaultSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,16 +32,4 @@ test.describe('Optional input', () => {
|
||||
// If the node's multiline text widget is visible, then it was loaded successfully
|
||||
expect(comfyPage.page.locator('.comfy-multiline-input')).toHaveCount(1)
|
||||
})
|
||||
test('Old workflow with converted input', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('old_workflow_converted_input')
|
||||
const node = await comfyPage.getNodeRefById('1')
|
||||
const inputs = await node.getProperty('inputs')
|
||||
const vaeInput = inputs.find((w) => w.name === 'vae')
|
||||
const convertedInput = inputs.find((w) => w.name === 'strength')
|
||||
|
||||
expect(vaeInput).toBeDefined()
|
||||
expect(convertedInput).toBeDefined()
|
||||
expect(vaeInput.link).toBeNull()
|
||||
expect(convertedInput.link).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { type NodeReference, comfyPageFixture as test } from './ComfyPage'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Primitive Node', () => {
|
||||
test('Can load with correct size', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('primitive_node')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
|
||||
})
|
||||
|
||||
// When link is dropped on widget, it should automatically convert the widget
|
||||
// to input.
|
||||
test('Can connect to widget', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('primitive_node_unconnected')
|
||||
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
|
||||
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
|
||||
// Connect the output of the primitive node to the input of first widget of the ksampler node
|
||||
await primitiveNode.connectWidget(0, ksamplerNode, 0)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'primitive_node_connected.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB |
@@ -3,7 +3,7 @@ import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Properties Panel', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -93,54 +93,16 @@ test.describe('Node Right Click Menu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Widget conversion', () => {
|
||||
const convertibleWidgetTypes = ['text', 'string', 'number', 'toggle']
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
// Restore default setting value
|
||||
await comfyPage.setSetting('Comfy.NodeInputConversionSubmenus', true)
|
||||
})
|
||||
|
||||
test('Can convert widget to input', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('Convert width to input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
|
||||
convertibleWidgetTypes.forEach((widgetType) => {
|
||||
test(`Can convert ${widgetType} widget to input`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeType = 'KSampler'
|
||||
|
||||
// To avoid needing multiple clicks, disable nesting of conversion options
|
||||
await comfyPage.setSetting('Comfy.NodeInputConversionSubmenus', false)
|
||||
|
||||
// Add the widget using the node's `addWidget` method
|
||||
await comfyPage.page.evaluate(
|
||||
([nodeType, widgetType]) => {
|
||||
const node = window['app'].graph.nodes.find(
|
||||
(n) => n.type === nodeType
|
||||
)
|
||||
node.addWidget(widgetType, widgetType, 'defaultValue', () => {}, {})
|
||||
},
|
||||
[nodeType, widgetType]
|
||||
)
|
||||
|
||||
// Verify the context menu includes the conversion option
|
||||
const node = (await comfyPage.getNodeRefsByType(nodeType))[0]
|
||||
const menuOptions = await node.getContextMenuOptionNames()
|
||||
expect(menuOptions.includes(`Convert ${widgetType} to input`)).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
test('Can convert widget to input', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('Convert width to input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can pin and unpin', async ({ comfyPage }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Templates', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
type PathParts = readonly [string, ...string[]]
|
||||
|
||||
const getBackupPath = (originalPath: string): string => `${originalPath}.bak`
|
||||
|
||||
const resolvePathIfExists = (pathParts: PathParts): string | null => {
|
||||
const resolvedPath = path.resolve(...pathParts)
|
||||
if (!fs.pathExistsSync(resolvedPath)) {
|
||||
console.warn(`Path not found: ${resolvedPath}`)
|
||||
return null
|
||||
}
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
const createScaffoldingCopy = (srcDir: string, destDir: string) => {
|
||||
// Get all items (files and directories) in the source directory
|
||||
const items = fs.readdirSync(srcDir, { withFileTypes: true })
|
||||
|
||||
for (const item of items) {
|
||||
const srcPath = path.join(srcDir, item.name)
|
||||
const destPath = path.join(destDir, item.name)
|
||||
|
||||
if (item.isDirectory()) {
|
||||
// Create the corresponding directory in the destination
|
||||
fs.ensureDirSync(destPath)
|
||||
|
||||
// Recursively copy the directory structure
|
||||
createScaffoldingCopy(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function backupPath(
|
||||
pathParts: PathParts,
|
||||
{ renameAndReplaceWithScaffolding = false } = {}
|
||||
) {
|
||||
const originalPath = resolvePathIfExists(pathParts)
|
||||
if (!originalPath) return
|
||||
|
||||
const backupPath = getBackupPath(originalPath)
|
||||
try {
|
||||
if (renameAndReplaceWithScaffolding) {
|
||||
// Rename the original path and create scaffolding in its place
|
||||
fs.moveSync(originalPath, backupPath)
|
||||
createScaffoldingCopy(backupPath, originalPath)
|
||||
} else {
|
||||
// Create a copy of the original path
|
||||
fs.copySync(originalPath, backupPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to backup ${originalPath} from ${backupPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
export function restorePath(pathParts: PathParts) {
|
||||
const originalPath = resolvePathIfExists(pathParts)
|
||||
if (!originalPath) return
|
||||
|
||||
const backupPath = getBackupPath(originalPath)
|
||||
if (!fs.pathExistsSync(backupPath)) return
|
||||
|
||||
try {
|
||||
fs.moveSync(backupPath, originalPath, { overwrite: true })
|
||||
} catch (error) {
|
||||
console.error(`Failed to restore ${originalPath} from ${backupPath}`, error)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import globals from 'globals'
|
||||
import pluginJs from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -36,16 +35,5 @@ export default [
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/prefer-as-const': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
'unused-imports/no-unused-imports': 'error'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="node_modules/reflect-metadata/Reflect.js"></script>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
918
package-lock.json
generated
16
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.3.19",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"deploy": "npm run build && node scripts/deploy.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "tsc --noEmit && tsc-strict",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
|
||||
"test": "npm run build && jest",
|
||||
"test:generate:examples": "npx tsx tests-ui/extractExamples",
|
||||
@@ -30,15 +30,13 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/vue3-jest": "^29.2.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||
"babel-plugin-transform-rename-import": "^2.3.0",
|
||||
"chalk": "^5.3.0",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globals": "^15.9.0",
|
||||
@@ -56,7 +54,6 @@
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"typescript-strict-plugin": "^2.4.4",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.6",
|
||||
@@ -65,17 +62,20 @@
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/litegraph": "^0.8.3",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.77",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"axios": "^1.7.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.5",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"vue": "^3.4.31",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
|
||||
@@ -30,10 +30,6 @@ export default defineConfig({
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
/* Path to global setup file. Exported function runs once before all the tests */
|
||||
globalSetup: './browser_tests/globalSetup.ts',
|
||||
/* Path to global teardown file. Exported function runs once after all the tests */
|
||||
globalTeardown: './browser_tests/globalTeardown.ts',
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -266,7 +266,7 @@
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
"v1-5-pruned-emaonly.ckpt"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -347,10 +347,5 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"models": [{
|
||||
"name": "v1-5-pruned-emaonly.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}]
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
BIN
public/templates/default.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -409,12 +409,5 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"name": "flux1-schnell-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/flux1-schnell/resolve/main/flux1-schnell-fp8.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}
|
||||
],
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
BIN
public/templates/flux_schnell.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@@ -330,7 +330,7 @@
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
"v1-5-pruned-emaonly.ckpt"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -438,10 +438,5 @@
|
||||
],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"models": [{
|
||||
"name": "v1-5-pruned-emaonly.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}]
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
BIN
public/templates/image2image.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 30 KiB |
@@ -429,7 +429,7 @@
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v2-1_768-ema-pruned.safetensors"
|
||||
"v2-1_768-ema-pruned.ckpt"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -641,12 +641,5 @@
|
||||
],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"models": [
|
||||
{
|
||||
"name": "v2-1_768-ema-pruned.safetensors",
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}
|
||||
]
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
BIN
public/templates/upscale.png
Normal file
|
After Width: | Height: | Size: 626 KiB |
@@ -18,9 +18,6 @@ def download_and_extract(version, temp_dir):
|
||||
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
zip_ref.extractall(temp_dir)
|
||||
|
||||
# Clean up the zip file after extraction
|
||||
os.remove(zip_path)
|
||||
else:
|
||||
raise Exception(
|
||||
f"Failed to download release asset. Status code: {response.status_code}"
|
||||
|
||||
11
src/App.vue
@@ -10,20 +10,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import config from '@/config'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
}
|
||||
useEventListener(window, 'keydown', handleKey)
|
||||
useEventListener(window, 'keyup', handleKey)
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
|
||||
onMounted(() => {
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
|
||||
@@ -126,8 +126,7 @@ body {
|
||||
/* Span across all columns */
|
||||
grid-column: 1/-1;
|
||||
grid-row: 3;
|
||||
/* Bottom menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */
|
||||
z-index: 1000;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -317,7 +316,6 @@ span.drag-handle {
|
||||
letter-spacing: 2px;
|
||||
color: var(--drag-text);
|
||||
text-shadow: 1px 0 1px black;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
span.drag-handle::after {
|
||||
|
||||
@@ -12,8 +12,6 @@ import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const TITLE_SUFFIX = ' - ComfyUI'
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
|
||||
@@ -30,9 +28,7 @@ const isUnsavedText = computed(() =>
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.name
|
||||
return workflowName
|
||||
? isUnsavedText.value + workflowName + TITLE_SUFFIX
|
||||
: DEFAULT_TITLE
|
||||
return workflowName ? isUnsavedText.value + workflowName : DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
>
|
||||
<slot name="side-bar-panel"></slot>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel class="graph-canvas-panel relative" :size="100">
|
||||
<slot name="graph-canvas-panel"></slot>
|
||||
<SplitterPanel class="graph-canvas-panel" :size="100">
|
||||
<div></div>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
class="side-bar-panel"
|
||||
@@ -37,7 +37,7 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
)
|
||||
|
||||
const sidebarPanelVisible = computed(
|
||||
() => useWorkspaceStore().sidebarTab.activeSidebarTab !== null
|
||||
() => useWorkspaceStore().activeSidebarTab !== null
|
||||
)
|
||||
const gutterClass = computed(() => {
|
||||
return sidebarPanelVisible.value ? '' : 'gutter-hidden'
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
<template>
|
||||
<Panel
|
||||
class="actionbar w-fit"
|
||||
:style="style"
|
||||
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
||||
>
|
||||
<div class="actionbar-content flex items-center" ref="panelRef">
|
||||
<span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef">
|
||||
</span>
|
||||
<ComfyQueueButton />
|
||||
<Divider layout="vertical" class="mx-1" />
|
||||
<ButtonGroup class="flex flex-nowrap">
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.refresh')"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.RefreshNodeDefinitions')"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, nextTick, onMounted, Ref, ref, watch } from 'vue'
|
||||
import Panel from 'primevue/panel'
|
||||
import Divider from 'primevue/divider'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
useDraggable,
|
||||
useElementBounding,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'lodash'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const visible = computed(
|
||||
() => settingsStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', false)
|
||||
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
style: style,
|
||||
isDragging
|
||||
} = useDraggable(panelRef, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
handle: dragHandleRef,
|
||||
containerElement: document.body
|
||||
})
|
||||
|
||||
// Update storedPosition when x or y changes
|
||||
watchDebounced(
|
||||
[x, y],
|
||||
([newX, newY]) => {
|
||||
storedPosition.value = { x: newX, y: newY }
|
||||
},
|
||||
{ debounce: 300 }
|
||||
)
|
||||
|
||||
// Set initial position to bottom center
|
||||
const setInitialPosition = () => {
|
||||
if (x.value !== 0 || y.value !== 0) {
|
||||
return
|
||||
}
|
||||
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
||||
x.value = storedPosition.value.x
|
||||
y.value = storedPosition.value.y
|
||||
captureLastDragState()
|
||||
return
|
||||
}
|
||||
if (panelRef.value) {
|
||||
const screenWidth = window.innerWidth
|
||||
const screenHeight = window.innerHeight
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
|
||||
if (menuWidth === 0 || menuHeight === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
x.value = (screenWidth - menuWidth) / 2
|
||||
y.value = screenHeight - menuHeight - 10 // 10px margin from bottom
|
||||
captureLastDragState()
|
||||
}
|
||||
}
|
||||
onMounted(setInitialPosition)
|
||||
watch(visible, (newVisible) => {
|
||||
if (newVisible) {
|
||||
nextTick(setInitialPosition)
|
||||
}
|
||||
})
|
||||
|
||||
const lastDragState = ref({
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight
|
||||
})
|
||||
const captureLastDragState = () => {
|
||||
lastDragState.value = {
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight
|
||||
}
|
||||
}
|
||||
watch(
|
||||
isDragging,
|
||||
(newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
captureLastDragState()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const adjustMenuPosition = () => {
|
||||
if (panelRef.value) {
|
||||
const screenWidth = window.innerWidth
|
||||
const screenHeight = window.innerHeight
|
||||
const menuWidth = panelRef.value.offsetWidth
|
||||
const menuHeight = panelRef.value.offsetHeight
|
||||
|
||||
// Calculate the distance from each edge
|
||||
const distanceRight =
|
||||
lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)
|
||||
const distanceBottom =
|
||||
lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)
|
||||
|
||||
// Determine if the menu is closer to right/bottom or left/top
|
||||
const anchorRight = distanceRight < lastDragState.value.x
|
||||
const anchorBottom = distanceBottom < lastDragState.value.y
|
||||
|
||||
// Calculate new position
|
||||
if (anchorRight) {
|
||||
x.value =
|
||||
screenWidth - (lastDragState.value.windowWidth - lastDragState.value.x)
|
||||
} else {
|
||||
x.value = lastDragState.value.x
|
||||
}
|
||||
|
||||
if (anchorBottom) {
|
||||
y.value =
|
||||
screenHeight -
|
||||
(lastDragState.value.windowHeight - lastDragState.value.y)
|
||||
} else {
|
||||
y.value = lastDragState.value.y
|
||||
}
|
||||
|
||||
// Ensure the menu stays within the screen bounds
|
||||
x.value = clamp(x.value, 0, screenWidth - menuWidth)
|
||||
y.value = clamp(y.value, 0, screenHeight - menuHeight)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(window, 'resize', adjustMenuPosition)
|
||||
|
||||
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
|
||||
const topMenuBounds = useElementBounding(topMenuRef)
|
||||
const overlapThreshold = 20 // pixels
|
||||
const isOverlappingWithTopMenu = computed(() => {
|
||||
if (!panelRef.value) {
|
||||
return false
|
||||
}
|
||||
const { height } = panelRef.value.getBoundingClientRect()
|
||||
const actionbarBottom = y.value + height
|
||||
const topMenuBottom = topMenuBounds.bottom.value
|
||||
|
||||
const overlapPixels =
|
||||
Math.min(actionbarBottom, topMenuBottom) -
|
||||
Math.max(y.value, topMenuBounds.top.value)
|
||||
return overlapPixels > overlapThreshold
|
||||
})
|
||||
|
||||
watch(isDragging, (newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
isDocked.value = isOverlappingWithTopMenu.value
|
||||
} else {
|
||||
// Start dragging
|
||||
isDocked.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const eventBus = useEventBus<string>('topMenu')
|
||||
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
eventBus.emit('updateHighlight', {
|
||||
isDragging: dragging,
|
||||
isOverlapping: overlapping
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actionbar {
|
||||
pointer-events: all;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.actionbar.is-docked {
|
||||
position: static;
|
||||
@apply bg-transparent border-none p-0;
|
||||
}
|
||||
|
||||
.actionbar.is-dragging {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
@apply p-1;
|
||||
}
|
||||
|
||||
:deep(.p-panel-header) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div class="queue-button-group flex">
|
||||
<SplitButton
|
||||
class="comfyui-queue-button"
|
||||
:label="activeQueueModeMenuItem.label"
|
||||
severity="primary"
|
||||
@click="queuePrompt"
|
||||
:model="queueModeMenuItems"
|
||||
data-testid="queue-button"
|
||||
v-tooltip.bottom="
|
||||
workspaceStore.shiftDown
|
||||
? $t('menu.queueWorkflowFront')
|
||||
: $t('menu.queueWorkflow')
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
|
||||
<i v-else :class="activeQueueModeMenuItem.icon" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
text
|
||||
v-tooltip="item.tooltip"
|
||||
/>
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
<ButtonGroup class="execution-actions flex flex-nowrap">
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.interrupt')"
|
||||
icon="pi pi-times"
|
||||
:severity="executingPrompt ? 'danger' : 'secondary'"
|
||||
:disabled="!executingPrompt"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.Interrupt')"
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
icon="pi pi-stop"
|
||||
:severity="hasPendingTasks ? 'danger' : 'secondary'"
|
||||
:disabled="!hasPendingTasks"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.ClearPendingTasks')"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import Button from 'primevue/button'
|
||||
import BatchCountEdit from './BatchCountEdit.vue'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
AutoQueueMode,
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup: Record<AutoQueueMode, MenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: 'Queue',
|
||||
icon: 'pi pi-play',
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
instant: {
|
||||
key: 'instant',
|
||||
label: 'Queue (Instant)',
|
||||
icon: 'pi pi-forward',
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: 'Queue (Change)',
|
||||
icon: 'pi pi-step-forward-alt',
|
||||
tooltip: t('menu.changeTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeQueueModeMenuItem = computed(
|
||||
() => queueModeMenuItemLookup[queueMode.value]
|
||||
)
|
||||
const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup)
|
||||
)
|
||||
|
||||
const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
const hasPendingTasks = computed(() => queueCountStore.count.value > 1)
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const queuePrompt = (e: MouseEvent) => {
|
||||
const commandId = e.shiftKey ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt'
|
||||
commandStore.execute(commandId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
165
src/components/appMenu/AppMenu.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<Panel v-if="visible" class="app-menu">
|
||||
<div class="app-menu-content flex align-center">
|
||||
<div class="queue-button-group flex">
|
||||
<SplitButton
|
||||
class="comfyui-queue-button"
|
||||
:label="activeQueueModeMenuItem.label"
|
||||
:icon="activeQueueModeMenuItem.icon"
|
||||
severity="primary"
|
||||
@click="queuePrompt"
|
||||
:model="queueModeMenuItems"
|
||||
data-testid="queue-button"
|
||||
v-tooltip.bottom="$t('menu.queueWorkflow')"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
text
|
||||
v-tooltip="item.tooltip"
|
||||
/>
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
<ButtonGroup class="execution-actions ml-2">
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.interrupt')"
|
||||
icon="pi pi-times"
|
||||
:severity="executingPrompt ? 'danger' : 'secondary'"
|
||||
:disabled="!executingPrompt"
|
||||
@click="() => commandStore.getCommandFunction('Comfy.Interrupt')()"
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
icon="pi pi-stop"
|
||||
:severity="hasPendingTasks ? 'danger' : 'secondary'"
|
||||
:disabled="!hasPendingTasks"
|
||||
@click="
|
||||
() => commandStore.getCommandFunction('Comfy.ClearPendingTasks')()
|
||||
"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Divider layout="vertical" class="mx-2" />
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.refresh')"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
@click="
|
||||
() =>
|
||||
commandStore.getCommandFunction('Comfy.RefreshNodeDefinitions')()
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.resetView')"
|
||||
icon="pi pi-expand"
|
||||
severity="secondary"
|
||||
@click="() => commandStore.getCommandFunction('Comfy.ResetView')()"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import Panel from 'primevue/panel'
|
||||
import Divider from 'primevue/divider'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import BatchCountEdit from './BatchCountEdit.vue'
|
||||
import {
|
||||
AutoQueueMode,
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { MenuItem } from 'primevue/menuitem'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { batchCount, mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const visible = computed(
|
||||
() => settingsStore.get('Comfy.UseNewMenu') === 'Floating'
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup: Record<AutoQueueMode, MenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: 'Queue',
|
||||
icon: 'pi pi-play',
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
instant: {
|
||||
key: 'instant',
|
||||
label: 'Queue (Instant)',
|
||||
icon: 'pi pi-forward',
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: 'Queue (Change)',
|
||||
icon: 'pi pi-step-forward-alt',
|
||||
tooltip: t('menu.changeTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeQueueModeMenuItem = computed(
|
||||
() => queueModeMenuItemLookup[queueMode.value]
|
||||
)
|
||||
const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup)
|
||||
)
|
||||
|
||||
const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
const hasPendingTasks = computed(() => queueCountStore.count.value > 1)
|
||||
|
||||
const queuePrompt = (e: MouseEvent) => {
|
||||
app.queuePrompt(e.shiftKey ? -1 : 0, batchCount.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-menu {
|
||||
pointer-events: all;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
:deep(.p-panel-header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -31,10 +31,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
@@ -47,11 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
const { batchCount } = storeToRefs(queueSettingsStore)
|
||||
const minQueueCount = 1
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const maxQueueCount = computed(() =>
|
||||
settingStore.get('Comfy.QueueButton.BatchCountLimit')
|
||||
)
|
||||
const maxQueueCount = 100
|
||||
|
||||
const handleClick = (increment: boolean) => {
|
||||
let newCount: number
|
||||
@@ -73,7 +73,7 @@ const vFocus = {
|
||||
|
||||
<style scoped>
|
||||
.editable-text {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
}
|
||||
.editable-text input {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="input-slider flex flex-row items-center gap-2">
|
||||
<div class="input-slider">
|
||||
<Slider
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="updateValue"
|
||||
@@ -17,7 +17,6 @@
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:allowEmpty="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -70,3 +69,19 @@ const updateValue = (newValue: number | null) => {
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.slider-part {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.input-part {
|
||||
width: 5rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<slot name="after-label" :node="props.node"></slot>
|
||||
</span>
|
||||
<Badge
|
||||
v-if="showNodeBadgeText"
|
||||
:value="nodeBadgeText"
|
||||
v-if="!props.node.leaf"
|
||||
:value="props.node.badgeText ?? props.node.totalLeaves"
|
||||
severity="secondary"
|
||||
class="leaf-count-badge"
|
||||
/>
|
||||
@@ -34,8 +34,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, Ref, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, inject, Ref, computed } from 'vue'
|
||||
import Badge from 'primevue/badge'
|
||||
import {
|
||||
dropTargetForElements,
|
||||
draggable
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import type {
|
||||
TreeExplorerDragAndDropData,
|
||||
RenderedTreeExplorerNode,
|
||||
@@ -43,7 +47,6 @@ import type {
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { usePragmaticDraggable, usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
|
||||
const props = defineProps<{
|
||||
node: RenderedTreeExplorerNode
|
||||
@@ -59,17 +62,6 @@ const emit = defineEmits<{
|
||||
(e: 'dragEnd', node: RenderedTreeExplorerNode): void
|
||||
}>()
|
||||
|
||||
const nodeBadgeText = computed<string>(() => {
|
||||
if (props.node.leaf) {
|
||||
return ''
|
||||
}
|
||||
if (props.node.badgeText !== undefined && props.node.badgeText !== null) {
|
||||
return props.node.badgeText
|
||||
}
|
||||
return props.node.totalLeaves.toString()
|
||||
})
|
||||
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
|
||||
|
||||
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
|
||||
const renameEditingNode =
|
||||
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
|
||||
@@ -86,44 +78,54 @@ const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
|
||||
const treeNodeElementGetter = () =>
|
||||
container.value?.closest('.p-tree-node-content') as HTMLElement
|
||||
|
||||
if (props.node.draggable) {
|
||||
usePragmaticDraggable(treeNodeElementGetter, {
|
||||
getInitialData: () => {
|
||||
return {
|
||||
type: 'tree-explorer-node',
|
||||
data: props.node
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
|
||||
if (props.node.droppable) {
|
||||
usePragmaticDroppable(treeNodeElementGetter, {
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
let dropTargetCleanup = () => {}
|
||||
let draggableCleanup = () => {}
|
||||
onMounted(() => {
|
||||
treeNodeElement.value = container.value?.closest(
|
||||
'.p-tree-node-content'
|
||||
) as HTMLElement
|
||||
if (props.node.droppable) {
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: treeNodeElement.value,
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: () => {
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: () => {
|
||||
canDrop.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (props.node.draggable) {
|
||||
draggableCleanup = draggable({
|
||||
element: treeNodeElement.value,
|
||||
getInitialData() {
|
||||
return {
|
||||
type: 'tree-explorer-node',
|
||||
data: props.node
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
})
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
draggableCleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeAll } from 'vitest'
|
||||
import EditableText from '../EditableText.vue'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
|
||||
@@ -11,16 +11,13 @@
|
||||
@hide="dialogStore.closeDialog"
|
||||
@maximize="onMaximize"
|
||||
@unmaximize="onUnmaximize"
|
||||
:pt="{ header: 'pb-0' }"
|
||||
:aria-labelledby="headerId"
|
||||
>
|
||||
<template #header>
|
||||
<component
|
||||
v-if="dialogStore.headerComponent"
|
||||
:is="dialogStore.headerComponent"
|
||||
:id="headerId"
|
||||
/>
|
||||
<h3 v-else :id="headerId">{{ dialogStore.title || ' ' }}</h3>
|
||||
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
|
||||
</template>
|
||||
|
||||
<component :is="dialogStore.component" v-bind="contentProps" />
|
||||
@@ -33,9 +30,7 @@ import { useDialogStore } from '@/stores/dialogStore'
|
||||
import Dialog from 'primevue/dialog'
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const maximizable = computed(
|
||||
() => dialogStore.dialogComponentProps.maximizable ?? false
|
||||
)
|
||||
const maximizable = computed(() => dialogStore.props.maximizable ?? false)
|
||||
const maximized = ref(false)
|
||||
|
||||
const onMaximize = () => {
|
||||
@@ -50,6 +45,4 @@ const contentProps = computed(() => ({
|
||||
...dialogStore.props,
|
||||
maximized: maximized.value
|
||||
}))
|
||||
|
||||
const headerId = `dialog-${Math.random().toString(36).substr(2, 9)}`
|
||||
</script>
|
||||
|
||||
@@ -191,4 +191,8 @@ const openNewGithubIssue = async () => {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.no-results-placeholder {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<div class="prompt-dialog-content flex flex-col gap-2 pt-8">
|
||||
<FloatLabel>
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
@keyup.enter="onConfirm"
|
||||
@focus="selectAllText"
|
||||
autofocus
|
||||
/>
|
||||
<label>{{ message }}</label>
|
||||
</FloatLabel>
|
||||
<Button @click="onConfirm">{{ $t('confirm') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import { ref } from 'vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const props = defineProps<{
|
||||
message: string
|
||||
defaultValue: string
|
||||
onConfirm: (value: string) => void
|
||||
}>()
|
||||
|
||||
const inputValue = ref<string>(props.defaultValue)
|
||||
|
||||
const onConfirm = () => {
|
||||
props.onConfirm(inputValue.value)
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
const inputRef = ref(null)
|
||||
const selectAllText = () => {
|
||||
if (!inputRef.value) return
|
||||
const inputElement = inputRef.value.$el
|
||||
inputElement.setSelectionRange(0, inputElement.value.length)
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-64">
|
||||
<div class="settings-sidebar">
|
||||
<SearchBox
|
||||
class="settings-search-box w-full mb-2"
|
||||
class="settings-search-box"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchSettings') + '...'"
|
||||
@@ -13,11 +13,11 @@
|
||||
optionLabel="label"
|
||||
scrollHeight="100%"
|
||||
:disabled="inSearch"
|
||||
class="border-none w-full"
|
||||
:pt="{ root: { class: 'border-none' } }"
|
||||
/>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<ScrollPanel class="settings-content flex-grow">
|
||||
<div class="settings-content">
|
||||
<Tabs :value="tabValue">
|
||||
<TabPanels class="settings-tab-panels">
|
||||
<TabPanel key="search-results" value="Search Results">
|
||||
@@ -54,36 +54,19 @@
|
||||
<TabPanel key="about" value="About">
|
||||
<AboutPanel />
|
||||
</TabPanel>
|
||||
<TabPanel key="keybinding" value="Keybinding">
|
||||
<Suspense>
|
||||
<KeybindingPanel />
|
||||
<template #fallback>
|
||||
<div>Loading keybinding panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
<TabPanel key="extension" value="Extension">
|
||||
<Suspense>
|
||||
<ExtensionPanel />
|
||||
<template #fallback>
|
||||
<div>Loading extension panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, defineAsyncComponent } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import SettingGroup from './setting/SettingGroup.vue'
|
||||
@@ -92,13 +75,6 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
)
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
settings: SettingParams[]
|
||||
@@ -110,30 +86,10 @@ const aboutPanelNode: SettingTreeNode = {
|
||||
children: []
|
||||
}
|
||||
|
||||
const keybindingPanelNode: SettingTreeNode = {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
}
|
||||
|
||||
const extensionPanelNode: SettingTreeNode = {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
}
|
||||
|
||||
const extensionPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
const settingStore = useSettingStore()
|
||||
const showExtensionPanel = settingStore.get('Comfy.Settings.ExtensionPanel')
|
||||
return showExtensionPanel ? [extensionPanelNode] : []
|
||||
})
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
||||
const categories = computed<SettingTreeNode[]>(() => [
|
||||
...(settingRoot.value.children || []),
|
||||
keybindingPanelNode,
|
||||
...extensionPanelNodeList.value,
|
||||
aboutPanelNode
|
||||
])
|
||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||
@@ -209,8 +165,44 @@ const tabValue = computed(() =>
|
||||
display: flex;
|
||||
height: 70vh;
|
||||
width: 60vw;
|
||||
max-width: 1024px;
|
||||
max-width: 1000px;
|
||||
overflow: hidden;
|
||||
/* Prevents container from scrolling */
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
/* Prevents sidebar from shrinking */
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.settings-search-box {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
/* Allows vertical scrolling */
|
||||
}
|
||||
|
||||
/* Ensure the Listbox takes full width of the sidebar */
|
||||
.settings-sidebar :deep(.p-listbox) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Optional: Style scrollbars for webkit browsers */
|
||||
.settings-sidebar::-webkit-scrollbar,
|
||||
.settings-content::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar-thumb,
|
||||
.settings-content::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -223,15 +215,4 @@ const tabValue = computed(() =>
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Show a separator line above the Keybinding tab */
|
||||
/* This indicates the start of custom setting panels */
|
||||
.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding']) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding'])::before {
|
||||
@apply content-[''] top-0 left-0 absolute w-full;
|
||||
border-top: 1px solid var(--p-divider-border-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div class="extension-panel">
|
||||
<DataTable :value="extensionStore.extensions" stripedRows size="small">
|
||||
<Column field="name" :header="$t('extensionName')" sortable></Column>
|
||||
<Column
|
||||
:pt="{
|
||||
bodyCell: 'flex items-center justify-end'
|
||||
}"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<ToggleSwitch
|
||||
v-model="editingEnabledExtensions[slotProps.data.name]"
|
||||
@change="updateExtensionStatus"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
<div class="mt-4">
|
||||
<Message v-if="hasChanges" severity="info">
|
||||
<ul>
|
||||
<li v-for="ext in changedExtensions" :key="ext.name">
|
||||
<span>
|
||||
{{ extensionStore.isExtensionEnabled(ext.name) ? '[-]' : '[+]' }}
|
||||
</span>
|
||||
{{ ext.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
<Button
|
||||
:label="$t('reloadToApplyChanges')"
|
||||
icon="pi pi-refresh"
|
||||
@click="applyChanges"
|
||||
:disabled="!hasChanges"
|
||||
text
|
||||
fluid
|
||||
severity="danger"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import Button from 'primevue/button'
|
||||
import Message from 'primevue/message'
|
||||
|
||||
const extensionStore = useExtensionStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const editingEnabledExtensions = ref<Record<string, boolean>>({})
|
||||
|
||||
onMounted(() => {
|
||||
extensionStore.extensions.forEach((ext) => {
|
||||
editingEnabledExtensions.value[ext.name] =
|
||||
extensionStore.isExtensionEnabled(ext.name)
|
||||
})
|
||||
})
|
||||
|
||||
const changedExtensions = computed(() => {
|
||||
return extensionStore.extensions.filter(
|
||||
(ext) =>
|
||||
editingEnabledExtensions.value[ext.name] !==
|
||||
extensionStore.isExtensionEnabled(ext.name)
|
||||
)
|
||||
})
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
return changedExtensions.value.length > 0
|
||||
})
|
||||
|
||||
const updateExtensionStatus = () => {
|
||||
const editingDisabledExtensionNames = Object.entries(
|
||||
editingEnabledExtensions.value
|
||||
)
|
||||
.filter(([_, enabled]) => !enabled)
|
||||
.map(([name]) => name)
|
||||
|
||||
settingStore.set('Comfy.Extension.Disabled', [
|
||||
...extensionStore.inactiveDisabledExtensionNames,
|
||||
...editingDisabledExtensionNames
|
||||
])
|
||||
}
|
||||
|
||||
const applyChanges = () => {
|
||||
// Refresh the page to apply changes
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
@@ -1,240 +0,0 @@
|
||||
<template>
|
||||
<div class="keybinding-panel">
|
||||
<DataTable
|
||||
:value="commandsData"
|
||||
v-model:selection="selectedCommandData"
|
||||
:global-filter-fields="['id']"
|
||||
:filters="filters"
|
||||
selectionMode="single"
|
||||
stripedRows
|
||||
:pt="{
|
||||
header: 'px-0'
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
v-model="filters['global'].value"
|
||||
:placeholder="$t('searchKeybindings') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<Column field="actions" header="">
|
||||
<template #body="slotProps">
|
||||
<div class="actions invisible">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-text"
|
||||
@click="editKeybinding(slotProps.data)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
class="p-button-text p-button-danger"
|
||||
@click="removeKeybinding(slotProps.data)"
|
||||
:disabled="!slotProps.data.keybinding"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="id" header="Command ID" sortable></Column>
|
||||
<Column field="keybinding" header="Keybinding">
|
||||
<template #body="slotProps">
|
||||
<KeyComboDisplay
|
||||
v-if="slotProps.data.keybinding"
|
||||
:keyCombo="slotProps.data.keybinding.combo"
|
||||
:isModified="
|
||||
keybindingStore.isCommandKeybindingModified(slotProps.data.id)
|
||||
"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<Dialog
|
||||
class="min-w-96"
|
||||
v-model:visible="editDialogVisible"
|
||||
modal
|
||||
:header="currentEditingCommand?.id"
|
||||
@hide="cancelEdit"
|
||||
>
|
||||
<div>
|
||||
<InputText
|
||||
class="mb-2 text-center"
|
||||
ref="keybindingInput"
|
||||
:modelValue="newBindingKeyCombo?.toString() ?? ''"
|
||||
placeholder="Press keys for new binding"
|
||||
@keydown.stop.prevent="captureKeybinding"
|
||||
autocomplete="off"
|
||||
fluid
|
||||
:invalid="!!existingKeybindingOnCombo"
|
||||
/>
|
||||
<Message v-if="existingKeybindingOnCombo" severity="error">
|
||||
Keybinding already exists on
|
||||
<Tag
|
||||
severity="secondary"
|
||||
:value="existingKeybindingOnCombo.commandId"
|
||||
/>
|
||||
</Message>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button
|
||||
label="Save"
|
||||
icon="pi pi-check"
|
||||
@click="saveKeybinding"
|
||||
:disabled="!!existingKeybindingOnCombo"
|
||||
autofocus
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Button
|
||||
class="mt-4"
|
||||
:label="$t('reset')"
|
||||
v-tooltip="$t('resetKeybindingsTooltip')"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
fluid
|
||||
text
|
||||
@click="resetKeybindings"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import {
|
||||
useKeybindingStore,
|
||||
KeyComboImpl,
|
||||
KeybindingImpl
|
||||
} from '@/stores/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Tag from 'primevue/tag'
|
||||
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { FilterMatchMode } from '@primevue/core/api'
|
||||
|
||||
const filters = ref({
|
||||
global: { value: '', matchMode: FilterMatchMode.CONTAINS }
|
||||
})
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
interface ICommandData {
|
||||
id: string
|
||||
keybinding: KeybindingImpl | null
|
||||
}
|
||||
|
||||
const commandsData = computed<ICommandData[]>(() => {
|
||||
return Object.values(commandStore.commands).map((command) => ({
|
||||
id: command.id,
|
||||
keybinding: keybindingStore.getKeybindingByCommandId(command.id)
|
||||
}))
|
||||
})
|
||||
|
||||
const selectedCommandData = ref<ICommandData | null>(null)
|
||||
const editDialogVisible = ref(false)
|
||||
const newBindingKeyCombo = ref<KeyComboImpl | null>(null)
|
||||
const currentEditingCommand = ref<ICommandData | null>(null)
|
||||
const keybindingInput = ref(null)
|
||||
|
||||
const existingKeybindingOnCombo = computed<KeybindingImpl | null>(() => {
|
||||
if (!currentEditingCommand.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If the new keybinding is the same as the current editing command, then don't show the error
|
||||
if (
|
||||
currentEditingCommand.value.keybinding?.combo?.equals(
|
||||
newBindingKeyCombo.value
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!newBindingKeyCombo.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return keybindingStore.getKeybinding(newBindingKeyCombo.value)
|
||||
})
|
||||
|
||||
function editKeybinding(commandData: ICommandData) {
|
||||
currentEditingCommand.value = commandData
|
||||
newBindingKeyCombo.value = commandData.keybinding
|
||||
? commandData.keybinding.combo
|
||||
: null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (editDialogVisible.value) {
|
||||
// nextTick doesn't work here, so we use a timeout instead
|
||||
setTimeout(() => {
|
||||
keybindingInput.value?.$el?.focus()
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
function removeKeybinding(commandData: ICommandData) {
|
||||
if (commandData.keybinding) {
|
||||
keybindingStore.unsetKeybinding(commandData.keybinding)
|
||||
keybindingStore.persistUserKeybindings()
|
||||
}
|
||||
}
|
||||
|
||||
function captureKeybinding(event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
newBindingKeyCombo.value = keyCombo
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editDialogVisible.value = false
|
||||
currentEditingCommand.value = null
|
||||
newBindingKeyCombo.value = null
|
||||
}
|
||||
|
||||
function saveKeybinding() {
|
||||
if (currentEditingCommand.value && newBindingKeyCombo.value) {
|
||||
const updated = keybindingStore.updateKeybindingOnCommand(
|
||||
new KeybindingImpl({
|
||||
commandId: currentEditingCommand.value.id,
|
||||
combo: newBindingKeyCombo.value
|
||||
})
|
||||
)
|
||||
if (updated) {
|
||||
keybindingStore.persistUserKeybindings()
|
||||
}
|
||||
}
|
||||
cancelEdit()
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
async function resetKeybindings() {
|
||||
keybindingStore.resetKeybindings()
|
||||
await keybindingStore.persistUserKeybindings()
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Info',
|
||||
detail: 'Keybindings reset',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-datatable-tbody) > tr > td {
|
||||
padding: 1px;
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-row-selected) .actions,
|
||||
:deep(.p-datatable-selectable-row:hover) .actions {
|
||||
@apply visible;
|
||||
}
|
||||
</style>
|
||||
@@ -5,10 +5,10 @@
|
||||
<div
|
||||
v-for="setting in group.settings"
|
||||
:key="setting.id"
|
||||
class="setting-item flex items-center mb-4"
|
||||
class="setting-item"
|
||||
>
|
||||
<div class="setting-label flex flex-grow items-center">
|
||||
<span class="text-[var(--p-text-muted-color)]">
|
||||
<div class="setting-label">
|
||||
<span>
|
||||
<Tag v-if="setting.experimental" :value="$t('experimental')" />
|
||||
<Tag
|
||||
v-if="setting.deprecated"
|
||||
@@ -17,11 +17,11 @@
|
||||
{{ setting.name }}
|
||||
<i
|
||||
v-if="setting.tooltip"
|
||||
class="pi pi-info-circle bg-transparent"
|
||||
class="pi pi-info-circle info-chip"
|
||||
v-tooltip="setting.tooltip"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="setting-input flex justify-end">
|
||||
<div class="setting-input">
|
||||
<component
|
||||
:is="markRaw(getSettingComponent(setting))"
|
||||
:id="setting.id"
|
||||
@@ -110,13 +110,45 @@ function getSettingComponent(setting: SettingParams): Component {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-input :deep(.input-slider) .p-inputnumber input,
|
||||
.setting-input :deep(.input-slider) .slider-part {
|
||||
@apply w-20;
|
||||
.info-chip {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Ensure PrimeVue components take full width of their container */
|
||||
.setting-input :deep(.p-inputtext),
|
||||
.setting-input :deep(.p-select) {
|
||||
@apply w-44;
|
||||
.setting-input :deep(.input-slider),
|
||||
.setting-input :deep(.p-select),
|
||||
.setting-input :deep(.p-togglebutton) {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.setting-input :deep(.p-inputtext) {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
/* Special case for ToggleSwitch to align it to the right */
|
||||
.setting-input :deep(.p-toggleswitch) {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
<template v-for="(sequence, index) in keySequences" :key="index">
|
||||
<Tag :severity="isModified ? 'info' : 'secondary'">
|
||||
{{ sequence }}
|
||||
</Tag>
|
||||
<span v-if="index < keySequences.length - 1" class="px-2">+</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
import { KeyComboImpl } from '@/stores/keybindingStore'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
keyCombo: KeyComboImpl
|
||||
isModified: boolean
|
||||
}>(),
|
||||
{
|
||||
isModified: false
|
||||
}
|
||||
)
|
||||
|
||||
const keySequences = computed(() => props.keyCombo.getKeySequences())
|
||||
</script>
|
||||
@@ -4,16 +4,12 @@
|
||||
<template #side-bar-panel>
|
||||
<SideToolbar />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
<TitleEditor />
|
||||
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
||||
<canvas ref="canvasRef" id="graph-canvas" tabindex="1" />
|
||||
</teleport>
|
||||
<NodeSearchboxPopover />
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
<NodeTooltip />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -22,10 +18,15 @@ import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue'
|
||||
import { ref, computed, onUnmounted, onMounted, watchEffect } from 'vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import {
|
||||
LiteGraph,
|
||||
@@ -39,14 +40,10 @@ import {
|
||||
LGraphBadge
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import {
|
||||
ModelNodeProvider,
|
||||
useModelToNodeStore
|
||||
} from '@/stores/modelToNodeStore'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -58,10 +55,6 @@ const modelToNodeStore = useModelToNodeStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
const canvasMenuEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Graph.CanvasMenu')
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
@@ -99,76 +92,7 @@ watchEffect(() => {
|
||||
})
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
if (canvasStore.canvas.dragging_canvas) {
|
||||
canvasStore.canvas.canvas.style.cursor = 'grabbing'
|
||||
return
|
||||
}
|
||||
|
||||
if (canvasStore.canvas.read_only) {
|
||||
canvasStore.canvas.canvas.style.cursor = 'grab'
|
||||
return
|
||||
}
|
||||
|
||||
canvasStore.canvas.canvas.style.cursor = 'default'
|
||||
})
|
||||
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = comfyApp.addNodeOnGraph(provider.nodeDef, {
|
||||
pos
|
||||
})
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
let dropTargetCleanup = () => {}
|
||||
|
||||
onMounted(async () => {
|
||||
// Backward compatible
|
||||
@@ -193,6 +117,57 @@ onMounted(async () => {
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: canvasRef.value,
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
const node = comfyApp.addNodeOnGraph(provider.nodeDef, { pos })
|
||||
const widget = node.widgets.find(
|
||||
(widget) => widget.name === provider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Migrate legacy bookmarks
|
||||
useNodeBookmarkStore().migrateLegacyBookmarks()
|
||||
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.endsWithFilterStartSequence('')
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
useNodeFrequencyStore().loadNodeFrequencies()
|
||||
emit('ready')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<ButtonGroup
|
||||
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000] pointer-events-auto"
|
||||
>
|
||||
<Button
|
||||
severity="secondary"
|
||||
icon="pi pi-plus"
|
||||
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
|
||||
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
|
||||
@mouseup="stopRepeat"
|
||||
/>
|
||||
<Button
|
||||
severity="secondary"
|
||||
icon="pi pi-minus"
|
||||
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
|
||||
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
|
||||
@mouseup="stopRepeat"
|
||||
/>
|
||||
<Button
|
||||
severity="secondary"
|
||||
icon="pi pi-expand"
|
||||
v-tooltip.left="t('graphCanvasMenu.resetView')"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ResetView')"
|
||||
/>
|
||||
<Button
|
||||
severity="secondary"
|
||||
v-tooltip.left="
|
||||
t(
|
||||
'graphCanvasMenu.' +
|
||||
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
|
||||
) + ' (Space)'
|
||||
"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-material-symbols:pan-tool-outline
|
||||
v-if="canvasStore.canvas?.read_only"
|
||||
/>
|
||||
<i-simple-line-icons:cursor v-else />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
severity="secondary"
|
||||
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||
data-testid="toggle-link-visibility-button"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import Button from 'primevue/button'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const linkHidden = computed(
|
||||
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
||||
)
|
||||
|
||||
let interval: number | null = null
|
||||
const repeat = (command: string) => {
|
||||
if (interval) return
|
||||
const cmd = () => commandStore.execute(command)
|
||||
cmd()
|
||||
interval = window.setInterval(cmd, 100)
|
||||
}
|
||||
const stopRepeat = () => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
interval = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-buttongroup-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--p-button-border-radius);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
|
||||
.p-buttongroup-vertical .p-button {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -10,14 +10,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { nextTick, ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingStore = useSettingStore()
|
||||
const tooltipRef = ref<HTMLDivElement>()
|
||||
const tooltipText = ref('')
|
||||
const left = ref<string>()
|
||||
@@ -128,8 +129,24 @@ const onMouseMove = (e: MouseEvent) => {
|
||||
idleTimeout = window.setTimeout(onIdle, 500)
|
||||
}
|
||||
|
||||
useEventListener(window, 'mousemove', onMouseMove)
|
||||
useEventListener(window, 'click', hideTooltip)
|
||||
watch(
|
||||
() => settingStore.get('Comfy.EnableTooltips'),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('click', hideTooltip)
|
||||
} else {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:iconBadge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip + getTabTooltipSuffix(tab)"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:tooltip="tab.tooltip"
|
||||
:selected="tab === selectedTab"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
CustomSidebarTabExtension,
|
||||
SidebarTabExtension
|
||||
} from '@/types/extensionTypes'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -61,12 +60,17 @@ const isSmall = computed(
|
||||
)
|
||||
|
||||
const tabs = computed(() => workspaceStore.getSidebarTabs())
|
||||
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
const selectedTab = computed<SidebarTabExtension | null>(() => {
|
||||
const tabId = workspaceStore.activeSidebarTab
|
||||
return tabs.value.find((tab) => tab.id === tabId) || null
|
||||
})
|
||||
const mountCustomTab = (tab: CustomSidebarTabExtension, el: HTMLElement) => {
|
||||
tab.render(el)
|
||||
}
|
||||
const onTabClick = (item: SidebarTabExtension) => {
|
||||
workspaceStore.sidebarTab.toggleSidebarTab(item.id)
|
||||
workspaceStore.updateActiveSidebarTab(
|
||||
workspaceStore.activeSidebarTab === item.id ? null : item.id
|
||||
)
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
tabs.value.forEach((tab) => {
|
||||
@@ -75,14 +79,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
const keybinding = keybindingStore.getKeybindingByCommandId(
|
||||
`Workspace.ToggleSidebarTab.${tab.id}`
|
||||
)
|
||||
return keybinding ? ` (${keybinding.combo.toString()})` : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
icon="pi pi-cog"
|
||||
class="comfy-settings-btn"
|
||||
@click="showSetting"
|
||||
:tooltip="$t('settings')"
|
||||
/>
|
||||
|
||||
@@ -8,19 +8,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const currentTheme = computed(() => settingStore.get('Comfy.ColorPalette'))
|
||||
const icon = computed(() =>
|
||||
currentTheme.value !== 'light' ? 'pi pi-moon' : 'pi pi-sun'
|
||||
)
|
||||
const previousDarkTheme = ref('dark')
|
||||
const currentTheme = computed(() => useSettingStore().get('Comfy.ColorPalette'))
|
||||
const isDarkMode = computed(() => currentTheme.value !== 'light')
|
||||
const icon = computed(() => (isDarkMode.value ? 'pi pi-moon' : 'pi pi-sun'))
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const toggleTheme = () => {
|
||||
commandStore.execute('Comfy.ToggleTheme')
|
||||
if (isDarkMode.value) {
|
||||
previousDarkTheme.value = currentTheme.value
|
||||
useSettingStore().set('Comfy.ColorPalette', 'light')
|
||||
} else {
|
||||
useSettingStore().set('Comfy.ColorPalette', previousDarkTheme.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.modelLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="model-lib-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
:placeholder="$t('searchModels') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.modelLibrary')">
|
||||
<template #tool-buttons> </template>
|
||||
<template #body>
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<ModelTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="model-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchModels') + '...'"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<ModelTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="model-library-model-preview-container" />
|
||||
@@ -68,8 +71,7 @@ const root: ComputedRef<TreeNode> = computed(() => {
|
||||
if (Object.values(models.models).length) {
|
||||
modelList.push(...Object.values(models.models))
|
||||
} else {
|
||||
// ModelDef with key 'folder/a/b/c/' is treated as empty folder
|
||||
const fakeModel = new ComfyModelDef('', folder)
|
||||
const fakeModel = new ComfyModelDef('(No Content)', folder)
|
||||
fakeModel.is_fake_object = true
|
||||
modelList.push(fakeModel)
|
||||
}
|
||||
@@ -82,23 +84,33 @@ const root: ComputedRef<TreeNode> = computed(() => {
|
||||
if (searchQuery.value) {
|
||||
const search = searchQuery.value.toLocaleLowerCase()
|
||||
modelList = modelList.filter((model: ComfyModelDef) => {
|
||||
return model.searchable.includes(search)
|
||||
return model.name.toLocaleLowerCase().includes(search)
|
||||
})
|
||||
}
|
||||
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) =>
|
||||
model.key.split('/')
|
||||
)
|
||||
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) => {
|
||||
return [model.directory, ...model.name.replaceAll('\\', '/').split('/')]
|
||||
})
|
||||
return tree
|
||||
})
|
||||
|
||||
const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
const nameFormat = settingStore.get('Comfy.ModelLibrary.NameFormat')
|
||||
const fillNodeInfo = (node: TreeNode): TreeExplorerNode<ComfyModelDef> => {
|
||||
const children = node.children?.map(fillNodeInfo)
|
||||
const model: ComfyModelDef | null =
|
||||
node.leaf && node.data ? node.data : null
|
||||
if (model?.is_fake_object) {
|
||||
if (model.file_name === 'Loading') {
|
||||
if (model.name === '(No Content)') {
|
||||
return {
|
||||
key: node.key,
|
||||
label: t('noContent'),
|
||||
leaf: true,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
|
||||
return 'pi pi-file'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
key: node.key,
|
||||
label: t('loading') + '...',
|
||||
@@ -114,11 +126,7 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
label: model
|
||||
? nameFormat === 'title'
|
||||
? model.title
|
||||
: model.simplified_file_name
|
||||
: node.label,
|
||||
label: model ? model.title : node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
|
||||
@@ -136,8 +144,10 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
if (node.children?.length === 1) {
|
||||
const onlyChild = node.children[0]
|
||||
if (onlyChild.data?.is_fake_object) {
|
||||
if (onlyChild.data.file_name === 'Loading') {
|
||||
return ''
|
||||
if (onlyChild.data.name === '(No Content)') {
|
||||
return '0'
|
||||
} else if (onlyChild.data.name === 'Loading') {
|
||||
return '?'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +169,7 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
(widget) => widget.name === provider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
widget.value = model.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +179,10 @@ const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
|
||||
return fillNodeInfo(root.value)
|
||||
})
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
// TODO
|
||||
}
|
||||
|
||||
const handleNodeClick = (
|
||||
node: RenderedTreeExplorerNode<ComfyModelDef>,
|
||||
e: MouseEvent
|
||||
@@ -197,9 +211,15 @@ watch(
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.pi-fake-spacer) {
|
||||
<style>
|
||||
.pi-fake-spacer {
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.nodeLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.nodeLibrary')">
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="new-folder-button"
|
||||
@@ -21,41 +18,44 @@
|
||||
v-tooltip="$t('sideToolbar.nodeLibraryTab.sortOrder')"
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
class="node-lib-search-box p-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
|
||||
<Popover ref="searchFilter" class="ml-[-13px]">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</template>
|
||||
<template #body>
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-show="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
class="m-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer py-0"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-shrink-0">
|
||||
<SearchBox
|
||||
class="node-lib-search-box mx-4 mt-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
@show-filter="($event) => searchFilter.toggle($event)"
|
||||
@remove-filter="onRemoveFilter"
|
||||
:placeholder="$t('searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
/>
|
||||
|
||||
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider
|
||||
v-if="nodeBookmarkStore.bookmarks.length > 0"
|
||||
type="dashed"
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer mt-1"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<div id="node-library-node-preview-container" />
|
||||
@@ -193,3 +193,23 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
handleSearch(searchQuery.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.node-lib-filter-popup {
|
||||
margin-left: -13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
|
||||
:deep(.node-lib-bookmark-tree-explorer) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
:deep(.p-divider) {
|
||||
margin: var(--comfy-tree-explorer-item-padding) 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
icon="pi pi-stop"
|
||||
severity="danger"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.ClearPendingTasks')"
|
||||
@click="
|
||||
() => commandStore.getCommandFunction('Comfy.ClearPendingTasks')()
|
||||
"
|
||||
v-tooltip.bottom="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
/>
|
||||
<Button
|
||||
@@ -249,8 +251,7 @@ const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: t('loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
||||
disabled: !menuTargetTask.value?.workflow
|
||||
command: () => menuTargetTask.value?.loadWorkflow()
|
||||
},
|
||||
{
|
||||
label: t('goToNode'),
|
||||
@@ -328,14 +329,6 @@ watch(
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.queue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
|
||||
@@ -1,36 +1,66 @@
|
||||
<template>
|
||||
<div
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full"
|
||||
:class="props.class"
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header">
|
||||
<Toolbar
|
||||
class="flex-shrink-0 border-x-0 border-t-0 rounded-none px-2 py-1 min-h-10"
|
||||
>
|
||||
<template #start>
|
||||
<span class="text-sm">{{ props.title.toUpperCase() }}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<slot name="header"></slot>
|
||||
<div class="comfy-vue-side-bar-container">
|
||||
<Toolbar class="comfy-vue-side-bar-header">
|
||||
<template #start>
|
||||
<span class="comfy-vue-side-bar-header-span">{{
|
||||
props.title.toUpperCase()
|
||||
}}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<div class="comfy-vue-side-bar-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<!-- h-0 to force scrollpanel to flex-grow -->
|
||||
<ScrollPanel class="comfy-vue-side-bar-body flex-grow h-0">
|
||||
<div class="h-full">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Toolbar from 'primevue/toolbar'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
class?: string
|
||||
}>()
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-vue-side-bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header {
|
||||
flex-shrink: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
padding: 0.25rem 1rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header-span {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,48 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.workflows')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.workflows')">
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
icon="pi pi-th-large"
|
||||
v-tooltip="$t('sideToolbar.browseTemplates')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.BrowseTemplates')"
|
||||
@click="
|
||||
() => commandStore.getCommandFunction('Comfy.BrowseTemplates')()
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
class="open-workflow-button"
|
||||
class="browse-workflows-button"
|
||||
icon="pi pi-folder-open"
|
||||
v-tooltip="$t('sideToolbar.openWorkflow')"
|
||||
v-tooltip="'Browse for an image or exported workflow'"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.OpenWorkflow')"
|
||||
@click="() => commandStore.getCommandFunction('Comfy.OpenWorkflow')()"
|
||||
/>
|
||||
<Button
|
||||
class="new-default-workflow-button"
|
||||
icon="pi pi-code"
|
||||
v-tooltip="'Load default workflow'"
|
||||
text
|
||||
@click="
|
||||
() => commandStore.getCommandFunction('Comfy.LoadDefaultWorkflow')()
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
v-tooltip="$t('sideToolbar.newBlankWorkflow')"
|
||||
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
|
||||
v-tooltip="'Create a new blank workflow'"
|
||||
@click="
|
||||
() => commandStore.getCommandFunction('Comfy.NewBlankWorkflow')()
|
||||
"
|
||||
text
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<template #body>
|
||||
<SearchBox
|
||||
class="workflows-search-box p-4"
|
||||
class="workflows-search-box mx-4 my-4"
|
||||
v-model:modelValue="searchQuery"
|
||||
@search="handleSearch"
|
||||
:placeholder="$t('searchWorkflows') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="comfyui-workflows-panel" v-if="!isSearching">
|
||||
<div
|
||||
class="comfyui-workflows-open"
|
||||
@@ -219,3 +227,9 @@ const selectionKeys = computed(() => ({
|
||||
[`root/${workflowStore.activeWorkflow?.name}.json`]: true
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.comfy-vue-side-bar-body) {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
{{ modelDef.title }}
|
||||
</div>
|
||||
<div class="model_preview_top_container">
|
||||
<div class="model_preview_filename">
|
||||
{{ modelDef.file_name }}
|
||||
</div>
|
||||
<div class="model_preview_architecture" v-if="modelDef.architecture_id">
|
||||
<span class="model_preview_prefix">Architecture: </span>
|
||||
{{ modelDef.architecture_id }}
|
||||
@@ -78,9 +75,7 @@ const modelDef = props.modelDef
|
||||
}
|
||||
.model_preview_top_container {
|
||||
text-align: center;
|
||||
line-height: 0.5;
|
||||
}
|
||||
.model_preview_filename,
|
||||
.model_preview_author,
|
||||
.model_preview_architecture {
|
||||
display: inline-block;
|
||||
|
||||
@@ -90,7 +90,6 @@ const showPreview = computed(() => {
|
||||
!modelDef.value.is_fake_object &&
|
||||
modelDef.value.has_loaded_metadata &&
|
||||
(modelDef.value.author ||
|
||||
modelDef.value.simplified_file_name != modelDef.value.title ||
|
||||
modelDef.value.description ||
|
||||
modelDef.value.usage_hint ||
|
||||
modelDef.value.trigger_phrase ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer py-0"
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
:src="item.url"
|
||||
:contain="false"
|
||||
class="galleria-image"
|
||||
v-if="item.isImage"
|
||||
/>
|
||||
<ResultVideo v-else-if="item.isVideo" :result="item" />
|
||||
</template>
|
||||
</Galleria>
|
||||
</template>
|
||||
@@ -37,7 +35,6 @@ import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import Galleria from 'primevue/galleria'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const galleryVisible = ref(false)
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
<template>
|
||||
<div class="result-container" ref="resultContainer">
|
||||
<ComfyImage
|
||||
v-if="result.isImage"
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
/>
|
||||
<ResultVideo v-else-if="result.isVideo" :result="result" />
|
||||
<template
|
||||
v-if="result.mediaType === 'images' || result.mediaType === 'gifs'"
|
||||
>
|
||||
<ComfyImage
|
||||
:src="result.url"
|
||||
class="task-output-image"
|
||||
:contain="imageFit === 'contain'"
|
||||
/>
|
||||
<div class="image-preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TODO: handle more media types -->
|
||||
<div v-else class="task-result-preview">
|
||||
<i class="pi pi-file"></i>
|
||||
<span>{{ result.mediaType }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="result.supportsPreview" class="preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +31,6 @@ import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
@@ -66,7 +67,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-mask {
|
||||
.image-preview-mask {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
@@ -79,7 +80,7 @@ onMounted(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.result-container:hover .preview-mask {
|
||||
.result-container:hover .image-preview-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<video controls width="100%" height="100%">
|
||||
<source :src="url" :type="htmlVideoType" />
|
||||
{{ $t('videoFailedToLoad') }}
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
result: ResultItemImpl
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const vhsAdvancedPreviews = computed(() =>
|
||||
settingStore.get('VHS.AdvancedPreviews')
|
||||
)
|
||||
|
||||
const url = computed(() =>
|
||||
vhsAdvancedPreviews.value
|
||||
? props.result.vhsAdvancedPreviewUrl
|
||||
: props.result.url
|
||||
)
|
||||
const htmlVideoType = computed(() =>
|
||||
vhsAdvancedPreviews.value ? 'video/webm' : props.result.htmlVideoType
|
||||
)
|
||||
</script>
|
||||
@@ -8,14 +8,10 @@
|
||||
@preview="handlePreview"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="task.displayStatus === TaskItemDisplayStatus.Running">
|
||||
<i v-if="!progressPreviewBlobUrl" class="pi pi-spin pi-spinner"></i>
|
||||
<img
|
||||
v-else
|
||||
:src="progressPreviewBlobUrl"
|
||||
class="progress-preview-img"
|
||||
/>
|
||||
</template>
|
||||
<i
|
||||
v-else-if="task.displayStatus === TaskItemDisplayStatus.Running"
|
||||
class="pi pi-spin pi-spinner"
|
||||
></i>
|
||||
<span v-else-if="task.displayStatus === TaskItemDisplayStatus.Pending"
|
||||
>...</span
|
||||
>
|
||||
@@ -64,14 +60,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
import ResultItem from './ResultItem.vue'
|
||||
import { TaskItemDisplayStatus, type TaskItemImpl } from '@/stores/queueStore'
|
||||
import { ComfyNode } from '@/types/comfyWorkflow'
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
const props = defineProps<{
|
||||
task: TaskItemImpl
|
||||
@@ -83,13 +77,11 @@ const coverResult = flatOutputs.length
|
||||
? props.task.previewOutput || flatOutputs[0]
|
||||
: null
|
||||
// Using `==` instead of `===` because NodeId can be a string or a number
|
||||
const node: ComfyNode | null =
|
||||
flatOutputs.length && props.task.workflow
|
||||
? props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult.nodeId
|
||||
) ?? null
|
||||
: null
|
||||
const progressPreviewBlobUrl = ref('')
|
||||
const node: ComfyNode | null = flatOutputs.length
|
||||
? props.task.workflow.nodes.find(
|
||||
(n: ComfyNode) => n.id == coverResult.nodeId
|
||||
) ?? null
|
||||
: null
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
@@ -100,14 +92,6 @@ const emit = defineEmits<{
|
||||
(e: 'task-output-length-clicked', value: TaskItemImpl): void
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('b_preview', onProgressPreviewReceived)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
api.removeEventListener('b_preview', onProgressPreviewReceived)
|
||||
})
|
||||
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
emit('contextmenu', { task: props.task, event: e, node })
|
||||
}
|
||||
@@ -140,7 +124,7 @@ const taskStatusText = (status: TaskItemDisplayStatus) => {
|
||||
case TaskItemDisplayStatus.Pending:
|
||||
return 'Pending'
|
||||
case TaskItemDisplayStatus.Running:
|
||||
return '<i class="pi pi-spin pi-spinner" style="font-weight: bold"></i> Running'
|
||||
return 'Running'
|
||||
case TaskItemDisplayStatus.Completed:
|
||||
return '<i class="pi pi-check" style="font-weight: bold"></i>'
|
||||
case TaskItemDisplayStatus.Failed:
|
||||
@@ -156,12 +140,6 @@ const formatTime = (time?: number) => {
|
||||
}
|
||||
return `${time.toFixed(2)}s`
|
||||
}
|
||||
|
||||
const onProgressPreviewReceived = async ({ detail }: CustomEvent) => {
|
||||
if (props.task.displayStatus === TaskItemDisplayStatus.Running) {
|
||||
progressPreviewBlobUrl.value = URL.createObjectURL(detail)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -219,11 +197,4 @@ are floating on top of images. */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.progress-preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
// Disabled because of https://github.com/Comfy-Org/ComfyUI_frontend/issues/1184
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { expect, describe, it } from 'vitest'
|
||||
import ResultGallery from '../ResultGallery.vue'
|
||||
@@ -15,12 +12,15 @@ describe('ResultGallery', () => {
|
||||
let mockResultItem: ResultItemImpl
|
||||
|
||||
beforeEach(() => {
|
||||
mockResultItem = new ResultItemImpl({
|
||||
mockResultItem = {
|
||||
filename: 'test.jpg',
|
||||
type: 'images',
|
||||
nodeId: 1,
|
||||
mediaType: 'images'
|
||||
})
|
||||
nodeId: 'test',
|
||||
mediaType: 'images',
|
||||
url: 'https://picsum.photos/200/300',
|
||||
urlWithTimestamp: 'https://picsum.photos/200/300?t=123456',
|
||||
supportsPreview: true
|
||||
}
|
||||
})
|
||||
|
||||
const mountResultGallery = (props: ResultGalleryProps, options = {}) => {
|
||||