From 568ff7788b5f310b088a3da9427826bf11900285 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Sun, 27 Apr 2025 09:53:57 +0300 Subject: [PATCH 1/7] WIP: fix number inputs not validating and text, number and selection input losing the value when going back --- .../src/components/ReportAnswersStep.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx index adca0797f..7964dae17 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx @@ -108,16 +108,20 @@ function ReportAnswersStep() { ); useEffect(() => { + const dirtyFieldsSet = new Set(Object.keys(form.formState.dirtyFields)); citizenReportForm.questions.forEach((question) => { + // do not reset if the user typed anything in that field + if (dirtyFieldsSet.has(`question-${question.id}`)) return; + if (isMultiSelectQuestion(question)) { form.setValue(`question-${question.id}.selection`, []); } - if (isTextQuestion(question)) { + if (isTextQuestion(question) || isNumberQuestion(question)) { form.setValue(`question-${question.id}`, ""); } }); - }, [form.setValue, citizenReportForm]); + }, [form.setValue, form.formState.dirtyFields, citizenReportForm]); const questionHasFreeTextOption = useCallback( (question: SingleSelectQuestion | MultiSelectQuestion) => { From f6e5873348932025eae79c48cbf715752d586ea8 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Sun, 27 Apr 2025 11:20:28 +0300 Subject: [PATCH 2/7] fix form validation --- .../src/pages/ReportingForm.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx index 39c87a978..3e5aecc05 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx @@ -80,7 +80,7 @@ const CitizenReportStepperComponent = () => { return (
- + e.preventDefault()} className="space-y-4"> {methods.all.map((step) => ( { Previous )} + {methods.isLast && ( + + )} From 825288cd819d28d925f864940dceb07335130897 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Sun, 27 Apr 2025 16:34:57 +0300 Subject: [PATCH 3/7] add number input component --- .../src/components/ReportAnswersStep.tsx | 5 +- .../src/components/ui/number-input.tsx | 128 ++++++++++++++++++ .../src/pages/ReportingForm.tsx | 2 +- 3 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 web-citizen-reporting/web-citizen-reporting-template/src/components/ui/number-input.tsx diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx index 7964dae17..9cd4df46c 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx @@ -22,7 +22,6 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, @@ -48,6 +47,7 @@ import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import React, { useCallback, useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; +import { NumberInput } from "./ui/number-input"; import { Select, SelectContent, @@ -243,8 +243,7 @@ function ReportAnswersStep() { languageCode={selectedLanguage} /> - { + label?: string; + helperText?: string; + error?: string; + min?: number; + max?: number; + step?: number; + onChange?: (value: any) => void; + className?: string; +} + +export function NumberInput({ + label, + helperText, + error, + min, + max, + step = 1, + onChange, + className, + id, + ...props +}: NumberInputProps) { + const inputId = React.useId(); + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value === "" ? undefined : Number(e.target.value); + if (onChange) { + onChange(value); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Allow: backspace, delete, tab, escape, enter, decimal point, minus sign (for negative numbers) + const allowedKeys = [ + "Backspace", + "Delete", + "Tab", + "Escape", + "Enter", + ".", + "-", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "Home", + "End", + ]; + + // Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X + if ( + (e.ctrlKey && ["a", "c", "v", "x"].includes(e.key.toLowerCase())) || + allowedKeys.includes(e.key) + ) { + // Check if decimal point is already present when trying to add another + if (e.key === "." && e.currentTarget.value.includes(".")) { + e.preventDefault(); + } + + // Check if minus sign is already present or not at the beginning + if ( + e.key === "-" && + (e.currentTarget.value.includes("-") || + e.currentTarget.selectionStart !== 0) + ) { + e.preventDefault(); + } + + return; + } + + // Allow numbers + if (/^\d$/.test(e.key)) { + return; + } + + // Block everything else + e.preventDefault(); + }; + + return ( +
+ {label && ( + + )} + + {helperText && !error && ( +

+ {helperText} +

+ )} + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx index 3e5aecc05..9817f2102 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/pages/ReportingForm.tsx @@ -49,7 +49,7 @@ const CitizenReportStepperComponent = () => { const methods = useStepper(); const form = useForm({ - mode: "onSubmit", + mode: "onChange", resolver: methods.current.schema ? zodResolver(methods.current.schema) : undefined, From b6b9837fb9257e1f64375d7368ffc6db95b51dd1 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Mon, 28 Apr 2025 17:41:40 +0300 Subject: [PATCH 4/7] fetch formData inside route --- .../src/components/ReportAnswersStep.tsx | 14 +++++--------- .../src/routes/forms/$formId.tsx | 13 +++++++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx index 9cd4df46c..3951d6c07 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx @@ -1,5 +1,6 @@ import { type BaseQuestion, + type FormModel, type MultiSelectQuestion, type SingleSelectQuestion, } from "@/common/types"; @@ -39,10 +40,8 @@ import { isSingleSelectQuestion, isTextQuestion, } from "@/lib/utils"; -import { formsOptions } from "@/queries/use-forms"; import { Route } from "@/routes/forms/$formId"; -import { useSuspenseQuery } from "@tanstack/react-query"; -import { notFound } from "@tanstack/react-router"; +import { useLoaderData } from "@tanstack/react-router"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import React, { useCallback, useEffect, useState } from "react"; @@ -87,17 +86,14 @@ function QuestionDescription({ function ReportAnswersStep() { const { formId } = Route.useParams(); - const { data: citizenReportFoms } = useSuspenseQuery(formsOptions()); const [loading, setLoading] = React.useState(false); const { onUpload, progresses, uploadedFiles, isUploading } = useUploadFile({ defaultUploadedFiles: [], }); - const citizenReportForm = citizenReportFoms.find((f) => f.id === formId); - - if (citizenReportForm === undefined) { - throw notFound({ throw: false }); - } + const citizenReportForm = useLoaderData({ + from: "/forms/$formId", + }) as FormModel; const form = useFormContext(); diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/routes/forms/$formId.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/routes/forms/$formId.tsx index 1978b9597..755023603 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/routes/forms/$formId.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/routes/forms/$formId.tsx @@ -1,10 +1,19 @@ import NotFound from "@/pages/NotFound"; import SubmitCitizenReport from "@/pages/ReportingForm"; import { formsOptions } from "@/queries/use-forms"; -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, notFound } from "@tanstack/react-router"; export const Route = createFileRoute("/forms/$formId")({ - loader: (opts) => opts.context.queryClient.ensureQueryData(formsOptions()), + loader: async (opts) => { + const { formId } = opts.params; + const allForms = await opts.context.queryClient.ensureQueryData( + formsOptions() + ); + const formsMap = new Map(allForms.map((form) => [form.id, form])); + if (!formsMap.has(formId)) throw notFound({ throw: false }); + + return formsMap.get(formId); + }, component: SubmitCitizenReport, notFoundComponent: NotFound, }); From 7bc8b83ebc4f301165d6c4048310c1407d6f15f5 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Tue, 29 Apr 2025 09:24:01 +0300 Subject: [PATCH 5/7] call useLoaderData from the Route obj --- .../src/components/ReportAnswersStep.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx index 3951d6c07..cc5c1c8cd 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx @@ -41,7 +41,6 @@ import { isTextQuestion, } from "@/lib/utils"; import { Route } from "@/routes/forms/$formId"; -import { useLoaderData } from "@tanstack/react-router"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import React, { useCallback, useEffect, useState } from "react"; @@ -85,15 +84,12 @@ function QuestionDescription({ } function ReportAnswersStep() { - const { formId } = Route.useParams(); const [loading, setLoading] = React.useState(false); const { onUpload, progresses, uploadedFiles, isUploading } = useUploadFile({ defaultUploadedFiles: [], }); - const citizenReportForm = useLoaderData({ - from: "/forms/$formId", - }) as FormModel; + const citizenReportForm = Route.useLoaderData() as FormModel; const form = useFormContext(); From 0df6e2ec1b873ef6b0a8f98aa656e70fabd33b2f Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Thu, 1 May 2025 19:43:46 +0300 Subject: [PATCH 6/7] WIP: add display logic for form questions --- .../src/components/ReportAnswersStep.tsx | 305 +++++++++--------- .../features/forms/hooks/useDisplayLogic.ts | 130 ++++++++ .../src/features/forms/hooks/useFormMaps.ts | 99 ++++++ 3 files changed, 384 insertions(+), 150 deletions(-) create mode 100644 web-citizen-reporting/web-citizen-reporting-template/src/features/forms/hooks/useDisplayLogic.ts create mode 100644 web-citizen-reporting/web-citizen-reporting-template/src/features/forms/hooks/useFormMaps.ts diff --git a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx index cc5c1c8cd..61fb418c2 100644 --- a/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx +++ b/web-citizen-reporting/web-citizen-reporting-template/src/components/ReportAnswersStep.tsx @@ -1,6 +1,5 @@ import { type BaseQuestion, - type FormModel, type MultiSelectQuestion, type SingleSelectQuestion, } from "@/common/types"; @@ -31,6 +30,7 @@ import { import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; +import { useFormDisplayLogic } from "@/features/forms/hooks/useDisplayLogic"; import { useUploadFile } from "@/hooks/use-upload-file"; import { cn, @@ -89,7 +89,10 @@ function ReportAnswersStep() { defaultUploadedFiles: [], }); - const citizenReportForm = Route.useLoaderData() as FormModel; + const citizenReportForm = Route.useLoaderData(); + const { shouldDisplayQuestion } = useFormDisplayLogic( + citizenReportForm.questions + ); const form = useFormContext(); @@ -187,7 +190,7 @@ function ReportAnswersStep() { {citizenReportForm.questions.map((question) => (
- {isTextQuestion(question) && ( + {shouldDisplayQuestion(question) && isTextQuestion(question) && ( )} - {isNumberQuestion(question) && ( + {shouldDisplayQuestion(question) && isNumberQuestion(question) && ( )} - {isDateQuestion(question) && ( + {shouldDisplayQuestion(question) && isDateQuestion(question) && ( )} - {isSingleSelectQuestion(question) && ( -
- ( - - - - - - {question.options.map((option) => ( - - - - - - {option.text[selectedLanguage]} - - - ))} - - - - - )} - /> - {questionHasFreeTextOption(question) && - isFreeTextOptionSelected(question) && ( - ( - - -