Skip to content

ENG-3174: Migrate privacy notice forms to Ant Design#7950

Merged
gilluminate merged 4 commits intomainfrom
gill/ENG-3174/privacy-notice-forms-ant
Apr 16, 2026
Merged

ENG-3174: Migrate privacy notice forms to Ant Design#7950
gilluminate merged 4 commits intomainfrom
gill/ENG-3174/privacy-notice-forms-ant

Conversation

@gilluminate
Copy link
Copy Markdown
Contributor

@gilluminate gilluminate commented Apr 16, 2026

Ticket ENG-3174

Description Of Changes

Migrates privacy notice create/edit forms from Formik + Chakra to Ant Design Form. This replaces Formik, Yup, ControlledSelect, CustomTextInput, CustomSwitch, CustomTextArea, and FormSection with native Ant Design Form, Form.Item, Select, Input, Switch, and Card components.

Key patterns used:

  • Hidden Form.Item elements for externally managed fields (children, translations) so useWatch and getFieldsValue track them reactively
  • Form.useWatch for derived state (dirty checking, submittable validation)
  • Inline rules on Form.Item instead of Yup validation schema
  • key prop on Form to remount cleanly when switching between notices
  • RTK Query isLoading for submit state instead of manual useState

Code Changes

  • Replaced Formik/Form with Ant Design Form and Form.useForm in PrivacyNoticeForm.tsx
  • Replaced FieldArray/useFormikContext with Form.useWatch and form.setFieldValue in PrivacyNoticeTranslationForm.tsx
  • Migrated NoticeKeyField from useFormikContext to Form.useFormInstance and Form.useWatch
  • Removed Yup ValidationSchema from form.ts, replaced with inline rules on Form.Item
  • Replaced Chakra layout components (Box, Flex, VStack, Stack, FormLabel, Heading) with Ant Flex, Card, Typography, Divider, and Tailwind utilities
  • Switched from default exports to named exports for all components
  • Updated Cypress tests to use new data-testid values (select-* instead of controlled-select-*)
  • Updated page shells ([id].tsx, new.tsx) to use Ant Typography and Tailwind layout

Steps to Confirm

  1. Navigate to Privacy Notices list page
  2. Click into an existing notice, verify all fields load correctly (name, consent mechanism, notice key, GPC flag, locations, child notices, data uses, enforcement level, translations)
  3. Edit a field, verify Save button enables (dirty + valid)
  4. Save changes, verify success toast
  5. Create a new privacy notice, fill all fields, save, verify redirect to list
  6. Verify notice key auto-generates from name on create (not on edit)
  7. If translations enabled: add/remove translation tabs, verify fields persist

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

gilluminate and others added 2 commits April 16, 2026 13:56
Replace Formik + Yup + Chakra form components with Ant Design Form,
Select, Switch, Input, and Card. Use hidden Form.Items for externally
managed fields (children, translations), Form.useWatch for reactivity,
and inline validation rules instead of Yup schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fides-plus-nightly Ready Ready Preview, Comment Apr 16, 2026 8:30pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored Apr 16, 2026 8:30pm

Request Review

@gilluminate gilluminate marked this pull request as ready for review April 16, 2026 19:59
@gilluminate gilluminate requested a review from a team as a code owner April 16, 2026 19:59
@gilluminate gilluminate requested review from speaker-ender and removed request for a team and speaker-ender April 16, 2026 19:59
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 16, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 8%
6.17% (2709/43840) 5.38% (1340/24884) 4.24% (550/12962)
fides-js Coverage: 78%
78.98% (1962/2484) 65.55% (1214/1852) 72.57% (336/463)
privacy-center Coverage: 88%
85.97% (331/385) 81.36% (179/220) 78.87% (56/71)

@gilluminate gilluminate enabled auto-merge April 16, 2026 20:03
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Code Review: PR #7950 — Migrate Privacy Notice Forms to Ant Design

Overall this is a clean migration from Formik/Chakra to Ant Design. The component structure is clearer, the RTK Query loading states are now surfaced properly, and the removal of FieldArray simplifies the translation tab logic considerably. A few issues to look at before merging:


Must Fix

1. translationIsOOB goes stale when clicking back to an OOB tab (inline comment on PrivacyNoticeTranslationForm.tsx:130)

handleTabSelected unconditionally resets translationIsOOB to false on every tab click. If a user is on an OOB tab, clicks to a non-OOB tab, then clicks back, the <OOBTranslationNotice> banner is gone. The fix is to derive isOOB from availableTranslations based on the currently active language, rather than from creation-event state.

2. isDirty may be true immediately on load (inline comment on PrivacyNoticeForm.tsx:183-185)

form.getFieldsValue(true) includes the hidden <Input>-backed children and translations fields. On first render those are undefined/"" while initialValues.children and initialValues.translations are arrays, so isEqual returns false before any user interaction — enabling the Save button from the start. Comparing allValues directly against initialValues avoids this.


Suggestions

3. Mutations aren't using .unwrap()

The handleSubmit function uses await mutationTrigger() and checks isErrorResult(result). That pattern is functionally correct for RTK Query's result-object style, but the rest of the codebase wraps mutations in .unwrap() + try/catch. Worth standardising:

try {
  if (isEditing) {
    await patchNoticesMutationTrigger(valuesToSubmit).unwrap();
  } else {
    await postNoticesMutationTrigger(values).unwrap();
  }
  message.success(`Privacy notice ${isEditing ? "updated" : "created"}`);
  if (!isEditing) router.push(PRIVACY_NOTICES_ROUTE);
} catch (error) {
  message.error(getErrorMessage(error));
}

4. .then/.catch in useEffect (inline comment on PrivacyNoticeForm.tsx:176-181)

Only place in the file using promise-chain style — easy to switch to async/await + try/catch for consistency.

5. Redundant as SupportedLanguage casts

handleSelect in AddTranslationMenu casts its already-typed SupportedLanguage parameter again on the call to handleCreateLanguage. Same pattern appears inside handleCreateLanguage's body. Both casts can be removed.

6. translation.title has no validation rules

NoticeFormFields renders a Form.Item for translations.${index}.title with no rules. The old Yup schema didn't validate it either, so this is pre-existing — but worth a conscious decision now that validation is done inline.

7. Arbitrary Tailwind value (inline comment on new.tsx:17)

lg:w-[70%] in both page files — prefer a standard Tailwind fraction like lg:w-3/4.


🔬 Codegraph: connected (46795 nodes)


💡 Write /code-review in a comment to re-run this review.

(Form.useWatch("translations", form) as
| NoticeTranslationCreate[]
| undefined) ?? [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

clients/admin-ui/src/features/privacy-notices/PrivacyNoticeTranslationForm.tsx:130

Bug: translationIsOOB is a single flag shared across all tabs — it goes stale when switching back to an OOB tab

translationIsOOB is set to true in handleCreateLanguage when the user adds a language that happens to be in availableTranslations, but handleTabSelected unconditionally resets it to false on every tab click. So:

  1. User has an OOB translation tab (e.g. English).
  2. User adds a new non-OOB language — translationIsOOB is correctly false.
  3. User clicks back to the English tab — handleTabSelected fires, sets translationIsOOB = false, and the <OOBTranslationNotice> banner disappears even though that tab is OOB.

Derive the value from data instead of creation-event state:

const isActiveTabOOB = !!availableTranslations?.some(
  (t) => t.language === activeLanguage,
);

Pass isActiveTabOOB to <TranslationFormBlock isOOB={isActiveTabOOB} />. This also lets you remove the translationIsOOB state and setTranslationIsOOB calls in handleCreateLanguage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! Derived isActiveTabOOB from availableTranslations and activeLanguage instead of tracking it as event-driven state. Removed the useState and both setter calls.

Comment on lines +183 to +185

const isDirty = useMemo(
() =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx:183-185

Potential false dirty on load: getFieldsValue(true) includes the hidden <Input>-backed fields, which may not match initialValues on first render

form.getFieldsValue(true) returns every registered field including the two hidden <Form.Item name="children"> and <Form.Item name="translations"> that back plain <Input> controls. When Ant Design first registers an uncontrolled <Input>, its value is undefined or "", while initialValues.children and initialValues.translations are arrays. This means isEqual(getFieldsValue(true), initialValues) is false immediately on load, so isDirty is true before the user has made any changes — and the Save button is incorrectly enabled.

Consider comparing allValues (the object from useWatch([], form)) directly against initialValues rather than calling getFieldsValue(true), since allValues only reflects fields that have actual <Form.Item> controls with real values, and the hidden Input fields won't interfere:

const isDirty = useMemo(
  () => !allValues ? false : !isEqual(allValues, initialValues),
  [allValues, initialValues],
);

If the hidden-Input approach is intentional for reactivity, you may also need to exclude children and translations from the dirty check since they're managed via setFieldValue and tracked separately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! Switched to comparing allValues (from useWatch) directly against initialValues instead of getFieldsValue(true).

Comment on lines +176 to +181
const [submittable, setSubmittable] = useState(false);
useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => setSubmittable(true))
.catch(() => setSubmittable(false));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx:176-181

Style: .then/.catch is the only place in this file that uses promise chaining — prefer async/await + try/catch for consistency

The rest of the codebase and this file use async/await. This is a minor inconsistency but worth standardising:

useEffect(() => {
  const check = async () => {
    try {
      await form.validateFields({ validateOnly: true });
      setSubmittable(true);
    } catch {
      setSubmittable(false);
    }
  };
  check();
}, [form, allValues]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! Switched to async/await for consistency.

@@ -14,15 +16,15 @@ const NewPrivacyNoticePage = () => (
{ title: "New privacy notice" },
]}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

clients/admin-ui/src/pages/consent/privacy-notices/new.tsx:17

Style: arbitrary Tailwind value lg:w-[70%] — prefer a standard fraction

w-[70%] is an arbitrary value. Consider lg:w-3/4 (75%) or using lg:max-w-[70%] to match the pattern used in other form pages. Same applies to [id].tsx.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped the lg: breakpoint since we don't support viewports below 1200px. Now just w-[70%].

@gilluminate gilluminate disabled auto-merge April 16, 2026 20:07
gilluminate and others added 2 commits April 16, 2026 14:16
- Derive isActiveTabOOB from data instead of stale event state
- Compare allValues directly for dirty check instead of getFieldsValue
- Switch mutations to .unwrap() + try/catch
- Convert .then/.catch to async/await for consistency
- Remove redundant SupportedLanguage casts
- Drop unnecessary lg: breakpoint on page width

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ant Design Form only includes fields with Form.Item bindings in
onFinish values. Without these, disabled and internal_description
were dropped from the submitted payload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gilluminate gilluminate enabled auto-merge April 16, 2026 20:23
@gilluminate gilluminate added this pull request to the merge queue Apr 16, 2026
Merged via the queue into main with commit 2e40a23 Apr 16, 2026
51 checks passed
@gilluminate gilluminate deleted the gill/ENG-3174/privacy-notice-forms-ant branch April 16, 2026 20:42
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.

2 participants