Repository navigation
Conversation
VidurShah
left a comment
There was a problem hiding this comment.
Thanks Aysel, this is a solid first version. The validator logic is easy to follow, showIf toggles right away, and labels are properly tied to inputs. A few things to fix before this merges, plus some smaller ones. Details are inline.
Must fix
- Import from
@internal-tool/sharedinstead of relative paths intopackages/shared/src(FormRenderer and the dev page). The workspace package is the boundary between apps and shared code. - Answers from hidden sections are still submitted. Pick Project, fill in the project question, then switch to Education:
project_experienceis still in the payload. The validator skips it, but once this submits to the API it will send answers the applicant can't see. Drop answers from hidden sections on submit, and add a test for it (a pure helper inpackages/sharedwould be easy to test). - Track values should be lowercase
project/education/both. They're promoted into theapplication_trackenum (TDD 3.1), and the existing definition test already uses lowercase. /dev/formsis dev-only per the ticket, but it ships in the production build. CallnotFound()whenprocess.env.NODE_ENV === "production".
Should fix
hasValuetreats any non-string as answered, so a requiredmulti_selectpasses with[]. That type isn't supported yet, but it will be soon, so it's worth handling now.- When an answer breaks both
maxLengthandmaxWords, the second error overwrites the first. Either stop after the first error (like the required check does) or decide which one wins on purpose. - Accessibility: add
aria-required(orrequired, since you havenoValidate) to required inputs, andaria-invalid+aria-describedbypointing at the error<p>. - Nit: the existing shared tests are in
packages/shared/tests/, so please movevalidator.test.tsthere. The PR description also says 10/10 tests, but the file has 7.
Push fixes to this branch and re-request review when it's ready.
| "use client"; | ||
|
|
||
| import React, { useState } from "react"; | ||
| import type { FormDefinition, FormField } from "../../../../packages/shared/src/forms/definition"; |
There was a problem hiding this comment.
Import from @internal-tool/shared (it already exports the definition types, and this PR adds the validator to the index) rather than reaching into packages/shared/src by relative path.
| @@ -0,0 +1,85 @@ | |||
| import type { FormDefinition } from "../../../../../../packages/shared/src/forms/definition"; | |||
There was a problem hiding this comment.
Same here: import type { FormDefinition } from "@internal-tool/shared";
| type: "select", | ||
| label: "Which track are you applying for?", | ||
| required: true, | ||
| options: ["Project", "Education", "Both"], |
There was a problem hiding this comment.
These are promoted into the application_track enum (project | education | both, TDD 3.1), so the option values need to be lowercase. Update the showIf.in arrays below to match. If we want capitalized labels, that's a later label/value change to the definition schema.
| }, | ||
| ], | ||
| }; | ||
| export default function DevFormsPage() { |
There was a problem hiding this comment.
The ticket says dev-only. Something like if (process.env.NODE_ENV === "production") notFound(); (from next/navigation) keeps it out of the deployed app.
| setSubmittedPayload(null); | ||
| } else { | ||
| setErrors({}); | ||
| setSubmittedPayload(answers); |
There was a problem hiding this comment.
answers still has values from sections that are now hidden (switch Project → Education after typing in the project field). Filter to visible sections' fields before setting the payload and calling onSubmitSuccess, since that's what will go to the API.
| } | ||
| for (const field of section.fields) { | ||
| const value = answers[field.id]; | ||
| const hasValue = |
There was a problem hiding this comment.
Any non-string counts as answered here, so a required multi_select passes with []. Treat empty arrays as missing too.
| if (field.maxWords !== undefined) { | ||
| const words = countWords(stringValue); | ||
| if (words > field.maxWords) { | ||
| errors[field.id] = `Cannot exceed ${field.maxWords} words.`; |
There was a problem hiding this comment.
If both maxLength and maxWords fail, this overwrites the maxLength message. A continue after each error, or an early return per field, would keep the first one.
| : "border-gray-300 focus:border-blue-600 focus:ring-1 focus:ring-blue-600"; | ||
| if (field.type === "short_text" || field.type === "email") { | ||
| return ( | ||
| <input |
There was a problem hiding this comment.
For screen readers: add aria-required={field.required}, plus aria-invalid={hasError} and aria-describedby pointing at an id on the error <p>. Same for the textarea and select.
… and secure dev route
|
Pushed the updates for all the review points!
|
Keep main's shared index (header comment, ./enums, ./errors) and export ./forms/validator.
Overview
Built out the dynamic form rendering and validation foundation
Key Changes
packages/shared:validateAnswers,isSectionVisible) covering required constraints, email formatting, select option verification, word/character limits, and conditional dependencies.packages/shared/src/forms/validator.test.ts(10/10 tests passing).apps/web:FormRenderer.tsxwith support forshort_text,long_text,email, andselectinputs.noValidate, and wired dynamic state to clear field errors on active input./dev/formsrendering the track selection logic (Project vs. Education vs. Both) to manually exercise conditional section rendering.Verification
bun testpassed cleanly across all monorepo workspaces (10/10 tests).lint-shared,lint-web) and pre-push typecheck (tsc --noEmitacross@internal-tool/web,@internal-tool/api, and@internal-tool/shared) passed with zero errors./dev/forms):showIflogic toggles project and education sections dynamically based on track dropdown selection.rating) renders the fallback UI.Closes #7