diff --git a/.changeset/@accounter_client-3961-dependencies.md b/.changeset/@accounter_client-3961-dependencies.md new file mode 100644 index 0000000000..d8e56b5c22 --- /dev/null +++ b/.changeset/@accounter_client-3961-dependencies.md @@ -0,0 +1,6 @@ +--- +"@accounter/client": patch +--- +dependencies updates: + - Updated dependency [`radix-ui@1.6.4` ↗︎](https://www.npmjs.com/package/radix-ui/v/1.6.4) (from `1.6.2`, in `dependencies`) + - Updated dependency [`react-hook-form@7.82.0` ↗︎](https://www.npmjs.com/package/react-hook-form/v/7.82.0) (from `7.81.0`, in `dependencies`) diff --git a/.changeset/@accounter_server-3961-dependencies.md b/.changeset/@accounter_server-3961-dependencies.md new file mode 100644 index 0000000000..3f5765dcee --- /dev/null +++ b/.changeset/@accounter_server-3961-dependencies.md @@ -0,0 +1,7 @@ +--- +"@accounter/server": patch +--- +dependencies updates: + - Updated dependency [`@ai-sdk/anthropic@4.0.16` ↗︎](https://www.npmjs.com/package/@ai-sdk/anthropic/v/4.0.16) (from `4.0.15`, in `dependencies`) + - Updated dependency [`@graphql-tools/utils@11.2.2` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/11.2.2) (from `11.2.0`, in `dependencies`) + - Updated dependency [`ai@7.0.31` ↗︎](https://www.npmjs.com/package/ai/v/7.0.31) (from `7.0.29`, in `dependencies`) diff --git a/packages/client/package.json b/packages/client/package.json index a7363631e4..4fd8970e58 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -66,7 +66,7 @@ "react-dnd": "16.0.1", "react-dom": "19.2.7", "react-error-boundary": "6.1.2", - "react-hook-form": "7.81.0", + "react-hook-form": "7.82.0", "react-number-format": "5.4.5", "react-router-dom": "7.18.1", "react-to-pdf": "3.2.2", diff --git a/packages/client/src/components/common/business-trip-report/parts/attendee-stay-input.tsx b/packages/client/src/components/common/business-trip-report/parts/attendee-stay-input.tsx index 811080d089..17fa19295b 100644 --- a/packages/client/src/components/common/business-trip-report/parts/attendee-stay-input.tsx +++ b/packages/client/src/components/common/business-trip-report/parts/attendee-stay-input.tsx @@ -2,9 +2,7 @@ import { useEffect, useState, type ReactElement } from 'react'; import { ListPlus, Trash2 } from 'lucide-react'; import { Controller, - useFieldArray, type ArrayPath, - type FieldArray, type FieldValues, type Path, type UseFormReturn, @@ -12,6 +10,10 @@ import { import { useQuery } from 'urql'; import { NumberInput, Select } from '@mantine/core'; import { AttendeesByBusinessTripDocument } from '../../../../gql/graphql.js'; +import { + useControlledFieldArray, + type FieldArrayItem, +} from '../../../../hooks/use-controlled-field-array.js'; import { Button } from '../../../ui/button.js'; type Props = { @@ -32,22 +34,14 @@ export function AttendeesStayInput({ fetchingAttendees = false, businessTripId, }: Props): ReactElement { - const { control, watch, trigger } = formManager; - const { fields, append, remove } = useFieldArray({ - control, - name: attendeesStayPath, - }); + const { control, trigger } = formManager; + const { controlledFields, append, remove } = useControlledFieldArray( + formManager, + attendeesStayPath, + ); const [fetching, setFetching] = useState(fetchingAttendees); const [attendees, setAttendees] = useState(attendeesData ?? []); - const watchFieldArray = watch(attendeesStayPath as Path); - const controlledFields = fields.map((field, index) => { - return { - ...field, - ...watchFieldArray[index], - }; - }); - useEffect(() => { if (attendeesData) { setAttendees(attendeesData); @@ -169,7 +163,7 @@ export function AttendeesStayInput({ size="icon" className="size-7.5" onClick={(): void => { - append({} as FieldArray>); + append({} as FieldArrayItem); trigger(attendeesStayPath as Path); }} > diff --git a/packages/client/src/components/common/business-trip-report/parts/flight-path-input.tsx b/packages/client/src/components/common/business-trip-report/parts/flight-path-input.tsx index 50328d92d0..143cfa87c9 100644 --- a/packages/client/src/components/common/business-trip-report/parts/flight-path-input.tsx +++ b/packages/client/src/components/common/business-trip-report/parts/flight-path-input.tsx @@ -1,13 +1,10 @@ import type { ReactElement } from 'react'; import { ListPlus, Trash2 } from 'lucide-react'; +import { type ArrayPath, type FieldValues, type Path, type UseFormReturn } from 'react-hook-form'; import { - useFieldArray, - type ArrayPath, - type FieldArray, - type FieldValues, - type Path, - type UseFormReturn, -} from 'react-hook-form'; + useControlledFieldArray, + type FieldArrayItem, +} from '../../../../hooks/use-controlled-field-array.js'; import { Button } from '../../../ui/button.js'; import { FormControl, FormField, FormItem, FormMessage } from '../../../ui/form'; import { Input } from '../../../ui/input'; @@ -24,19 +21,11 @@ export function FlightPathInput({ flightPathPath, flightPathData, }: Props): ReactElement { - const { control, watch, trigger } = formManager; - const { fields, append, remove } = useFieldArray({ - control, - name: flightPathPath as ArrayPath, - }); - - const watchFieldArray = watch(flightPathPath); - const controlledFields = fields.map((field, index) => { - return { - ...field, - ...watchFieldArray[index], - }; - }); + const { control, trigger } = formManager; + const { controlledFields, append, remove } = useControlledFieldArray( + formManager, + flightPathPath as ArrayPath, + ); if (!flightPathData) { return
Cannot edit flight path stay, lacking some mandatory information!
; @@ -87,7 +76,7 @@ export function FlightPathInput({ size="icon" className="size-7.5" onClick={(): void => { - append('' as FieldArray>); + append('' as FieldArrayItem); trigger(flightPathPath); }} > diff --git a/packages/client/src/components/common/inputs/charge-spread-input.tsx b/packages/client/src/components/common/inputs/charge-spread-input.tsx index fbc4a5bdcc..391756ca4b 100644 --- a/packages/client/src/components/common/inputs/charge-spread-input.tsx +++ b/packages/client/src/components/common/inputs/charge-spread-input.tsx @@ -3,15 +3,17 @@ import { format } from 'date-fns'; import { ListPlus, Trash2 } from 'lucide-react'; import { Controller, - useFieldArray, type ArrayPath, - type FieldArray, type FieldValues, type Path, type UseFormReturn, } from 'react-hook-form'; import { NumberInput } from '@mantine/core'; import { YearPickerInput } from '@mantine/dates'; +import { + useControlledFieldArray, + type FieldArrayItem, +} from '../../../hooks/use-controlled-field-array.js'; import { Button } from '../../ui/button.js'; type Props = { @@ -23,19 +25,11 @@ export function ChargeSpreadInput({ formManager, chargeSpreadPath, }: Props): ReactElement { - const { control, watch, trigger } = formManager; - const { fields, append, remove } = useFieldArray({ - control, - name: chargeSpreadPath, - }); - - const watchFieldArray = watch(chargeSpreadPath as Path); - const controlledFields = fields.map((field, index) => { - return { - ...field, - ...watchFieldArray[index], - }; - }); + const { control, trigger } = formManager; + const { controlledFields, append, remove, watchFieldArray } = useControlledFieldArray( + formManager, + chargeSpreadPath, + ); return (
@@ -50,14 +44,13 @@ export function ChargeSpreadInput({ rules={{ required: 'Required', validate: (value: string): boolean | string => { - const years = watchFieldArray.map((record: { year: string }) => record.year); - if (years.filter((year: string) => year === value).length > 1) { + const years = watchFieldArray.map(record => (record as { year?: string }).year); + if (years.filter(year => year === value).length > 1) { return 'Years must be unique'; } return true; }, }} - // defaultValue={`${new Date().getFullYear()}-01-01` as PathValue>} render={({ field: { value, ...field }, fieldState }): ReactElement => { return ( ({ size="icon" className="size-7.5" onClick={(): void => { - append({ year: `${new Date().getFullYear()}-01-01` } as FieldArray>); + append({ year: `${new Date().getFullYear()}-01-01` } as FieldArrayItem); trigger(chargeSpreadPath as Path); }} > diff --git a/packages/client/src/components/common/inputs/string-array-input.tsx b/packages/client/src/components/common/inputs/string-array-input.tsx index 53550dbef1..f8d2f7443d 100644 --- a/packages/client/src/components/common/inputs/string-array-input.tsx +++ b/packages/client/src/components/common/inputs/string-array-input.tsx @@ -1,13 +1,10 @@ import type { ReactElement } from 'react'; import { ListPlus, Trash2 } from 'lucide-react'; +import { type ArrayPath, type FieldValues, type Path, type UseFormReturn } from 'react-hook-form'; import { - useFieldArray, - type ArrayPath, - type FieldArray, - type FieldValues, - type Path, - type UseFormReturn, -} from 'react-hook-form'; + useControlledFieldArray, + type FieldArrayItem, +} from '../../../hooks/use-controlled-field-array.js'; import { Button } from '../../ui/button.js'; import { FormControl, FormField, FormItem, FormMessage } from '../../ui/form.js'; import { Input } from '../../ui/input.js'; @@ -23,19 +20,11 @@ export function StringArrayInput({ formManager, arrayPath, }: Props): ReactElement { - const { control, watch } = formManager; - const { fields, append, remove } = useFieldArray({ - control, - name: arrayPath as ArrayPath, - }); - - const watchStringsArray = watch(arrayPath); - const controlledFields = fields.map((field, index) => { - return { - id: field.id, - ...watchStringsArray[index], - }; - }); + const { control } = formManager; + const { controlledFields, append, remove } = useControlledFieldArray( + formManager, + arrayPath as ArrayPath, + ); return (
@@ -82,7 +71,7 @@ export function StringArrayInput({ variant="ghost" size="icon" className="size-7.5" - onClick={() => append(undefined as FieldArray)} + onClick={() => append(undefined as FieldArrayItem)} > diff --git a/packages/client/src/hooks/use-controlled-field-array.ts b/packages/client/src/hooks/use-controlled-field-array.ts new file mode 100644 index 0000000000..3462f6198f --- /dev/null +++ b/packages/client/src/hooks/use-controlled-field-array.ts @@ -0,0 +1,58 @@ +import { + useFieldArray, + type ArrayPath, + type FieldArrayPathValue, + type FieldValues, + type Path, + type UseFieldArrayReturn, + type UseFormReturn, +} from 'react-hook-form'; + +/** + * Item type of the field array at the given path. Reconstruction of react-hook-form's + * `FieldArray` type, which is shadowed since v7.82 by the new `FieldArray` component + * and can no longer be imported from the package root. + */ +export type FieldArrayItem< + TFieldValues extends FieldValues, + TPath extends ArrayPath = ArrayPath, +> = + FieldArrayPathValue extends ReadonlyArray | null | undefined + ? TItem + : never; + +type UseControlledFieldArrayReturn< + TFieldValues extends FieldValues, + TPath extends ArrayPath, +> = Pick, 'append' | 'remove'> & { + controlledFields: UseFieldArrayReturn['fields']; + watchFieldArray: FieldArrayItem[]; +}; + +/** + * Wraps `useFieldArray` and merges the registered fields with the watched values, + * so consumers render always-current values while keeping stable field ids as keys. + */ +export function useControlledFieldArray< + TFieldValues extends FieldValues, + TPath extends ArrayPath = ArrayPath, +>( + formManager: UseFormReturn, + arrayPath: TPath, +): UseControlledFieldArrayReturn { + const { control, watch } = formManager; + const { fields, append, remove } = useFieldArray({ control, name: arrayPath }); + + const watchFieldArray = (watch(arrayPath as unknown as Path) ?? + []) as FieldArrayItem[]; + + const controlledFields = fields.map((field, index) => { + const watchedValue = watchFieldArray[index]; + return { + ...field, + ...(typeof watchedValue === 'object' && watchedValue !== null ? watchedValue : {}), + }; + }); + + return { controlledFields, append, remove, watchFieldArray }; +} diff --git a/yarn.lock b/yarn.lock index 347fa3a22b..33593015e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -117,7 +117,7 @@ __metadata: react-dnd: "npm:16.0.1" react-dom: "npm:19.2.7" react-error-boundary: "npm:6.1.2" - react-hook-form: "npm:7.81.0" + react-hook-form: "npm:7.82.0" react-number-format: "npm:5.4.5" react-router-dom: "npm:7.18.1" react-to-pdf: "npm:3.2.2" @@ -22413,12 +22413,12 @@ __metadata: languageName: node linkType: hard -"react-hook-form@npm:7.81.0": - version: 7.81.0 - resolution: "react-hook-form@npm:7.81.0" +"react-hook-form@npm:7.82.0": + version: 7.82.0 + resolution: "react-hook-form@npm:7.82.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10c0/bdb5b0bb44050ffa54425dbbea855e55696847ae8ff87f77fe43bc85d830d67735ecdeaaf188ba30d9567fad8b0c47a5f42a9a522ed9411222773512e0e7f37b + checksum: 10c0/89a3f775a9f2503a2ba4586e6080ddcb435331ef2fcf8cb3d2b29e69386fe21c387b94d8b7000d968357c354dcbf38f650bedeec319971d99c67249a6eec9137 languageName: node linkType: hard