Compare commits

..

25 Commits

Author SHA1 Message Date
Austin Mroz
867c50f7cb Prune dead code, tighten interval, add screenshot
Code related to possible options has been pruned. It was pointless for
updateNode to take a node as input. Collapsing documentation items does
not make sense given the greater space of a sidebar tab compared to a
floating window on the graph, so the corresponding code has been fully
pruned as well.

The topmost node is used instead of the current_node. While current_node
displayed the desired properties when canvas was still shallowReactive
of notifying a change in node, it's not intended to be used external to
litegraph and at best, tracks the topmost visible node instead of the
actual topmost node.

A test expectation screenshot has been added for verifying theming works
for the documentation tab.
2024-11-28 10:45:23 -06:00
Austin Mroz
89027ea969 Update tests 2024-11-28 02:43:25 -06:00
Austin Mroz
ce9cfdb975 Check for swapped node on interval.
Not ideal, but implementation is low cost and ensures the displayed
documentation properly updates.
2024-11-28 02:23:50 -06:00
Austin Mroz
ef191033c8 Merge sidebar-documentation onto main
The sidebar-documentation branch has diverged enough from main to
merging non-trivial

Remove deprecated type usage.
Move localization to language file.
2024-11-27 18:04:36 -06:00
Austin Mroz
293f4295a8 Add tests for advanced description formats 2024-11-15 15:34:14 -06:00
Austin Mroz
3252d62edf Fix missing await 2024-10-18 21:46:10 -05:00
Austin Mroz
1dfcc7a0d4 Migrate tooltip tracking to a pinia store
While I was concerned that doing this would remove the capability to
suppress tooltips on the active node, clearing the hoveredItem when it
used for documentation functions without even producing a temporary
tooltip.

A future commit will likely be made so that disabling tooltips for nodes
doesn't also prevent the hovered item from being tracked in the store.
2024-10-18 18:56:53 -05:00
Austin Mroz
f48594fbd5 Properly mirror the new description type
Remove errant logging
2024-10-18 15:53:04 -05:00
Austin Mroz
9263330379 Update tests for vue port
Mostly minor changes to selectors

Also fixes the glaringly obvious omission of the description field
2024-10-15 13:41:59 -05:00
Austin Mroz
214f48a6c4 Functional node swaps under vue, icon update 2024-10-15 13:41:59 -05:00
Austin Mroz
f8ba0ab24f Migrate to new sidebar registration 2024-10-15 13:41:59 -05:00
Austin Mroz
b2ef66e058 Update tooltip handling 2024-10-15 13:41:58 -05:00
Austin Mroz
95a4fe7e08 Port sidebar documentation to vue component 2024-10-15 13:41:58 -05:00
Austin Mroz
95cec85c3f Move css to style.css
Since the the css is now static the clutter of an added style element is
no longer needed
2024-10-15 13:41:58 -05:00
Austin Mroz
bc6630742b Move render callback to trigger on node change 2024-10-15 13:41:58 -05:00
Austin Mroz
3b679f1194 Return styling to body, streamline tests
Styling was moved to the sidebar element for better organization, but
this caused errors when the new menu was not in use.
2024-10-15 13:41:58 -05:00
Austin Mroz
52933e13f5 Properly handle theme with css variables 2024-10-15 13:41:58 -05:00
Austin Mroz
44f900ef56 Typing fixes, initial tests 2024-10-15 13:41:58 -05:00
Austin Mroz
7a5d39f41f Temporarily highlight item doc item on selection 2024-10-15 13:41:58 -05:00
Austin Mroz
1d0ae76f8c Connect hover functionality, scroll fixes
Basic connecting for using the existing documentation hover code to
select an item from the active help pane.

Scrolling on selection will now properly perform the minium required
scroll to place the element on screen
2024-10-15 13:41:58 -05:00
Austin Mroz
a8ac7296c2 Theming, pruning, and optional callbacks
Basic styling has been added to the display of documentation for nodes
using the existing tooltip system. This will need another pass to ensure
that style updates immediately when the light/dark toggle is hit instead
of requiring a change of node.

VHS specific namings have been replaced and the code for determining
what the mouse is hovering over has been removed. The existing tooltip
implementation is cleaner and will need to be integrated anyways so
tooltips are temporarily suppressed for the node actively being
displayed in the documentation sidebar.

Optional callbacks have been added for the initial sidebar display and a
user selecting a node element by hovering over it. While selection is
not yet implemented, this should cover any developer needs from more
involved collapsables to automated seeking to video timestamps.
2024-10-15 13:41:58 -05:00
Austin Mroz
4aa04d1419 type implementation for detailed descriptions
Previously, description was a simple string, but supporting more complex
descriptions requires that new data be passed.

The type of a nodes description has been updated to be either a simple
string as before, or an array consisting of short description string, an
html string for the full description, and a placeholder dict for future
usage.

Definitions and usage points for description have been updated to
accommodate this change
2024-10-15 13:41:58 -05:00
Austin Mroz
8160ca0342 Formatting improvements,
The formatting of ndoes using the existing standardized tooltips has
been improved.

Experiemental work for assisting nodes with display of more detailed
descriptions
2024-10-15 13:41:58 -05:00
Austin Mroz
da936d69b6 Remove unused styling code, unwrap 2024-10-15 13:41:58 -05:00
Austin Mroz
7eaa54fe3f Initial sidebar documentation implementation 2024-10-15 13:41:58 -05:00
202 changed files with 3280 additions and 16274 deletions

View File

@@ -1,38 +0,0 @@
name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]
jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git push origin HEAD:${{ github.head_ref }}
working-directory: ComfyUI_frontend

View File

@@ -41,21 +41,3 @@ jobs:
draft: true
prerelease: false
make_latest: "true"
publish_types:
runs-on: ubuntu-latest
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'Release')
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: lts/*
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build:types
- name: Publish package
run: npm publish --access public
working-directory: ./dist
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,15 +0,0 @@
// This file is intentionally kept in CommonJS format (.cjs)
// to resolve compatibility issues with dependencies that require CommonJS.
// Do not convert this file to ESModule format unless all dependencies support it.
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'src/locales/en.json',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko'],
reference: `Keep following model names untranslated:
- flux
- photomaker
`
});

View File

@@ -1,18 +0,0 @@
# Admins
* @Comfy-Org/comfy_frontend_devs
# Maintainers
*.md @Comfy-Org/comfy_maintainer
/tests-ui/ @Comfy-Org/comfy_maintainer
/browser_tests/ @Comfy-Org/comfy_maintainer
/.env_example @Comfy-Org/comfy_maintainer
/src/locales/ @Comfy-Org/comfy_maintainer
# Japanese translation
/src/locales/ja.json @shinshin86 @Comfy-Org/comfy_maintainer
# Load 3D extension
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Mask Editor extension
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs

View File

@@ -414,7 +414,6 @@ We will support custom icons later.
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
### Git pre-commit hooks
@@ -481,84 +480,6 @@ This repo is using litegraph package hosted on <https://github.com/Comfy-Org/lit
This will replace the litegraph package in this repo with the local litegraph repo.
## Internationalization (i18n)
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
### Supported Languages
- en (English)
- zh (中文)
- ru (Русский)
- ja (日本語)
- ko (한국어)
### How to Add a New Language
We welcome the addition of new languages. You can add a new language by following these steps:
#### 1. Generate language files
We use [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/blob/master/packages/lobe-i18n/README.md) as our translation tool, which integrates with LLM for efficient localization.
Update the configuration file to include the new language(s) you wish to add:
```javascript
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'src/locales/en.json', // Base language file
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja'], // Add the new language(s) here
});
```
Set your OpenAI API Key by running the following command:
```sh
npx lobe-i18n --option
```
Once configured, generate the translation files with:
```sh
npx lobe-i18n locale
```
This will create the language files for the specified languages in the configuration.
#### 2. Update i18n Configuration
Import the newly generated locale file(s) in the `src/i18n.ts` file to include them in the application's i18n setup.
#### 3. Enable Selection of the New Language
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
```typescript
{
id: 'Comfy.Locale',
name: 'Locale',
type: 'combo',
// Add the new language(s) here
options: [
{ value: 'en', text: 'English' },
{ value: 'zh', text: '中文' },
{ value: 'ru', text: 'Русский' },
{ value: 'ja', text: '日本語' }
],
defaultValue: navigator.language.split('-')[0] || 'en'
},
```
This will make the new language selectable in the application's settings.
#### 4. Test the Translations
Start the development server, switch to the new language, and verify the translations.
You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
## Deploy
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1,125 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const nodeDef = {
title: 'TestNodeAdvancedDoc'
}
test.describe('Documentation Sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
})
test.afterEach(async ({ comfyPage }) => {
const currentThemeId = await comfyPage.menu.getThemeId()
if (currentThemeId !== 'dark') {
await comfyPage.menu.toggleTheme()
}
})
test('Sidebar registered', async ({ comfyPage }) => {
await expect(
comfyPage.page.locator('.documentation-tab-button')
).toBeVisible()
})
test('Parses help for basic node', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
//Check that each independently parsed element exists
await expect(docPane).toContainText('Load Checkpoint')
await expect(docPane).toContainText('Loads a diffusion model')
await expect(docPane).toContainText('The name of the checkpoint')
await expect(docPane).toContainText('The VAE model used')
})
test('Responds to hovering over node', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.page.mouse.move(321, 593)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
await expect(
comfyPage.page.locator('.sidebar-content-container>div>div:nth-child(4)')
).toBeFocused()
})
test('Updates when a new node is selected', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.page.mouse.click(557, 440)
await expect(docPane).not.toContainText('Load Checkpoint')
await expect(docPane).toContainText('CLIP Text Encode (Prompt)')
await expect(docPane).toContainText('The text to be encoded')
await expect(docPane).toContainText(
'A conditioning containing the embedded text'
)
})
test('Responds to a change in theme', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.menu.toggleTheme()
await expect(docPane).toHaveScreenshot(
'documentation-sidebar-light-theme.png'
)
})
})
test.describe('Advanced Description tests', () => {
test.beforeEach(async ({ comfyPage }) => {
//register test node and add to graph
await comfyPage.page.evaluate(async (node) => {
const app = window['app']
await app.registerNodeDef(node.name, node)
app.addNodeOnGraph(node)
}, advDocNode)
})
test('Description displays as raw html', async ({ comfyPage }) => {
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container>div')
await expect(docPane).toHaveJSProperty(
'innerHTML',
advDocNode.description[1]
)
})
test('selected function', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const app = window['app']
const desc =
LiteGraph.registered_node_types['Test_AdvancedDescription'].nodeData
.description
desc[2].select = (element, name, value) => {
element.children[0].innerText = name + ' ' + value
}
})
await comfyPage.page.locator('.documentation-tab-button').click()
const docPane = comfyPage.page.locator('.sidebar-content-container')
await comfyPage.page.mouse.move(307, 80)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
await expect(comfyPage.page.locator('.node-tooltip')).not.toBeVisible()
await expect(docPane).toContainText('int_input 0')
})
})
const advDocNode = {
display_name: 'Node With Advanced Description',
name: 'Test_AdvancedDescription',
input: {
required: {
int_input: [
'INT',
{ tooltip: "an input tooltip that won't be displayed in sidebar" }
]
}
},
output: ['INT'],
output_name: ['int_output'],
output_tooltips: ["An output tooltip that won't be displayed in the sidebar"],
output_is_list: false,
description: [
'A node with description in the advanced format',
`
A long form description that will be displayed in the sidebar.
<div doc_title="INT">Can include arbitrary html</div>
<div doc_title="int_input">or out of order widgets</div>
`,
{}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -75,28 +75,6 @@ type FolderStructure = {
[key: string]: FolderStructure | string
}
type KeysOfType<T, Match> = {
[K in keyof T]: T[K] extends Match ? K : never
}[keyof T]
class ConfirmDialog {
public readonly delete: Locator
public readonly overwrite: Locator
public readonly reject: Locator
constructor(public readonly page: Page) {
this.delete = page.locator('button.p-button[aria-label="Delete"]')
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
}
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
const loc = this[locator]
await expect(loc).toBeVisible()
await loc.click()
}
}
export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
@@ -116,7 +94,6 @@ export class ComfyPage {
public readonly actionbar: ComfyActionbar
public readonly templates: ComfyTemplates
public readonly settingDialog: SettingDialog
public readonly confirmDialog: ConfirmDialog
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -141,7 +118,6 @@ export class ComfyPage {
this.actionbar = new ComfyActionbar(page)
this.templates = new ComfyTemplates(page)
this.settingDialog = new SettingDialog(page)
this.confirmDialog = new ConfirmDialog(page)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {
@@ -514,10 +490,6 @@ export class ComfyPage {
return { x: 427, y: 98 }
}
get promptDialogInput() {
return this.page.locator('.p-dialog-content input[type="text"]')
}
async disconnectEdge() {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
@@ -768,29 +740,28 @@ export class ComfyPage {
)
}
async clickDialogButton(prompt: string, buttonText: string = 'Yes') {
async confirmDialog(prompt: string, text: string = 'Yes') {
const modal = this.page.locator(
`.comfy-modal-content:has-text("${prompt}")`
)
await expect(modal).toBeVisible()
await modal
.locator('.comfyui-button', {
hasText: buttonText
hasText: text
})
.click()
await expect(modal).toBeHidden()
}
async convertAllNodesToGroupNode(groupNodeName: string) {
this.page.on('dialog', async (dialog) => {
await dialog.accept(groupNodeName)
})
await this.canvas.press('Control+a')
const node = await this.getFirstNodeRef()
await node!.clickContextMenuOption('Convert to Group Node')
await this.promptDialogInput.fill(groupNodeName)
await this.page.keyboard.press('Enter')
await this.promptDialogInput.waitFor({ state: 'hidden' })
await this.nextFrame()
}
async convertOffsetToCanvas(pos: [number, number]) {
return this.page.evaluate((pos) => {
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
@@ -851,23 +822,18 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
// Hide canvas menu/info by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId
})
} catch (e) {
console.error(e)
}
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
// Hide canvas menu/info by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId
})
await comfyPage.setup()
await use(comfyPage)
}

View File

@@ -1,41 +0,0 @@
import { Page } from 'playwright'
import { test as base } from '@playwright/test'
export class UserSelectPage {
constructor(
public readonly url: string,
public readonly page: Page
) {}
get selectionUrl() {
return this.url + '/user-select'
}
get container() {
return this.page.locator('#comfy-user-selection')
}
get newUserInput() {
return this.container.locator('#new-user-input')
}
get existingUserSelect() {
return this.container.locator('#existing-user-select')
}
get nextButton() {
return this.container.getByText('Next')
}
}
export const userSelectPageFixture = base.extend<{
userSelectPage: UserSelectPage
}>({
userSelectPage: async ({ page }, use) => {
const userSelectPage = new UserSelectPage(
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
page
)
await use(userSelectPage)
}
})

View File

@@ -233,10 +233,10 @@ export class NodeReference {
await ctx.getByText(optionText).click()
}
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
this.comfyPage.page.once('dialog', async (dialog) => {
await dialog.accept(groupNodeName)
})
await this.clickContextMenuOption('Convert to Group Node')
await this.comfyPage.promptDialogInput.fill(groupNodeName)
await this.comfyPage.page.keyboard.press('Enter')
await this.comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await this.comfyPage.nextFrame()
const nodes = await this.comfyPage.getNodeRefsByType(
`workflow>${groupNodeName}`

View File

@@ -103,36 +103,6 @@ test.describe('Group Node', () => {
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
})
test('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
const makeGroup = async (name, type1, type2) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
await node1.click('title')
await node2.click('title', {
modifiers: ['Shift']
})
return await node2.convertToGroupNode(name)
}
const group1 = await makeGroup(
'g1',
'CLIPTextEncode',
'CheckpointLoaderSimple'
)
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
const manage1 = await group1.manageGroupNode()
await comfyPage.nextFrame()
expect(await manage1.getSelectedNodeType()).toBe('g1')
await manage1.close()
await expect(manage1.root).not.toBeVisible()
const manage2 = await group2.manageGroupNode()
expect(await manage2.getSelectedNodeType()).toBe('g2')
})
test('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {

View File

@@ -1,14 +1,12 @@
import { Locator, Page } from '@playwright/test'
export class ManageGroupNode {
footer: Locator
header: Locator
constructor(
readonly page: Page,
readonly root: Locator
) {
this.footer = root.locator('footer')
this.header = root.locator('header')
}
async setLabel(name: string, label: string) {
@@ -25,11 +23,6 @@ export class ManageGroupNode {
await this.footer.getByText('Close').click()
}
async getSelectedNodeType() {
const select = this.header.locator('select').first()
return await select.inputValue()
}
async selectNode(name: string) {
const list = this.root.locator('.comfy-group-manage-list-items')
const item = list.getByText(name)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -350,6 +350,23 @@ test.describe('Menu', () => {
await comfyPage.page.waitForTimeout(1000)
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
})
test('Can migrate legacy bookmarks', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [
'foo/',
'foo/KSampler (Advanced)',
'UNKNOWN',
'KSampler'
])
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await comfyPage.reload()
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
[]
)
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['foo/', 'foo/KSamplerAdvanced', 'KSampler'])
})
})
test.describe('Workflows sidebar', () => {
@@ -442,7 +459,7 @@ test.describe('Menu', () => {
).toEqual(['workflow5.json'])
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
expect(
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
).toEqual(['workflow5.json'])
@@ -477,7 +494,7 @@ test.describe('Menu', () => {
)
await topbar.saveWorkflowAs('workflow1.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
// The old workflow1.json should be deleted and the new one should be saved.
expect(
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
@@ -519,43 +536,6 @@ test.describe('Menu', () => {
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
).toEqual(['*Unsaved Workflow.json'])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18.json'
await topbar.saveWorkflow(filename)
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.nextFrame()
await comfyPage.clickContextMenuItem('Delete')
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
})
test('Can delete workflows', async ({ comfyPage }) => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18.json'
await topbar.saveWorkflow(filename)
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.clickContextMenuItem('Delete')
await comfyPage.confirmDialog.click('delete')
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
})
})
test.describe('Workflows topbar tabs', () => {

View File

@@ -32,11 +32,11 @@ test.describe('Canvas Right Click Menu', () => {
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
comfyPage.page.on('dialog', async (dialog) => {
await dialog.accept('GroupNode2CLIP')
})
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
@@ -186,22 +186,4 @@ test.describe('Node Right Click Menu', () => {
'selected-nodes-unpinned.png'
)
})
test('Can clone pinned nodes', async ({ comfyPage }) => {
const nodeCount = await comfyPage.getGraphNodesCount()
const node = (await comfyPage.getFirstNodeRef())!
await node.clickContextMenuOption('Pin')
await comfyPage.nextFrame()
await node.click('title', { button: 'right' })
await expect(
comfyPage.page.locator('.litemenu-entry:has-text("Unpin")')
).toBeAttached()
const cloneItem = comfyPage.page.locator(
'.litemenu-entry:has-text("Clone")'
)
await cloneItem.click()
await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame()
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -1,42 +0,0 @@
import { expect } from '@playwright/test'
import { userSelectPageFixture as test } from './fixtures/UserSelectPage'
/**
* Expects ComfyUI backend to be launched with `--multi-user` flag.
*/
test.describe('User Select View', () => {
test.beforeEach(async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url)
await page.evaluate(() => {
localStorage.clear()
sessionStorage.clear()
})
})
test('Redirects to user select view if no user is logged in', async ({
userSelectPage,
page
}) => {
await page.goto(userSelectPage.url)
await expect(userSelectPage.container).toBeVisible()
expect(page.url()).toBe(userSelectPage.selectionUrl)
})
test('Can create new user', async ({ userSelectPage, page }) => {
const randomUser = `test-user-${Math.random().toString(36).substring(2, 7)}`
await page.goto(userSelectPage.url)
await expect(page).toHaveURL(userSelectPage.selectionUrl)
await userSelectPage.newUserInput.fill(randomUser)
await userSelectPage.nextButton.click()
await expect(page).toHaveURL(userSelectPage.url)
})
test('Can choose existing user', async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url)
await expect(page).toHaveURL(userSelectPage.selectionUrl)
await userSelectPage.existingUserSelect.click()
await page.locator('.p-select-list .p-select-option').first().click()
await userSelectPage.nextButton.click()
await expect(page).toHaveURL(userSelectPage.url)
})
})

View File

@@ -9,6 +9,33 @@
</head>
<body class="litegraph grid">
<div id="vue-app"></div>
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
<main class="comfy-user-selection-inner">
<h1>ComfyUI</h1>
<form>
<section>
<label>New user:
<input placeholder="Enter a username" />
</label>
</section>
<div class="comfy-user-existing">
<span class="or-separator">OR</span>
<section>
<label>
Existing user:
<select>
<option hidden disabled selected value> Select a user </option>
</select>
</label>
</section>
</div>
<footer>
<span class="comfy-user-error">&nbsp;</span>
<button class="comfy-btn comfy-user-button-next">Next</button>
</footer>
</form>
</main>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -3,7 +3,6 @@ export default {
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
...formatFiles(stagedFiles),
'vue-tsc --noEmit',
'tsc --noEmit',
'tsc-strict'
]

5012
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,17 @@
{
"name": "@comfyorg/comfyui-frontend",
"name": "comfyui-frontend",
"private": true,
"version": "1.5.10",
"version": "1.4.13",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
"description": "Official front-end implementation of ComfyUI",
"license": "GPL-3.0-only",
"scripts": {
"dev": "vite",
"dev:electron": "vite --config vite.electron.config.mts",
"build": "npm run typecheck && vite build",
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
"deploy": "npm run build && node scripts/deploy.js",
"release": "node scripts/release.js",
"update-litegraph": "node scripts/update-litegraph.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
"typecheck": "tsc --noEmit && tsc-strict",
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
"test": "jest --config jest.config.base.ts",
"test:jest:fast": "jest --config jest.config.fast.ts",
@@ -28,16 +23,13 @@
"prepare": "husky || true",
"preview": "vite preview",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts"
"lint:fix": "eslint src --fix"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.22.20",
"@eslint/js": "^9.8.0",
"@iconify/json": "^2.2.245",
"@lobehub/i18n-cli": "^1.20.1",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.44.1",
"@types/jest": "^29.5.12",
@@ -74,16 +66,14 @@
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.6",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.0.5",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.3.25",
"@comfyorg/litegraph": "^0.8.44",
"@comfyorg/comfyui-electron-types": "^0.3.19",
"@comfyorg/litegraph": "^0.8.37",
"@primevue/themes": "^4.0.5",
"@vueuse/core": "^11.0.0",
"@xterm/addon-fit": "^0.10.0",

View File

@@ -1,14 +0,0 @@
import { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
testDir: './scripts',
use: {
baseURL: 'http://localhost:5173',
headless: true
},
reporter: 'list',
timeout: 10000,
testMatch: /collect-i18n\.ts/
}
export default config

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="292" height="92pt" viewBox="0 0 219 92"><defs><clipPath id="a"><path d="M159 .79h25V69h-25Zm0 0"/></clipPath><clipPath id="b"><path d="M183 9h35.371v60H183Zm0 0"/></clipPath><clipPath id="c"><path d="M0 .79h92V92H0Zm0 0"/></clipPath></defs><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"/><g clip-path="url(#a)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"/></g><g clip-path="url(#b)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"/></g><g clip-path="url(#c)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,148 +0,0 @@
import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
import type { ComfyCommandImpl } from '../src/stores/commandStore'
import type { FormItem, SettingParams } from '../src/types/settingTypes'
import type { ComfyApi } from '../src/scripts/api'
import type { ComfyNodeDef } from '../src/types/apiTypes'
const localePath = './src/locales/en.json'
const extractMenuCommandLocaleStrings = (): Set<string> => {
const labels = new Set<string>()
for (const [category, _] of CORE_MENU_COMMANDS) {
category.forEach((category) => labels.add(category))
}
return labels
}
test('collect-i18n', async ({ comfyPage }) => {
const commands = await comfyPage.page.evaluate(() => {
const workspace = window['app'].extensionManager
const commands = workspace.command.commands as ComfyCommandImpl[]
return commands.map((command) => ({
id: command.id,
label: command.label,
menubarLabel: command.menubarLabel
}))
})
const locale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
// Commands
const menuLabels = extractMenuCommandLocaleStrings()
const commandMenuLabels = new Set(
commands.map((command) => command.menubarLabel ?? command.label ?? '')
)
const allLabels = new Set([...menuLabels, ...commandMenuLabels])
allLabels.delete('')
const allLabelsLocale = Object.fromEntries(
Array.from(allLabels).map((label) => [normalizeI18nKey(label), label])
)
// Settings
const settings = await comfyPage.page.evaluate(() => {
const workspace = window['app'].extensionManager
const settings = workspace.setting.settings as Record<string, SettingParams>
return Object.values(settings)
.sort((a, b) => a.id.localeCompare(b.id))
.map((setting) => ({
id: setting.id,
name: setting.name,
tooltip: setting.tooltip,
category: setting.category
}))
})
const allSettingsLocale = Object.fromEntries(
settings.map((setting) => [
normalizeI18nKey(setting.id),
{
name: setting.name,
tooltip: setting.tooltip
}
])
)
const allSettingCategoriesLocale = Object.fromEntries(
settings
.flatMap((setting) => {
return (setting.category ?? setting.id.split('.')).slice(0, 2)
})
.map((category: string) => [
normalizeI18nKey(category),
formatCamelCase(category)
])
)
// Server Configs
const allServerConfigsLocale = Object.fromEntries(
SERVER_CONFIG_ITEMS.map((config) => [
normalizeI18nKey(config.id),
{
name: (config as unknown as FormItem).name,
tooltip: (config as unknown as FormItem).tooltip
}
])
)
const allServerConfigCategoriesLocale = Object.fromEntries(
SERVER_CONFIG_ITEMS.flatMap((config) => {
return config.category ?? ['General']
}).map((category) => [
normalizeI18nKey(category),
formatCamelCase(category)
])
)
// Node Definitions
const nodeDefs = (await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})) as Record<string, ComfyNodeDef>
const allNodeDefsLocale = Object.fromEntries(
Object.values(nodeDefs)
.sort((a, b) => a.name.localeCompare(b.name))
.map((nodeDef) => [
normalizeI18nKey(nodeDef.name),
{
display_name: nodeDef.display_name ?? nodeDef.name,
description: nodeDef.description || undefined
}
])
)
const allNodeCategoriesLocale = Object.fromEntries(
Object.values(nodeDefs).flatMap((nodeDef) =>
nodeDef.category
.split('/')
.map((category) => [normalizeI18nKey(category), category])
)
)
fs.writeFileSync(
localePath,
JSON.stringify(
{
...locale,
menuLabels: allLabelsLocale,
settingsDialog: allSettingsLocale,
// Do merge for settingsCategories as there are some manual translations
// for special panels like "About" and "Keybinding".
settingsCategories: {
...(locale.settingsCategories ?? {}),
...allSettingCategoriesLocale
},
serverConfigItems: allServerConfigsLocale,
serverConfigCategories: allServerConfigCategoriesLocale,
nodeDefs: allNodeDefsLocale,
nodeCategories: allNodeCategoriesLocale
},
null,
2
)
)
})

View File

@@ -1,40 +0,0 @@
import fs from 'fs'
import path from 'path'
const mainPackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
// Create the types-only package.json
const typesPackage = {
name: `${mainPackage.name}-types`,
version: mainPackage.version,
types: './index.d.ts',
files: ['index.d.ts'],
publishConfig: {
access: 'public'
},
repository: mainPackage.repository,
homepage: mainPackage.homepage,
description: `TypeScript definitions for ${mainPackage.name}`,
license: mainPackage.license,
dependencies: {
'@comfyorg/litegraph': mainPackage.dependencies['@comfyorg/litegraph']
},
peerDependencies: {
vue: mainPackage.dependencies.vue,
zod: mainPackage.dependencies.zod
}
}
// Ensure dist directory exists
const distDir = './dist'
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true })
}
// Write the new package.json to the dist directory
fs.writeFileSync(
path.join(distDir, 'package.json'),
JSON.stringify(typesPackage, null, 2)
)
console.log('Types package.json have been prepared in the dist directory')

View File

@@ -61,9 +61,11 @@ 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'
@@ -74,10 +76,10 @@ const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => ({
const queueModeMenuItemLookup: Record<AutoQueueMode, MenuItem> = {
disabled: {
key: 'disabled',
label: t('menu.queue'),
label: 'Queue',
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
@@ -85,7 +87,7 @@ const queueModeMenuItemLookup = computed(() => ({
},
instant: {
key: 'instant',
label: `${t('menu.queue')} (${t('menu.instant')})`,
label: 'Queue (Instant)',
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
@@ -93,19 +95,19 @@ const queueModeMenuItemLookup = computed(() => ({
},
change: {
key: 'change',
label: `${t('menu.queue')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
label: 'Queue (Change)',
tooltip: t('menu.changeTooltip'),
command: () => {
queueMode.value = 'change'
}
}
}))
}
const activeQueueModeMenuItem = computed(
() => queueModeMenuItemLookup.value[queueMode.value]
() => queueModeMenuItemLookup[queueMode.value]
)
const queueModeMenuItems = computed(() =>
Object.values(queueModeMenuItemLookup.value)
Object.values(queueModeMenuItemLookup)
)
const executingPrompt = computed(() => !!queueCountStore.count.value)

View File

@@ -22,7 +22,7 @@
</span>
<div v-if="imageBroken" class="broken-image-placeholder">
<i class="pi pi-image"></i>
<span>{{ $t('g.imageFailedToLoad') }}</span>
<span>{{ $t('imageFailedToLoad') }}</span>
</div>
</template>

View File

@@ -1,8 +1,8 @@
<template>
<Dialog v-model:visible="visible" :header="$t('g.customizeFolder')">
<Dialog v-model:visible="visible" :header="$t('customizeFolder')">
<div class="p-fluid">
<div class="field icon-field">
<label for="icon">{{ $t('g.icon') }}</label>
<label for="icon">{{ $t('icon') }}</label>
<SelectButton
v-model="selectedIcon"
:options="iconOptions"
@@ -19,7 +19,7 @@
</div>
<Divider />
<div class="field color-field">
<label for="color">{{ $t('g.color') }}</label>
<label for="color">{{ $t('color') }}</label>
<div class="color-picker-container">
<SelectButton
v-model="selectedColor"
@@ -41,7 +41,7 @@
v-else
class="pi pi-palette"
:style="{ fontSize: '1.2rem' }"
v-tooltip="$t('g.customColor')"
v-tooltip="$t('customColor')"
></i>
</template>
</SelectButton>
@@ -54,13 +54,13 @@
</div>
<template #footer>
<Button
:label="$t('g.reset')"
:label="$t('reset')"
icon="pi pi-refresh"
@click="resetCustomization"
class="p-button-text"
/>
<Button
:label="$t('g.confirm')"
:label="$t('confirm')"
icon="pi pi-check"
@click="confirmCustomization"
autofocus
@@ -100,24 +100,24 @@ const visible = computed({
const nodeBookmarkStore = useNodeBookmarkStore()
const iconOptions = [
{ name: t('icon.bookmark'), value: nodeBookmarkStore.defaultBookmarkIcon },
{ name: t('icon.folder'), value: 'pi-folder' },
{ name: t('icon.star'), value: 'pi-star' },
{ name: t('icon.heart'), value: 'pi-heart' },
{ name: t('icon.file'), value: 'pi-file' },
{ name: t('icon.inbox'), value: 'pi-inbox' },
{ name: t('icon.box'), value: 'pi-box' },
{ name: t('icon.briefcase'), value: 'pi-briefcase' }
{ name: t('bookmark'), value: nodeBookmarkStore.defaultBookmarkIcon },
{ name: t('folder'), value: 'pi-folder' },
{ name: t('star'), value: 'pi-star' },
{ name: t('heart'), value: 'pi-heart' },
{ name: t('file'), value: 'pi-file' },
{ name: t('inbox'), value: 'pi-inbox' },
{ name: t('box'), value: 'pi-box' },
{ name: t('briefcase'), value: 'pi-briefcase' }
]
const colorOptions = [
{ name: t('color.default'), value: nodeBookmarkStore.defaultBookmarkColor },
{ name: t('color.blue'), value: '#007bff' },
{ name: t('color.green'), value: '#28a745' },
{ name: t('color.red'), value: '#dc3545' },
{ name: t('color.pink'), value: '#e83e8c' },
{ name: t('color.yellow'), value: '#ffc107' },
{ name: t('color.custom'), value: 'custom' }
{ name: t('default'), value: nodeBookmarkStore.defaultBookmarkColor },
{ name: t('blue'), value: '#007bff' },
{ name: t('green'), value: '#28a745' },
{ name: t('red'), value: '#dc3545' },
{ name: t('pink'), value: '#e83e8c' },
{ name: t('yellow'), value: '#ffc107' },
{ name: t('custom'), value: 'custom' }
]
const defaultIcon = iconOptions.find(
@@ -157,7 +157,7 @@ const resetCustomization = () => {
selectedColor.value = defaultColor
} else if (!colorOption) {
customColor.value = props.initialColor.replace('#', '')
selectedColor.value = { name: t('color.custom'), value: 'custom' }
selectedColor.value = { name: 'Custom', value: 'custom' }
} else {
selectedColor.value = colorOption
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in deviceColumns" :key="col.field">
<div class="font-medium">{{ col.header }}</div>
<div class="font-medium">{{ $t(col.header) }}</div>
<div>{{ formatValue(props.device[col.field], col.field) }}</div>
</template>
</div>

View File

@@ -14,7 +14,7 @@
<div class="file-action">
<Button
class="file-action-button"
:label="$t('g.download') + ' (' + fileSize + ')'"
:label="$t('download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="props.error"

View File

@@ -12,7 +12,7 @@
<div class="file-action">
<Button
class="file-action-button"
:label="$t('g.download') + ' (' + fileSize + ')'"
:label="$t('download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="props.error"

View File

@@ -44,22 +44,21 @@ import Button from 'primevue/button'
import SearchFilterChip from './SearchFilterChip.vue'
import { toRefs } from 'vue'
const props = withDefaults(
defineProps<{
class?: string
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}>(),
{
placeholder: 'Search...',
icon: 'pi pi-search',
debounceTime: 300
}
)
interface Props {
class?: string
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}
const props = withDefaults(defineProps<Props>(), {
placeholder: 'Search...',
icon: 'pi pi-search',
debounceTime: 300
})
const { filters } = toRefs(props)

View File

@@ -1,10 +1,10 @@
<template>
<div class="system-stats">
<div class="mb-6">
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.systemInfo') }}</h2>
<h2 class="text-2xl font-semibold mb-4">{{ $t('systemInfo') }}</h2>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in systemColumns" :key="col.field">
<div class="font-medium">{{ col.header }}</div>
<div class="font-medium">{{ $t(col.header) }}</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
</template>
</div>
@@ -13,7 +13,7 @@
<Divider />
<div>
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.devices') }}</h2>
<h2 class="text-2xl font-semibold mb-4">{{ $t('devices') }}</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"

View File

@@ -130,13 +130,13 @@ const deleteCommand = async (node: RenderedTreeExplorerNode) => {
const menuItems = computed<MenuItem[]>(() =>
[
{
label: t('g.rename'),
label: t('rename'),
icon: 'pi pi-file-edit',
command: () => renameCommand(menuTargetNode.value),
visible: menuTargetNode.value?.handleRename !== undefined
},
{
label: t('g.delete'),
label: t('delete'),
icon: 'pi pi-trash',
command: () => deleteCommand(menuTargetNode.value),
visible: menuTargetNode.value?.handleDelete !== undefined,

View File

@@ -128,7 +128,7 @@ describe('TreeExplorerTreeNode', () => {
expect(handleRenameMock).toHaveBeenCalledOnce()
expect(addToastSpy).toHaveBeenCalledWith({
severity: 'error',
summary: 'g.error',
summary: 'error',
detail: 'Rename failed',
life: 3000
})

View File

@@ -1,86 +0,0 @@
<template>
<section class="prompt-dialog-content flex flex-col gap-6 m-2 mt-4">
<span>{{ message }}</span>
<ul v-if="itemList?.length" class="pl-4 m-0 flex flex-col gap-2">
<li v-for="item of itemList" :key="item">{{ item }}</li>
</ul>
<div class="flex gap-4 justify-end">
<Button
:label="$t('g.cancel')"
icon="pi pi-undo"
severity="secondary"
@click="onCancel"
autofocus
/>
<Button
v-if="type === 'delete'"
:label="$t('g.delete')"
severity="danger"
@click="onConfirm"
icon="pi pi-trash"
/>
<Button
v-else-if="type === 'overwrite'"
:label="$t('g.overwrite')"
severity="warn"
@click="onConfirm"
icon="pi pi-save"
/>
<template v-else-if="type === 'dirtyClose'">
<Button
:label="$t('g.no')"
severity="secondary"
@click="onDeny"
icon="pi pi-times"
/>
<Button :label="$t('g.save')" @click="onConfirm" icon="pi pi-save" />
</template>
<Button
v-else-if="type === 'reinstall'"
:label="$t('desktopMenu.reinstall')"
severity="warn"
@click="onConfirm"
icon="pi pi-eraser"
/>
<!-- Invalid - just show a close button. -->
<Button
v-else
:label="$t('g.close')"
severity="primary"
@click="onCancel"
icon="pi pi-times"
/>
</div>
</section>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useDialogStore } from '@/stores/dialogStore'
import type { ConfirmationDialogType } from '@/services/dialogService'
const props = defineProps<{
message: string
type: ConfirmationDialogType
onConfirm: (value?: boolean) => void
itemList?: string[]
}>()
const onCancel = () => useDialogStore().closeDialog()
const onDeny = () => {
props.onConfirm(false)
useDialogStore().closeDialog()
}
const onConfirm = () => {
props.onConfirm(true)
useDialogStore().closeDialog()
}
</script>
<style lang="css" scoped>
.prompt-dialog-content {
white-space: pre-wrap;
}
</style>

View File

@@ -7,7 +7,7 @@
<div class="comfy-error-report">
<Button
v-show="!reportOpen"
:label="$t('g.showReport')"
:label="$t('showReport')"
@click="showReport"
text
/>
@@ -28,7 +28,7 @@
/>
<Button
v-if="reportOpen"
:label="$t('g.copyToClipboard')"
:label="$t('copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>

View File

@@ -10,7 +10,7 @@
/>
<label>{{ message }}</label>
</FloatLabel>
<Button @click="onConfirm">{{ $t('g.confirm') }}</Button>
<Button @click="onConfirm">{{ $t('confirm') }}</Button>
</div>
</template>

View File

@@ -5,12 +5,12 @@
class="settings-search-box w-full mb-2"
v-model:modelValue="searchQuery"
@search="handleSearch"
:placeholder="$t('g.searchSettings') + '...'"
:placeholder="$t('searchSettings') + '...'"
/>
<Listbox
v-model="activeCategory"
:options="categories"
optionLabel="translatedLabel"
optionLabel="label"
scrollHeight="100%"
:disabled="inSearch"
class="border-none w-full"
@@ -29,7 +29,6 @@
:value="category.label"
>
<template #header>
<CurrentUserMessage v-if="tabValue === 'Comfy'" />
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
</template>
<SettingsPanel :settingGroups="sortedGroups(category)" />
@@ -75,11 +74,8 @@ import SettingsPanel from './setting/SettingsPanel.vue'
import PanelTemplate from './setting/PanelTemplate.vue'
import AboutPanel from './setting/AboutPanel.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import { flattenTree } from '@/utils/treeUtil'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { useI18n } from 'vue-i18n'
const KeybindingPanel = defineAsyncComponent(
() => import('./setting/KeybindingPanel.vue')
@@ -134,23 +130,13 @@ const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
const settingCategories = computed<SettingTreeNode[]>(
() => settingRoot.value.children ?? []
)
const { t } = useI18n()
const categories = computed<SettingTreeNode[]>(() =>
[
...settingCategories.value,
keybindingPanelNode,
...extensionPanelNodeList.value,
...serverConfigPanelNodeList.value,
aboutPanelNode
].map((node) => ({
...node,
translatedLabel: t(
`settingsCategories.${normalizeI18nKey(node.label)}`,
node.label
)
}))
)
const categories = computed<SettingTreeNode[]>(() => [
...settingCategories.value,
keybindingPanelNode,
...extensionPanelNodeList.value,
...serverConfigPanelNodeList.value,
aboutPanelNode
])
const activeCategory = ref<SettingTreeNode | null>(null)
const searchResults = ref<ISettingGroup[]>([])

View File

@@ -1,7 +1,7 @@
<template>
<Button
@click="openGitHubIssues"
:label="$t('g.findIssues')"
:label="$t('findIssues')"
severity="secondary"
icon="pi pi-github"
>

View File

@@ -1,11 +1,11 @@
<template>
<Button
@click="reportIssue"
:label="$t('g.reportIssue')"
:label="$t('reportIssue')"
:severity="submitted ? 'success' : 'secondary'"
:icon="icon"
:disabled="submitted"
v-tooltip="$t('g.reportIssueTooltip')"
v-tooltip="$t('reportIssueTooltip')"
>
</Button>
</template>
@@ -41,7 +41,7 @@ const reportIssue = async () => {
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
summary: t('reportSent'),
life: 3000
})
} finally {

View File

@@ -1,6 +1,6 @@
<template>
<PanelTemplate value="About" class="about-container">
<h2 class="text-2xl font-bold mb-2">{{ $t('g.about') }}</h2>
<h2 class="text-2xl font-bold mb-2">{{ $t('about') }}</h2>
<div class="space-y-2">
<a
v-for="badge in aboutPanelStore.badges"

View File

@@ -1,28 +0,0 @@
<!-- A message that displays the current user -->
<template>
<Message
v-if="userStore.isMultiUserServer"
severity="info"
icon="pi pi-user"
pt:text="w-full"
>
<div class="flex items-center justify-between">
<div>
{{ $t('g.currentUser') }}: {{ userStore.currentUser?.username }}
</div>
<Button icon="pi pi-sign-out" @click="logout" text />
</div>
</Message>
</template>
<script setup lang="ts">
import Message from 'primevue/message'
import Button from 'primevue/button'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const logout = () => {
userStore.logout()
window.location.reload()
}
</script>

View File

@@ -3,7 +3,7 @@
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="$t('g.searchExtensions') + '...'"
:placeholder="$t('searchExtensions') + '...'"
/>
<Message v-if="hasChanges" severity="info" pt:text="w-full">
<ul>
@@ -16,7 +16,7 @@
</ul>
<div class="flex justify-end">
<Button
:label="$t('g.reloadToApplyChanges')"
:label="$t('reloadToApplyChanges')"
@click="applyChanges"
outlined
severity="danger"
@@ -30,7 +30,7 @@
size="small"
:filters="filters"
>
<Column field="name" :header="$t('g.extensionName')" sortable></Column>
<Column field="name" :header="$t('extensionName')" sortable></Column>
<Column
:pt="{
bodyCell: 'flex items-center justify-end'

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