feat(workspace): rework form-origin question into two-question flow (FRM-2572) - #9852
Draft
ruchelp wants to merge 6 commits into
Draft
feat(workspace): rework form-origin question into two-question flow (FRM-2572)#9852ruchelp wants to merge 6 commits into
ruchelp wants to merge 6 commits into
Conversation
Captures the Process / Existing process / New process / Form origin vocabulary resolved during the paper-tracking v2.0 grilling session, so implementation and future PRs in this area use consistent language instead of reintroducing the "digital vs paper" framing that caused the original misclassification bug this feature fixes.
Documents why this redesign ships with no per-form logging of which UI version an admin saw: before/after comparison relies purely on each form's creation timestamp relative to a boundary. Originally that boundary was going to be a new GrowthBook flag's flip; the v2.1 PRD revision dropped the new flag entirely (ships behind the existing paper-tracking flag only), so the boundary is now this change's deploy timestamp instead. The accepted consequence is the same either way: forms created right at the boundary can't be reliably attributed to a version after the fact.
Adds FormOrigin.DigitalFormsg ("another FormSG form"), a distinct
medium option from the existing "Other Form Builders", inserted
between the spreadsheet and form-builder members.
Also adds two derived primitives so the upcoming two-question screen
doesn't need to maintain its own parallel option list:
- FORM_ORIGIN_MEDIUM_OPTIONS: the master list filtered to exclude
DigitalNew, for the "how is this being collected today" question.
- isNewProcessFormOrigin: whether a formOrigins.value array represents
a new-process answer (i.e. consists solely of DigitalNew) — treats
legacy rows that mix DigitalNew with a real medium as not-new-process,
since the new UI can no longer produce that combination but old rows
might have it.
No backend validator changes needed: the existing Joi schema validates
against Object.values(FormOrigin), which picks up the new member
automatically.
The wizard now carries Q1's raw answer (formOriginProcess: 'new' |
'existing') as its own field, separate from formOrigins (Q2's medium
picks) — previously the single-question UI wrote Q1 and Q2 into the
same array, but a radio Q1 needs a plain single value, not an array
entry.
buildFormClientMetadata branches on it: 'new' short-circuits to
{ value: [DigitalNew] }, discarding any medium selections retained in
Q2's UI state; 'existing' keeps the current medium+othersInput
merge logic unchanged; undefined (nothing answered yet) still produces
no payload. No new backend validation — the existing Joi schema
already enforces "at least 1" on the merged array, and mutual
exclusivity between DigitalNew and real mediums is a frontend-only
guarantee (radio semantics), not re-checked server-side.
Wired through all three entry points that build this payload (create,
duplicate, use-template) — the compiler caught the two call sites this
change would otherwise have silently left broken.
Splits the single "how is this being filled today?" checkbox list into
two questions on the same screen, per the locked v2.1 decisions:
- A topic sentence ("Tell us about your process") now sits in the
screen's ModalHeader, replacing the question text that used to live
there. Q1 and Q2 each get their own in-body FormLabel instead,
matched in size/weight to the form-name label on the previous
(Details) screen.
- Q1 ("is this form based on a new or existing process?") is a
RadioGroup with terse "New"/"Existing" copy — not a checkbox capped
at one, since a real radio already exists in the design system and
gives single-select for free.
- Q2 ("how is this data being collected today?") stays a CheckboxGroup
over FORM_ORIGIN_MEDIUM_OPTIONS, rendered only when Q1 = "Existing".
Q2's Controller stays mounted at all times — only its visible markup
is conditional on Q1's value, and its validation rule is skipped
(rather than the field being unregistered) whenever Q1 isn't
"Existing". This is what makes ticks survive toggling Q1 away and back
for free: react-hook-form never clears a field it hasn't unregistered.
It also means selections survive navigating back to the title screen
and forward again, since the wizard's form instance is created once in
CreateFormWizardProvider and outlives individual screen mounts.
Both questions still share the "Please select at least 1 option."
validation copy. Stories updated with the new state variants (New
selected, Existing selected, both validation errors) so Chromatic
regression-tests them going forward.
Both flows already inherit the two-question screen and payload-builder wiring for free via useCommonFormWizardProvider — no production code changes were needed here beyond the call-site fixes in the previous commit. This adds the one case that wasn't yet proven for these two entry points specifically: selecting "new process" discards any stale Q2 medium ticks and submits only DigitalNew, matching the fresh-create flow's behaviour already covered in CreateFormWizardProvider.test.tsx.
ruchelp
commented
Aug 14, 2026
| </FormErrorMessage> | ||
| </FormControl> | ||
|
|
||
| <Controller |
Author
There was a problem hiding this comment.
This Controller for Q2 (formOrigins.value) stays mounted at all times — only the markup it renders is conditional on formOriginProcess === 'existing' (see the ternary in the render prop below). This is deliberate: it's what makes Q2's ticks survive toggling Q1 away and back (and navigating back to the title screen and forward again) without any extra state — react-hook-form never unregisters a field it hasn't unmounted. The rules.validate closure below reads formOriginProcess to skip the "at least 1" check whenever Q1 isn't "existing", rather than conditionally registering the field itself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When admins create a form, they're asked "How is this being filled in today?" and pick from a single checkbox list that conflates two different questions: whether the underlying process is new or already exists, and (if it exists) what medium currently collects it. Because "This is a new process" sits in that list next to options like "Paper form" and "Emails", admins conflate "new to me digitally" with "genuinely new" — someone digitizing a paper form often picks "new process" simply because the digital form is new, even though the underlying data collection has existed for years. This produces form-origin data that doesn't reflect reality, undermining Ops' ability to tell forms that digitize an existing process apart from forms that collect genuinely new data.
Solution
Splits the single question into two, shown on the same screen. A topic sentence ("Tell us about your process") now sits in the header where the question used to be; Q1 asks whether this is a new or existing process (a plain "New"/"Existing" radio); Q2 — shown only when Q1 = "Existing" — asks how the data is collected today, with a new "Another FormSG form" option added alongside the existing mediums (paper, email, documents, spreadsheets, other form builders, other). Toggling Q1 away from "Existing" and back preserves whatever was ticked in Q2, and the same holds across navigating back to the title screen and forward again.
Storage is unchanged — everything still lands in
metadata.formOriginsin the same shape it always has (one new enum member, picked up automatically by the existing backend validator), so there's no migration and no downstream consumer changes. This ships behind the existing paper-tracking flag only; there's no new flag and no gradual rollout — whoever already reaches this screen sees the two-question version as soon as this deploys (seedocs/adr/0001-no-variant-logging-for-form-origin-v2.mdfor why no per-form variant is logged, and what that means for forms created right at the deploy boundary).Alternatives considered
FormOriginProcessAnswer) directly in the payload-builder util where it's first used, but moved it into the wizard context instead, since it's really wizard state, not something the payload builder itself owns.Breaking Changes
No - backwards compatible.
metadata.formOriginskeeps the same{ value, othersInput }shape; the newFormOriginenum member is additive and validated automatically by the existing Joi schema.Tests
TC1: Visual check on the create-form flow
TC2: Selection-preserving behavior
TC3: Duplicate and use-template parity