mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-03 11:09:10 +00:00
Compare commits
7 Commits
fix/load-a
...
drjkl/vee-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42c7878b8 | ||
|
|
68e33760b0 | ||
|
|
d91a1e8c9b | ||
|
|
d63cea48fc | ||
|
|
296cfced2c | ||
|
|
e39aa6a2ca | ||
|
|
1ad4327079 |
@@ -29,7 +29,6 @@ const config: KnipConfig = {
|
||||
// Weird importmap things
|
||||
'@iconify-json/lucide',
|
||||
'@iconify/json',
|
||||
'@primeuix/forms',
|
||||
'@primeuix/styled',
|
||||
'@primeuix/utils',
|
||||
'@primevue/icons'
|
||||
|
||||
15
package.json
15
package.json
@@ -61,11 +61,9 @@
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@iconify/json": "catalog:",
|
||||
"@primeuix/forms": "catalog:",
|
||||
"@primeuix/styled": "catalog:",
|
||||
"@primeuix/utils": "catalog:",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/forms": "catalog:",
|
||||
"@primevue/icons": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
"@sentry/vue": "catalog:",
|
||||
@@ -106,6 +104,7 @@
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"typegpu": "catalog:",
|
||||
"vee-validate": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-i18n": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
@@ -194,6 +193,18 @@
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"packageExtensions": {
|
||||
"openai@4.104.0": {
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8 || ^4.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
pnpm-lock.yaml
generated
126
pnpm-lock.yaml
generated
@@ -48,9 +48,6 @@ catalogs:
|
||||
'@playwright/test':
|
||||
specifier: ^1.58.1
|
||||
version: 1.58.1
|
||||
'@primeuix/forms':
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2
|
||||
'@primeuix/styled':
|
||||
specifier: 0.3.2
|
||||
version: 0.3.2
|
||||
@@ -60,9 +57,6 @@ catalogs:
|
||||
'@primevue/core':
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
'@primevue/forms':
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
'@primevue/icons':
|
||||
specifier: 4.2.5
|
||||
version: 4.2.5
|
||||
@@ -276,6 +270,9 @@ catalogs:
|
||||
unplugin-vue-components:
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0
|
||||
vee-validate:
|
||||
specifier: 5.0.0-beta.0
|
||||
version: 5.0.0-beta.0
|
||||
vite-plugin-dts:
|
||||
specifier: ^4.5.4
|
||||
version: 4.5.4
|
||||
@@ -316,18 +313,20 @@ catalogs:
|
||||
specifier: ^13.6.27
|
||||
version: 13.6.27
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.24.1
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
zod-to-json-schema:
|
||||
specifier: ^3.24.1
|
||||
version: 3.24.1
|
||||
specifier: ^3.25.1
|
||||
version: 3.25.1
|
||||
zod-validation-error:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
|
||||
overrides:
|
||||
vite: 8.0.0-beta.13
|
||||
|
||||
packageExtensionsChecksum: sha256-49yZu1KDcXKFdjguhO09kXpBzb+YokPynnw5gHOLOps=
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -356,9 +355,6 @@ importers:
|
||||
'@iconify/json':
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.380
|
||||
'@primeuix/forms':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.2
|
||||
'@primeuix/styled':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.2
|
||||
@@ -368,9 +364,6 @@ importers:
|
||||
'@primevue/core':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.5(vue@3.5.13(typescript@5.9.3))
|
||||
'@primevue/forms':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.5(vue@3.5.13(typescript@5.9.3))
|
||||
'@primevue/icons':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.5(vue@3.5.13(typescript@5.9.3))
|
||||
@@ -491,6 +484,9 @@ importers:
|
||||
typegpu:
|
||||
specifier: 'catalog:'
|
||||
version: 0.8.2
|
||||
vee-validate:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.0-beta.0(vue@3.5.13(typescript@5.9.3))
|
||||
vue:
|
||||
specifier: 'catalog:'
|
||||
version: 3.5.13(typescript@5.9.3)
|
||||
@@ -511,10 +507,10 @@ importers:
|
||||
version: 13.6.27
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 3.24.1
|
||||
version: 4.3.6
|
||||
zod-validation-error:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.0(zod@3.24.1)
|
||||
version: 5.0.0(zod@4.3.6)
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: 'catalog:'
|
||||
@@ -524,7 +520,7 @@ importers:
|
||||
version: 4.1.0(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)))(yaml-eslint-parser@1.3.0)
|
||||
'@lobehub/i18n-cli':
|
||||
specifier: 'catalog:'
|
||||
version: 1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(ws@8.18.3)(zod@3.24.1)
|
||||
version: 1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(ws@8.18.3)(zod@4.3.6)
|
||||
'@nx/eslint':
|
||||
specifier: 'catalog:'
|
||||
version: 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)
|
||||
@@ -737,7 +733,7 @@ importers:
|
||||
version: 2.0.0
|
||||
zod-to-json-schema:
|
||||
specifier: 'catalog:'
|
||||
version: 3.24.1(zod@3.24.1)
|
||||
version: 3.25.1(zod@4.3.6)
|
||||
|
||||
apps/desktop-ui:
|
||||
dependencies:
|
||||
@@ -2804,10 +2800,6 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@primeuix/forms@0.0.2':
|
||||
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
|
||||
engines: {node: '>=12.11.0'}
|
||||
|
||||
'@primeuix/styled@0.3.2':
|
||||
resolution: {integrity: sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==}
|
||||
engines: {node: '>=12.11.0'}
|
||||
@@ -2822,10 +2814,6 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.3.0
|
||||
|
||||
'@primevue/forms@4.2.5':
|
||||
resolution: {integrity: sha512-5jarJQ9Qv32bOo/0tY5bqR3JZI6+YmmoUQ2mjhVSbVElQsE4FNfhT7a7JwF+xgBPMPc8KWGNA1QB248HhPNVAg==}
|
||||
engines: {node: '>=12.11.0'}
|
||||
|
||||
'@primevue/icons@4.2.5':
|
||||
resolution: {integrity: sha512-WFbUMZhQkXf/KmwcytkjGVeJ9aGEDXjP3uweOjQZMmRdEIxFnqYYpd90wE90JE1teZn3+TVnT4ZT7ejGyEXnFQ==}
|
||||
engines: {node: '>=12.11.0'}
|
||||
@@ -3224,6 +3212,9 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@storybook/addon-docs@10.1.9':
|
||||
resolution: {integrity: sha512-SvwEZ32lyk5p3PRmE3pmfAhs4HMiVo5zxjTBVmK9kgz9zGgWCTlikb56tJ998hVe52CFyCvt3I9rkHeYMCKPww==}
|
||||
peerDependencies:
|
||||
@@ -8127,6 +8118,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
vee-validate@5.0.0-beta.0:
|
||||
resolution: {integrity: sha512-uGIRnODDMM0A8Weu8AJcZFFJceUpgbSX6G4UYZgWhBc90VcXDK+v7yO16G+sj+6vU1eML11M2BH4HxwoPE62rw==}
|
||||
peerDependencies:
|
||||
vue: ^3.4.26
|
||||
|
||||
vfile-message@4.0.3:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
|
||||
@@ -8276,6 +8272,9 @@ packages:
|
||||
vue-component-type-helpers@3.2.4:
|
||||
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
|
||||
|
||||
vue-component-type-helpers@3.2.5:
|
||||
resolution: {integrity: sha512-tkvNr+bU8+xD/onAThIe7CHFvOJ/BO6XCOrxMzeytJq40nTfpGDJuVjyCM8ccGZKfAbGk2YfuZyDMXM56qheZQ==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -8566,22 +8565,19 @@ packages:
|
||||
zip-dir@2.0.0:
|
||||
resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==}
|
||||
|
||||
zod-to-json-schema@3.24.1:
|
||||
resolution: {integrity: sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==}
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
zod: ^3.24.1
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod-validation-error@3.3.0:
|
||||
resolution: {integrity: sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==}
|
||||
zod-validation-error@5.0.0:
|
||||
resolution: {integrity: sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.18.0
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
zod@3.24.1:
|
||||
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
|
||||
|
||||
zod@4.2.1:
|
||||
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
zustand@5.0.8:
|
||||
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||
@@ -10241,7 +10237,7 @@ snapshots:
|
||||
- react-devtools-core
|
||||
- utf-8-validate
|
||||
|
||||
'@lobehub/i18n-cli@1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(ws@8.18.3)(zod@3.24.1)':
|
||||
'@lobehub/i18n-cli@1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(ws@8.18.3)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@lobehub/cli-ui': 1.13.0(@types/react@19.1.9)
|
||||
'@yutengjing/eld': 0.0.2
|
||||
@@ -10260,7 +10256,7 @@ snapshots:
|
||||
json-stable-stringify: 1.3.0
|
||||
just-diff: 6.0.2
|
||||
lodash-es: 4.17.21
|
||||
openai: 4.104.0(ws@8.18.3)(zod@3.24.1)
|
||||
openai: 4.104.0(ws@8.18.3)(zod@4.3.6)
|
||||
p-map: 7.0.3
|
||||
pangu: 4.0.7
|
||||
react: 19.2.3
|
||||
@@ -10892,10 +10888,6 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@primeuix/forms@0.0.2':
|
||||
dependencies:
|
||||
'@primeuix/utils': 0.3.2
|
||||
|
||||
'@primeuix/styled@0.3.2':
|
||||
dependencies:
|
||||
'@primeuix/utils': 0.3.2
|
||||
@@ -10908,14 +10900,6 @@ snapshots:
|
||||
'@primeuix/utils': 0.3.2
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@primevue/forms@4.2.5(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/utils': 0.3.2
|
||||
'@primevue/core': 4.2.5(vue@3.5.13(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@primevue/icons@4.2.5(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@primeuix/utils': 0.3.2
|
||||
@@ -11225,6 +11209,8 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@storybook/addon-docs@10.1.9(@types/react@19.1.9)(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.1)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.1.9)(react@19.2.3)
|
||||
@@ -11322,7 +11308,7 @@ snapshots:
|
||||
storybook: 10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
vue-component-type-helpers: 3.2.4
|
||||
vue-component-type-helpers: 3.2.5
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
@@ -14620,7 +14606,7 @@ snapshots:
|
||||
smol-toml: 1.5.2
|
||||
strip-json-comments: 5.0.3
|
||||
typescript: 5.9.3
|
||||
zod: 4.2.1
|
||||
zod: 4.3.6
|
||||
|
||||
known-css-properties@0.37.0: {}
|
||||
|
||||
@@ -15517,7 +15503,7 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openai@4.104.0(ws@8.18.3)(zod@3.24.1):
|
||||
openai@4.104.0(ws@8.18.3)(zod@4.3.6):
|
||||
dependencies:
|
||||
'@types/node': 18.19.130
|
||||
'@types/node-fetch': 2.6.13
|
||||
@@ -15528,7 +15514,7 @@ snapshots:
|
||||
node-fetch: 2.7.0
|
||||
optionalDependencies:
|
||||
ws: 8.18.3
|
||||
zod: 3.24.1
|
||||
zod: 4.3.6
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -17099,6 +17085,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
vee-validate@5.0.0-beta.0(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@standard-schema/utils': 0.3.0
|
||||
'@vue/devtools-api': 7.7.9
|
||||
type-fest: 4.41.0
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
vfile-message@4.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -17362,6 +17356,8 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.2.4: {}
|
||||
|
||||
vue-component-type-helpers@3.2.5: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
@@ -17655,17 +17651,15 @@ snapshots:
|
||||
async: 3.2.5
|
||||
jszip: 3.10.1
|
||||
|
||||
zod-to-json-schema@3.24.1(zod@3.24.1):
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 3.24.1
|
||||
zod: 4.3.6
|
||||
|
||||
zod-validation-error@3.3.0(zod@3.24.1):
|
||||
zod-validation-error@5.0.0(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 3.24.1
|
||||
zod: 4.3.6
|
||||
|
||||
zod@3.24.1: {}
|
||||
|
||||
zod@4.2.1: {}
|
||||
zod@4.3.6: {}
|
||||
|
||||
zustand@5.0.8(@types/react@19.1.9)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)):
|
||||
optionalDependencies:
|
||||
|
||||
@@ -17,11 +17,9 @@ catalog:
|
||||
'@nx/vite': 22.2.6
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@playwright/test': ^1.58.1
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
'@primevue/core': ^4.2.5
|
||||
'@primevue/forms': ^4.2.5
|
||||
'@primevue/icons': 4.2.5
|
||||
'@primevue/themes': ^4.2.5
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
@@ -93,6 +91,7 @@ catalog:
|
||||
unplugin-icons: ^22.5.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^30.0.0
|
||||
vee-validate: 5.0.0-beta.0
|
||||
vite: 8.0.0-beta.13
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
@@ -107,9 +106,9 @@ catalog:
|
||||
vuefire: ^3.2.1
|
||||
wwobjloader2: ^6.2.1
|
||||
yjs: ^13.6.27
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
zod-validation-error: ^3.3.0
|
||||
zod: ^4.3.6
|
||||
zod-to-json-schema: ^3.25.1
|
||||
zod-validation-error: ^5.0.0
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
<template>
|
||||
<Form
|
||||
class="flex w-96 flex-col gap-6"
|
||||
:resolver="zodResolver(updatePasswordSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<form class="flex w-96 flex-col gap-6" @submit="onSubmit">
|
||||
<PasswordFields />
|
||||
|
||||
<!-- Submit Button -->
|
||||
<Button type="submit" class="mt-4 h-10 font-medium" :loading="loading">
|
||||
{{ $t('userSettings.updatePassword') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
|
||||
@@ -31,15 +25,23 @@ const { onSuccess } = defineProps<{
|
||||
onSuccess: () => void
|
||||
}>()
|
||||
|
||||
const onSubmit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
const { handleSubmit } = useForm({
|
||||
initialValues: {
|
||||
confirmPassword: '',
|
||||
password: ''
|
||||
},
|
||||
validationSchema: updatePasswordSchema
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit(async (submittedValues) => {
|
||||
if (submittedValues.password) {
|
||||
loading.value = true
|
||||
try {
|
||||
await authActions.updatePassword(event.values.password)
|
||||
await authActions.updatePassword(submittedValues.password)
|
||||
onSuccess()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
import { Form } from '@primevue/forms'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -69,7 +68,7 @@ describe('ApiKeyForm', () => {
|
||||
return mount(ApiKeyForm, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: { Button, Form, InputText, Message }
|
||||
components: { Button, InputText, Message }
|
||||
},
|
||||
props
|
||||
})
|
||||
|
||||
@@ -18,14 +18,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(apiKeySchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<Message v-if="$form.apiKey?.invalid" severity="error" class="mb-4">
|
||||
{{ $form.apiKey.error.message }}
|
||||
<form class="flex flex-col gap-6" @submit="onSubmit">
|
||||
<Message v-if="errors.apiKey" severity="error" class="mb-4">
|
||||
{{ errors.apiKey }}
|
||||
</Message>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -37,13 +32,14 @@
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<InputText
|
||||
pt:root:id="comfy-org-api-key"
|
||||
pt:root:autocomplete="off"
|
||||
id="comfy-org-api-key"
|
||||
v-model="apiKey"
|
||||
v-bind="apiKeyAttrs"
|
||||
autocomplete="off"
|
||||
class="h-10"
|
||||
name="apiKey"
|
||||
type="password"
|
||||
:placeholder="t('auth.apiKey.placeholder')"
|
||||
:invalid="$form.apiKey?.invalid"
|
||||
:invalid="Boolean(errors.apiKey)"
|
||||
/>
|
||||
<small class="text-muted">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
@@ -79,16 +75,14 @@
|
||||
{{ t('g.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -120,9 +114,24 @@ const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const onSubmit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
await apiKeyStore.storeApiKey(event.values.apiKey)
|
||||
const { defineField, errors, validate } = useForm({
|
||||
initialValues: {
|
||||
apiKey: ''
|
||||
},
|
||||
validationSchema: apiKeySchema
|
||||
})
|
||||
|
||||
const [apiKey, apiKeyAttrs] = defineField('apiKey')
|
||||
|
||||
const onSubmit = async (event: Event) => {
|
||||
event.preventDefault()
|
||||
const { valid, values: submittedValues } = await validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
if (submittedValues?.apiKey) {
|
||||
await apiKeyStore.storeApiKey(submittedValues.apiKey)
|
||||
emit('success')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +1,124 @@
|
||||
<template>
|
||||
<!-- Password Field -->
|
||||
<FormField v-slot="$field" name="password" class="flex flex-col gap-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label
|
||||
class="text-base font-medium opacity-80"
|
||||
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': $field.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<small v-if="$field.dirty || $field.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>
|
||||
<FormField v-slot="{ componentField, meta }" name="password">
|
||||
<FormItem class="flex flex-col gap-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<FormLabel class="text-base font-medium opacity-80">
|
||||
{{ t('auth.signup.passwordLabel') }}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Password
|
||||
v-bind="componentField"
|
||||
pt:pc-input-text:root:autocomplete="new-password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.signup.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': Boolean(errors.password) }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
</FormControl>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FormDescription
|
||||
v-if="meta.dirty || Boolean(errors.password)"
|
||||
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>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<FormField v-slot="$field" name="confirmPassword" class="flex flex-col gap-2">
|
||||
<label
|
||||
class="mb-2 text-base font-medium opacity-80"
|
||||
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': $field.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$field.error" class="text-red-500">{{
|
||||
$field.error.message
|
||||
}}</small>
|
||||
<FormField v-slot="{ componentField }" name="confirmPassword">
|
||||
<FormItem class="flex flex-col gap-2">
|
||||
<FormLabel class="mb-2 text-base font-medium opacity-80">
|
||||
{{ t('auth.login.confirmPasswordLabel') }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Password
|
||||
v-bind="componentField"
|
||||
pt:pc-input-text:root:autocomplete="new-password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.confirmPasswordPlaceholder')"
|
||||
:class="{ 'p-invalid': Boolean(errors.confirmPassword) }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormField } from '@primevue/forms'
|
||||
import Password from 'primevue/password'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useFormContext } from 'vee-validate'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const password = ref('')
|
||||
import FormControl from '@/components/ui/form/FormControl.vue'
|
||||
import FormDescription from '@/components/ui/form/FormDescription.vue'
|
||||
import FormField from '@/components/ui/form/FormField.vue'
|
||||
import FormItem from '@/components/ui/form/FormItem.vue'
|
||||
import FormLabel from '@/components/ui/form/FormLabel.vue'
|
||||
import FormMessage from '@/components/ui/form/FormMessage.vue'
|
||||
|
||||
// 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)
|
||||
}))
|
||||
type PasswordFormValues = {
|
||||
confirmPassword: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const { errors, values } = useFormContext<PasswordFormValues>()
|
||||
|
||||
const passwordChecks = computed(() => {
|
||||
const password = values.password ?? ''
|
||||
return {
|
||||
length: password.length >= 8 && password.length <= 32,
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
lowercase: /[a-z]/.test(password),
|
||||
number: /\d/.test(password),
|
||||
special: /[^A-Za-z0-9]/.test(password)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
@@ -79,13 +77,7 @@ describe('SignInForm', () => {
|
||||
return mount(SignInForm, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n, ToastService],
|
||||
components: {
|
||||
Form,
|
||||
Button,
|
||||
InputText,
|
||||
Password,
|
||||
ProgressSpinner
|
||||
}
|
||||
stubs: { ProgressSpinner: true }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
@@ -93,111 +85,68 @@ describe('SignInForm', () => {
|
||||
}
|
||||
|
||||
describe('Forgot Password Link', () => {
|
||||
function findForgotPasswordButton(wrapper: VueWrapper<ComponentInstance>) {
|
||||
const btn = wrapper
|
||||
.findAll('button[type="button"]')
|
||||
.find((btn) =>
|
||||
btn.text().includes(enMessages.auth.login.forgotPassword)
|
||||
)
|
||||
if (!btn) throw new Error('Forgot password button not found')
|
||||
return btn
|
||||
}
|
||||
|
||||
it('shows disabled style when email is empty', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await nextTick()
|
||||
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
)
|
||||
|
||||
expect(forgotPasswordSpan.classes()).toContain('text-link-disabled')
|
||||
const forgotBtn = findForgotPasswordButton(wrapper)
|
||||
expect(forgotBtn.classes()).toContain('text-link-disabled')
|
||||
})
|
||||
|
||||
it('shows toast and focuses email input when clicked while disabled', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
)
|
||||
const forgotBtn = findForgotPasswordButton(wrapper)
|
||||
|
||||
// Mock getElementById to track focus
|
||||
const mockFocus = vi.fn()
|
||||
const mockElement: Partial<HTMLElement> = { focus: mockFocus }
|
||||
vi.spyOn(document, 'getElementById').mockReturnValue(
|
||||
mockElement as HTMLElement
|
||||
)
|
||||
|
||||
// Click forgot password link while email is empty
|
||||
await forgotPasswordSpan.trigger('click')
|
||||
await forgotBtn.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// Should show toast warning
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'warn',
|
||||
summary: enMessages.auth.login.emailPlaceholder,
|
||||
life: 5000
|
||||
})
|
||||
|
||||
// Should focus email input
|
||||
expect(document.getElementById).toHaveBeenCalledWith(
|
||||
'comfy-org-sign-in-email'
|
||||
)
|
||||
expect(mockFocus).toHaveBeenCalled()
|
||||
|
||||
// Should NOT call sendPasswordReset
|
||||
expect(mockSendPasswordReset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls handleForgotPassword with email when link is clicked', async () => {
|
||||
it('sends reset email when link is clicked with a valid email', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as typeof wrapper.vm & {
|
||||
handleForgotPassword: (email: string, valid: boolean) => void
|
||||
onSubmit: (data: { valid: boolean; values: unknown }) => void
|
||||
}
|
||||
await wrapper
|
||||
.find('#comfy-org-sign-in-email')
|
||||
.setValue('test@example.com')
|
||||
|
||||
// Spy on handleForgotPassword
|
||||
const handleForgotPasswordSpy = vi.spyOn(
|
||||
component,
|
||||
'handleForgotPassword'
|
||||
)
|
||||
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
)
|
||||
|
||||
// Click the forgot password link
|
||||
await forgotPasswordSpan.trigger('click')
|
||||
|
||||
// Should call handleForgotPassword
|
||||
expect(handleForgotPasswordSpy).toHaveBeenCalled()
|
||||
const forgotBtn = findForgotPasswordButton(wrapper)
|
||||
await forgotBtn.trigger('click')
|
||||
expect(mockSendPasswordReset).toHaveBeenCalledWith('test@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('emits submit event when onSubmit is called with valid data', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as typeof wrapper.vm & {
|
||||
handleForgotPassword: (email: string, valid: boolean) => void
|
||||
onSubmit: (data: { valid: boolean; values: unknown }) => void
|
||||
}
|
||||
|
||||
// Call onSubmit directly with valid data
|
||||
component.onSubmit({
|
||||
valid: true,
|
||||
values: { email: 'test@example.com', password: 'password123' }
|
||||
})
|
||||
|
||||
// Check emitted event
|
||||
expect(wrapper.emitted('submit')).toBeTruthy()
|
||||
expect(wrapper.emitted('submit')?.[0]).toEqual([
|
||||
{
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('does not emit submit event when form is invalid', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as typeof wrapper.vm & {
|
||||
handleForgotPassword: (email: string, valid: boolean) => void
|
||||
onSubmit: (data: { valid: boolean; values: unknown }) => void
|
||||
}
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await nextTick()
|
||||
|
||||
// Call onSubmit with invalid form
|
||||
component.onSubmit({ valid: false, values: {} })
|
||||
|
||||
// Should not emit submit event
|
||||
expect(wrapper.emitted('submit')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -205,29 +154,19 @@ describe('SignInForm', () => {
|
||||
describe('Loading State', () => {
|
||||
it('shows spinner when loading', async () => {
|
||||
mockLoading = true
|
||||
const wrapper = mountComponent()
|
||||
await nextTick()
|
||||
|
||||
try {
|
||||
const wrapper = mountComponent()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(false)
|
||||
} catch (error) {
|
||||
// Fallback test - check HTML content if component rendering fails
|
||||
mockLoading = true
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.html()).toContain('p-progressspinner')
|
||||
expect(wrapper.html()).not.toContain('<button')
|
||||
}
|
||||
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(true)
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows button when not loading', () => {
|
||||
it('shows submit button when not loading', () => {
|
||||
mockLoading = false
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -238,7 +177,6 @@ describe('SignInForm', () => {
|
||||
|
||||
expect(emailInput.attributes('id')).toBe('comfy-org-sign-in-email')
|
||||
expect(emailInput.attributes('autocomplete')).toBe('email')
|
||||
expect(emailInput.attributes('name')).toBe('email')
|
||||
expect(emailInput.attributes('type')).toBe('text')
|
||||
})
|
||||
|
||||
@@ -246,20 +184,10 @@ describe('SignInForm', () => {
|
||||
const wrapper = mountComponent()
|
||||
const passwordInput = wrapper.findComponent(Password)
|
||||
|
||||
// Check props instead of attributes for Password component
|
||||
expect(passwordInput.props('inputId')).toBe('comfy-org-sign-in-password')
|
||||
// Password component passes name as prop, not attribute
|
||||
expect(passwordInput.props('name')).toBe('password')
|
||||
expect(passwordInput.props('feedback')).toBe(false)
|
||||
expect(passwordInput.props('toggleMask')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders form with correct resolver', () => {
|
||||
const wrapper = mountComponent()
|
||||
const form = wrapper.findComponent(Form)
|
||||
|
||||
expect(form.props('resolver')).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Focus Behavior', () => {
|
||||
@@ -267,7 +195,6 @@ describe('SignInForm', () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as typeof wrapper.vm & {
|
||||
handleForgotPassword: (email: string, valid: boolean) => void
|
||||
onSubmit: (data: { valid: boolean; values: unknown }) => void
|
||||
}
|
||||
|
||||
// Mock getElementById to track focus
|
||||
@@ -291,7 +218,6 @@ describe('SignInForm', () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as typeof wrapper.vm & {
|
||||
handleForgotPassword: (email: string, valid: boolean) => void
|
||||
onSubmit: (data: { valid: boolean; values: unknown }) => void
|
||||
}
|
||||
|
||||
// Mock getElementById
|
||||
@@ -304,11 +230,8 @@ describe('SignInForm', () => {
|
||||
// Call handleForgotPassword with valid email
|
||||
await component.handleForgotPassword('test@example.com', true)
|
||||
|
||||
// Should NOT focus email input
|
||||
expect(document.getElementById).not.toHaveBeenCalled()
|
||||
expect(mockFocus).not.toHaveBeenCalled()
|
||||
|
||||
// Should call sendPasswordReset
|
||||
expect(mockSendPasswordReset).toHaveBeenCalledWith('test@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,63 +1,68 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signInSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<form class="flex flex-col gap-6" @submit="onSubmit">
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="mb-2 text-base font-medium opacity-80" :for="emailInputId">
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
:id="emailInputId"
|
||||
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>
|
||||
<FormField v-slot="{ componentField }" name="email">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="mb-2 text-base font-medium opacity-80"
|
||||
:for="emailInputId"
|
||||
>
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
v-bind="componentField"
|
||||
:id="emailInputId"
|
||||
autocomplete="email"
|
||||
class="h-10"
|
||||
type="text"
|
||||
:placeholder="t('auth.login.emailPlaceholder')"
|
||||
:invalid="Boolean(errors.email)"
|
||||
/>
|
||||
<small v-if="errors.email" class="text-red-500">{{
|
||||
errors.email
|
||||
}}</small>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label
|
||||
class="text-base font-medium opacity-80"
|
||||
for="comfy-org-sign-in-password"
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<span
|
||||
class="cursor-pointer text-base font-medium text-muted select-none"
|
||||
:class="{
|
||||
'text-link-disabled': !$form.email?.value || $form.email?.invalid
|
||||
}"
|
||||
@click="handleForgotPassword($form.email?.value, $form.email?.valid)"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</span>
|
||||
<FormField v-slot="{ componentField }" name="password">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label
|
||||
class="text-base font-medium opacity-80"
|
||||
for="comfy-org-sign-in-password"
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
class="text-base font-medium text-muted select-none"
|
||||
:class="{
|
||||
'text-link-disabled': !values.email || Boolean(errors.email)
|
||||
}"
|
||||
@click="handleForgotPassword(values.email, !errors.email)"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</Button>
|
||||
</div>
|
||||
<Password
|
||||
v-bind="componentField"
|
||||
input-id="comfy-org-sign-in-password"
|
||||
pt:pc-input-text:root:autocomplete="current-password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': Boolean(errors.password) }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="errors.password" class="text-red-500">{{
|
||||
errors.password
|
||||
}}</small>
|
||||
</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>
|
||||
</FormField>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="mx-auto h-8 w-8" />
|
||||
@@ -65,26 +70,25 @@
|
||||
v-else
|
||||
type="submit"
|
||||
class="mt-4 h-10 font-medium"
|
||||
:disabled="!$form.valid"
|
||||
:disabled="!meta.valid"
|
||||
>
|
||||
{{ t('auth.login.loginButton') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import FormField from '@/components/ui/form/FormField.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
@@ -103,9 +107,23 @@ const emit = defineEmits<{
|
||||
|
||||
const emailInputId = 'comfy-org-sign-in-email'
|
||||
|
||||
const onSubmit = useThrottleFn((event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
const { errors, meta, validate, values } = useForm<SignInData>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validateOnMount: true,
|
||||
validationSchema: signInSchema
|
||||
})
|
||||
|
||||
const onSubmit = useThrottleFn(async (event: Event) => {
|
||||
event.preventDefault()
|
||||
const { valid, values: submittedValues } = await validate()
|
||||
if (valid && submittedValues?.email && submittedValues.password) {
|
||||
emit('submit', {
|
||||
email: submittedValues.email,
|
||||
password: submittedValues.password
|
||||
})
|
||||
}
|
||||
}, 1_500)
|
||||
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signUpSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<form class="flex flex-col gap-6" @submit="onSubmit">
|
||||
<!-- Email Field -->
|
||||
<FormField v-slot="$field" name="email" class="flex flex-col gap-2">
|
||||
<label
|
||||
class="mb-2 text-base font-medium opacity-80"
|
||||
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"
|
||||
type="text"
|
||||
:placeholder="t('auth.signup.emailPlaceholder')"
|
||||
:invalid="$field.invalid"
|
||||
/>
|
||||
<small v-if="$field.error" class="text-red-500">{{
|
||||
$field.error.message
|
||||
}}</small>
|
||||
<FormField v-slot="{ componentField }" name="email">
|
||||
<FormItem class="flex flex-col gap-2">
|
||||
<FormLabel class="mb-2 text-base font-medium opacity-80">
|
||||
{{ t('auth.signup.emailLabel') }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<InputText
|
||||
v-bind="componentField"
|
||||
autocomplete="email"
|
||||
class="h-10"
|
||||
type="text"
|
||||
:placeholder="t('auth.signup.emailPlaceholder')"
|
||||
:invalid="Boolean(errors.email)"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<PasswordFields />
|
||||
@@ -34,24 +28,27 @@
|
||||
v-else
|
||||
type="submit"
|
||||
class="mt-4 h-10 font-medium"
|
||||
:disabled="!$form.valid"
|
||||
:disabled="!meta.valid"
|
||||
>
|
||||
{{ t('auth.signup.signUpButton') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form, FormField } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import FormControl from '@/components/ui/form/FormControl.vue'
|
||||
import FormField from '@/components/ui/form/FormField.vue'
|
||||
import FormItem from '@/components/ui/form/FormItem.vue'
|
||||
import FormLabel from '@/components/ui/form/FormLabel.vue'
|
||||
import FormMessage from '@/components/ui/form/FormMessage.vue'
|
||||
import { signUpSchema } from '@/schemas/signInSchema'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
@@ -66,9 +63,20 @@ const emit = defineEmits<{
|
||||
submit: [values: SignUpData]
|
||||
}>()
|
||||
|
||||
const onSubmit = useThrottleFn((event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignUpData)
|
||||
}
|
||||
}, 1_500)
|
||||
const { errors, handleSubmit, meta } = useForm<SignUpData>({
|
||||
initialValues: {
|
||||
confirmPassword: '',
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validateOnMount: true,
|
||||
validationSchema: signUpSchema
|
||||
})
|
||||
|
||||
const onSubmit = useThrottleFn(
|
||||
handleSubmit((submittedValues) => {
|
||||
emit('submit', submittedValues)
|
||||
}),
|
||||
1_500
|
||||
)
|
||||
</script>
|
||||
|
||||
26
src/components/ui/form/FormControl.vue
Normal file
26
src/components/ui/form/FormControl.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { Slot } from 'reka-ui'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useFormField } from './useFormField'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const { describedBy, errorMessage, formItemId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot
|
||||
:id="formItemId"
|
||||
:aria-describedby="describedBy"
|
||||
:aria-invalid="Boolean(errorMessage)"
|
||||
:class="cn(className)"
|
||||
>
|
||||
<slot />
|
||||
</Slot>
|
||||
</template>
|
||||
22
src/components/ui/form/FormDescription.vue
Normal file
22
src/components/ui/form/FormDescription.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useFormField } from './useFormField'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const { formDescriptionId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
:id="formDescriptionId"
|
||||
:class="cn('text-[0.8rem] text-muted-foreground', className)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
21
src/components/ui/form/FormField.vue
Normal file
21
src/components/ui/form/FormField.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { Field } from 'vee-validate'
|
||||
import { computed, provide } from 'vue'
|
||||
|
||||
import { FORM_FIELD_NAME_INJECTION_KEY } from './injectionKeys'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
}>()
|
||||
|
||||
provide(
|
||||
FORM_FIELD_NAME_INJECTION_KEY,
|
||||
computed(() => props.name)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Field v-slot="slotProps" :name="props.name">
|
||||
<slot v-bind="slotProps" />
|
||||
</Field>
|
||||
</template>
|
||||
22
src/components/ui/form/FormItem.vue
Normal file
22
src/components/ui/form/FormItem.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useId, provide } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { FORM_ITEM_ID_INJECTION_KEY } from './injectionKeys'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const id = useId()
|
||||
provide(FORM_ITEM_ID_INJECTION_KEY, id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('space-y-2', className)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
28
src/components/ui/form/FormLabel.vue
Normal file
28
src/components/ui/form/FormLabel.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useFormField } from './useFormField'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const { errorMessage, formItemId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
:for="formItemId"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm leading-none font-medium',
|
||||
errorMessage && 'text-danger',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
</template>
|
||||
23
src/components/ui/form/FormMessage.vue
Normal file
23
src/components/ui/form/FormMessage.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useFormField } from './useFormField'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const { errorMessage, formMessageId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
v-if="errorMessage"
|
||||
:id="formMessageId"
|
||||
:class="cn('text-[0.8rem] font-medium text-danger', className)"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</template>
|
||||
7
src/components/ui/form/injectionKeys.ts
Normal file
7
src/components/ui/form/injectionKeys.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
export const FORM_FIELD_NAME_INJECTION_KEY: InjectionKey<Ref<string>> =
|
||||
Symbol('FORM_FIELD_NAME')
|
||||
|
||||
export const FORM_ITEM_ID_INJECTION_KEY: InjectionKey<string> =
|
||||
Symbol('FORM_ITEM_ID')
|
||||
48
src/components/ui/form/useFormField.ts
Normal file
48
src/components/ui/form/useFormField.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useFieldError } from 'vee-validate'
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import {
|
||||
FORM_FIELD_NAME_INJECTION_KEY,
|
||||
FORM_ITEM_ID_INJECTION_KEY
|
||||
} from './injectionKeys'
|
||||
|
||||
/**
|
||||
* Exposes form field identifiers and validation state for a form field component.
|
||||
*
|
||||
* @returns An object with:
|
||||
* - `errorMessage`: a reactive validation message for the injected field name
|
||||
* - `formDescriptionId`: the element id for the field description (`<itemId>-form-item-description`)
|
||||
* - `formItemId`: the element id for the form item container (`<itemId>-form-item`)
|
||||
* - `formMessageId`: the element id for the field validation message (`<itemId>-form-item-message`)
|
||||
* - `describedBy`: a computed string listing ids to use for `aria-describedby` (includes the message id when an error exists)
|
||||
* - `name`: the injected field name
|
||||
*
|
||||
* @throws Error if the required injection keys (field name or item id) are not found
|
||||
*/
|
||||
export function useFormField() {
|
||||
const fieldName = inject(FORM_FIELD_NAME_INJECTION_KEY)
|
||||
const itemId = inject(FORM_ITEM_ID_INJECTION_KEY)
|
||||
|
||||
if (!fieldName || !itemId) {
|
||||
throw new Error('useFormField must be used within FormField and FormItem')
|
||||
}
|
||||
|
||||
const errorMessage = useFieldError(fieldName)
|
||||
const formItemId = `${itemId}-form-item`
|
||||
const formDescriptionId = `${itemId}-form-item-description`
|
||||
const formMessageId = `${itemId}-form-item-message`
|
||||
const describedBy = computed(() =>
|
||||
errorMessage.value
|
||||
? `${formDescriptionId} ${formMessageId}`
|
||||
: formDescriptionId
|
||||
)
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
formDescriptionId,
|
||||
formItemId,
|
||||
formMessageId,
|
||||
describedBy,
|
||||
name: fieldName
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export const useBrowserTabTitle = () => {
|
||||
const progress = Math.round((state.value / state.max) * 100)
|
||||
const nodeType =
|
||||
executionStore.activeJob?.workflow?.changeTracker?.activeState.nodes.find(
|
||||
(n) => String(n.id) === nodeId
|
||||
(n: { id: string | number; type?: string }) => String(n.id) === nodeId
|
||||
)?.type || 'Node'
|
||||
|
||||
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||
|
||||
@@ -14,8 +14,8 @@ const zAsset = z.object({
|
||||
updated_at: z.string().optional(),
|
||||
is_immutable: z.boolean().optional(),
|
||||
last_access_time: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(), // API allows arbitrary key-value pairs
|
||||
user_metadata: z.record(z.unknown()).optional() // API allows arbitrary key-value pairs
|
||||
metadata: z.record(z.string(), z.unknown()).optional(), // API allows arbitrary key-value pairs
|
||||
user_metadata: z.record(z.string(), z.unknown()).optional() // API allows arbitrary key-value pairs
|
||||
})
|
||||
|
||||
const zAssetResponse = z.object({
|
||||
|
||||
@@ -34,11 +34,13 @@ interface CreateAssetWidgetParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an asset widget that opens the Asset Browser dialog for model selection.
|
||||
* Used by both regular nodes (via useComboWidget) and PrimitiveNode.
|
||||
* Build and attach an asset-selection widget to a LiteGraph node that opens the Asset Browser for choosing an asset.
|
||||
*
|
||||
* @param params - Configuration for the asset widget
|
||||
* @returns The created asset widget
|
||||
* The widget validates the selected asset and its filename, updates the widget value on success, and triggers the optional
|
||||
* onValueChange callback. Validation failures are surfaced via console errors and toast notifications.
|
||||
*
|
||||
* @param params - Configuration for the asset widget (see CreateAssetWidgetParams)
|
||||
* @returns The created asset widget attached to the provided node
|
||||
*/
|
||||
export function createAssetWidget(
|
||||
params: CreateAssetWidgetParams
|
||||
@@ -55,6 +57,13 @@ export function createAssetWidget(
|
||||
const displayLabel = defaultValue ?? t('widgets.selectModel')
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
|
||||
/**
|
||||
* Opens the Asset Browser, validates the selected asset and its filename, and updates the provided widget with the validated filename.
|
||||
*
|
||||
* If the selected asset or its filename fails validation, logs a descriptive error and displays an error toast; on success the widget's value is set to the validated filename and the optional `onValueChange` callback is invoked with the widget, new value, and old value.
|
||||
*
|
||||
* @param widget - The IBaseWidget instance whose value will be updated and which will be passed to the `onValueChange` callback
|
||||
*/
|
||||
async function openModal(widget: IBaseWidget) {
|
||||
const toastStore = useToastStore()
|
||||
|
||||
@@ -85,7 +94,7 @@ export function createAssetWidget(
|
||||
if (!validatedFilename.success) {
|
||||
console.error(
|
||||
'Invalid asset filename:',
|
||||
validatedFilename.error.errors,
|
||||
validatedFilename.error.issues,
|
||||
'for asset:',
|
||||
validatedAsset.data.id
|
||||
)
|
||||
@@ -111,4 +120,4 @@ export function createAssetWidget(
|
||||
}
|
||||
|
||||
return node.addWidget('asset', widgetName, displayLabel, () => {}, options)
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export function createModelNodeFromAsset(
|
||||
const validatedAsset = assetItemSchema.safeParse(asset)
|
||||
|
||||
if (!validatedAsset.success) {
|
||||
const errorMessage = validatedAsset.error.errors
|
||||
const errorMessage = validatedAsset.error.issues
|
||||
.map((e) => `${e.path.join('.')}: ${e.message}`)
|
||||
.join(', ')
|
||||
console.error('Invalid asset item:', errorMessage)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signInSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<form class="flex flex-col gap-6" @submit="onSubmit">
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="mb-2 text-base font-medium opacity-80" :for="emailInputId">
|
||||
@@ -12,16 +7,15 @@
|
||||
</label>
|
||||
<InputText
|
||||
:id="emailInputId"
|
||||
v-model="email"
|
||||
v-bind="emailAttrs"
|
||||
autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.login.emailPlaceholder')"
|
||||
:invalid="$form.email?.invalid"
|
||||
:invalid="Boolean(errors.email)"
|
||||
/>
|
||||
<small v-if="$form.email?.invalid" class="text-red-500">{{
|
||||
$form.email.error.message
|
||||
}}</small>
|
||||
<small v-if="errors.email" class="text-red-500">{{ errors.email }}</small>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
@@ -35,18 +29,19 @@
|
||||
</label>
|
||||
</div>
|
||||
<Password
|
||||
v-model="password"
|
||||
v-bind="passwordAttrs"
|
||||
input-id="cloud-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 }"
|
||||
:class="{ 'p-invalid': Boolean(errors.password) }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$form.password?.invalid" class="text-red-500">{{
|
||||
$form.password.error.message
|
||||
<small v-if="errors.password" class="text-red-500">{{
|
||||
errors.password
|
||||
}}</small>
|
||||
|
||||
<router-link
|
||||
@@ -68,21 +63,19 @@
|
||||
v-else
|
||||
type="submit"
|
||||
class="mt-4 h-10 font-medium text-white"
|
||||
:disabled="!$form.valid"
|
||||
:disabled="!meta.valid"
|
||||
>
|
||||
{{ t('auth.login.loginButton') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -106,11 +99,21 @@ const emit = defineEmits<{
|
||||
|
||||
const emailInputId = 'cloud-sign-in-email'
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
const { defineField, errors, handleSubmit, meta } = useForm<SignInData>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validateOnMount: true,
|
||||
validationSchema: signInSchema
|
||||
})
|
||||
|
||||
const [email, emailAttrs] = defineField('email')
|
||||
const [password, passwordAttrs] = defineField('password')
|
||||
|
||||
const onSubmit = handleSubmit((submittedValues) => {
|
||||
emit('submit', submittedValues)
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.p-inputtext) {
|
||||
|
||||
@@ -23,7 +23,7 @@ const zDownloadFileResult = z.object({
|
||||
hash: z.string().optional(),
|
||||
filename: z.string().optional(),
|
||||
asset_id: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
error: z.string().optional()
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ const zTaskResponse = z.object({
|
||||
id: z.string().uuid(),
|
||||
idempotency_key: z.string(),
|
||||
task_name: z.string(),
|
||||
payload: z.record(z.unknown()),
|
||||
payload: z.record(z.string(), z.unknown()),
|
||||
status: zTaskStatus,
|
||||
result: zDownloadFileResult.optional(),
|
||||
error_message: z.string().optional(),
|
||||
|
||||
@@ -69,12 +69,14 @@ describe('parseComfyWorkflow', () => {
|
||||
// Should automatically transform the legacy format object to array.
|
||||
workflow.nodes[0].pos = { '0': 3, '1': 4 }
|
||||
let validatedWorkflow = await validateComfyWorkflow(workflow)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(validatedWorkflow).not.toBeNull()
|
||||
if (!validatedWorkflow) return
|
||||
expect(validatedWorkflow.nodes[0].pos).toEqual([3, 4])
|
||||
|
||||
workflow.nodes[0].pos = { 0: 3, 1: 4 }
|
||||
validatedWorkflow = await validateComfyWorkflow(workflow)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(validatedWorkflow).not.toBeNull()
|
||||
if (!validatedWorkflow) return
|
||||
expect(validatedWorkflow.nodes[0].pos).toEqual([3, 4])
|
||||
|
||||
// Should accept the legacy bugged format object.
|
||||
@@ -92,7 +94,8 @@ describe('parseComfyWorkflow', () => {
|
||||
'9': 0
|
||||
}
|
||||
validatedWorkflow = await validateComfyWorkflow(workflow)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(validatedWorkflow).not.toBeNull()
|
||||
if (!validatedWorkflow) return
|
||||
expect(validatedWorkflow.nodes[0].pos).toEqual([600, 340])
|
||||
})
|
||||
|
||||
@@ -111,7 +114,8 @@ describe('parseComfyWorkflow', () => {
|
||||
// dynamic widgets display.
|
||||
workflow.nodes[0].widgets_values = { foo: 'bar' }
|
||||
const validatedWorkflow = await validateComfyWorkflow(workflow)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(validatedWorkflow).not.toBeNull()
|
||||
if (!validatedWorkflow) return
|
||||
expect(validatedWorkflow.nodes[0].widgets_values).toEqual({ foo: 'bar' })
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from 'zod'
|
||||
import type { SafeParseReturnType } from 'zod'
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
import type { RendererType } from '@/lib/litegraph/src/LGraph'
|
||||
|
||||
@@ -198,7 +197,7 @@ const zProperties = z
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const zWidgetValues = z.union([z.array(z.any()), z.record(z.any())])
|
||||
const zWidgetValues = z.union([z.array(z.any()), z.record(z.string(), z.any())])
|
||||
|
||||
const zComfyNode = z
|
||||
.object({
|
||||
@@ -346,6 +345,12 @@ interface ComfyWorkflow1BaseOutput extends ComfyWorkflow1BaseType {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const -- Forward declaration required for recursive schema.
|
||||
let zSubgraphDefinition: z.ZodType<
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseOutput>,
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||
>
|
||||
|
||||
/** Schema version 1 */
|
||||
export const zComfyWorkflow1 = zBaseExportableGraph
|
||||
.extend({
|
||||
@@ -363,16 +368,7 @@ export const zComfyWorkflow1 = zBaseExportableGraph
|
||||
models: z.array(zModelFile).optional(),
|
||||
definitions: z
|
||||
.object({
|
||||
subgraphs: z.lazy(
|
||||
(): z.ZodArray<
|
||||
z.ZodType<
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseOutput>,
|
||||
z.ZodTypeDef,
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||
>,
|
||||
'many'
|
||||
> => z.array(zSubgraphDefinition)
|
||||
)
|
||||
subgraphs: z.lazy(() => z.array(zSubgraphDefinition))
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -426,7 +422,7 @@ interface SubgraphDefinitionBase<
|
||||
}
|
||||
|
||||
/** A subgraph definition `worfklow.definitions.subgraphs` */
|
||||
const zSubgraphDefinition = zComfyWorkflow1
|
||||
zSubgraphDefinition = zComfyWorkflow1
|
||||
.extend({
|
||||
/** Unique graph ID. Automatically generated if not provided. */
|
||||
id: z.string().uuid(),
|
||||
@@ -444,16 +440,7 @@ const zSubgraphDefinition = zComfyWorkflow1
|
||||
widgets: z.array(zExposedWidget).optional(),
|
||||
definitions: z
|
||||
.object({
|
||||
subgraphs: z.lazy(
|
||||
(): z.ZodArray<
|
||||
z.ZodType<
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>,
|
||||
z.ZodTypeDef,
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||
>,
|
||||
'many'
|
||||
> => zSubgraphDefinition.array()
|
||||
)
|
||||
subgraphs: z.lazy(() => zSubgraphDefinition.array())
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -467,7 +454,12 @@ export type WorkflowJSON04 = z.infer<typeof zComfyWorkflow>
|
||||
export type ComfyWorkflowJSON = z.infer<
|
||||
typeof zComfyWorkflow | typeof zComfyWorkflow1
|
||||
>
|
||||
type SubgraphDefinition = z.infer<typeof zSubgraphDefinition>
|
||||
type SubgraphDefinition = z.infer<typeof zComfyWorkflow1> & {
|
||||
id: string
|
||||
name: string
|
||||
inputNode: unknown
|
||||
outputNode: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a SubgraphDefinition.
|
||||
@@ -490,13 +482,22 @@ const zWorkflowVersion = z.object({
|
||||
version: z.number()
|
||||
})
|
||||
|
||||
/**
|
||||
* Validate and normalize a Comfy workflow payload.
|
||||
*
|
||||
* @param data - The workflow data to validate.
|
||||
* @param onError - Callback invoked with a human-readable validation error message when validation fails.
|
||||
* @returns The parsed ComfyWorkflowJSON when validation succeeds, `null` when validation fails or the payload lacks a valid version.
|
||||
*/
|
||||
export async function validateComfyWorkflow(
|
||||
data: unknown,
|
||||
onError: (error: string) => void = console.warn
|
||||
): Promise<ComfyWorkflowJSON | null> {
|
||||
const versionResult = zWorkflowVersion.safeParse(data)
|
||||
|
||||
let result: SafeParseReturnType<unknown, ComfyWorkflowJSON>
|
||||
let result:
|
||||
| { success: true; data: ComfyWorkflowJSON }
|
||||
| { success: false; error: z.ZodError }
|
||||
if (!versionResult.success) {
|
||||
// Invalid workflow
|
||||
const error = fromZodError(versionResult.error)
|
||||
@@ -536,4 +537,4 @@ const zNodeData = z.object({
|
||||
})
|
||||
|
||||
const zComfyApiWorkflow = z.record(zNodeId, zNodeData)
|
||||
export type ComfyApiWorkflow = z.infer<typeof zComfyApiWorkflow>
|
||||
export type ComfyApiWorkflow = z.infer<typeof zComfyApiWorkflow>
|
||||
@@ -259,7 +259,7 @@ const zUser = z.object({
|
||||
// `users` is only available in multi-user server mode.
|
||||
users: z.record(z.string(), z.string()).optional()
|
||||
})
|
||||
const zUserData = z.array(z.array(z.string(), z.string()))
|
||||
const zUserData = z.array(z.tuple([z.string(), z.string()]))
|
||||
const zUserDataFullInfo = z.object({
|
||||
path: z.string(),
|
||||
size: z.number(),
|
||||
@@ -310,7 +310,7 @@ const zSettings = z.object({
|
||||
'Comfy.Graph.CtrlShiftZoom': z.boolean(),
|
||||
'Comfy.Graph.DeduplicateSubgraphNodeIds': z.boolean(),
|
||||
'Comfy.Graph.LiveSelection': z.boolean(),
|
||||
'Comfy.Graph.LinkMarkers': z.nativeEnum(LinkMarkerShape),
|
||||
'Comfy.Graph.LinkMarkers': z.enum(LinkMarkerShape),
|
||||
'Comfy.Graph.ZoomSpeed': z.number(),
|
||||
'Comfy.Group.DoubleClickTitleToEdit': z.boolean(),
|
||||
'Comfy.GroupSelectedNodes.Padding': z.number(),
|
||||
|
||||
@@ -116,7 +116,7 @@ const completedPaletteSchema = z
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const colorPalettesSchema = z.record(paletteSchema)
|
||||
export const colorPalettesSchema = z.record(z.string(), paletteSchema)
|
||||
|
||||
export type Colors = z.infer<typeof colorsSchema>
|
||||
export type Palette = z.infer<typeof paletteSchema>
|
||||
|
||||
@@ -54,14 +54,14 @@ const zImageInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('IMAGE'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z.record(z.unknown()).optional()
|
||||
options: z.record(z.string(), z.unknown()).optional()
|
||||
})
|
||||
|
||||
const zImageCompareInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('IMAGECOMPARE'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z.record(z.unknown()).optional()
|
||||
options: z.record(z.string(), z.unknown()).optional()
|
||||
})
|
||||
|
||||
const zBoundingBoxInputSpec = zBaseInputOptions.extend({
|
||||
@@ -161,9 +161,9 @@ const zOutputSpec = z.object({
|
||||
|
||||
// Main node definition schema
|
||||
export const zComfyNodeDef = z.object({
|
||||
inputs: z.record(zInputSpec),
|
||||
inputs: z.record(z.string(), zInputSpec),
|
||||
outputs: z.array(zOutputSpec),
|
||||
hidden: z.record(z.any()).optional(),
|
||||
hidden: z.record(z.string(), z.any()).optional(),
|
||||
|
||||
name: z.string(),
|
||||
display_name: z.string(),
|
||||
|
||||
@@ -199,11 +199,11 @@ const zInputSpec = z.union([
|
||||
])
|
||||
|
||||
const zComfyInputsSpec = z.object({
|
||||
required: z.record(zInputSpec).optional(),
|
||||
optional: z.record(zInputSpec).optional(),
|
||||
required: z.record(z.string(), zInputSpec).optional(),
|
||||
optional: z.record(z.string(), zInputSpec).optional(),
|
||||
// Frontend repo is not using it, but some custom nodes are using the
|
||||
// hidden field to pass various values.
|
||||
hidden: z.record(z.any()).optional()
|
||||
hidden: z.record(z.string(), z.any()).optional()
|
||||
})
|
||||
|
||||
const zComfyNodeDataType = z.string()
|
||||
@@ -285,7 +285,7 @@ export const zComfyNodeDef = z.object({
|
||||
* Used to ensure consistent widget ordering regardless of JSON serialization.
|
||||
* Keys are 'required', 'optional', etc., values are arrays of input names.
|
||||
*/
|
||||
input_order: z.record(z.array(z.string())).optional(),
|
||||
input_order: z.record(z.string(), z.array(z.string())).optional(),
|
||||
/**
|
||||
* Alternative names for search. Useful for synonyms, abbreviations,
|
||||
* or old names after renaming a node.
|
||||
|
||||
@@ -1224,7 +1224,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error Discrepancies between zod and litegraph - in progress
|
||||
// @ts-expect-error Zod schema type allows broader slot types than LiteGraph configure typings.
|
||||
this.rootGraph.configure(graphData)
|
||||
|
||||
// Save original renderer version before scaling (it gets modified during scaling)
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
@@ -425,6 +428,12 @@ export class ChangeTracker {
|
||||
}
|
||||
|
||||
private static graphDiff(a: ComfyWorkflowJSON, b: ComfyWorkflowJSON) {
|
||||
/**
|
||||
* Create a copy of a workflow object with its `nodes` array ordered by numeric node id.
|
||||
*
|
||||
* @param graph - The workflow JSON to normalize
|
||||
* @returns A new workflow-like object containing the same top-level properties as `graph` but with `nodes` sorted in ascending order when node `id`s are numbers
|
||||
*/
|
||||
function sortGraphNodes(graph: ComfyWorkflowJSON) {
|
||||
return {
|
||||
links: graph.links,
|
||||
@@ -434,7 +443,7 @@ export class ChangeTracker {
|
||||
extra: graph.extra,
|
||||
definitions: graph.definitions,
|
||||
subgraphs: graph.subgraphs,
|
||||
nodes: graph.nodes.sort((a, b) => {
|
||||
nodes: graph.nodes.sort((a: ComfyNode, b: ComfyNode) => {
|
||||
if (typeof a.id === 'number' && typeof b.id === 'number') {
|
||||
return a.id - b.id
|
||||
}
|
||||
@@ -444,4 +453,4 @@ export class ChangeTracker {
|
||||
}
|
||||
return jsondiffpatch.diff(sortGraphNodes(a), sortGraphNodes(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,8 +209,9 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
if (!canvasState) return null
|
||||
|
||||
return (
|
||||
canvasState.nodes.find((n) => String(n.id) === executingNodeId.value) ??
|
||||
null
|
||||
canvasState.nodes.find(
|
||||
(n: { id: string | number }) => String(n.id) === executingNodeId.value
|
||||
) ?? null
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -60,9 +60,14 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
)
|
||||
const { subgraphs } = this.activeState.definitions
|
||||
const { nodes } = this.activeState
|
||||
//Instanceof doesn't function as nodes are serialized
|
||||
/**
|
||||
* Determines whether a given node represents a subgraph node.
|
||||
*
|
||||
* @param node - The node to test
|
||||
* @returns `true` if the node corresponds to a registered subgraph type, `false` otherwise.
|
||||
*/
|
||||
function isSubgraphNode(node: ComfyNode) {
|
||||
return node && subgraphs.some((s) => s.id === node.type)
|
||||
return node && subgraphs.some((s: { id: string }) => s.id === node.type)
|
||||
}
|
||||
if (nodes.length == 1 && isSubgraphNode(nodes[0])) return
|
||||
const errors: Record<NodeId, NodeError> = {}
|
||||
@@ -146,7 +151,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const loaded = await super.load({ force })
|
||||
const st = loaded.activeState
|
||||
const sg = (st.definitions?.subgraphs ?? []).find(
|
||||
(sg) => sg.id == st.nodes[0].type
|
||||
(sg: { id: string }) => sg.id == st.nodes[0].type
|
||||
)
|
||||
if (!sg)
|
||||
throw new Error(
|
||||
@@ -185,6 +190,11 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const subgraphBlueprints = computed(() => [
|
||||
...subgraphDefCache.value.values()
|
||||
])
|
||||
/**
|
||||
* Loads user and installed subgraph blueprints, registers their node definitions, and attaches them to the workflow store.
|
||||
*
|
||||
* Loads blueprints from the user's SubgraphBlueprints folder and from the platform-provided (global) set, filters installed blueprints by distribution and custom-node requirements, registers resulting node definitions, and attaches the corresponding workflows to the workflow store. Any load failures are logged to the console and surfaced to the user as an error toast summarizing the number or list of failures.
|
||||
*/
|
||||
async function fetchSubgraphs() {
|
||||
async function loadBlueprint(options: {
|
||||
path: string
|
||||
@@ -249,7 +259,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
loadInstalledBlueprints()
|
||||
])
|
||||
|
||||
const errors = settled.filter((i) => 'reason' in i).map((i) => i.reason)
|
||||
const errors = settled
|
||||
.filter((i): i is PromiseRejectedResult => i.status === 'rejected')
|
||||
.map((i) => i.reason)
|
||||
errors.forEach((e) => console.error('Failed to load subgraph blueprint', e))
|
||||
if (errors.length > 0) {
|
||||
useToastStore().add({
|
||||
@@ -260,6 +272,18 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Build and register a Comfy node definition for a loaded subgraph blueprint.
|
||||
*
|
||||
* Creates a ComfyNodeDefV1 from the blueprint's root subgraph node, applies any provided overrides,
|
||||
* stores the resulting node definition in the subgraph definition cache, and associates the
|
||||
* loaded workflow in the subgraph workflow cache under the given name.
|
||||
*
|
||||
* @param workflow - The loaded subgraph workflow to register as a node definition
|
||||
* @param overrides - Partial node definition fields to apply on top of the generated definition
|
||||
* @param name - The blueprint name to register (defaults to the workflow filename)
|
||||
* @throws Error if the workflow does not contain a root subgraph node suitable for registration
|
||||
*/
|
||||
function registerNodeDef(
|
||||
workflow: LoadedComfyWorkflow,
|
||||
overrides: Partial<ComfyNodeDefV1> = {},
|
||||
@@ -271,10 +295,12 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
subgraphNode.outputs ??= []
|
||||
//NOTE: Types are cast to string. This is only used for input coloring on previews
|
||||
const inputs = Object.fromEntries(
|
||||
subgraphNode.inputs.map((i) => [
|
||||
i.name,
|
||||
[`${i.type}`, undefined] satisfies InputSpec
|
||||
])
|
||||
subgraphNode.inputs.map(
|
||||
(i: { name: string; type: string | number | string[] }) => [
|
||||
i.name,
|
||||
[`${i.type}`, undefined] satisfies InputSpec
|
||||
]
|
||||
)
|
||||
)
|
||||
const workflowExtra = workflow.initialState.extra
|
||||
const description =
|
||||
@@ -282,8 +308,10 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const search_aliases = workflowExtra?.BlueprintSearchAliases
|
||||
const nodedefv1: ComfyNodeDefV1 = {
|
||||
input: { required: inputs },
|
||||
output: subgraphNode.outputs.map((o) => `${o.type}`),
|
||||
output_name: subgraphNode.outputs.map((o) => o.name),
|
||||
output: subgraphNode.outputs.map(
|
||||
(o: { type: string | number | string[] }) => `${o.type}`
|
||||
),
|
||||
output_name: subgraphNode.outputs.map((o: { name: string }) => o.name),
|
||||
name: typePrefix + name,
|
||||
display_name: name,
|
||||
description,
|
||||
@@ -424,4 +452,4 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
subgraphBlueprints,
|
||||
typePrefix
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -61,8 +61,8 @@ const mergeNumericInputSpec = <T extends IntInputSpec | FloatInputSpec>(
|
||||
}
|
||||
|
||||
return mergeCommonInputSpec(
|
||||
[type, { ...options1, ...mergedOptions }] as T,
|
||||
[type, { ...options2, ...mergedOptions }] as T
|
||||
[type, { ...options1, ...mergedOptions }] as unknown as T,
|
||||
[type, { ...options2, ...mergedOptions }] as unknown as T
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ const mergeComboInputSpec = <T extends ComboInputSpec | ComboInputSpecV2>(
|
||||
}
|
||||
|
||||
return mergeCommonInputSpec(
|
||||
['COMBO', { ...options1, options: intersection }] as T,
|
||||
['COMBO', { ...options2, options: intersection }] as T
|
||||
['COMBO', { ...options1, options: intersection }] as unknown as T,
|
||||
['COMBO', { ...options2, options: intersection }] as unknown as T
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,9 @@ const mergeCommonInputSpec = <T extends InputSpec>(
|
||||
return value1 === value2 || (_.isNil(value1) && _.isNil(value2))
|
||||
})
|
||||
|
||||
return mergeIsValid ? ([type, { ...options1, ...options2 }] as T) : null
|
||||
return mergeIsValid
|
||||
? ([type, { ...options1, ...options2 }] as unknown as T)
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user