Compare commits

...

67 Commits

Author SHA1 Message Date
Terry Jia
e6a98e3286 export generateUUID 2025-04-17 21:59:47 -04:00
Chenlei Hu
9ce3cccfd4 [API Nodes] Wire password login (#3469) 2025-04-15 19:41:22 -04:00
Chenlei Hu
9935b322f0 [API Nodes] Signin/Signup dialog (#3466)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-15 17:37:53 -04:00
Comfy Org PR Bot
60dd242b23 [chore] Update Comfy Registry API types from comfy-api@b664e39 (#3465)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-15 17:16:02 -04:00
Christian Byrne
cec0dcbccd [Api Node] Firebase auth and user auth store (#3467) 2025-04-15 17:15:51 -04:00
Chenlei Hu
907632a250 Use bundler moduleResolution mode in tsconfig (#3464) 2025-04-15 11:17:07 -04:00
Comfy Org PR Bot
45c450cdb9 [chore] Update litegraph to 0.13.3 (#3463)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-15 11:03:32 -04:00
Chenlei Hu
ca85b2b144 [DevExperience] Add recommended extensions to .vscode (#3459) 2025-04-15 10:27:43 -04:00
Comfy Org PR Bot
1f28e6ef33 1.17.0 (#3458)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-14 21:06:33 -04:00
Comfy Org PR Bot
fee444c64b [chore] Update litegraph to 0.13.2 (#3460)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-14 21:06:23 -04:00
Chenlei Hu
851739a768 [API Node] Sign in required dialog (#3457)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-14 17:49:17 -04:00
Benjamin Lu
1631665efb Fix minor typo (#3456)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-14 13:24:13 -04:00
Terry Jia
1a066c7062 [3d] move default values of showPreview, showGrid, cameraType to settings panel (#3443)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-13 19:44:47 -04:00
Chenlei Hu
e45f5bdebb [lint] Fix remaining vue lint warnings (#3435) 2025-04-12 22:29:29 -04:00
Dr.Lt.Data
c270e7734a refine locales/ko (#3434) 2025-04-12 22:19:11 -04:00
Chenlei Hu
8d7a21e008 [lint] Add eslint-plugin-prettier (#3433) 2025-04-12 22:12:50 -04:00
Comfy Org PR Bot
29e63baca6 1.16.7 (#3430)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-12 21:22:02 -04:00
Benjamin Lu
b22713daf0 Add source prop to commands (#3429)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-12 21:20:53 -04:00
Benjamin Lu
c8b8953e0a Add reset individual keybind button (#3423)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-12 16:59:00 -04:00
Benjamin Lu
731ce8599d Allow scrolling on .settings-content (#3427)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-12 16:58:08 -04:00
filtered
ec8e55c1c1 Allow zooming inside multi-line string widgets (#3422)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-12 13:13:00 -04:00
dependabot[bot]
04d38f2538 Bump vite from 5.4.17 to 5.4.18 (#3426)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-12 12:19:10 -04:00
Chenlei Hu
1c41db75f8 [Type] Enable strict schema type for setting entries (#3425) 2025-04-12 11:34:02 -04:00
Christian Byrne
c7a7397000 [Types] Make more types available directly in @comfyorg/comfyui-frontend-types package (#3418) 2025-04-12 11:33:34 -04:00
Terry Jia
e660e1d678 [3d] add support to output camera state (#3421) 2025-04-12 11:27:05 -04:00
Christian Byrne
fb19752389 [Types] Adds missing settings fields types (#3417) 2025-04-12 11:12:07 -04:00
Christian Byrne
d098d6ae4e [Types] Move enum to types file (#3416) 2025-04-12 11:11:41 -04:00
Christian Byrne
e4a5355f58 [Manager] Fix loading state on uninstall button (#3413) 2025-04-11 22:14:49 -04:00
Comfy Org PR Bot
42c004d41d 1.16.6 (#3410)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-11 20:33:25 -04:00
Benjamin Lu
009c389607 Rename reset keybindings to reset all keybindings (#3411)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-11 20:01:28 -04:00
Christian Byrne
b449dbd26b [Manager] Allowing changing sort field of registry search results (#3409)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-11 18:45:21 -04:00
Christian Byrne
67835edfca [Manager] Preview the individual nodes for packs on the registry (#3408)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-11 16:18:41 -04:00
filtered
60c0ce228a Generate a new workflow id when using "save as" (#3407) 2025-04-11 15:43:10 -04:00
Benjamin Lu
1990f25638 Allow keybind overwriting (#3393)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-11 14:28:36 -04:00
Comfy Org PR Bot
30c473db77 [chore] Update Comfy Registry API types from comfy-api@a28605f (#3405)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-04-11 14:02:37 -04:00
Chenlei Hu
2371288fed [Bug] Fix widgets values migration (#3404) 2025-04-11 13:44:14 -04:00
Christian Byrne
2337fe6f8e [Manager] Fix hot reload after install of missing node pack (#3397)
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-04-11 12:55:02 -04:00
Benjamin Lu
25e6386b2a Do not trigger autosave on workflows that are not persisted (#3400)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-11 12:53:49 -04:00
Chenlei Hu
a03841cb1a [lint] Enable plugin vue recommended rules (#3403) 2025-04-11 12:53:20 -04:00
Chenlei Hu
dc5d7ea1be [lint] Enforce @typescript-eslint/no-floating-promises (#3402) 2025-04-11 12:19:22 -04:00
Christian Byrne
59e20964a0 [Manager] Add fallback to infer node pack when metadata is missing from workflow (#3396) 2025-04-11 09:50:30 -04:00
Comfy Org PR Bot
8f00d8ca6a 1.16.5 (#3394)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-10 21:29:17 -04:00
Christian Byrne
05e0036898 [Manager] Button to open manager from Missing Nodes dialog (#3395) 2025-04-10 21:28:42 -04:00
Benjamin Lu
9e7690405a keep user input on top of selection toolbox (#3389)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-10 20:50:19 -04:00
Christian Byrne
d687ea2cde [Manager] Fix infinite fetch attempts when response is empty (#3392) 2025-04-10 16:39:54 -07:00
Chenlei Hu
c801a0c854 Attach frontend version metadata on workflow export (#3386) 2025-04-10 14:19:54 -04:00
Chenlei Hu
615c183059 Disable reroute migration (2/2) (#3385) 2025-04-10 14:06:09 -04:00
Chenlei Hu
27c8389b9f [Cleanup] Remove ComfyApp.serializeGraph (#3383) 2025-04-10 11:50:27 -04:00
Chenlei Hu
261f671ef0 Disable native reroute migration (#3379)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-10 08:50:07 -04:00
Benjamin Lu
22ae30132c Add tag pi-language icon for Comfy.Locale setting (#3378)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-04-09 22:47:40 -04:00
Chenlei Hu
7d3bf372b0 [Bug] Apply link fixes regardless of fixed state (#3376) 2025-04-09 20:28:49 -04:00
Comfy Org PR Bot
cd35373c25 1.16.4 (#3372)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-09 19:41:01 -04:00
Chenlei Hu
a500a96c4a [TS] Fix widget GET_CONFIG on loading primitive nodes (#3374) 2025-04-09 17:48:28 -04:00
Chenlei Hu
dc9ea44f3a Prevent widget value serialization for media preview widgets (#3370) 2025-04-09 17:01:02 -04:00
Christian Byrne
2dc33b1eb9 [Manager] Remove outdated comment (#3371) 2025-04-09 16:51:16 -04:00
Comfy Org PR Bot
ed8f9a5a4f [chore] Update litegraph to 0.13.1 (#3369)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-09 16:45:31 -04:00
Chenlei Hu
6e72e1924e Update ComfyUI_devtools version in CI (#3368) 2025-04-09 14:59:07 -04:00
Christian Byrne
f7854a4e0b [Manager] Fix node pack icons not displaying in search results (#3366) 2025-04-09 13:51:47 -04:00
Chenlei Hu
05023b7889 Migrate defaultInput widget option (#3364)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-09 13:51:34 -04:00
Chenlei Hu
609496957b [Cleanup] Remove WidgetOptions.forceInput (#3362) 2025-04-09 11:36:35 -04:00
Chenlei Hu
a879f413bb [Cleanup] Remove WidgetOptions.inputIsOptional (#3361) 2025-04-09 10:45:51 -04:00
Comfy Org PR Bot
21d679a662 1.16.3 (#3357)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-04-08 23:05:29 -04:00
Chenlei Hu
34f9603961 [Feature Request]: Add resize keybinding (#3356) (#3358)
Co-authored-by: Emanuel F. <70411130+efrancisworks@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-04-08 23:05:21 -04:00
Christian Byrne
cf27a896f3 [Manager] Handle display of git hash versions for NIGHTLY node packs (#3359) 2025-04-08 23:00:36 -04:00
Chenlei Hu
e9a98161ca [Bug] Fix converted widget compression on export (#3354) 2025-04-08 20:20:19 -04:00
Christian Byrne
fa132e4106 Don't translate 'Stable Zero' (#3352) 2025-04-08 19:51:11 -04:00
Chenlei Hu
a489c19b07 Upstream rgthree's link fixer (#3350) 2025-04-08 18:32:43 -04:00
252 changed files with 7897 additions and 1636 deletions

View File

@@ -8,6 +8,15 @@ const vue3CompositionApiBestPractices = [
"Use watch and watchEffect for side effects",
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
"Utilize provide/inject for dependency injection",
"Use vue 3.5 style of default prop declaration. Example:
const { nodes, showTotal = true } = defineProps<{
nodes: ApiNodeCost[]
showTotal?: boolean
}>()
",
"Organize vue component in <template> <script> <style> order",
]
// Folder structure
@@ -40,4 +49,6 @@ const additionalInstructions = `
7. Implement proper error handling
8. Follow Vue 3 style guide and naming conventions
9. Use Vite for fast development and building
10. Use vue-i18n in composition API for any string literals. Place new translation
entries in src/locales/en/main.json.
`;

View File

@@ -30,7 +30,7 @@ jobs:
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be'
ref: '49c8220be49120dbaff85f32813d854d6dff2d05'
- uses: actions/setup-node@v4
with:

View File

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

25
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"recommendations": [
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"figma.figma-vscode-extension",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"hbenl.vscode-test-explorer",
"lokalise.i18n-ally",
"ms-playwright.playwright",
"vitest.explorer",
"vue.volar",
"sonarsource.sonarlint-vscode",
"deque-systems.vscode-axe-linter",
"kisstkondoros.vscode-codemetrics",
"donjayamanne.githistory",
"wix.vscode-import-cost",
"prograhammer.tslint-vue",
"antfu.vite"
]
}

View File

@@ -0,0 +1,126 @@
{
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
"revision": 0,
"last_node_id": 5,
"last_link_id": 3,
"nodes": [
{
"id": 4,
"type": "KSampler",
"pos": [
867.4669799804688,
347.22369384765625
],
"size": [
315,
262
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
},
{
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": 3
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 5,
"type": "PrimitiveInt",
"pos": [
443.0852355957031,
441.131591796875
],
"size": [
315,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "PrimitiveInt"
},
"widgets_values": [
0,
"randomize"
]
}
],
"links": [
[
3,
5,
0,
4,
5,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.9487171000000016,
"offset": [
-325.57196748514497,
-168.13150517966463
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,53 @@
{
"id": "9bcb9451-8319-492a-88d4-fb711d8c3d25",
"revision": 0,
"last_node_id": 6,
"last_link_id": 0,
"nodes": [
{
"id": 6,
"type": "DevToolsNodeWithDefaultInput",
"pos": [
8.39722728729248,
29.727279663085938
],
"size": [
315,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "float_input",
"shape": 7,
"type": "FLOAT",
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithDefaultInput"
},
"widgets_values": [
0,
1,
0
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 2.1600300525920346,
"offset": [
63.071794466403446,
75.18055335968394
]
}
},
"version": 0.4
}

File diff suppressed because it is too large Load Diff

View File

@@ -412,7 +412,7 @@ export class ComfyPage {
}
async getVisibleToastCount() {
return await this.page.locator('.p-toast:visible').count()
return await this.page.locator('.p-toast-message:visible').count()
}
async clickTextEncodeNode1() {

View File

@@ -13,4 +13,9 @@ test.describe('Graph', () => {
})
).toBe(1)
})
test('Validate workflow links', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('bad_link')
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
})
})

View File

@@ -25,6 +25,11 @@ test.describe('Optional input', () => {
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
})
test('Default input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default_input')
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
})
test('Only optional inputs', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('only_optional_inputs')
expect(await comfyPage.getGraphNodesCount()).toBe(1)

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -43,4 +43,15 @@ test.describe('Primitive Node', () => {
'static_primitive_connected.png'
)
})
test('Report missing nodes when connect to missing node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'primitive/primitive_node_connect_missing_node'
)
// Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
await expect(missingNodesWarning).toBeVisible()
})
})

View File

@@ -192,3 +192,19 @@ test.describe('Load audio widget', () => {
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
})
})
test.describe('Unserialized widgets', () => {
test('Unserialized widgets values do not mark graph as modified', async ({
comfyPage
}) => {
// Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized)
await comfyPage.loadWorkflow('widgets/load_image_widget')
// Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts`
await comfyPage.page.mouse.move(10, 10)
await comfyPage.page.mouse.click(10, 10)
// Expect the graph to not be modified
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
})
})

View File

@@ -1,4 +1,5 @@
import pluginJs from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
@@ -20,21 +21,26 @@ export default [
globals: {
...globals.browser,
__COMFYUI_FRONTEND_VERSION__: 'readonly'
},
parser: tseslint.parser,
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2020,
sourceType: 'module',
extraFileExtensions: ['.vue']
}
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
...pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
{
files: ['src/**/*.vue'],
languageOptions: { parserOptions: { parser: tseslint.parser } }
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off'
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
@@ -42,10 +48,12 @@ export default [
'unused-imports': unusedImports
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
'unused-imports/no-unused-imports': 'error'
'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off'
}
}
]

1281
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.16.2",
"version": "1.17.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -44,6 +44,8 @@
"autoprefixer": "^10.4.19",
"chalk": "^5.3.0",
"eslint": "^9.12.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-vue": "^9.27.0",
"fs-extra": "^11.2.0",
@@ -60,7 +62,7 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.17",
"vite": "^5.4.18",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
@@ -71,7 +73,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.13.0",
"@comfyorg/litegraph": "^0.13.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -89,6 +91,7 @@
"algoliasearch": "^5.21.0",
"axios": "^1.8.2",
"dotenv": "^16.4.5",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"jsondiffpatch": "^0.6.0",
"lodash": "^4.17.21",
@@ -101,6 +104,7 @@
"vue": "^3.5.13",
"vue-i18n": "^9.14.3",
"vue-router": "^4.4.3",
"vuefire": "^3.2.1",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -398,6 +398,7 @@ button.comfy-queue-btn {
.graphdialog {
min-height: 1em;
background-color: var(--comfy-menu-bg);
z-index: 41; /* z-index is set to 41 here in order to appear over selection-overlay-container which should have a z-index of 40 */
}
.graphdialog .name {

View File

@@ -1,19 +1,19 @@
<template>
<Splitter
:key="activeSidebarTabId ?? undefined"
class="splitter-overlay-root splitter-overlay"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:key="activeSidebarTabId ?? undefined"
:stateKey="activeSidebarTabId ?? undefined"
stateStorage="local"
:state-key="activeSidebarTabId ?? undefined"
state-storage="local"
>
<SplitterPanel
class="side-bar-panel"
:minSize="10"
:size="20"
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'left'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel"></slot>
<slot name="side-bar-panel" />
</SplitterPanel>
<SplitterPanel :size="100">
@@ -21,26 +21,26 @@
class="splitter-overlay max-w-full"
layout="vertical"
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
stateKey="bottom-panel-splitter"
stateStorage="local"
state-key="bottom-panel-splitter"
state-storage="local"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel"></slot>
<slot name="graph-canvas-panel" />
</SplitterPanel>
<SplitterPanel class="bottom-panel" v-show="bottomPanelVisible">
<slot name="bottom-panel"></slot>
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
<slot name="bottom-panel" />
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
class="side-bar-panel"
:minSize="10"
:size="20"
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'right'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel"></slot>
<slot name="side-bar-panel" />
</SplitterPanel>
</Splitter>
</template>

View File

@@ -5,11 +5,11 @@
:style="positionCSS"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
icon="pi pi-bars"
severity="secondary"
text
size="large"
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"

View File

@@ -1,19 +1,19 @@
<template>
<div
class="batch-count"
v-tooltip.bottom="{
value: $t('menu.batchCount'),
showDelay: 600
}"
class="batch-count"
:aria-label="$t('menu.batchCount')"
>
<InputNumber
class="w-14"
v-model="batchCount"
class="w-14"
:min="minQueueCount"
:max="maxQueueCount"
fluid
showButtons
show-buttons
:pt="{
incrementButton: {
class: 'w-6',

View File

@@ -4,9 +4,8 @@
:style="style"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
>
<div class="actionbar-content flex items-center select-none" ref="panelRef">
<span class="drag-handle cursor-move mr-2 p-0!" ref="dragHandleRef">
</span>
<div ref="panelRef" class="actionbar-content flex items-center select-none">
<span ref="dragHandleRef" class="drag-handle cursor-move mr-2 p-0!" />
<ComfyQueueButton />
</div>
</Panel>
@@ -89,9 +88,9 @@ const setInitialPosition = () => {
}
}
onMounted(setInitialPosition)
watch(visible, (newVisible) => {
watch(visible, async (newVisible) => {
if (newVisible) {
nextTick(setInitialPosition)
await nextTick(setInitialPosition)
}
})

View File

@@ -1,19 +1,19 @@
<template>
<div class="queue-button-group flex">
<SplitButton
class="comfyui-queue-button"
:label="activeQueueModeMenuItem.label"
severity="primary"
size="small"
@click="queuePrompt"
:model="queueModeMenuItems"
data-testid="queue-button"
v-tooltip.bottom="{
value: workspaceStore.shiftDown
? $t('menu.runWorkflowFront')
: $t('menu.runWorkflow'),
showDelay: 600
}"
class="comfyui-queue-button"
:label="activeQueueModeMenuItem.label"
severity="primary"
size="small"
:model="queueModeMenuItems"
data-testid="queue-button"
@click="queuePrompt"
>
<template #icon>
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
@@ -23,15 +23,15 @@
</template>
<template #item="{ item }">
<Button
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
:label="String(item.label)"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"
text
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
/>
</template>
</SplitButton>
@@ -48,8 +48,7 @@
text
:aria-label="$t('menu.interrupt')"
@click="() => commandStore.execute('Comfy.Interrupt')"
>
</Button>
/>
<Button
v-tooltip.bottom="{
value: $t('sideToolbar.queueTab.clearPendingTasks'),
@@ -135,12 +134,12 @@ const hasPendingTasks = computed(
)
const commandStore = useCommandStore()
const queuePrompt = (e: Event) => {
const queuePrompt = async (e: Event) => {
const commandId =
'shiftKey' in e && e.shiftKey
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
commandStore.execute(commandId)
await commandStore.execute(commandId)
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col h-full">
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
<TabList pt:tabList="border-none">
<TabList pt:tab-list="border-none">
<div class="w-full flex justify-between">
<div class="tabs-container">
<Tab

View File

@@ -1,7 +1,7 @@
<template>
<div class="relative overflow-hidden h-full w-full bg-black" ref="rootEl">
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
<div class="p-terminal rounded-none h-full w-full p-2">
<div class="h-full terminal-host" ref="terminalEl"></div>
<div ref="terminalEl" class="h-full terminal-host" />
</div>
</div>
</template>

View File

@@ -24,17 +24,17 @@ const terminalCreated = (
root,
autoRows: true,
autoCols: true,
onResize: () => {
onResize: async () => {
// If we aren't visible, don't resize
if (!terminal.element?.offsetParent) return
terminalApi.resize(terminal.cols, terminal.rows)
await terminalApi.resize(terminal.cols, terminal.rows)
}
})
onMounted(async () => {
offData = terminal.onData(async (message: string) => {
terminalApi.write(message)
await terminalApi.write(message)
})
offOutput = terminalApi.onOutput((message) => {

View File

@@ -1,6 +1,8 @@
<template>
<div class="bg-black h-full w-full">
<p v-if="errorMessage" class="p-4 text-center">{{ errorMessage }}</p>
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>
<ProgressSpinner
v-else-if="loading"
class="relative inset-0 flex justify-center items-center h-full z-10"
@@ -57,7 +59,7 @@ const terminalCreated = (
if (!clientId.value) {
await until(clientId).not.toBeNull()
}
api.subscribeLogs(true)
await api.subscribeLogs(true)
api.addEventListener('logs', logReceived)
}
@@ -76,9 +78,9 @@ const terminalCreated = (
loading.value = false
})
onUnmounted(() => {
onUnmounted(async () => {
if (api.clientId) {
api.subscribeLogs(false)
await api.subscribeLogs(false)
}
api.removeEventListener('logs', logReceived)
})

View File

@@ -0,0 +1,75 @@
<template>
<div class="flex flex-col gap-3 h-full">
<div class="flex justify-between text-xs">
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
</div>
<ScrollPanel class="flex-grow h-0">
<div class="flex flex-col gap-2">
<div
v-for="node in nodes"
:key="node.name"
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
>
<div class="flex items-center gap-2">
<span class="text-base font-medium leading-tight">{{
node.name
}}</span>
</div>
<div class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<span class="text-base font-medium leading-tight">
{{ node.cost.toFixed(costPrecision) }}
</span>
</div>
</div>
</div>
</ScrollPanel>
<template v-if="showTotal && nodes.length > 1">
<Divider class="my-2" />
<div class="flex justify-between items-center border-t px-3">
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
<div class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-yellow-500 p-1"
/>
<span>{{ totalCost.toFixed(costPrecision) }}</span>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import Tag from 'primevue/tag'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ApiNodeCost } from '@/types/apiNodeTypes'
const { t } = useI18n()
const {
nodes,
showTotal = true,
costPrecision = 3
} = defineProps<{
nodes: ApiNodeCost[]
showTotal?: boolean
costPrecision?: number
}>()
const totalCost = computed(() =>
nodes.reduce((sum, node) => sum + node.cost, 0)
)
</script>

View File

@@ -5,8 +5,8 @@
<SelectButton
v-model="selectedColorOption"
:options="colorOptionsWithCustom"
optionLabel="name"
dataKey="value"
option-label="name"
data-key="value"
:allow-empty="false"
>
<template #option="slotProps">
@@ -18,8 +18,8 @@
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
></div>
<i v-else class="pi pi-palette text-lg"></i>
/>
<i v-else class="pi pi-palette text-lg" />
</template>
</SelectButton>
<ColorPicker

View File

@@ -8,22 +8,22 @@
<img
v-if="contain"
:src="src"
@error="handleImageError"
:data-test="src"
class="comfy-image-blur"
:style="{ 'background-image': `url(${src})` }"
:alt="alt"
@error="handleImageError"
/>
<img
:src="src"
@error="handleImageError"
class="comfy-image-main"
:class="classProp"
:alt="alt"
@error="handleImageError"
/>
</span>
<div v-if="imageBroken" class="broken-image-placeholder">
<i class="pi pi-image"></i>
<i class="pi pi-image" />
<span>{{ $t('g.imageFailedToLoad') }}</span>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<div ref="container"></div>
<div ref="container" />
</template>
<script setup lang="ts">

View File

@@ -6,14 +6,14 @@
<SelectButton
v-model="selectedIcon"
:options="iconOptions"
optionLabel="name"
dataKey="value"
option-label="name"
data-key="value"
>
<template #option="slotProps">
<i
:class="['pi', slotProps.option.value, 'mr-2']"
:style="{ color: finalColor }"
></i>
/>
</template>
</SelectButton>
</div>
@@ -30,14 +30,14 @@
<Button
:label="$t('g.reset')"
icon="pi pi-refresh"
@click="resetCustomization"
class="p-button-text"
@click="resetCustomization"
/>
<Button
:label="$t('g.confirm')"
icon="pi pi-check"
@click="confirmCustomization"
autofocus
@click="confirmCustomization"
/>
</template>
</Dialog>

View File

@@ -1,7 +1,9 @@
<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">
{{ col.header }}
</div>
<div>
{{ formatValue(props.device[col.field], col.field) }}
</div>

View File

@@ -6,19 +6,19 @@
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
<InputText
v-else
ref="inputRef"
v-model:modelValue="inputValue"
v-focus
type="text"
size="small"
fluid
v-model:modelValue="inputValue"
ref="inputRef"
@keyup.enter="blurInputElement"
@click.stop
:pt="{
root: {
onBlur: finishEditing
}
}"
v-focus
@keyup.enter="blurInputElement"
@click.stop
/>
</div>
</template>
@@ -45,10 +45,10 @@ const finishEditing = () => {
}
watch(
() => isEditing,
(newVal) => {
async (newVal) => {
if (newVal) {
inputValue.value = modelValue
nextTick(() => {
await nextTick(() => {
if (!inputRef.value) return
const fileName = inputValue.value.includes('.')
? inputValue.value.split('.').slice(0, -1).join('.')

View File

@@ -2,7 +2,7 @@
<template>
<div class="flex flex-col">
<div class="flex flex-row items-center gap-2">
<i class="pi pi-check text-green-500" v-if="status === 'completed'" />
<i v-if="status === 'completed'" class="pi pi-check text-green-500" />
<div class="file-info">
<div class="file-details">
<span class="file-type" :title="hint">{{ label }}</span>
@@ -14,20 +14,20 @@
<div class="file-action">
<Button
v-if="status === null || status === 'error'"
class="file-action-button"
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="!!props.error"
@click="triggerDownload"
v-if="status === null || status === 'error'"
icon="pi pi-download"
@click="triggerDownload"
/>
</div>
</div>
<div
class="flex flex-row items-center gap-2"
v-if="status === 'in_progress' || status === 'paused'"
class="flex flex-row items-center gap-2"
>
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
@@ -39,36 +39,36 @@
/>
<Button
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
@click="triggerPauseDownload"
v-if="status === 'in_progress'"
icon="pi pi-pause-circle"
v-tooltip.top="t('electronFileDownload.pause')"
/>
<Button
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
@click="triggerResumeDownload"
icon="pi pi-pause-circle"
@click="triggerPauseDownload"
/>
<Button
v-if="status === 'paused'"
icon="pi pi-play-circle"
v-tooltip.top="t('electronFileDownload.resume')"
/>
<Button
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
@click="triggerCancelDownload"
icon="pi pi-play-circle"
@click="triggerResumeDownload"
/>
<Button
v-tooltip.top="t('electronFileDownload.cancel')"
class="file-action-button"
size="small"
outlined
:disabled="!!props.error"
icon="pi pi-times-circle"
severity="danger"
v-tooltip.top="t('electronFileDownload.cancel')"
@click="triggerCancelDownload"
/>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<component v-if="extension.type === 'vue'" :is="extension.component" />
<component :is="extension.component" v-if="extension.type === 'vue'" />
<div
v-else
:ref="
@@ -11,7 +11,7 @@
)
}
"
></div>
/>
</template>
<script setup lang="ts">

View File

@@ -10,7 +10,7 @@
:src="modelValue"
class="max-w-full max-h-full object-contain"
/>
<i v-else class="pi pi-image text-gray-400 text-xl"></i>
<i v-else class="pi pi-image text-gray-400 text-xl" />
</div>
<div class="flex flex-col gap-2">

View File

@@ -3,26 +3,26 @@
<div class="flex flex-row items-center gap-2">
<div class="form-label flex flex-grow items-center">
<span
:id="`${props.id}-label`"
class="text-muted"
:class="props.labelClass"
:id="`${props.id}-label`"
>
<slot name="name-prefix"></slot>
<slot name="name-prefix" />
{{ props.item.name }}
<i
v-if="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
v-tooltip="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
/>
<slot name="name-suffix"></slot>
<slot name="name-suffix" />
</span>
</div>
<div class="form-input flex justify-end">
<component
:is="markRaw(getFormComponent(props.item))"
:id="props.id"
:aria-labelledby="`${props.id}-label`"
v-model:modelValue="formValue"
:aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)"
/>
</div>

View File

@@ -1,26 +1,26 @@
<template>
<div class="input-knob flex flex-row items-center gap-2">
<Knob
:modelValue="modelValue"
@update:modelValue="updateValue"
:valueTemplate="displayValue"
:model-value="modelValue"
:value-template="displayValue"
class="knob-part"
:class="knobClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="updateValue"
/>
<InputNumber
:modelValue="modelValue"
@update:modelValue="updateValue"
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allowEmpty="false"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>

View File

@@ -1,25 +1,25 @@
<template>
<div class="input-slider flex flex-row items-center gap-2">
<Slider
:modelValue="modelValue"
@update:modelValue="(value) => updateValue(value as number)"
:model-value="modelValue"
class="slider-part"
:class="sliderClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="(value) => updateValue(value as number)"
/>
<InputNumber
:modelValue="modelValue"
@update:modelValue="updateValue"
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allowEmpty="false"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>

View File

@@ -3,14 +3,16 @@
<Card>
<template #content>
<div class="flex flex-col items-center">
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem"></i>
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem" />
<h3>{{ title }}</h3>
<p class="whitespace-pre-line text-center">{{ message }}</p>
<p class="whitespace-pre-line text-center">
{{ message }}
</p>
<Button
v-if="buttonLabel"
:label="buttonLabel"
@click="$emit('action')"
class="p-button-text"
@click="$emit('action')"
/>
</div>
</template>

View File

@@ -22,7 +22,7 @@
class="p-button-icon pi pi-refresh transition-all"
:class="{ 'opacity-0': active }"
data-pc-section="icon"
></span>
/>
<span class="p-button-label" data-pc-section="label">&nbsp;</span>
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
</Button>

View File

@@ -11,9 +11,9 @@
/>
<InputText
class="search-box-input w-full"
@input="handleInput"
:modelValue="modelValue"
:model-value="modelValue"
:placeholder="placeholder"
@input="handleInput"
/>
<InputIcon v-if="!modelValue" :class="icon" />
<Button
@@ -26,8 +26,8 @@
/>
</IconField>
<div
class="search-filters pt-2 flex flex-wrap gap-2"
v-if="filters?.length"
class="search-filters pt-2 flex flex-wrap gap-2"
>
<SearchFilterChip
v-for="filter in filters"

View File

@@ -1,10 +1,14 @@
<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('g.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">
{{ col.header }}
</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
</template>
</div>
@@ -13,7 +17,9 @@
<Divider />
<div>
<h2 class="text-2xl font-semibold mb-4">{{ $t('g.devices') }}</h2>
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"

View File

@@ -1,11 +1,11 @@
<template>
<Tree
class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class"
v-model:expandedKeys="expandedKeys"
v-model:selectionKeys="selectionKeys"
class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class"
:value="renderedRoot.children"
selectionMode="single"
selection-mode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ context }) => ({
@@ -186,9 +186,9 @@ const menuItems = computed<MenuItem[]>(() =>
{
label: t('g.delete'),
icon: 'pi pi-trash',
command: () => {
command: async () => {
if (menuTargetNode.value) {
deleteCommand(menuTargetNode.value)
await deleteCommand(menuTargetNode.value)
}
},
visible: menuTargetNode.value?.handleDelete !== undefined,

View File

@@ -1,5 +1,6 @@
<template>
<div
ref="container"
:class="[
'tree-node',
{
@@ -8,17 +9,16 @@
'tree-leaf': props.node.leaf
}
]"
ref="container"
>
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="props.node"></slot>
<slot name="before-label" :node="props.node" />
<EditableText
:modelValue="node.label"
:isEditing="isEditing"
:model-value="node.label"
:is-editing="isEditing"
@edit="handleRename"
/>
<slot name="after-label" :node="props.node"></slot>
<slot name="after-label" :node="props.node" />
</span>
<Badge
v-if="showNodeBadgeText"
@@ -30,7 +30,7 @@
<div
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node"></slot>
<slot name="actions" :node="props.node" />
</div>
</div>
</template>

View File

@@ -3,7 +3,7 @@
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
<div :style="gridStyle">
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
<slot name="item" :item="item"> </slot>
<slot name="item" :item="item" />
</div>
</div>
<div

View File

@@ -60,7 +60,7 @@ describe('UrlInput', () => {
})
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
@@ -74,7 +74,7 @@ describe('UrlInput', () => {
validateUrlFn: () => Promise.resolve(true)
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
@@ -88,7 +88,7 @@ describe('UrlInput', () => {
validateUrlFn: () => Promise.resolve(false)
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
@@ -141,14 +141,14 @@ describe('UrlInput', () => {
}
})
wrapper.setProps({ modelValue: 'https://test.com' })
await wrapper.setProps({ modelValue: 'https://test.com' })
await nextTick()
await nextTick()
// Trigger multiple validations in quick succession
wrapper.find('.pi-spinner').trigger('click')
wrapper.find('.pi-spinner').trigger('click')
wrapper.find('.pi-spinner').trigger('click')
await wrapper.find('.pi-spinner').trigger('click')
await wrapper.find('.pi-spinner').trigger('click')
await wrapper.find('.pi-spinner').trigger('click')
await nextTick()
await nextTick()

View File

@@ -13,11 +13,13 @@
>
<template #header>
<component
v-if="item.headerComponent"
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">{{ item.title || ' ' }}</h3>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
</template>
<component
@@ -26,7 +28,7 @@
:maximized="item.dialogComponentProps.maximized"
/>
<template #footer v-if="item.footerComponent">
<template v-if="item.footerComponent" #footer>
<component :is="item.footerComponent" />
</template>
</Dialog>

View File

@@ -0,0 +1,43 @@
<!-- Prompt user that the workflow contains API nodes that needs login to run -->
<template>
<div class="flex flex-col gap-4 max-w-96 h-110 p-2">
<div class="text-2xl font-medium mb-2">
{{ t('apiNodesSignInDialog.title') }}
</div>
<div class="text-base mb-4">
{{ t('apiNodesSignInDialog.message') }}
</div>
<ApiNodesCostBreakdown :nodes="apiNodes" :show-total="true" />
<div class="flex justify-between items-center">
<Button :label="t('g.learnMore')" link />
<div class="flex gap-2">
<Button
:label="t('g.cancel')"
outlined
severity="secondary"
@click="onCancel?.()"
/>
<Button :label="t('g.login')" @click="onLogin?.()" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import ApiNodesCostBreakdown from '@/components/common/ApiNodesCostBreakdown.vue'
import type { ApiNodeCost } from '@/types/apiNodeTypes'
const { t } = useI18n()
const { apiNodes, onLogin, onCancel } = defineProps<{
apiNodes: ApiNodeCost[]
onLogin?: () => void
onCancel?: () => void
}>()
</script>

View File

@@ -2,7 +2,9 @@
<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>
<li v-for="item of itemList" :key="item">
{{ item }}
</li>
</ul>
<Message
v-if="hint"
@@ -18,53 +20,53 @@
:label="$t('g.cancel')"
icon="pi pi-undo"
severity="secondary"
@click="onCancel"
autofocus
@click="onCancel"
/>
<Button
v-if="type === 'default'"
:label="$t('g.confirm')"
severity="primary"
@click="onConfirm"
icon="pi pi-check"
@click="onConfirm"
/>
<Button
v-else-if="type === 'delete'"
:label="$t('g.delete')"
severity="danger"
@click="onConfirm"
icon="pi pi-trash"
@click="onConfirm"
/>
<Button
v-else-if="type === 'overwrite'"
:label="$t('g.overwrite')"
severity="warn"
@click="onConfirm"
icon="pi pi-save"
@click="onConfirm"
/>
<template v-else-if="type === 'dirtyClose'">
<Button
:label="$t('g.no')"
severity="secondary"
@click="onDeny"
icon="pi pi-times"
@click="onDeny"
/>
<Button :label="$t('g.save')" @click="onConfirm" icon="pi pi-save" />
<Button :label="$t('g.save')" icon="pi pi-save" @click="onConfirm" />
</template>
<Button
v-else-if="type === 'reinstall'"
:label="$t('desktopMenu.reinstall')"
severity="warn"
@click="onConfirm"
icon="pi pi-eraser"
@click="onConfirm"
/>
<!-- Invalid - just show a close button. -->
<Button
v-else
:label="$t('g.close')"
severity="primary"
@click="onCancel"
icon="pi pi-times"
@click="onCancel"
/>
</div>
</section>

View File

@@ -45,9 +45,9 @@
/>
<div class="flex gap-4 justify-end">
<FindIssueButton
:errorMessage="error.exceptionMessage"
:repoOwner="repoOwner"
:repoName="repoName"
:error-message="error.exceptionMessage"
:repo-owner="repoOwner"
:repo-name="repoName"
/>
<Button
v-if="reportOpen"

View File

@@ -8,7 +8,9 @@
>
<template #header>
<header class="flex flex-col items-center w-full">
<h2 id="issue-report-title" class="text-4xl">{{ title }}</h2>
<h2 id="issue-report-title" class="text-4xl">
{{ title }}
</h2>
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
</header>
</template>

View File

@@ -7,8 +7,8 @@
/>
<ListBox
:options="uniqueNodes"
optionLabel="label"
scrollHeight="100%"
option-label="label"
scroll-height="100%"
class="comfy-missing-nodes"
:pt="{
list: { class: 'border-none' }
@@ -22,14 +22,17 @@
}}</span>
<Button
v-if="slotProps.option.action"
@click="slotProps.option.action.callback"
:label="slotProps.option.action.text"
size="small"
outlined
@click="slotProps.option.action.callback"
/>
</div>
</template>
</ListBox>
<div class="flex justify-end py-3">
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
</template>
<script setup lang="ts">
@@ -38,7 +41,9 @@ import ListBox from 'primevue/listbox'
import { computed } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { useDialogService } from '@/services/dialogService'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
@@ -64,6 +69,12 @@ const uniqueNodes = computed(() => {
return { label: node }
})
})
const openManager = () => {
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
}
</script>
<style scoped>

View File

@@ -129,9 +129,12 @@ const missingModels = computed(() => {
})
})
onBeforeUnmount(() => {
onBeforeUnmount(async () => {
if (doNotAskAgain.value) {
useSettingStore().set('Comfy.Workflow.ShowMissingModelsWarning', false)
await useSettingStore().set(
'Comfy.Workflow.ShowMissingModelsWarning',
false
)
}
})
</script>

View File

@@ -4,13 +4,15 @@
<InputText
ref="inputRef"
v-model="inputValue"
autofocus
@keyup.enter="onConfirm"
@focus="selectAllText"
autofocus
/>
<label>{{ message }}</label>
</FloatLabel>
<Button @click="onConfirm">{{ $t('g.confirm') }}</Button>
<Button @click="onConfirm">
{{ $t('g.confirm') }}
</Button>
</div>
</template>

View File

@@ -2,18 +2,18 @@
<div class="settings-container">
<ScrollPanel class="settings-sidebar flex-shrink-0 p-2 w-48 2xl:w-64">
<SearchBox
class="settings-search-box w-full mb-2"
v-model:modelValue="searchQuery"
@search="handleSearch"
class="settings-search-box w-full mb-2"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
@search="handleSearch"
/>
<Listbox
v-model="activeCategory"
:options="categories"
optionLabel="translatedLabel"
scrollHeight="100%"
:optionDisabled="
option-label="translatedLabel"
scroll-height="100%"
:option-disabled="
(option: SettingTreeNode) =>
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
"
@@ -25,7 +25,7 @@
<Tabs :value="tabValue" :lazy="true" class="settings-content h-full w-full">
<TabPanels class="settings-tab-panels h-full w-full pr-0">
<PanelTemplate value="Search Results">
<SettingsPanel :settingGroups="searchResults" />
<SettingsPanel :setting-groups="searchResults" />
</PanelTemplate>
<PanelTemplate
@@ -38,7 +38,7 @@
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
</template>
<SettingsPanel :settingGroups="sortedGroups(category)" />
<SettingsPanel :setting-groups="sortedGroups(category)" />
</PanelTemplate>
<AboutPanel />
@@ -293,6 +293,10 @@ watch(activeCategory, (_, oldValue) => {
overflow: hidden;
}
.settings-content {
overflow-x: auto;
}
@media (max-width: 768px) {
.settings-container {
flex-direction: column;

View File

@@ -0,0 +1,124 @@
<template>
<div class="w-96 p-2">
<!-- Header -->
<div class="flex flex-col gap-4 mb-8">
<h1 class="text-2xl font-medium leading-normal my-0">
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
</h1>
<p class="text-base my-0">
<span class="text-muted">{{
isSignIn
? t('auth.login.newUser')
: t('auth.signup.alreadyHaveAccount')
}}</span>
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
}}</span>
</p>
</div>
<!-- Form -->
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
<SignUpForm v-else @submit="signInWithEmail" />
<!-- Divider -->
<Divider align="center" layout="horizontal" class="my-8">
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
</Divider>
<!-- Social Login Buttons -->
<div class="flex flex-col gap-6">
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGoogle')
: t('auth.signup.signUpWithGoogle')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGithub')
: t('auth.signup.signUpWithGithub')
}}
</Button>
</div>
<!-- Terms -->
<p class="text-xs text-muted mt-8">
{{ t('auth.login.termsText') }}
<span class="text-blue-500 cursor-pointer">{{
t('auth.login.termsLink')
}}</span>
{{ t('auth.login.andText') }}
<span class="text-blue-500 cursor-pointer">{{
t('auth.login.privacyLink')
}}</span
>.
</p>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { SignInData, SignUpData } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import SignInForm from './signin/SignInForm.vue'
import SignUpForm from './signin/SignUpForm.vue'
const { t } = useI18n()
const { onSuccess } = defineProps<{
onSuccess: () => void
}>()
const firebaseAuthStore = useFirebaseAuthStore()
const isSignIn = ref(true)
const toggleState = () => {
isSignIn.value = !isSignIn.value
}
const signInWithGoogle = () => {
// Implement Google login
console.log(isSignIn.value)
console.log('Google login clicked')
onSuccess()
}
const signInWithGithub = () => {
// Implement Github login
console.log(isSignIn.value)
console.log('Github login clicked')
onSuccess()
}
const signInWithEmail = async (values: SignInData | SignUpData) => {
const { email, password } = values
if (isSignIn.value) {
await firebaseAuthStore.login(email, password)
} else {
await firebaseAuthStore.register(email, password)
}
onSuccess()
}
</script>

View File

@@ -1,11 +1,10 @@
<template>
<Button
@click="openGitHubIssues"
:label="$t('g.findIssues')"
severity="secondary"
icon="pi pi-github"
>
</Button>
@click="openGitHubIssues"
/>
</template>
<script setup lang="ts">

View File

@@ -1,8 +1,8 @@
<template>
<Form
v-slot="$form"
@submit="submit"
:resolver="zodResolver(issueReportSchema)"
@submit="submit"
>
<Panel :pt="$attrs.pt as any">
<template #header>
@@ -33,15 +33,15 @@
>
<Checkbox
v-bind="$field"
:inputId="field.value"
:value="field.value"
v-model="selection"
:input-id="field.value"
:value="field.value"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
<FormField class="mb-4" v-slot="$field" name="details">
<FormField v-slot="$field" class="mb-4" name="details">
<Textarea
v-bind="$field"
class="w-full"
@@ -83,9 +83,9 @@
>
<Checkbox
v-bind="$field"
:inputId="checkbox.value"
:value="checkbox.value"
v-model="contactPrefs"
:input-id="checkbox.value"
:value="checkbox.value"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
@@ -101,7 +101,6 @@
<script setup lang="ts">
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'

View File

@@ -14,8 +14,8 @@
<div class="flex flex-1 relative overflow-hidden">
<ManagerNavSidebar
v-if="isSideNavOpen"
:tabs="tabs"
v-model:selectedTab="selectedTab"
:tabs="tabs"
/>
<div
class="flex-1 overflow-auto pr-80"
@@ -29,7 +29,8 @@
<RegistrySearchBar
v-model:searchQuery="searchQuery"
v-model:searchMode="searchMode"
:searchResults="searchResults"
v-model:sortField="sortField"
:search-results="searchResults"
:suggestions="suggestions"
/>
<div class="flex-1 overflow-auto">
@@ -56,16 +57,16 @@
<VirtualGrid
:items="resultsWithKeys"
:buffer-rows="3"
:gridStyle="GRID_STYLE"
:grid-style="GRID_STYLE"
@approach-end="onApproachEnd"
>
<template #item="{ item }">
<PackCard
@click.stop="(event) => selectNodePack(item, event)"
:node-pack="item"
:is-selected="
selectedNodePacks.some((pack) => pack.id === item.id)
"
@click.stop="(event) => selectNodePack(item, event)"
/>
</template>
</VirtualGrid>
@@ -113,15 +114,12 @@ import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
enum ManagerTab {
All = 'all',
Installed = 'installed',
Workflow = 'workflow',
Missing = 'missing',
UpdateAvailable = 'updateAvailable'
}
const { initialTab = ManagerTab.All } = defineProps<{
initialTab: ManagerTab
}>()
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
@@ -159,7 +157,9 @@ const tabs = ref<TabItem[]>([
icon: 'pi-sync'
}
])
const selectedTab = ref<TabItem>(tabs.value[0])
const selectedTab = ref<TabItem>(
tabs.value.find((tab) => tab.id === initialTab) || tabs.value[0]
)
const {
searchQuery,
@@ -167,6 +167,7 @@ const {
isLoading: isSearchLoading,
searchResults,
searchMode,
sortField,
suggestions
} = useRegistrySearch()
pageNumber.value = 0
@@ -185,14 +186,16 @@ const {
startFetchInstalled,
filterInstalledPack,
installedPacks,
isLoading: isLoadingInstalled
isLoading: isLoadingInstalled,
isReady: installedPacksReady
} = useInstalledPacks()
const {
startFetchWorkflowPacks,
filterWorkflowPack,
workflowPacks,
isLoading: isLoadingWorkflow
isLoading: isLoadingWorkflow,
isReady: workflowPacksReady
} = useWorkflowPacks()
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
@@ -219,45 +222,68 @@ const isOutdatedPack = (pack: components['schemas']['Node']) => {
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
packs.filter(isOutdatedPack)
watch([isUpdateAvailableTab, installedPacks], () => {
if (!isUpdateAvailableTab.value) return
watch(
[isUpdateAvailableTab, installedPacks],
async () => {
if (!isUpdateAvailableTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
} else if (!installedPacks.value.length) {
startFetchInstalled()
} else {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
}
})
if (!isEmptySearch.value) {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
} else if (!installedPacks.value.length) {
await startFetchInstalled()
} else {
displayPacks.value = filterOutdatedPacks(installedPacks.value)
}
},
{ immediate: true }
)
watch([isInstalledTab, installedPacks], () => {
if (!isInstalledTab.value) return
watch(
[isInstalledTab, installedPacks],
async () => {
if (!isInstalledTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = filterInstalledPack(searchResults.value)
} else if (!installedPacks.value.length) {
startFetchInstalled()
} else {
displayPacks.value = installedPacks.value
}
})
if (!isEmptySearch.value) {
displayPacks.value = filterInstalledPack(searchResults.value)
} else if (
!installedPacks.value.length &&
!installedPacksReady.value &&
!isLoadingInstalled.value
) {
await startFetchInstalled()
} else {
displayPacks.value = installedPacks.value
}
},
{ immediate: true }
)
watch([isMissingTab, isWorkflowTab, workflowPacks], () => {
if (!isWorkflowTab.value && !isMissingTab.value) return
watch(
[isMissingTab, isWorkflowTab, workflowPacks, installedPacks],
async () => {
if (!isWorkflowTab.value && !isMissingTab.value) return
if (!isEmptySearch.value) {
displayPacks.value = isMissingTab.value
? filterMissingPacks(filterWorkflowPack(searchResults.value))
: filterWorkflowPack(searchResults.value)
} else if (!workflowPacks.value.length) {
startFetchWorkflowPacks()
} else {
displayPacks.value = isMissingTab.value
? filterMissingPacks(workflowPacks.value)
: workflowPacks.value
}
})
if (!isEmptySearch.value) {
displayPacks.value = isMissingTab.value
? filterMissingPacks(filterWorkflowPack(searchResults.value))
: filterWorkflowPack(searchResults.value)
} else if (
!workflowPacks.value.length &&
!isLoadingWorkflow.value &&
!workflowPacksReady.value
) {
await startFetchWorkflowPacks()
if (isMissingTab.value) {
await startFetchInstalled()
}
} else {
displayPacks.value = isMissingTab.value
? filterMissingPacks(workflowPacks.value)
: workflowPacks.value
}
},
{ immediate: true }
)
watch([isAllTab, searchResults], () => {
if (!isAllTab.value) return
@@ -267,25 +293,33 @@ watch([isAllTab, searchResults], () => {
const onResultsChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
displayPacks.value = filterInstalledPack(searchResults.value)
displayPacks.value = isEmptySearch.value
? installedPacks.value
: filterInstalledPack(searchResults.value)
break
case ManagerTab.Workflow:
displayPacks.value = filterWorkflowPack(searchResults.value)
displayPacks.value = isEmptySearch.value
? workflowPacks.value
: filterWorkflowPack(searchResults.value)
break
case ManagerTab.Missing:
displayPacks.value = filterMissingPacks(
filterWorkflowPack(searchResults.value)
)
if (!isEmptySearch.value) {
displayPacks.value = filterMissingPacks(
filterWorkflowPack(searchResults.value)
)
}
break
case ManagerTab.UpdateAvailable:
displayPacks.value = filterOutdatedPacks(searchResults.value)
displayPacks.value = isEmptySearch.value
? filterOutdatedPacks(installedPacks.value)
: filterOutdatedPacks(searchResults.value)
break
default:
displayPacks.value = searchResults.value
}
}
watch(searchResults, onResultsChange, { flush: 'pre' })
watch(searchResults, onResultsChange, { flush: 'post' })
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
const isLoading = computed(() => {

View File

@@ -6,8 +6,8 @@
<Listbox
v-model="selectedTab"
:options="tabs"
optionLabel="label"
listStyle="max-height:unset"
option-label="label"
list-style="max-height:unset"
class="w-full border-0 bg-transparent shadow-none"
:pt="{
list: { class: 'p-5' },
@@ -17,7 +17,7 @@
>
<template #option="slotProps">
<div class="text-left flex items-center">
<i :class="['pi', slotProps.option.icon, 'mr-3']"></i>
<i :class="['pi', slotProps.option.icon, 'mr-3']" />
<span class="text-lg">{{ slotProps.option.label }}</span>
</div>
</template>

View File

@@ -10,7 +10,7 @@
<i
class="pi pi-circle-fill mr-1.5 text-[0.6rem] p-0"
:style="{ opacity: 0.8 }"
></i>
/>
{{ $t(`manager.status.${statusLabel}`) }}
</Message>
</template>

View File

@@ -39,6 +39,9 @@ import PackVersionSelectorPopover from '@/components/dialog/content/manager/Pack
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { SelectedVersion } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import { isSemVer } from '@/utils/formatUtil'
const TRUNCATED_HASH_LENGTH = 7
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
@@ -50,11 +53,13 @@ const managerStore = useComfyManagerStore()
const installedVersion = computed(() => {
if (!nodePack.id) return SelectedVersion.NIGHTLY
return (
const version =
managerStore.installedPacks[nodePack.id]?.ver ??
nodePack.latest_version?.version ??
SelectedVersion.NIGHTLY
)
// If Git hash, truncate to 7 characters
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
})
const toggleVersionSelector = (event: Event) => {

View File

@@ -75,6 +75,7 @@ import {
SelectedVersion
} from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import { isSemVer } from '@/utils/formatUtil'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
@@ -93,12 +94,26 @@ const isQueueing = ref(false)
const selectedVersion = ref<string>(SelectedVersion.LATEST)
onMounted(() => {
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
selectedVersion.value =
nodePack.publisher?.name === 'Unclaimed'
? SelectedVersion.NIGHTLY
: nodePack.latest_version?.version ?? SelectedVersion.NIGHTLY
// Use NIGHTLY when version is a Git hash
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
})
const getInitialSelectedVersion = () => {
if (!nodePack.id) return
// If unclaimed, set selected version to nightly
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
// If node pack is installed, set selected version to the installed version
if (managerStore.isPackInstalled(nodePack.id))
return managerStore.getInstalledPackVersion(nodePack.id)
// If node pack is not installed, set selected version to latest
return nodePack.latest_version?.version
}
const fetchVersions = async () => {
if (!nodePack?.id) return []
return (await registryService.getPackVersions(nodePack.id)) || []

View File

@@ -43,7 +43,9 @@ vi.mock('@/stores/comfyManagerStore', () => ({
installPack: {
call: mockInstallPack,
clear: vi.fn()
}
},
isPackInstalled: vi.fn(() => false),
getInstalledPackVersion: vi.fn(() => undefined)
}))
}))

View File

@@ -6,12 +6,12 @@
'w-full': fullWidth,
'w-min-content': !fullWidth
}"
:disabled="isInstalling"
:disabled="loading"
v-bind="$attrs"
@click="onClick"
>
<span class="py-2.5 px-3">
<template v-if="isInstalling">
<template v-if="loading">
{{ loadingMessage ?? $t('g.loading') }}
</template>
<template v-else>
@@ -23,9 +23,6 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { inject, ref } from 'vue'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
const {
label,
@@ -33,6 +30,7 @@ const {
fullWidth = false
} = defineProps<{
label: string
loading?: boolean
loadingMessage?: string
fullWidth?: boolean
}>()
@@ -45,10 +43,7 @@ defineOptions({
inheritAttrs: false
})
const isInstalling = inject(IsInstallingKey, ref(false))
const onClick = (): void => {
isInstalling.value = true
emit('action')
}
</script>

View File

@@ -5,8 +5,10 @@
nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install')
"
severity="secondary"
:loading="isInstalling"
:loading-message="$t('g.installing')"
@action="installAllPacks"
@click="onClick"
/>
</template>
@@ -31,6 +33,10 @@ const { nodePacks } = defineProps<{
const isInstalling = inject(IsInstallingKey, ref(false))
const onClick = (): void => {
isInstalling.value = true
}
const managerStore = useComfyManagerStore()
const createPayload = (installItem: NodePack) => {

View File

@@ -4,7 +4,10 @@
<div class="top-0 z-10 px-6 pt-6 w-full">
<InfoPanelHeader :node-packs="[nodePack]" />
</div>
<div class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar">
<div
ref="scrollContainer"
class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar"
>
<div class="mb-6">
<MetadataRow
v-if="isPackInstalled(nodePack.id)"
@@ -46,7 +49,7 @@
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { useScroll, whenever } from '@vueuse/core'
import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -70,6 +73,8 @@ const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const scrollContainer = ref<HTMLElement | null>(null)
const managerStore = useComfyManagerStore()
const isInstalled = computed(() => managerStore.isPackInstalled(nodePack.id))
const isInstalling = ref(false)
@@ -103,6 +108,17 @@ const infoItems = computed<InfoItem[]>(() => [
: undefined
}
])
const { y } = useScroll(scrollContainer, {
eventListenerOptions: {
passive: true
}
})
const onNodePackChange = () => {
y.value = 0
}
whenever(() => nodePack, onNodePackChange, { immediate: true, deep: true })
</script>
<style scoped>
.hidden-scrollbar {

View File

@@ -31,7 +31,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
@@ -46,7 +46,14 @@ const { nodePacks } = defineProps<{
const managerStore = useComfyManagerStore()
const isAllInstalled = computed(() =>
nodePacks.every((nodePack) => managerStore.isPackInstalled(nodePack.id))
const isAllInstalled = ref(false)
watch(
[() => nodePacks, () => managerStore.installedPacks],
() => {
isAllInstalled.value = nodePacks.every((nodePack) =>
managerStore.isPackInstalled(nodePack.id)
)
},
{ immediate: true }
)
</script>

View File

@@ -31,28 +31,29 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { computed } from 'vue'
import { computed, onUnmounted } from 'vue'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components } from '@/types/comfyRegistryTypes'
const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]
}>()
const comfyRegistryService = useComfyRegistryService()
const { getNodeDefs } = useComfyRegistryStore()
const getPackNodes = async (pack: components['schemas']['Node']) => {
if (!comfyRegistryService.packNodesAvailable(pack)) return []
return comfyRegistryService.getNodeDefs({
if (!pack.latest_version?.version) return []
const nodeDefs = await getNodeDefs.call({
packId: pack.id,
versionId: pack.latest_version?.id
version: pack.latest_version?.version
})
return nodeDefs?.comfy_nodes ?? []
}
const { state: allNodeDefs } = useAsyncState(
@@ -69,4 +70,8 @@ const totalNodesCount = computed(() =>
0
)
)
onUnmounted(() => {
getNodeDefs.cancel()
})
</script>

View File

@@ -2,15 +2,19 @@
<div class="overflow-hidden">
<Tabs :value="activeTab">
<TabList>
<Tab value="description">{{ $t('g.description') }}</Tab>
<Tab value="nodes">{{ $t('g.nodes') }}</Tab>
<Tab value="description">
{{ $t('g.description') }}
</Tab>
<Tab value="nodes">
{{ $t('g.nodes') }}
</Tab>
</TabList>
<TabPanels class="overflow-auto">
<TabPanel value="description">
<DescriptionTabPanel :node-pack="nodePack" />
</TabPanel>
<TabPanel value="nodes">
<NodesTabPanel :node-pack="nodePack" />
<NodesTabPanel :node-pack="nodePack" :node-names="nodeNames" />
</TabPanel>
</TabPanels>
</Tabs>
@@ -23,15 +27,21 @@ import TabList from 'primevue/tablist'
import TabPanel from 'primevue/tabpanel'
import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
import { components } from '@/types/comfyRegistryTypes'
defineProps<{
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const nodeNames = computed(() => {
// @ts-expect-error comfy_nodes is an Algolia-specific field
const { comfy_nodes } = nodePack
return comfy_nodes ?? []
})
const activeTab = ref('description')
</script>

View File

@@ -1,7 +1,9 @@
<template>
<div class="flex flex-col gap-4 text-sm">
<div v-for="(section, index) in sections" :key="index" class="mb-4">
<div class="mb-1">{{ section.title }}</div>
<div class="mb-1">
{{ section.title }}
</div>
<div class="text-muted break-words">
<a
v-if="section.isUrl"
@@ -10,10 +12,7 @@
rel="noopener noreferrer"
class="flex items-center gap-2"
>
<i
v-if="isGitHubLink(section.text)"
class="pi pi-github text-base"
></i>
<i v-if="isGitHubLink(section.text)" class="pi pi-github text-base" />
<span class="break-all">{{ section.text }}</span>
</a>
<MarkdownText v-else :text="section.text" class="text-muted" />

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div v-if="!hasMarkdown" v-text="text" class="break-words"></div>
<div v-if="!hasMarkdown" class="break-words" v-text="text" />
<div v-else class="break-words">
<template v-for="(segment, index) in parsedSegments" :key="index">
<a

View File

@@ -1,47 +1,92 @@
<template>
<div class="flex flex-col gap-4 mt-4 overflow-auto text-sm">
<div v-if="nodeDefs?.length">
<!-- TODO: when registry returns node defs, use them here -->
</div>
<div
v-else
v-for="i in 3"
:key="i"
class="border border-surface-border rounded-lg p-4"
>
<NodePreview
:node-def="placeholderNodeDef"
class="!text-[.625rem] !min-w-full"
<div class="flex flex-col gap-4 mt-4 text-sm">
<template v-if="mappedNodeDefs?.length">
<div
v-for="nodeDef in mappedNodeDefs"
:key="createNodeDefKey(nodeDef)"
class="border border-surface-border rounded-lg p-4"
>
<NodePreview :node-def="nodeDef" class="!text-[.625rem] !min-w-full" />
</div>
</template>
<template v-else-if="isLoading">
<ProgressSpinner />
</template>
<template v-else-if="nodeNames.length">
<div v-for="node in nodeNames" :key="node" class="text-muted truncate">
{{ node }}
</div>
</template>
<template v-else>
<NoResultsPlaceholder
:title="$t('manager.noNodesFound')"
:message="$t('manager.noNodesFoundDescription')"
/>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import NodePreview from '@/components/node/NodePreview.vue'
import { ComfyNodeDef } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { components } from '@/types/comfyRegistryTypes'
import { whenever } from '@vueuse/core'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref, shallowRef, useId } from 'vue'
defineProps<{
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import NodePreview from '@/components/node/NodePreview.vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components, operations } from '@/types/comfyRegistryTypes'
import { registryToFrontendV2NodeDef } from '@/utils/mapperUtil'
type ListComfyNodesResponse =
operations['ListComfyNodes']['responses'][200]['content']['application/json']['comfy_nodes']
const { nodePack, nodeNames } = defineProps<{
nodePack: components['schemas']['Node']
nodeDefs?: components['schemas']['ComfyNode'][]
nodeNames: string[]
}>()
// TODO: when registry returns node defs, use them here
const placeholderNodeDef: ComfyNodeDef = {
name: 'Sample Node',
display_name: 'Sample Node',
description: 'This is a sample node for preview purposes',
inputs: {
input1: { name: 'Input 1', type: 'IMAGE' },
input2: { name: 'Input 2', type: 'CONDITIONING' }
},
outputs: [
{ name: 'Output 1', type: 'IMAGE', index: 0, is_list: false },
{ name: 'Output 2', type: 'MASK', index: 1, is_list: false }
],
category: 'Utility',
output_node: false,
python_module: 'nodes'
const { getNodeDefs } = useComfyRegistryStore()
const isLoading = ref(false)
const registryNodeDefs = shallowRef<ListComfyNodesResponse | null>(null)
const fetchNodeDefs = async () => {
isLoading.value = true
const { id: packId } = nodePack
const version = nodePack.latest_version?.version
if (!packId || !version) {
registryNodeDefs.value = null
} else {
const response = await getNodeDefs.call({
packId,
version,
page: 1,
limit: 256
})
registryNodeDefs.value = response?.comfy_nodes ?? null
}
isLoading.value = false
}
whenever(() => nodePack, fetchNodeDefs, { immediate: true, deep: true })
const toFrontendNodeDef = (nodeDef: components['schemas']['ComfyNode']) => {
try {
return registryToFrontendV2NodeDef(nodeDef, nodePack)
} catch (error) {
return null
}
}
const mappedNodeDefs = computed(() => {
if (!registryNodeDefs.value) return null
return registryNodeDefs.value
.map(toFrontendNodeDef)
.filter((nodeDef) => nodeDef !== null)
})
const createNodeDefKey = (nodeDef: components['schemas']['ComfyNode']) =>
`${nodeDef.category}${nodeDef.comfy_node_name ?? useId()}`
</script>

View File

@@ -7,10 +7,8 @@
:placeholder="$t('manager.searchPlaceholder')"
:complete-on-focus="false"
:delay="8"
optionLabel="query"
option-label="query"
class="w-full"
@complete="stubTrue"
@option-select="onOptionSelect"
:pt="{
pcInputText: {
root: {
@@ -22,8 +20,9 @@
style: 'display: none'
}
}"
>
</AutoComplete>
@complete="stubTrue"
@option-select="onOptionSelect"
/>
</div>
<div class="flex mt-3 text-sm">
<div class="flex gap-6 ml-1">
@@ -57,7 +56,10 @@ import { useI18n } from 'vue-i18n'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
import type { PackField, SearchOption } from '@/types/comfyManagerTypes'
import {
type SearchOption,
SortableAlgoliaField
} from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
const { searchResults } = defineProps<{
@@ -67,7 +69,9 @@ const { searchResults } = defineProps<{
const searchQuery = defineModel<string>('searchQuery')
const searchMode = defineModel<string>('searchMode', { default: 'packs' })
const sortField = defineModel<PackField>('sortField', { default: 'downloads' })
const sortField = defineModel<SortableAlgoliaField>('sortField', {
default: SortableAlgoliaField.Downloads
})
const { t } = useI18n()
@@ -75,11 +79,12 @@ const hasResults = computed(
() => searchQuery.value?.trim() && searchResults?.length
)
const sortOptions: SearchOption<PackField>[] = [
{ id: 'downloads', label: t('manager.sort.downloads') },
{ id: 'name', label: t('g.name') },
{ id: 'rating', label: t('manager.sort.rating') },
{ id: 'category', label: t('g.category') }
const sortOptions: SearchOption<SortableAlgoliaField>[] = [
{ id: SortableAlgoliaField.Downloads, label: t('manager.sort.downloads') },
{ id: SortableAlgoliaField.Created, label: t('manager.sort.created') },
{ id: SortableAlgoliaField.Updated, label: t('manager.sort.updated') },
{ id: SortableAlgoliaField.Publisher, label: t('manager.sort.publisher') },
{ id: SortableAlgoliaField.Name, label: t('g.name') }
]
const filterOptions: SearchOption<string>[] = [
{ id: 'packs', label: t('manager.filter.nodePack') },

View File

@@ -2,11 +2,10 @@
<div class="flex items-center gap-1">
<span class="text-muted">{{ label }}:</span>
<Dropdown
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
:model-value="modelValue"
:options="options"
optionLabel="label"
optionValue="id"
option-label="label"
option-value="id"
class="min-w-[6rem] border-none bg-transparent shadow-none"
:pt="{
input: { class: 'py-0 px-1 border-none' },
@@ -14,6 +13,7 @@
panel: { class: 'shadow-md' },
item: { class: 'py-2 px-3 text-sm' }
}"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
</template>

View File

@@ -7,52 +7,44 @@
<div class="w-full px-4 py-3 flex justify-between items-center border-b">
<div class="flex items-center">
<div class="w-6 h-6 flex items-center justify-center">
<Skeleton shape="circle" width="1.5rem" height="1.5rem"></Skeleton>
<Skeleton shape="circle" width="1.5rem" height="1.5rem" />
</div>
<Skeleton width="5rem" height="1rem" class="ml-2"></Skeleton>
<Skeleton width="5rem" height="1rem" class="ml-2" />
</div>
<Skeleton width="4rem" height="1.75rem" borderRadius="0.75rem"></Skeleton>
<Skeleton width="4rem" height="1.75rem" border-radius="0.75rem" />
</div>
<!-- Card content with icon on left and text on right -->
<div class="flex-1 p-4 flex">
<!-- Left icon - 64x64 -->
<div class="flex-shrink-0 mr-4">
<Skeleton width="4rem" height="4rem" borderRadius="0.5rem"></Skeleton>
<Skeleton width="4rem" height="4rem" border-radius="0.5rem" />
</div>
<!-- Right content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Title -->
<Skeleton width="80%" height="1rem" class="mb-2"></Skeleton>
<Skeleton width="80%" height="1rem" class="mb-2" />
<!-- Description -->
<div class="mb-3">
<Skeleton width="100%" height="0.75rem" class="mb-1"></Skeleton>
<Skeleton width="95%" height="0.75rem" class="mb-1"></Skeleton>
<Skeleton width="90%" height="0.75rem"></Skeleton>
<Skeleton width="100%" height="0.75rem" class="mb-1" />
<Skeleton width="95%" height="0.75rem" class="mb-1" />
<Skeleton width="90%" height="0.75rem" />
</div>
<!-- Tags/Badges -->
<div class="flex gap-2">
<Skeleton
width="4rem"
height="1.5rem"
borderRadius="0.75rem"
></Skeleton>
<Skeleton
width="5rem"
height="1.5rem"
borderRadius="0.75rem"
></Skeleton>
<Skeleton width="4rem" height="1.5rem" border-radius="0.75rem" />
<Skeleton width="5rem" height="1.5rem" border-radius="0.75rem" />
</div>
</div>
</div>
<!-- Card footer - similar to header -->
<div class="w-full px-5 py-4 flex justify-between items-center border-t">
<Skeleton width="4rem" height="0.8rem"></Skeleton>
<Skeleton width="6rem" height="0.8rem"></Skeleton>
<Skeleton width="4rem" height="0.8rem" />
<Skeleton width="6rem" height="0.8rem" />
</div>
</div>
</template>

View File

@@ -1,6 +1,8 @@
<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('g.about') }}
</h2>
<div class="space-y-2">
<a
v-for="badge in aboutPanelStore.badges"
@@ -13,7 +15,7 @@
>
<Tag class="mr-2">
<template #icon>
<i :class="[badge.icon, 'mr-2 text-xl']"></i>
<i :class="[badge.icon, 'mr-2 text-xl']" />
</template>
{{ badge.label }}
</Tag>

View File

@@ -6,11 +6,11 @@
</div>
<div class="actions">
<Select
class="w-44"
v-model="activePaletteId"
class="w-44"
:options="palettes"
optionLabel="name"
optionValue="id"
option-label="name"
option-value="id"
/>
<Button
icon="pi pi-file-export"
@@ -29,8 +29,8 @@
severity="danger"
text
:title="$t('g.delete')"
@click="colorPaletteService.deleteCustomColorPalette(activePaletteId)"
:disabled="!colorPaletteStore.isCustomPalette(activePaletteId)"
@click="colorPaletteService.deleteCustomColorPalette(activePaletteId)"
/>
</div>
</div>
@@ -55,7 +55,7 @@ const { palettes, activePaletteId } = storeToRefs(colorPaletteStore)
const importCustomPalette = async () => {
const palette = await colorPaletteService.importColorPalette()
if (palette) {
settingStore.set('Comfy.ColorPalette', palette.id)
await settingStore.set('Comfy.ColorPalette', palette.id)
}
}
</script>

View File

@@ -10,7 +10,7 @@
<div>
{{ $t('g.currentUser') }}: {{ userStore.currentUser?.username }}
</div>
<Button icon="pi pi-sign-out" @click="logout" text />
<Button icon="pi pi-sign-out" text @click="logout" />
</div>
</Message>
</template>
@@ -22,8 +22,8 @@ import Message from 'primevue/message'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const logout = () => {
userStore.logout()
const logout = async () => {
await userStore.logout()
window.location.reload()
}
</script>

View File

@@ -22,16 +22,16 @@
<div class="flex justify-end">
<Button
:label="$t('g.reloadToApplyChanges')"
@click="applyChanges"
outlined
severity="danger"
@click="applyChanges"
/>
</div>
</Message>
</template>
<DataTable
:value="extensionStore.extensions"
stripedRows
striped-rows
size="small"
:filters="filters"
>
@@ -61,8 +61,8 @@
</template>
<template #body="slotProps">
<ToggleSwitch
:disabled="extensionStore.isExtensionReadOnly(slotProps.data.name)"
v-model="editingEnabledExtensions[slotProps.data.name]"
:disabled="extensionStore.isExtensionReadOnly(slotProps.data.name)"
@change="updateExtensionStatus"
/>
</template>
@@ -116,44 +116,44 @@ const hasChanges = computed(() => {
return changedExtensions.value.length > 0
})
const updateExtensionStatus = () => {
const updateExtensionStatus = async () => {
const editingDisabledExtensionNames = Object.entries(
editingEnabledExtensions.value
)
.filter(([_, enabled]) => !enabled)
.map(([name]) => name)
settingStore.set('Comfy.Extension.Disabled', [
await settingStore.set('Comfy.Extension.Disabled', [
...extensionStore.inactiveDisabledExtensionNames,
...editingDisabledExtensionNames
])
}
const enableAllExtensions = () => {
const enableAllExtensions = async () => {
extensionStore.extensions.forEach((ext) => {
if (extensionStore.isExtensionReadOnly(ext.name)) return
editingEnabledExtensions.value[ext.name] = true
})
updateExtensionStatus()
await updateExtensionStatus()
}
const disableAllExtensions = () => {
const disableAllExtensions = async () => {
extensionStore.extensions.forEach((ext) => {
if (extensionStore.isExtensionReadOnly(ext.name)) return
editingEnabledExtensions.value[ext.name] = false
})
updateExtensionStatus()
await updateExtensionStatus()
}
const disableThirdPartyExtensions = () => {
const disableThirdPartyExtensions = async () => {
extensionStore.extensions.forEach((ext) => {
if (extensionStore.isCoreExtension(ext.name)) return
editingEnabledExtensions.value[ext.name] = false
})
updateExtensionStatus()
await updateExtensionStatus()
}
const applyChanges = () => {

View File

@@ -18,9 +18,9 @@ import { useSettingStore } from '@/stores/settingStore'
const settingStore = useSettingStore()
const show = computed(() => !settingStore.exists('Comfy.UseNewMenu'))
const handleClose = () => {
const handleClose = async () => {
// Explicitly write the current value to the store.
const currentValue = settingStore.get('Comfy.UseNewMenu')
settingStore.set('Comfy.UseNewMenu', currentValue)
await settingStore.set('Comfy.UseNewMenu', currentValue)
}
</script>

View File

@@ -8,16 +8,16 @@
</template>
<DataTable
:value="commandsData"
v-model:selection="selectedCommandData"
:value="commandsData"
:global-filter-fields="['id', 'label']"
:filters="filters"
selectionMode="single"
stripedRows
selection-mode="single"
striped-rows
:pt="{
header: 'px-0'
}"
@rowDblclick="editKeybinding($event.data)"
@row-dblclick="editKeybinding($event.data)"
>
<Column field="actions" header="">
<template #body="slotProps">
@@ -27,11 +27,19 @@
class="p-button-text"
@click="editKeybinding(slotProps.data)"
/>
<Button
icon="pi pi-replay"
class="p-button-text p-button-warn"
:disabled="
!keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
@click="resetKeybinding(slotProps.data)"
/>
<Button
icon="pi pi-trash"
class="p-button-text p-button-danger"
@click="removeKeybinding(slotProps.data)"
:disabled="!slotProps.data.keybinding"
@click="removeKeybinding(slotProps.data)"
/>
</div>
</template>
@@ -55,35 +63,41 @@
<template #body="slotProps">
<KeyComboDisplay
v-if="slotProps.data.keybinding"
:keyCombo="slotProps.data.keybinding.combo"
:isModified="
:key-combo="slotProps.data.keybinding.combo"
:is-modified="
keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
/>
<span v-else>-</span>
</template>
</Column>
<Column field="source" :header="$t('g.source')">
<template #body="slotProps">
<span class="overflow-hidden text-ellipsis">{{
slotProps.data.source || '-'
}}</span>
</template>
</Column>
</DataTable>
<Dialog
class="min-w-96"
v-model:visible="editDialogVisible"
class="min-w-96"
modal
:header="currentEditingCommand?.label"
@hide="cancelEdit"
>
<div>
<InputText
class="mb-2 text-center"
ref="keybindingInput"
:modelValue="newBindingKeyCombo?.toString() ?? ''"
class="mb-2 text-center"
:model-value="newBindingKeyCombo?.toString() ?? ''"
placeholder="Press keys for new binding"
@keydown.stop.prevent="captureKeybinding"
autocomplete="off"
fluid
:invalid="!!existingKeybindingOnCombo"
@keydown.stop.prevent="captureKeybinding"
/>
<Message v-if="existingKeybindingOnCombo" severity="error">
<Message v-if="existingKeybindingOnCombo" severity="warn">
Keybinding already exists on
<Tag
severity="secondary"
@@ -93,23 +107,23 @@
</div>
<template #footer>
<Button
label="Save"
icon="pi pi-check"
@click="saveKeybinding"
:disabled="!!existingKeybindingOnCombo"
:label="existingKeybindingOnCombo ? 'Overwrite' : 'Save'"
:icon="existingKeybindingOnCombo ? 'pi pi-pencil' : 'pi pi-check'"
:severity="existingKeybindingOnCombo ? 'warn' : undefined"
autofocus
@click="saveKeybinding"
/>
</template>
</Dialog>
<Button
v-tooltip="$t('g.resetAllKeybindingsTooltip')"
class="mt-4"
:label="$t('g.reset')"
v-tooltip="$t('g.resetKeybindingsTooltip')"
icon="pi pi-trash"
:label="$t('g.resetAll')"
icon="pi pi-replay"
severity="danger"
fluid
text
@click="resetKeybindings"
@click="resetAllKeybindings"
/>
</PanelTemplate>
</template>
@@ -153,6 +167,7 @@ interface ICommandData {
id: string
keybinding: KeybindingImpl | null
label: string
source?: string
}
const commandsData = computed<ICommandData[]>(() => {
@@ -162,7 +177,8 @@ const commandsData = computed<ICommandData[]>(() => {
`commands.${normalizeI18nKey(command.id)}.label`,
command.label ?? ''
),
keybinding: keybindingStore.getKeybindingByCommandId(command.id)
keybinding: keybindingStore.getKeybindingByCommandId(command.id),
source: command.source
}))
})
@@ -211,14 +227,14 @@ watchEffect(() => {
}
})
function removeKeybinding(commandData: ICommandData) {
async function removeKeybinding(commandData: ICommandData) {
if (commandData.keybinding) {
keybindingStore.unsetKeybinding(commandData.keybinding)
keybindingService.persistUserKeybindings()
await keybindingService.persistUserKeybindings()
}
}
function captureKeybinding(event: KeyboardEvent) {
async function captureKeybinding(event: KeyboardEvent) {
// Allow the use of keyboard shortcuts when adding keyboard shortcuts
if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
switch (event.key) {
@@ -226,7 +242,7 @@ function captureKeybinding(event: KeyboardEvent) {
cancelEdit()
return
case 'Enter':
saveKeybinding()
await saveKeybinding()
return
}
}
@@ -240,7 +256,7 @@ function cancelEdit() {
newBindingKeyCombo.value = null
}
function saveKeybinding() {
async function saveKeybinding() {
if (currentEditingCommand.value && newBindingKeyCombo.value) {
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({
@@ -249,20 +265,30 @@ function saveKeybinding() {
})
)
if (updated) {
keybindingService.persistUserKeybindings()
await keybindingService.persistUserKeybindings()
}
}
cancelEdit()
}
async function resetKeybinding(commandData: ICommandData) {
if (keybindingStore.resetKeybindingForCommand(commandData.id)) {
await keybindingService.persistUserKeybindings()
} else {
console.warn(
`No changes made when resetting keybinding for command: ${commandData.id}`
)
}
}
const toast = useToast()
async function resetKeybindings() {
keybindingStore.resetKeybindings()
async function resetAllKeybindings() {
keybindingStore.resetAllKeybindings()
await keybindingService.persistUserKeybindings()
toast.add({
severity: 'info',
summary: 'Info',
detail: 'Keybindings reset',
detail: 'All keybindings reset',
life: 3000
})
}

View File

@@ -18,14 +18,14 @@
<div class="flex justify-end gap-2">
<Button
:label="$t('serverConfig.revertChanges')"
@click="revertChanges"
outlined
@click="revertChanges"
/>
<Button
:label="$t('serverConfig.restart')"
@click="restartApp"
outlined
severity="danger"
@click="restartApp"
/>
</div>
</Message>
@@ -37,9 +37,9 @@
<p>{{ commandLineArgs }}</p>
<Button
icon="pi pi-clipboard"
@click="copyCommandLineArgs"
severity="secondary"
text
@click="copyCommandLineArgs"
/>
</div>
</Message>
@@ -53,10 +53,10 @@
<h3>{{ $t(`serverConfigCategories.${label}`, label) }}</h3>
<div v-for="item in items" :key="item.name" class="mb-4">
<FormItem
:item="translateItem(item)"
v-model:formValue="item.value"
:id="item.id"
:labelClass="{
v-model:formValue="item.value"
:item="translateItem(item)"
:label-class="{
'text-highlight': item.initialValue !== item.value
}"
/>
@@ -97,16 +97,16 @@ const revertChanges = () => {
serverConfigStore.revertChanges()
}
const restartApp = () => {
electronAPI().restartApp()
const restartApp = async () => {
await electronAPI().restartApp()
}
watch(launchArgs, (newVal) => {
settingStore.set('Comfy.Server.LaunchArgs', newVal)
watch(launchArgs, async (newVal) => {
await settingStore.set('Comfy.Server.LaunchArgs', newVal)
})
watch(serverConfigValues, (newVal) => {
settingStore.set('Comfy.Server.ServerConfigValues', newVal)
watch(serverConfigValues, async (newVal) => {
await settingStore.set('Comfy.Server.ServerConfigValues', newVal)
})
const { copyToClipboard } = useCopyToClipboard()

View File

@@ -1,11 +1,12 @@
<template>
<FormItem
:item="formItem"
:id="setting.id"
:formValue="settingValue"
@update:formValue="updateSettingValue"
:item="formItem"
:form-value="settingValue"
@update:form-value="updateSettingValue"
>
<template #name-prefix>
<Tag v-if="setting.id === 'Comfy.Locale'" class="pi pi-language" />
<Tag v-if="setting.experimental" :value="$t('g.experimental')" />
<Tag
v-if="setting.deprecated"
@@ -68,7 +69,7 @@ const formItem = computed(() => {
const settingStore = useSettingStore()
const settingValue = computed(() => settingStore.get(props.setting.id))
const updateSettingValue = (value: any) => {
settingStore.set(props.setting.id, value)
const updateSettingValue = async (value: any) => {
await settingStore.set(props.setting.id, value)
}
</script>

View File

@@ -0,0 +1,89 @@
<template>
<Form
v-slot="$form"
class="flex flex-col gap-6"
:resolver="zodResolver(signInSchema)"
@submit="onSubmit"
>
<!-- Email Field -->
<div class="flex flex-col gap-2">
<label
class="opacity-80 text-base font-medium mb-2"
for="comfy-org-sign-in-email"
>
{{ t('auth.login.emailLabel') }}
</label>
<InputText
pt:root:id="comfy-org-sign-in-email"
pt:root:autocomplete="email"
class="h-10"
name="email"
type="text"
:placeholder="t('auth.login.emailPlaceholder')"
:invalid="$form.email?.invalid"
/>
<small v-if="$form.email?.invalid" class="text-red-500">{{
$form.email.error.message
}}</small>
</div>
<!-- Password Field -->
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center mb-2">
<label
class="opacity-80 text-base font-medium"
for="comfy-org-sign-in-password"
>
{{ t('auth.login.passwordLabel') }}
</label>
<span class="text-muted text-base font-medium cursor-pointer">
{{ t('auth.login.forgotPassword') }}
</span>
</div>
<Password
input-id="comfy-org-sign-in-password"
pt:pc-input-text:root:autocomplete="current-password"
name="password"
:feedback="false"
toggle-mask
:placeholder="t('auth.login.passwordPlaceholder')"
:class="{ 'p-invalid': $form.password?.invalid }"
fluid
class="h-10"
/>
<small v-if="$form.password?.invalid" class="text-red-500">{{
$form.password.error.message
}}</small>
</div>
<!-- Submit Button -->
<Button
type="submit"
:label="t('auth.login.loginButton')"
class="h-10 font-medium mt-4"
/>
</Form>
</template>
<script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import { useI18n } from 'vue-i18n'
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
const { t } = useI18n()
const emit = defineEmits<{
submit: [values: SignInData]
}>()
const onSubmit = (event: FormSubmitEvent) => {
if (event.valid) {
emit('submit', event.values as SignInData)
}
}
</script>

View File

@@ -0,0 +1,165 @@
<template>
<Form
v-slot="$form"
class="flex flex-col gap-6"
:resolver="zodResolver(signUpSchema)"
@submit="onSubmit"
>
<!-- Email Field -->
<div class="flex flex-col gap-2">
<label
class="opacity-80 text-base font-medium mb-2"
for="comfy-org-sign-up-email"
>
{{ t('auth.signup.emailLabel') }}
</label>
<InputText
pt:root:id="comfy-org-sign-up-email"
pt:root:autocomplete="email"
class="h-10"
name="email"
type="text"
:placeholder="t('auth.signup.emailPlaceholder')"
:invalid="$form.email?.invalid"
/>
<small v-if="$form.email?.invalid" class="text-red-500">{{
$form.email.error.message
}}</small>
</div>
<!-- Password Field -->
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center mb-2">
<label
class="opacity-80 text-base font-medium"
for="comfy-org-sign-up-password"
>
{{ t('auth.signup.passwordLabel') }}
</label>
</div>
<Password
v-model="password"
input-id="comfy-org-sign-up-password"
pt:pc-input-text:root:autocomplete="new-password"
name="password"
:feedback="false"
toggle-mask
:placeholder="t('auth.signup.passwordPlaceholder')"
:class="{ 'p-invalid': $form.password?.invalid }"
fluid
class="h-10"
/>
<div class="flex flex-col gap-1">
<small
v-if="$form.password?.dirty || $form.password?.invalid"
class="text-sm"
>
{{ t('validation.password.requirements') }}:
<ul class="mt-1 space-y-1">
<li
:class="{
'text-red-500': !passwordChecks.length
}"
>
{{ t('validation.password.minLength') }}
</li>
<li
:class="{
'text-red-500': !passwordChecks.uppercase
}"
>
{{ t('validation.password.uppercase') }}
</li>
<li
:class="{
'text-red-500': !passwordChecks.lowercase
}"
>
{{ t('validation.password.lowercase') }}
</li>
<li
:class="{
'text-red-500': !passwordChecks.number
}"
>
{{ t('validation.password.number') }}
</li>
<li
:class="{
'text-red-500': !passwordChecks.special
}"
>
{{ t('validation.password.special') }}
</li>
</ul>
</small>
</div>
</div>
<!-- Confirm Password Field -->
<div class="flex flex-col gap-2">
<label
class="opacity-80 text-base font-medium mb-2"
for="comfy-org-sign-up-confirm-password"
>
{{ t('auth.login.confirmPasswordLabel') }}
</label>
<Password
name="confirmPassword"
input-id="comfy-org-sign-up-confirm-password"
pt:pc-input-text:root:autocomplete="new-password"
:feedback="false"
toggle-mask
:placeholder="t('auth.login.confirmPasswordPlaceholder')"
:class="{ 'p-invalid': $form.confirmPassword?.invalid }"
fluid
class="h-10"
/>
<small v-if="$form.confirmPassword?.error" class="text-red-500">{{
$form.confirmPassword.error.message
}}</small>
</div>
<!-- Submit Button -->
<Button
type="submit"
:label="t('auth.signup.signUpButton')"
class="h-10 font-medium mt-4"
/>
</Form>
</template>
<script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
const { t } = useI18n()
const password = ref('')
// TODO: Use dynamic form to better organize the password checks.
// Ref: https://primevue.org/forms/#dynamic
const passwordChecks = computed(() => ({
length: password.value.length >= 8 && password.value.length <= 32,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /\d/.test(password.value),
special: /[^A-Za-z0-9]/.test(password.value)
}))
const emit = defineEmits<{
submit: [values: SignUpData]
}>()
const onSubmit = (event: FormSubmitEvent) => {
if (event.valid) {
emit('submit', event.values as SignUpData)
}
}
</script>

View File

@@ -70,6 +70,7 @@ import { useI18n } from 'vue-i18n'
import { api } from '@/scripts/api'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useWorkflowService } from '@/services/workflowService'
import {
useComfyManagerStore,
useManagerProgressDialogStore
@@ -96,14 +97,21 @@ const currentTaskName = computed(() => {
})
const handleRestart = async () => {
await useComfyManagerService().rebootComfyUI()
closeDialog()
const onReconnect = async () => {
// Refresh manager state
const onReconnect = () => {
useCommandStore().execute('Comfy.RefreshNodeDefinitions')
comfyManagerStore.clearLogs()
comfyManagerStore.setStale()
// Refresh node definitions
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
// Reload workflow
await useWorkflowService().reloadCurrentWorkflow()
}
useEventListener(api, 'reconnected', onReconnect, { once: true })
await useComfyManagerService().rebootComfyUI()
closeDialog()
}
</script>

View File

@@ -0,0 +1,6 @@
<!-- A dialog header with ComfyOrg logo -->
<template>
<div class="px-2 py-4">
<img src="/assets/images/Comfy_Logo_x32.png" alt="ComfyOrg Logo" />
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<h2 class="px-4">
<i class="pi pi-cog"></i>
<i class="pi pi-cog" />
<span>{{ $t('g.settings') }}</span>
</h2>
</div>

View File

@@ -22,8 +22,8 @@
<TitleEditor />
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
ref="canvasRef"
id="graph-canvas"
ref="canvasRef"
tabindex="1"
class="w-full h-full touch-none"
/>
@@ -150,16 +150,16 @@ const colorPaletteService = useColorPaletteService()
const colorPaletteStore = useColorPaletteStore()
watch(
[() => canvasStore.canvas, () => settingStore.get('Comfy.ColorPalette')],
([canvas, currentPaletteId]) => {
async ([canvas, currentPaletteId]) => {
if (!canvas) return
colorPaletteService.loadColorPalette(currentPaletteId)
await colorPaletteService.loadColorPalette(currentPaletteId)
}
)
watch(
() => colorPaletteStore.activePaletteId,
(newValue) => {
settingStore.set('Comfy.ColorPalette', newValue)
async (newValue) => {
await settingStore.set('Comfy.ColorPalette', newValue)
}
)
@@ -292,7 +292,7 @@ onMounted(async () => {
() => settingStore.get('Comfy.Locale'),
async () => {
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
useWorkflowService().reloadCurrentWorkflow()
await useWorkflowService().reloadCurrentWorkflow()
}
)

View File

@@ -3,36 +3,36 @@
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
>
<Button
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
severity="secondary"
icon="pi pi-plus"
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
:aria-label="$t('graphCanvasMenu.zoomIn')"
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
/>
<Button
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
severity="secondary"
icon="pi pi-minus"
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
:aria-label="$t('graphCanvasMenu.zoomOut')"
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
/>
<Button
v-tooltip.left="t('graphCanvasMenu.fitView')"
severity="secondary"
icon="pi pi-expand"
v-tooltip.left="t('graphCanvasMenu.fitView')"
:aria-label="$t('graphCanvasMenu.fitView')"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
/>
<Button
severity="secondary"
v-tooltip.left="
t(
'graphCanvasMenu.' +
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
) + ' (Space)'
"
severity="secondary"
:aria-label="
t(
'graphCanvasMenu.' +
@@ -49,12 +49,12 @@
</template>
</Button>
<Button
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
severity="secondary"
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
:aria-label="$t('graphCanvasMenu.toggleLinkVisibility')"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
data-testid="toggle-link-visibility-button"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
/>
</ButtonGroup>
</template>
@@ -80,10 +80,10 @@ const linkHidden = computed(
)
let interval: number | null = null
const repeat = (command: string) => {
const repeat = async (command: string) => {
if (interval) return
const cmd = () => commandStore.execute(command)
cmd()
await cmd()
interval = window.setInterval(cmd, 100)
}
const stopRepeat = () => {

View File

@@ -1,14 +1,14 @@
<!-- This component is used to bound the selected items on the canvas. -->
<template>
<div
v-show="visible"
class="selection-overlay-container pointer-events-none z-40"
:class="{
'show-border': showBorder
}"
:style="style"
v-show="visible"
>
<slot></slot>
<slot />
</div>
</template>

View File

@@ -9,16 +9,16 @@
<ColorPickerButton v-show="nodeSelected || groupSelected" />
<Button
v-show="nodeSelected"
severity="secondary"
text
@click="
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
"
data-testid="bypass-button"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
showDelay: 1000
}"
severity="secondary"
text
data-testid="bypass-button"
@click="
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
"
>
<template #icon>
<i-game-icons:detour />
@@ -26,24 +26,24 @@
</Button>
<Button
v-show="nodeSelected || groupSelected"
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Pin.label'),
showDelay: 1000
}"
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
/>
<Button
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
}"
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
<Button
v-show="isRefreshable"
@@ -55,15 +55,15 @@
<Button
v-for="command in extensionToolboxCommands"
:key="command.id"
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
@click="() => commandStore.execute(command.id)"
v-tooltip.top="{
value:
st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,
showDelay: 1000
}"
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
@click="() => commandStore.execute(command.id)"
/>
</Panel>
</template>

View File

@@ -5,8 +5,8 @@
:style="inputStyle"
>
<EditableText
:isEditing="showInput"
:modelValue="editedTitle"
:is-editing="showInput"
:model-value="editedTitle"
@edit="onEdit"
/>
</div>

View File

@@ -17,19 +17,19 @@
class="color-picker-container absolute -top-10 left-1/2"
>
<SelectButton
:modelValue="selectedColorOption"
@update:modelValue="applyColor"
:model-value="selectedColorOption"
:options="colorOptions"
optionLabel="name"
dataKey="value"
option-label="name"
data-key="value"
@update:model-value="applyColor"
>
<template #option="{ option }">
<i
v-tooltip.top="option.localizedName"
class="pi pi-circle-fill"
:style="{
color: isLightTheme ? option.value.light : option.value.dark
}"
v-tooltip.top="option.localizedName"
:data-testid="option.name"
/>
</template>

View File

@@ -1,17 +1,17 @@
<template>
<div
v-show="widgetState.visible"
ref="widgetElement"
class="dom-widget"
:title="tooltip"
ref="widgetElement"
:style="style"
v-show="widgetState.visible"
>
<component
v-if="isComponentWidget(widget)"
:is="widget.component"
:modelValue="widget.value"
@update:modelValue="emit('update:widgetValue', $event)"
v-if="isComponentWidget(widget)"
:model-value="widget.value"
:widget="widget"
@update:model-value="emit('update:widgetValue', $event)"
/>
</div>
</template>

View File

@@ -5,7 +5,7 @@
:options="options"
filter
:placeholder="placeholder"
:maxSelectedLabels="3"
:max-selected-labels="3"
:display="display"
class="w-full"
/>

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