New SendouForm fix 2 user reported bugs

This commit is contained in:
Kalle 2026-01-20 17:48:27 +02:00
parent 400f5ce301
commit 562cfab0d0
5 changed files with 133 additions and 6 deletions

View File

@ -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

View File

@ -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<Record<string, unknown>>;
setItemFieldRef = (field: string, value: unknown) => {
setValueFromPrev("matches", (prev) => {
const currentArray = (prev ?? []) as Record<string, unknown>[];
const newArray = [...currentArray];
newArray[0] = { ...currentArray[0], [field]: value };
return newArray;
});
};
return (
<div data-testid="values">
{JSON.stringify({
weaponsTeamOne: matches[0]?.weaponsTeamOne,
weaponsTeamTwo: matches[0]?.weaponsTeamTwo,
})}
</div>
);
}
const router = createMemoryRouter(
[
{
path: "/",
element: (
<SendouForm
title="Test VOD Form"
schema={vodFormBaseSchema}
defaultValues={createDefaultValues({
matches: [
{
weaponsTeamOne: [],
weaponsTeamTwo: [],
},
],
})}
>
{() => <CaptureSetItemField />}
</SendouForm>
),
},
],
{ initialEntries: ["/"] },
);
const screen = await render(<RouterProvider router={router} />);
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);
});
});
});

View File

@ -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<string, unknown>[];
const newArray = [...currentArray];
newArray[idx] = {
...currentArray[idx],
[fieldName]: fieldValue,
};
return newArray;
});
};
const remove = () => {

View File

@ -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");
});
});
});

View File

@ -33,6 +33,7 @@ export interface FormContextValue<T extends z.ZodRawShape = z.ZodRawShape> {
onFieldChange?: (name: string, newValue: unknown) => void;
values: Record<string, unknown>;
setValue: (name: string, value: unknown) => void;
setValueFromPrev: (name: string, updater: (prev: unknown) => unknown) => void;
revalidateAll: (updatedValues: Record<string, unknown>) => void;
submitToServer: (values: Record<string, unknown>) => void;
fetcherState: "idle" | "loading" | "submitting";
@ -160,6 +161,17 @@ export function SendouForm<T extends z.ZodRawShape>({
}
};
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<T extends z.ZodRawShape>({
revalidateAll,
values,
setValue,
setValueFromPrev,
submitToServer,
fetcherState: fetcher.state,
};