diff --git a/app/features/vods/VodRepository.server.ts b/app/features/vods/VodRepository.server.ts index 124231393..ab3aa3373 100644 --- a/app/features/vods/VodRepository.server.ts +++ b/app/features/vods/VodRepository.server.ts @@ -86,11 +86,12 @@ export async function findVods({ query = query.where("VideoMatch.stageId", "=", stageId); } } - if (typeof weapon === "number") { + if (weapon) { query = query.where( "VideoMatchPlayer.weaponSplId", "in", - weaponIdToArrayWithAlts(weapon), + // TODO: temporary fix until we have a proper search params parsing in place + weaponIdToArrayWithAlts(Number(weapon) as MainWeaponId), ); } const result = await query diff --git a/app/features/vods/routes/vods.new.browser.test.tsx b/app/features/vods/routes/vods.new.browser.test.tsx index 9233b65dc..2eb8f9d91 100644 --- a/app/features/vods/routes/vods.new.browser.test.tsx +++ b/app/features/vods/routes/vods.new.browser.test.tsx @@ -4,7 +4,7 @@ import { userEvent } from "vitest/browser"; import { render } from "vitest-browser-react"; import { FormField } from "~/form/FormField"; import type { WeaponPoolItem } from "~/form/fields/WeaponPoolFormField"; -import { SendouForm } from "~/form/SendouForm"; +import { SendouForm, useFormFieldContext } from "~/form/SendouForm"; import type { MainWeaponId, ModeShort, @@ -216,4 +216,82 @@ describe("VodForm", () => { await expect.element(addButton).toBeDisabled(); }); }); + + describe("setItemField batching", () => { + test("updating multiple fields on same array item preserves all updates", async () => { + let setItemFieldRef: ((field: string, value: unknown) => void) | null = + null; + + function CaptureSetItemField() { + const { values, setValueFromPrev } = useFormFieldContext(); + const matches = values.matches as Array>; + + setItemFieldRef = (field: string, value: unknown) => { + setValueFromPrev("matches", (prev) => { + const currentArray = (prev ?? []) as Record[]; + const newArray = [...currentArray]; + newArray[0] = { ...currentArray[0], [field]: value }; + return newArray; + }); + }; + + return ( +
+ {JSON.stringify({ + weaponsTeamOne: matches[0]?.weaponsTeamOne, + weaponsTeamTwo: matches[0]?.weaponsTeamTwo, + })} +
+ ); + } + + const router = createMemoryRouter( + [ + { + path: "/", + element: ( + + {() => } + + ), + }, + ], + { initialEntries: ["/"] }, + ); + + const screen = await render(); + + const teamOneWeapons = [ + { id: 0 as MainWeaponId, isFavorite: false }, + { id: 10 as MainWeaponId, isFavorite: false }, + ]; + const teamTwoWeapons = [ + { id: 20 as MainWeaponId, isFavorite: false }, + { id: 30 as MainWeaponId, isFavorite: false }, + ]; + + setItemFieldRef!("weaponsTeamOne", teamOneWeapons); + setItemFieldRef!("weaponsTeamTwo", teamTwoWeapons); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const valuesEl = screen.getByTestId("values"); + const valuesText = valuesEl.element().textContent ?? ""; + const parsedValues = JSON.parse(valuesText); + + expect(parsedValues.weaponsTeamOne).toEqual(teamOneWeapons); + expect(parsedValues.weaponsTeamTwo).toEqual(teamTwoWeapons); + }); + }); }); diff --git a/app/form/FormField.tsx b/app/form/FormField.tsx index 100150283..eb05bbf03 100644 --- a/app/form/FormField.tsx +++ b/app/form/FormField.tsx @@ -310,9 +310,15 @@ export function FormField({ const itemValues = arrayValue[idx] ?? {}; const setItemField = (fieldName: string, fieldValue: unknown) => { - const newArray = [...arrayValue]; - newArray[idx] = { ...newArray[idx], [fieldName]: fieldValue }; - handleChange(newArray); + context?.setValueFromPrev(name, (prev) => { + const currentArray = (prev ?? []) as Record[]; + const newArray = [...currentArray]; + newArray[idx] = { + ...currentArray[idx], + [fieldName]: fieldValue, + }; + return newArray; + }); }; const remove = () => { diff --git a/app/form/SendouForm.browser.test.tsx b/app/form/SendouForm.browser.test.tsx index 3c232e8a9..03f6736b4 100644 --- a/app/form/SendouForm.browser.test.tsx +++ b/app/form/SendouForm.browser.test.tsx @@ -1117,5 +1117,34 @@ describe("SendouForm", () => { .element(screen.getByText("This field is required")) .toBeVisible(); }); + + test("setItemField batches multiple field updates correctly", async () => { + const schema = z.object({ + members: array({ + label: "labels.members", + min: 1, + max: 10, + field: fieldset({ + fields: z.object({ + name: textFieldOptional({ label: "labels.name", maxLength: 100 }), + bio: textFieldOptional({ label: "labels.bio", maxLength: 100 }), + }), + }), + }), + }); + + const screen = await renderForm(schema, { + defaultValues: { members: [{ name: "", bio: "" }] }, + }); + + const inputA = screen.getByLabelText("Name"); + const inputB = screen.getByLabelText("Bio"); + + await userEvent.type(inputA.element(), "Value A"); + await userEvent.type(inputB.element(), "Value B"); + + await expect.element(inputA).toHaveValue("Value A"); + await expect.element(inputB).toHaveValue("Value B"); + }); }); }); diff --git a/app/form/SendouForm.tsx b/app/form/SendouForm.tsx index 5ca5974a0..d00ba2ffb 100644 --- a/app/form/SendouForm.tsx +++ b/app/form/SendouForm.tsx @@ -33,6 +33,7 @@ export interface FormContextValue { onFieldChange?: (name: string, newValue: unknown) => void; values: Record; setValue: (name: string, value: unknown) => void; + setValueFromPrev: (name: string, updater: (prev: unknown) => unknown) => void; revalidateAll: (updatedValues: Record) => void; submitToServer: (values: Record) => void; fetcherState: "idle" | "loading" | "submitting"; @@ -160,6 +161,17 @@ export function SendouForm({ } }; + const setValueFromPrev = ( + name: string, + updater: (prev: unknown) => unknown, + ) => { + setValues((prevValues) => { + const prevValue = prevValues[name]; + const newValue = updater(prevValue); + return { ...prevValues, [name]: newValue }; + }); + }; + const validateAndPrepare = (): boolean => { setHasSubmitted(true); setVisibleServerErrors({}); @@ -286,6 +298,7 @@ export function SendouForm({ revalidateAll, values, setValue, + setValueFromPrev, submitToServer, fetcherState: fetcher.state, };