Compare commits
25 Commits
v1.5.10
...
sidebar-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
867c50f7cb | ||
|
|
89027ea969 | ||
|
|
ce9cfdb975 | ||
|
|
ef191033c8 | ||
|
|
293f4295a8 | ||
|
|
3252d62edf | ||
|
|
1dfcc7a0d4 | ||
|
|
f48594fbd5 | ||
|
|
9263330379 | ||
|
|
214f48a6c4 | ||
|
|
f8ba0ab24f | ||
|
|
b2ef66e058 | ||
|
|
95a4fe7e08 | ||
|
|
95cec85c3f | ||
|
|
bc6630742b | ||
|
|
3b679f1194 | ||
|
|
52933e13f5 | ||
|
|
44f900ef56 | ||
|
|
7a5d39f41f | ||
|
|
1d0ae76f8c | ||
|
|
a8ac7296c2 | ||
|
|
4aa04d1419 | ||
|
|
8160ca0342 | ||
|
|
da936d69b6 | ||
|
|
7eaa54fe3f |
38
.github/workflows/i18n.yaml
vendored
@@ -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
|
||||
18
.github/workflows/release.yaml
vendored
@@ -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 }}
|
||||
|
||||
15
.i18nrc.cjs
@@ -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
|
||||
`
|
||||
});
|
||||
18
CODEOWNERS
@@ -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
|
||||
79
README.md
@@ -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`.
|
||||
|
||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
125
browser_tests/documentationSidebar.spec.ts
Normal 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>
|
||||
`,
|
||||
{}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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}`
|
||||
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
@@ -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)
|
||||
})
|
||||
})
|
||||
27
index.html
@@ -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"> </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>
|
||||
|
||||
@@ -3,7 +3,6 @@ export default {
|
||||
|
||||
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
|
||||
...formatFiles(stagedFiles),
|
||||
'vue-tsc --noEmit',
|
||||
'tsc --noEmit',
|
||||
'tsc-strict'
|
||||
]
|
||||
|
||||
5012
package-lock.json
generated
22
package.json
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
expect(addToastSpy).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
summary: 'error',
|
||||
detail: 'Rename failed',
|
||||
life: 3000
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
<label>{{ message }}</label>
|
||||
</FloatLabel>
|
||||
<Button @click="onConfirm">{{ $t('g.confirm') }}</Button>
|
||||
<Button @click="onConfirm">{{ $t('confirm') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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[]>([])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Button
|
||||
@click="openGitHubIssues"
|
||||
:label="$t('g.findIssues')"
|
||||
:label="$t('findIssues')"
|
||||
severity="secondary"
|
||||
icon="pi pi-github"
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||