mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-20 04:17:33 +00:00
Compare commits
67 Commits
v1.16.2
...
export-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a98e3286 | ||
|
|
9ce3cccfd4 | ||
|
|
9935b322f0 | ||
|
|
60dd242b23 | ||
|
|
cec0dcbccd | ||
|
|
907632a250 | ||
|
|
45c450cdb9 | ||
|
|
ca85b2b144 | ||
|
|
1f28e6ef33 | ||
|
|
fee444c64b | ||
|
|
851739a768 | ||
|
|
1631665efb | ||
|
|
1a066c7062 | ||
|
|
e45f5bdebb | ||
|
|
c270e7734a | ||
|
|
8d7a21e008 | ||
|
|
29e63baca6 | ||
|
|
b22713daf0 | ||
|
|
c8b8953e0a | ||
|
|
731ce8599d | ||
|
|
ec8e55c1c1 | ||
|
|
04d38f2538 | ||
|
|
1c41db75f8 | ||
|
|
c7a7397000 | ||
|
|
e660e1d678 | ||
|
|
fb19752389 | ||
|
|
d098d6ae4e | ||
|
|
e4a5355f58 | ||
|
|
42c004d41d | ||
|
|
009c389607 | ||
|
|
b449dbd26b | ||
|
|
67835edfca | ||
|
|
60c0ce228a | ||
|
|
1990f25638 | ||
|
|
30c473db77 | ||
|
|
2371288fed | ||
|
|
2337fe6f8e | ||
|
|
25e6386b2a | ||
|
|
a03841cb1a | ||
|
|
dc5d7ea1be | ||
|
|
59e20964a0 | ||
|
|
8f00d8ca6a | ||
|
|
05e0036898 | ||
|
|
9e7690405a | ||
|
|
d687ea2cde | ||
|
|
c801a0c854 | ||
|
|
615c183059 | ||
|
|
27c8389b9f | ||
|
|
261f671ef0 | ||
|
|
22ae30132c | ||
|
|
7d3bf372b0 | ||
|
|
cd35373c25 | ||
|
|
a500a96c4a | ||
|
|
dc9ea44f3a | ||
|
|
2dc33b1eb9 | ||
|
|
ed8f9a5a4f | ||
|
|
6e72e1924e | ||
|
|
f7854a4e0b | ||
|
|
05023b7889 | ||
|
|
609496957b | ||
|
|
a879f413bb | ||
|
|
21d679a662 | ||
|
|
34f9603961 | ||
|
|
cf27a896f3 | ||
|
|
e9a98161ca | ||
|
|
fa132e4106 | ||
|
|
a489c19b07 |
11
.cursorrules
11
.cursorrules
@@ -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.
|
||||
`;
|
||||
|
||||
2
.github/workflows/test-ui.yaml
vendored
2
.github/workflows/test-ui.yaml
vendored
@@ -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:
|
||||
|
||||
@@ -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
25
.vscode/extensions.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
126
browser_tests/assets/bad_link.json
Normal file
126
browser_tests/assets/bad_link.json
Normal 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
|
||||
}
|
||||
53
browser_tests/assets/default_input.json
Normal file
53
browser_tests/assets/default_input.json
Normal 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
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 |
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
1281
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -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"
|
||||
}
|
||||
|
||||
BIN
public/assets/images/Comfy_Logo_x32.png
Normal file
BIN
public/assets/images/Comfy_Logo_x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
75
src/components/common/ApiNodesCostBreakdown.vue
Normal file
75
src/components/common/ApiNodesCostBreakdown.vue
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="container"></div>
|
||||
<div ref="container" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('.')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> </span>
|
||||
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
|
||||
</Button>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
43
src/components/dialog/content/ApiNodesSignInContent.vue
Normal file
43
src/components/dialog/content/ApiNodesSignInContent.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
124
src/components/dialog/content/SignInContent.vue
Normal file
124
src/components/dialog/content/SignInContent.vue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)) || []
|
||||
|
||||
@@ -43,7 +43,9 @@ vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
installPack: {
|
||||
call: mockInstallPack,
|
||||
clear: vi.fn()
|
||||
}
|
||||
},
|
||||
isPackInstalled: vi.fn(() => false),
|
||||
getInstalledPackVersion: vi.fn(() => undefined)
|
||||
}))
|
||||
}))
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
89
src/components/dialog/content/signin/SignInForm.vue
Normal file
89
src/components/dialog/content/signin/SignInForm.vue
Normal 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>
|
||||
165
src/components/dialog/content/signin/SignUpForm.vue
Normal file
165
src/components/dialog/content/signin/SignUpForm.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
6
src/components/dialog/header/ComfyOrgHeader.vue
Normal file
6
src/components/dialog/header/ComfyOrgHeader.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
:style="inputStyle"
|
||||
>
|
||||
<EditableText
|
||||
:isEditing="showInput"
|
||||
:modelValue="editedTitle"
|
||||
:is-editing="showInput"
|
||||
:model-value="editedTitle"
|
||||
@edit="onEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user