From d125713da189daa902869fa8253ce616b70fa064 Mon Sep 17 00:00:00 2001 From: Les Date: Fri, 12 Jul 2024 14:13:20 +0200 Subject: [PATCH] add errors and warnings to CandidateVotesForm --- .../CandidatesVotesForm.tsx | 72 ++++++------ .../voters_and_votes/VotersAndVotesForm.tsx | 103 ++++++++---------- frontend/app/test/unit/test-utils.ts | 19 +++- frontend/lib/api/form/index.ts | 1 + frontend/lib/api/form/useErrorsAndWarnings.ts | 53 +++++++++ frontend/lib/ui/InputGrid/InputGridRow.tsx | 16 +-- frontend/lib/util/fields.test.ts | 1 + frontend/lib/util/fields.ts | 12 +- 8 files changed, 174 insertions(+), 103 deletions(-) create mode 100644 frontend/lib/api/form/useErrorsAndWarnings.ts diff --git a/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx b/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx index 4099e807a..522ccb492 100644 --- a/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx +++ b/frontend/app/component/form/candidates_votes_form/CandidatesVotesForm.tsx @@ -5,13 +5,14 @@ import { CandidateVotes, PoliticalGroup, PoliticalGroupVotes, + useErrorsAndWarnings, usePoliticalGroup, } from "@kiesraad/api"; -import { BottomBar, Button, Feedback, InputGrid } from "@kiesraad/ui"; +import { BottomBar, Button, Feedback, InputGrid, InputGridRow } from "@kiesraad/ui"; import { usePositiveNumberInputMask, usePreventFormEnterSubmit } from "@kiesraad/util"; interface FormElements extends HTMLFormControlsCollection { - listtotal: HTMLInputElement; + total: HTMLInputElement; "candidatevotes[]": HTMLInputElement[]; } @@ -23,8 +24,10 @@ export interface CandidatesVotesFormProps { group: PoliticalGroup; } +//political_group_votes[1].candidate_votes[1].votes + export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) { - const { register, format, deformat } = usePositiveNumberInputMask(); + const { register, format, deformat, warnings: inputMaskWarnings } = usePositiveNumberInputMask(); const formRef = React.useRef(null); const { sectionValues, @@ -37,6 +40,8 @@ export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) { setTemporaryCache, } = usePoliticalGroup(group.number); + console.log(errors); + usePreventFormEnterSubmit(formRef); const getValues = React.useCallback( @@ -50,13 +55,16 @@ export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) { } return { number: group.number, - total: deformat(elements.listtotal.value), + total: deformat(elements.total.value), candidate_votes: candidate_votes, }; }, [deformat, group], ); + const errorsAndWarnings = useErrorsAndWarnings(errors, warnings, inputMaskWarnings); + + console.log(errorsAndWarnings); //const blocker = useBlocker() use const blocker to render confirmation UI. useBlocker(() => { if (formRef.current && !isCalled) { @@ -127,42 +135,32 @@ export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) { const addSeparator = (index + 1) % 25 == 0; const defaultValue = sectionValues?.candidate_votes[index]?.votes || ""; return ( - - {index + 1} - - - - - {candidate.last_name}, {candidate.initials} ({candidate.first_name}) - - + field={`${index + 1}`} + name="candidatevotes[]" + id={`candidate_votes-${candidate.number}.votes`} + title={`${candidate.last_name}, ${candidate.initials} (${candidate.first_name})`} + errorsAndWarnings={errorsAndWarnings} + inputProps={register()} + format={format} + addSeparator={addSeparator} + defaultValue={defaultValue} + /> ); })} - - - - - - Totaal lijst {group.number} - + diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx index 5c33bfa9f..6c9f94d64 100644 --- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx +++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx @@ -1,17 +1,8 @@ import * as React from "react"; -import { - ValidationResult, - ErrorsAndWarnings, - useVotersAndVotes, - VotersAndVotesValues, -} from "@kiesraad/api"; +import { useVotersAndVotes, VotersAndVotesValues, useErrorsAndWarnings } from "@kiesraad/api"; import { Button, InputGrid, Feedback, BottomBar, InputGridRow, useTooltip } from "@kiesraad/ui"; -import { - usePositiveNumberInputMask, - usePreventFormEnterSubmit, - fieldNameFromPath, -} from "@kiesraad/util"; +import { usePositiveNumberInputMask, usePreventFormEnterSubmit } from "@kiesraad/util"; import { useBlocker } from "react-router-dom"; interface FormElements extends HTMLFormControlsCollection { @@ -93,46 +84,48 @@ export function VotersAndVotesForm() { return false; }); - const errorsAndWarnings: Map = React.useMemo(() => { - const result = new Map(); + const errorsAndWarnings = useErrorsAndWarnings(errors, warnings, inputMaskWarnings); - const process = (target: keyof ErrorsAndWarnings, arr: ValidationResult[]) => { - arr.forEach((v) => { - v.fields.forEach((f) => { - const fieldName = fieldNameFromPath(f); - if (!result.has(fieldName)) { - result.set(fieldName, { errors: [], warnings: [] }); - } - const field = result.get(fieldName); - if (field) { - field[target].push({ - code: v.code, - id: fieldName, - }); - } - }); - }); - }; + // const errorsAndWarnings: Map = React.useMemo(() => { + // const result = new Map(); - if (errors.length > 0) { - process("errors", errors); - } - if (warnings.length > 0) { - process("warnings", warnings); - } + // const process = (target: keyof ErrorsAndWarnings, arr: ValidationResult[]) => { + // arr.forEach((v) => { + // v.fields.forEach((f) => { + // const fieldName = fieldNameFromPath(f); + // if (!result.has(fieldName)) { + // result.set(fieldName, { errors: [], warnings: [] }); + // } + // const field = result.get(fieldName); + // if (field) { + // field[target].push({ + // code: v.code, + // id: fieldName, + // }); + // } + // }); + // }); + // }; + + // if (errors.length > 0) { + // process("errors", errors); + // } + // if (warnings.length > 0) { + // process("warnings", warnings); + // } - inputMaskWarnings.forEach((warning) => { - if (!result.has(warning.id)) { - result.set(warning.id, { errors: [], warnings: [] }); - } - const field = result.get(warning.id); - if (field) { - field.warnings.push(warning); - } - }); + // inputMaskWarnings.forEach((warning) => { + // if (!result.has(warning.id)) { + // result.set(warning.id, { errors: [], warnings: [] }); + // } + // const field = result.get(warning.id); + // if (field) { + // field.warnings.push(warning); + // } + // }); - return result; - }, [errors, warnings, inputMaskWarnings]); + // return result; + // }, [errors, warnings, inputMaskWarnings]); React.useEffect(() => { if (isCalled) { @@ -189,7 +182,7 @@ export function VotersAndVotesForm() { ) => - render(ui, { wrapper: Providers, ...options }); +const customRender = (ui: ReactElement, options?: Omit) => { + const result: RenderResult = render(ui, { wrapper: Providers, ...options }); + + const fillFormValues = (values: Record) => { + Object.keys(values).forEach((key) => { + const input = result.getByTestId(key); + fireEvent.change(input, { target: { value: values[key] } }); + }); + }; + + return { + ...result, + fillFormValues, + }; +}; export * from "@testing-library/react"; export { customRender as render }; diff --git a/frontend/lib/api/form/index.ts b/frontend/lib/api/form/index.ts index 4eba7a29d..1c6b20e00 100644 --- a/frontend/lib/api/form/index.ts +++ b/frontend/lib/api/form/index.ts @@ -1 +1,2 @@ export * from "./pollingstation"; +export * from "./useErrorsAndWarnings"; diff --git a/frontend/lib/api/form/useErrorsAndWarnings.ts b/frontend/lib/api/form/useErrorsAndWarnings.ts new file mode 100644 index 000000000..8812cc9af --- /dev/null +++ b/frontend/lib/api/form/useErrorsAndWarnings.ts @@ -0,0 +1,53 @@ +import * as React from "react"; +import { ErrorsAndWarnings, FieldValidationResult } from "../api"; +import { ValidationResult } from "../gen/openapi"; +import { fieldNameFromPath } from "@kiesraad/util"; + +export function useErrorsAndWarnings( + errors: ValidationResult[], + warnings: ValidationResult[], + clientWarnings: FieldValidationResult[], +) { + const errorsAndWarnings: Map = React.useMemo(() => { + const result = new Map(); + + const process = (target: keyof ErrorsAndWarnings, arr: ValidationResult[]) => { + arr.forEach((v) => { + v.fields.forEach((f) => { + const fieldName = fieldNameFromPath(f); + if (!result.has(fieldName)) { + result.set(fieldName, { errors: [], warnings: [] }); + } + const field = result.get(fieldName); + if (field) { + field[target].push({ + code: v.code, + id: fieldName, + }); + } + }); + }); + }; + + if (errors.length > 0) { + process("errors", errors); + } + if (warnings.length > 0) { + process("warnings", warnings); + } + + clientWarnings.forEach((warning) => { + if (!result.has(warning.id)) { + result.set(warning.id, { errors: [], warnings: [] }); + } + const field = result.get(warning.id); + if (field) { + field.warnings.push(warning); + } + }); + + return result; + }, [errors, warnings, clientWarnings]); + + return errorsAndWarnings; +} diff --git a/frontend/lib/ui/InputGrid/InputGridRow.tsx b/frontend/lib/ui/InputGrid/InputGridRow.tsx index 46f20c838..30e9e216b 100644 --- a/frontend/lib/ui/InputGrid/InputGridRow.tsx +++ b/frontend/lib/ui/InputGrid/InputGridRow.tsx @@ -9,12 +9,13 @@ import { Icon } from "../Icon/Icon"; import { InputGrid } from "./InputGrid"; export interface InputGridRowProps { + id: string; field: string; - name: string; title: string; errorsAndWarnings: Map; inputProps: Partial>; format: FormatFunc; + name?: string; defaultValue?: string | number; isTotal?: boolean; isFocused?: boolean; @@ -30,15 +31,16 @@ export function InputGridRow({ defaultValue, inputProps, isTotal, + id, isFocused = false, addSeparator, }: InputGridRowProps) { - const errors = errorsAndWarnings.get(name)?.errors; + const errors = errorsAndWarnings.get(id)?.errors; const warnings = errorsAndWarnings - .get(name) + .get(id) ?.warnings.filter((warning) => warning.code !== "REFORMAT_WARNING"); const tooltip = errorsAndWarnings - .get(name) + .get(id) ?.warnings.find((warning) => warning.code === "REFORMAT_WARNING")?.value; const [value, setValue] = React.useState(() => (defaultValue ? format(defaultValue) : "")); @@ -62,9 +64,9 @@ export function InputGridRow({ } > { expect(fieldNameFromPath(input)).equals(expected); diff --git a/frontend/lib/util/fields.ts b/frontend/lib/util/fields.ts index a2d4d1f31..44e4688d5 100644 --- a/frontend/lib/util/fields.ts +++ b/frontend/lib/util/fields.ts @@ -1,6 +1,16 @@ export function fieldNameFromPath(path: string): string { + let result = ""; const bits = path.split("."); - return bits[bits.length - 1] || path; + // flatten path at depth 4: data.political_group_votes[1].candidate_votes[1].votes + if (bits.length === 4) { + const [, , subsection, field] = bits; + // replace [1] with -1 + const b = subsection?.replace(/\[(\d+)\]/, "-$1"); + return `${b}.${field}`; + } else { + result = bits[bits.length - 1] || ""; + } + return result || path; } /**