diff --git a/knip.config.ts b/knip.config.ts index a9f6310f767..e9790d8c0c5 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -29,7 +29,6 @@ const config: KnipConfig = { // Weird importmap things '@iconify-json/lucide', '@iconify/json', - '@primeuix/forms', '@primeuix/styled', '@primeuix/utils', '@primevue/icons' diff --git a/package.json b/package.json index e85987dff55..74b87dba753 100644 --- a/package.json +++ b/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" + } } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 604bc26a923..15e7dae7fd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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@3.24.1: {} + zod: 4.3.6 - 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: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 99c88edcb05..0d715f00979 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/src/components/dialog/content/UpdatePasswordContent.vue b/src/components/dialog/content/UpdatePasswordContent.vue index ef99a77880b..d1c46d0393c 100644 --- a/src/components/dialog/content/UpdatePasswordContent.vue +++ b/src/components/dialog/content/UpdatePasswordContent.vue @@ -1,22 +1,16 @@ diff --git a/src/components/dialog/content/signin/ApiKeyForm.test.ts b/src/components/dialog/content/signin/ApiKeyForm.test.ts index 4d073cb0883..41efd96f7e4 100644 --- a/src/components/dialog/content/signin/ApiKeyForm.test.ts +++ b/src/components/dialog/content/signin/ApiKeyForm.test.ts @@ -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 }) diff --git a/src/components/dialog/content/signin/ApiKeyForm.vue b/src/components/dialog/content/signin/ApiKeyForm.vue index 22ecb3205af..17ccc3e8852 100644 --- a/src/components/dialog/content/signin/ApiKeyForm.vue +++ b/src/components/dialog/content/signin/ApiKeyForm.vue @@ -18,14 +18,9 @@ -
- - {{ $form.apiKey.error.message }} + + + {{ errors.apiKey }}
@@ -37,13 +32,14 @@
{{ t('auth.apiKey.helpText') }} @@ -79,16 +75,14 @@ {{ t('g.save') }}
- +
diff --git a/src/components/dialog/content/signin/SignInForm.test.ts b/src/components/dialog/content/signin/SignInForm.test.ts index c27d1592953..ed499177f19 100644 --- a/src/components/dialog/content/signin/SignInForm.test.ts +++ b/src/components/dialog/content/signin/SignInForm.test.ts @@ -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) { + 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 = { 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 - } - - // Spy on handleForgotPassword - const handleForgotPasswordSpy = vi.spyOn( - component, - 'handleForgotPassword' - ) - - const forgotPasswordSpan = wrapper.find( - 'span.text-muted.text-base.font-medium.cursor-pointer' - ) + await wrapper + .find('#comfy-org-sign-in-email') + .setValue('test@example.com') - // 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 - } - - // Call onSubmit with invalid form - component.onSubmit({ valid: false, values: {} }) + await wrapper.find('form').trigger('submit') + await nextTick() - // 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(' { + 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') }) }) diff --git a/src/components/dialog/content/signin/SignInForm.vue b/src/components/dialog/content/signin/SignInForm.vue index cbc946fea6f..438d5ea0ff3 100644 --- a/src/components/dialog/content/signin/SignInForm.vue +++ b/src/components/dialog/content/signin/SignInForm.vue @@ -1,63 +1,68 @@ diff --git a/src/components/ui/form/FormControl.vue b/src/components/ui/form/FormControl.vue new file mode 100644 index 00000000000..ef584fd8979 --- /dev/null +++ b/src/components/ui/form/FormControl.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/ui/form/FormDescription.vue b/src/components/ui/form/FormDescription.vue new file mode 100644 index 00000000000..f3d79b2395c --- /dev/null +++ b/src/components/ui/form/FormDescription.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/ui/form/FormField.vue b/src/components/ui/form/FormField.vue new file mode 100644 index 00000000000..bc7339388dc --- /dev/null +++ b/src/components/ui/form/FormField.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/ui/form/FormItem.vue b/src/components/ui/form/FormItem.vue new file mode 100644 index 00000000000..74fbd70ebfd --- /dev/null +++ b/src/components/ui/form/FormItem.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/ui/form/FormLabel.vue b/src/components/ui/form/FormLabel.vue new file mode 100644 index 00000000000..e9ada43269c --- /dev/null +++ b/src/components/ui/form/FormLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/ui/form/FormMessage.vue b/src/components/ui/form/FormMessage.vue new file mode 100644 index 00000000000..2c39cd0e5b6 --- /dev/null +++ b/src/components/ui/form/FormMessage.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/ui/form/injectionKeys.ts b/src/components/ui/form/injectionKeys.ts new file mode 100644 index 00000000000..c2b1bd96563 --- /dev/null +++ b/src/components/ui/form/injectionKeys.ts @@ -0,0 +1,7 @@ +import type { InjectionKey, Ref } from 'vue' + +export const FORM_FIELD_NAME_INJECTION_KEY: InjectionKey> = + Symbol('FORM_FIELD_NAME') + +export const FORM_ITEM_ID_INJECTION_KEY: InjectionKey = + Symbol('FORM_ITEM_ID') diff --git a/src/components/ui/form/useFormField.ts b/src/components/ui/form/useFormField.ts new file mode 100644 index 00000000000..5a3301c2c92 --- /dev/null +++ b/src/components/ui/form/useFormField.ts @@ -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 (`-form-item-description`) + * - `formItemId`: the element id for the form item container (`-form-item`) + * - `formMessageId`: the element id for the field validation message (`-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 + } +} \ No newline at end of file diff --git a/src/composables/useBrowserTabTitle.ts b/src/composables/useBrowserTabTitle.ts index 32f8334cbff..7202f3d9b08 100644 --- a/src/composables/useBrowserTabTitle.ts +++ b/src/composables/useBrowserTabTitle.ts @@ -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}` diff --git a/src/platform/assets/schemas/assetSchema.ts b/src/platform/assets/schemas/assetSchema.ts index d6941e18cfa..9f1c26a2172 100644 --- a/src/platform/assets/schemas/assetSchema.ts +++ b/src/platform/assets/schemas/assetSchema.ts @@ -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({ diff --git a/src/platform/assets/utils/createAssetWidget.ts b/src/platform/assets/utils/createAssetWidget.ts index 7e80290c047..1350a029930 100644 --- a/src/platform/assets/utils/createAssetWidget.ts +++ b/src/platform/assets/utils/createAssetWidget.ts @@ -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) -} +} \ No newline at end of file diff --git a/src/platform/assets/utils/createModelNodeFromAsset.ts b/src/platform/assets/utils/createModelNodeFromAsset.ts index b6010965992..8960aff5501 100644 --- a/src/platform/assets/utils/createModelNodeFromAsset.ts +++ b/src/platform/assets/utils/createModelNodeFromAsset.ts @@ -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) diff --git a/src/platform/cloud/onboarding/components/CloudSignInForm.vue b/src/platform/cloud/onboarding/components/CloudSignInForm.vue index 57718328c67..c94fca69462 100644 --- a/src/platform/cloud/onboarding/components/CloudSignInForm.vue +++ b/src/platform/cloud/onboarding/components/CloudSignInForm.vue @@ -1,10 +1,5 @@