Skip to content

feat(workspace): rework form-origin question into two-question flow (FRM-2572) - #9852

Draft
ruchelp wants to merge 6 commits into
developfrom
paper-tracking-v2.0
Draft

feat(workspace): rework form-origin question into two-question flow (FRM-2572)#9852
ruchelp wants to merge 6 commits into
developfrom
paper-tracking-v2.0

Conversation

@ruchelp

@ruchelp ruchelp commented Aug 14, 2026

Copy link
Copy Markdown

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.formOrigins in 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 (see docs/adr/0001-no-variant-logging-for-form-origin-v2.md for why no per-form variant is logged, and what that means for forms created right at the deploy boundary).

Alternatives considered

  • We considered defining Q1's answer type (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.
  • We considered running a full visual regression pass against a design reference before opening this PR, but neither Figma nor a browser-automation tool was available while implementing this, so verification leaned on the RTL test suite and a shared mockup instead. The Storybook states are still updated with the new variants, so Chromatic picks up real regression coverage in CI from here.

Breaking Changes

No - backwards compatible. metadata.formOrigins keeps the same { value, othersInput } shape; the new FormOrigin enum member is additive and validated automatically by the existing Joi schema.

Tests

TC1: Visual check on the create-form flow

  • Start a new form, reach the origin screen
  • Confirm "Tell us about your process" appears as the screen header, with Q1 ("New" / "Existing") shown as radio buttons beneath it
  • Select "Existing" and confirm Q2 appears with all medium options, including "Another FormSG form" shown distinctly from "Other form builders"
  • Select "Other" and confirm the free-text field appears with its character counter

TC2: Selection-preserving behavior

  • On the origin screen, select "Existing" and tick a couple of mediums
  • Switch to "New", then back to "Existing" — confirm the same mediums are still ticked
  • Go back to the title screen, then forward again to the origin screen — confirm the selections are still there

TC3: Duplicate and use-template parity

  • Duplicate an existing form and confirm the same two-question screen appears, for both a "new process" and an "existing process" answer
  • Create a form from a template and confirm the same two-question screen appears, for both answers

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.
</FormErrorMessage>
</FormControl>

<Controller

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ruchelp ruchelp changed the title feat(workspace): rework form-origin question into two-question flow feat(workspace): rework form-origin question into two-question flow (FRM-2572) Aug 14, 2026
@linear-code

linear-code Bot commented Aug 14, 2026

Copy link
Copy Markdown

FRM-2572

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant