From b802dcd8cfedd813979379f8821ab71d49cbfdef Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 6 Apr 2025 10:26:03 -0400 Subject: [PATCH 01/11] fix: remove border color interop --- src/tailwind/globals.css | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/tailwind/globals.css b/src/tailwind/globals.css index 6326340e..f062b877 100644 --- a/src/tailwind/globals.css +++ b/src/tailwind/globals.css @@ -118,24 +118,6 @@ } } -/* - The default border color has changed to `currentColor` in Tailwind CSS v4, - so we've added these compatibility styles to make sure everything still - looks the same as it did with Tailwind CSS v3. - - If we ever want to remove these styles, we need to add an explicit border - color utility to any element that depends on these defaults. -*/ -@layer base { - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentColor); - } -} - @layer base { :root { --background: var(--color-slate-100); From b06902c7ae3751fd2d763067e7b5023686590fff Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 6 Apr 2025 10:38:18 -0400 Subject: [PATCH 02/11] fix: dialog styles --- src/components/Dialog/DialogContent.tsx | 2 +- src/components/Dialog/DialogOverlay.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Dialog/DialogContent.tsx b/src/components/Dialog/DialogContent.tsx index f94b7278..94f4cc22 100644 --- a/src/components/Dialog/DialogContent.tsx +++ b/src/components/Dialog/DialogContent.tsx @@ -16,7 +16,7 @@ export const DialogContent = forwardRef< Date: Sun, 6 Apr 2025 10:40:04 -0400 Subject: [PATCH 03/11] chore: add local to gitignore --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 46228988..22359798 100644 --- a/.gitignore +++ b/.gitignore @@ -132,5 +132,8 @@ dist # Storybook storybook-static -#VScode -.vscode \ No newline at end of file +# VScode +.vscode + +# Local Scripts +local/ \ No newline at end of file From 2b122e5be36ddd27767a1ff747d2f6d0e32aed20 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 6 Apr 2025 12:18:47 -0400 Subject: [PATCH 04/11] fix: set cursor for Button --- src/components/Button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 6c80e862..86a7d0c5 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -14,7 +14,7 @@ export const BUTTON_ICON_SIZE = { }; export const buttonVariants = cva( - 'flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + 'flex items-center justify-center whitespace-nowrap cursor-pointer rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', { defaultVariants: { size: 'md', From 4b7ad3cb5bf7923c1bb155a7f9f453317879d59a Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 6 Apr 2025 15:00:32 -0400 Subject: [PATCH 05/11] chore: update libjs --- package.json | 2 +- pnpm-lock.yaml | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a26e56ff..6b06abfe 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "zod": "^3.23.6" }, "dependencies": { - "@douglasneuroinformatics/libjs": "^2.7.0", + "@douglasneuroinformatics/libjs": "^2.8.0", "@douglasneuroinformatics/libui-form-types": "^0.11.0", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0a6b3bb..b4fa653a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,8 @@ importers: .: dependencies: '@douglasneuroinformatics/libjs': - specifier: ^2.7.0 - version: 2.7.0(neverthrow@8.2.0)(zod@3.24.2) + specifier: ^2.8.0 + version: 2.8.0(neverthrow@8.2.0)(zod@3.24.2) '@douglasneuroinformatics/libui-form-types': specifier: ^0.11.0 version: 0.11.0 @@ -467,9 +467,9 @@ packages: typescript: optional: true - '@douglasneuroinformatics/libjs@2.7.0': + '@douglasneuroinformatics/libjs@2.8.0': resolution: - { integrity: sha512-mx6jhGPbPC5SK6Xv9+N2y5uNtBkSwsJuRudUfDj5C8UB3sigPUsVeWPRhkkVZCHNUXIWNvXEd4i+NW7lY4FLbQ== } + { integrity: sha512-2hNFONcYsfQ5wSsTn3NjXSrqEpwdmQ0GmTajaUrX1DOQpRKSt5MmlxhTzLon51S86eR4rbSUkBEsD0AUonFGiw== } peerDependencies: neverthrow: ^8.2.0 zod: ^3.22.6 @@ -5452,6 +5452,11 @@ packages: engines: { node: '>=10' } hasBin: true + serialize-error@12.0.0: + resolution: + { integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw== } + engines: { node: '>=18' } + set-function-length@1.2.2: resolution: { integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== } @@ -6582,11 +6587,12 @@ snapshots: - svelte - ts-node - '@douglasneuroinformatics/libjs@2.7.0(neverthrow@8.2.0)(zod@3.24.2)': + '@douglasneuroinformatics/libjs@2.8.0(neverthrow@8.2.0)(zod@3.24.2)': dependencies: clean-stack: 5.2.0 extract-stack: 3.0.0 neverthrow: 8.2.0 + serialize-error: 12.0.0 stringify-object: 5.0.0 type-fest: 4.39.0 zod: 3.24.2 @@ -10814,6 +10820,10 @@ snapshots: semver@7.7.1: {} + serialize-error@12.0.0: + dependencies: + type-fest: 4.39.0 + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 From 4731ca5fdf8e80213e9df23d332e25565e6366fd Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 6 Apr 2025 15:10:58 -0400 Subject: [PATCH 06/11] test: add ErrorFallback test --- .../ErrorFallback/ErrorFallback.spec.tsx | 14 ++++++++++++++ src/components/ErrorFallback/ErrorFallback.tsx | 9 ++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/components/ErrorFallback/ErrorFallback.spec.tsx diff --git a/src/components/ErrorFallback/ErrorFallback.spec.tsx b/src/components/ErrorFallback/ErrorFallback.spec.tsx new file mode 100644 index 00000000..71588305 --- /dev/null +++ b/src/components/ErrorFallback/ErrorFallback.spec.tsx @@ -0,0 +1,14 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { ErrorFallback } from './ErrorFallback'; + +const TEST_ID = 'error-fallback'; + +describe('ErrorFallback', () => { + it('should render', () => { + const error = new Error('Something went wrong'); + render(); + expect(screen.getByTestId(TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/src/components/ErrorFallback/ErrorFallback.tsx b/src/components/ErrorFallback/ErrorFallback.tsx index 6f64811a..5608e947 100644 --- a/src/components/ErrorFallback/ErrorFallback.tsx +++ b/src/components/ErrorFallback/ErrorFallback.tsx @@ -12,10 +12,13 @@ export const ErrorFallback = ({ error }: ErrorFallbackProps) => { }, [error]); return ( -
-

Unexpected Error

+
+

Unexpected Error

Something Went Wrong

-

+

We apologize for the inconvenience. Please contact us for further assistance.

From c98db6c5556effe107260370dadf0fe08375cf08 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Fri, 18 Apr 2025 11:27:33 -0400 Subject: [PATCH 07/11] fix: add customStyles option to Form --- src/components/Form/Form.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index b014df68..5c282ca0 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -31,6 +31,10 @@ type FormProps, TData extends z.TypeOf; + customStyles?: { + resetBtn?: string; + submitBtn?: string; + }; fieldsFooter?: React.ReactNode; id?: string; initialValues?: PartialNullableFormDataType>; @@ -49,6 +53,7 @@ const Form = , TData extends z.TypeOf, TData extends z.TypeOf )} {fieldGroup.description && ( -

{fieldGroup.description}

+

{fieldGroup.description}

)}
, TData extends z.TypeOf, TData extends z.TypeOf Date: Fri, 18 Apr 2025 13:10:23 -0400 Subject: [PATCH 08/11] fix: allow inline optional inline translations --- src/i18n/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 34d21010..21f7ddac 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -47,5 +47,5 @@ export type TranslationKey = TNamespace extends TranslationNamespace export interface TranslateFunction { (key: TranslationKey, ...args: Exclude[]): string; - (translations: { [L in Language]: string }, ...args: Exclude[]): string; + (translations: { [L in Language]?: string }, ...args: Exclude[]): string; } From 67710bf474fc99a95f4ea2441a1b0e279a5c7380 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sat, 19 Apr 2025 16:48:03 -0400 Subject: [PATCH 09/11] feat: add beforeSubmit callback to form --- src/components/Form/ErrorMessage.tsx | 6 ++++-- src/components/Form/Form.stories.tsx | 24 ++++++++++++++++++++++++ src/components/Form/Form.tsx | 17 +++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/components/Form/ErrorMessage.tsx b/src/components/Form/ErrorMessage.tsx index 8d2a09cd..2612bb75 100644 --- a/src/components/Form/ErrorMessage.tsx +++ b/src/components/Form/ErrorMessage.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import { CircleAlertIcon } from 'lucide-react'; -export const ErrorMessage: React.FC<{ error?: null | string[] }> = ({ error }) => { +import { cn } from '@/utils'; + +export const ErrorMessage: React.FC<{ className?: string; error?: null | string[] }> = ({ className, error }) => { return error ? (
{error.map((message) => ( -
+
{message}
diff --git a/src/components/Form/Form.stories.tsx b/src/components/Form/Form.stories.tsx index 1a0b6272..ec7cdb5e 100644 --- a/src/components/Form/Form.stories.tsx +++ b/src/components/Form/Form.stories.tsx @@ -530,3 +530,27 @@ export const WithSuspend: StoryObj, { dela }) } }; + +export const WithError: StoryObj = { + args: { + content: { + name: { + kind: 'string', + label: 'Name', + variant: 'input' + } + }, + beforeSubmit: (data) => { + if (data.name === 'Winston') { + return { success: true }; + } + return { success: false, errorMessage: "Name must be 'Winston'" }; + }, + onSubmit: () => { + alert('Success!'); + }, + validationSchema: $SimpleExampleFormData.extend({ + name: z.string().min(3) + }) + } +}; diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 5c282ca0..cf42703b 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -29,6 +29,7 @@ type FormProps, TData extends z.TypeOf) => Promisable<{ errorMessage: string; success: false } | { success: true }>; className?: string; content: FormContent; customStyles?: { @@ -51,6 +52,7 @@ type FormProps, TData extends z.TypeOf, TData extends z.TypeOf = z.TypeOf>({ additionalButtons, + beforeSubmit, className, content, customStyles, @@ -78,6 +80,7 @@ const Form = , TData extends z.TypeOf) => { const fieldErrors: FormErrors = {}; + const rootErrors: string[] = []; for (const issue of error.issues) { if (issue.path.length > 0) { const current = get(fieldErrors, issue.path) as string[] | undefined; @@ -87,10 +90,11 @@ const Form = , TData extends z.TypeOf [...prevErrors, issue.message]); + rootErrors.push(issue.message); } } setErrors(fieldErrors); + setRootErrors(rootErrors); if (onError) { onError(error); } @@ -112,6 +116,15 @@ const Form = , TData extends z.TypeOf, TData extends z.TypeOf )} + {Boolean(rootErrors.length) && } {fieldsFooter}
{additionalButtons?.left} @@ -234,7 +248,6 @@ const Form = , TData extends z.TypeOf - {Boolean(rootErrors.length) && } ); }; From e3114852566f9994f09a6afaa70cbd29839ba97d Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sat, 19 Apr 2025 17:06:23 -0400 Subject: [PATCH 10/11] fix: add id to NumberFieldInput --- src/components/Form/NumberField/NumberFieldInput.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Form/NumberField/NumberFieldInput.tsx b/src/components/Form/NumberField/NumberFieldInput.tsx index 222decba..88802be5 100644 --- a/src/components/Form/NumberField/NumberFieldInput.tsx +++ b/src/components/Form/NumberField/NumberFieldInput.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useId, useRef, useState } from 'react'; import { parseNumber } from '@douglasneuroinformatics/libjs'; import type { NumberFormField } from '@douglasneuroinformatics/libui-form-types'; @@ -27,6 +27,7 @@ export const NumberFieldInput = ({ setValue, value }: NumberFieldInputProps) => { + const id = useId(); const [inputValue, setInputValue] = useState(value?.toString() ?? ''); const valueRef = useRef(value); @@ -65,11 +66,12 @@ export const NumberFieldInput = ({ return ( - + Date: Sat, 19 Apr 2025 17:08:25 -0400 Subject: [PATCH 11/11] test: add test for new form feature --- src/components/Form/ErrorMessage.tsx | 2 +- src/components/Form/Form.test.tsx | 65 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/components/Form/ErrorMessage.tsx b/src/components/Form/ErrorMessage.tsx index 2612bb75..30de0914 100644 --- a/src/components/Form/ErrorMessage.tsx +++ b/src/components/Form/ErrorMessage.tsx @@ -10,7 +10,7 @@ export const ErrorMessage: React.FC<{ className?: string; error?: null | string[ {error.map((message) => (
- {message} + {message}
)) ?? null}
diff --git a/src/components/Form/Form.test.tsx b/src/components/Form/Form.test.tsx index dc3239b0..fb1c93f6 100644 --- a/src/components/Form/Form.test.tsx +++ b/src/components/Form/Form.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; import { z } from 'zod'; import { Form } from './Form'; @@ -114,4 +115,68 @@ describe('Form', () => { expect(onSubmit.mock.lastCall?.[0].b).toBeUndefined(); }); }); + + describe('custom beforeSubmit error', () => { + let beforeSubmit: Mock; + + beforeEach(() => { + beforeSubmit = vi.fn(); + render( +
+ ); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render', () => { + expect(screen.getByTestId(testid)).toBeInTheDocument(); + }); + + it('should not allow submitting the form with a zod error', async () => { + fireEvent.submit(screen.getByTestId(testid)); + await waitFor(() => + expect(screen.getAllByTestId('error-message-text').map((e) => e.innerHTML)).toMatchObject([ + 'Please enter a number' + ]) + ); + expect(beforeSubmit).not.toHaveBeenCalled(); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('should not allow submitting the form with the beforeSubmit error', async () => { + beforeSubmit.mockResolvedValueOnce({ errorMessage: 'Invalid!', success: false }); + const field: HTMLInputElement = screen.getByLabelText('Value'); + await userEvent.type(field, '-1'); + fireEvent.submit(screen.getByTestId(testid)); + await waitFor(() => + expect(screen.getAllByTestId('error-message-text').map((e) => e.innerHTML)).toMatchObject(['Invalid!']) + ); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('should allow submitting the form if beforeSubmit returns true', async () => { + beforeSubmit.mockResolvedValueOnce({ success: true }); + const field: HTMLInputElement = screen.getByLabelText('Value'); + await userEvent.type(field, '-1'); + fireEvent.submit(screen.getByTestId(testid)); + await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce()); + }); + }); });