Skip to content

ENG-3549: Migrate SystemInformationForm + DictSuggestion + VendorSelector to antd#8235

Merged
gilluminate merged 16 commits into
mainfrom
gill/eng-3549/system-info-form-antd
May 20, 2026
Merged

ENG-3549: Migrate SystemInformationForm + DictSuggestion + VendorSelector to antd#8235
gilluminate merged 16 commits into
mainfrom
gill/eng-3549/system-info-form-antd

Conversation

@gilluminate
Copy link
Copy Markdown
Contributor

Ticket ENG-3549

Description Of Changes

Part of ENG-3187 (System inventory + add-system forms → Ant). This is the biggest single chunk: the main SystemInformationForm (30 fields across 5 sections), the VendorSelector typeahead, the DictSuggestionInputs family, and the SystemFormInputGroup layout wrapper. After this lands, the system-information surface is fully off Formik/Chakra.

Two prior migrations made this tractable:

CustomFieldsList and FormGuard were migrated alongside because SystemInformationForm was their only remaining Formik consumer — keeping them on Formik would have left a dangling dependency.

Code Changes

  • clients/admin-ui/src/features/system/SystemInformationForm.tsx — full rewrite. <Formik> → antd <Form> with Form.useForm, all 30 fields ported to Form.Item + native antd components (Input, Switch, Select, InputNumber). Yup async name-uniqueness check ported to a Form.Item validator rule. Conditional sections (processes_personal_data, exempt_from_privacy_regulations, uses_profiling, does_international_transfers, requires_data_protection_assessments) now driven by Form.useWatch. enableReinitialize replaced by key={passedInSystem?.fides_key ?? "create"} on the <Form> element.
  • clients/admin-ui/src/features/system/dictionary-form/DictSuggestionInputs.tsx — rewrote useDictSuggestion against Form.useFormInstance/useWatch/setFieldValue; replaced the 4 field components (Chakra → antd Input, Input.TextArea, Switch, InputNumber); dropped the Formik blur/setTimeout(300) workaround entirely.
  • clients/admin-ui/src/features/system/dictionary-form/DictSuggestionInputs.module.scss — new SCSS module for the --fidesui-complimentary-500 "suggested" coloring (per fidesui color-token convention).
  • clients/admin-ui/src/features/system/SystemFormInputGroup.tsx — Chakra layout primitives → antd Card.
  • clients/admin-ui/src/features/system/VendorSelector.tsxdeleted. VendorSelectorAnt is now the sole vendor selector. The stale "when those migrate, swap their imports here" comment on VendorSelectorAnt was also cleaned up.
  • clients/admin-ui/src/features/common/custom-fields/CustomFieldsList.tsx — Formik <Field> → antd Form.Item. Existing consumers (system form, plus the already-migrated privacy declaration forms via PrivacyDeclarationCustomFields) continue to work.
  • clients/admin-ui/src/features/common/hooks/useIsAnyFormDirty.tsxFormGuard rewritten against Form.useFormInstance + Form.useWatch([], form) to keep isFieldsTouched() reactive across all field updates.
  • clients/admin-ui/cypress/e2e/systems.cy.ts — updated the duplicate-name assertion to use the existing cy.getAntFormError("name") helper (antd renders errors in .ant-form-item-explain-error, not the old data-testid="error-name").

The submit button uses a deep-compare dirty check (isEqual(form.getFieldsValue(true), initialValues)) per the antd-migration playbook's escape hatch — VendorSelectorAnt and the dict-suggestion fields commit values via setFieldsValue, which doesn't trigger isFieldsTouched.

Steps to Confirm

  1. Create flow (with Compass) — Open Add New System → typeahead → select a Compass vendor → confirm vendor logo locks GVL fields when applicable, dictionary suggestions populate fields with the complimentary-500 "suggested" color, and Save persists name + vendor_id.
  2. Create flow, duplicate name — Try a name that already exists → error appears under the field on blur and Save is disabled.
  3. Edit flow — Open existing system → all 5 sections populate, toggle processes_personal_data off → "Cookie properties" / "Administrative properties" sections collapse. Toggle back on → they re-appear with preserved values.
  4. Conditional sub-sections — Toggle uses_profiling → "Legal basis for profiling" appears below it; toggle off → it disappears. Repeat for does_international_transfers (→ legal basis for transfer), requires_data_protection_assessments (→ DPIA/DPA location), exempt_from_privacy_regulations (→ reason for exemption).
  5. GVL system edit — Edit a GVL-sourced system → name field locks, GVL-controlled fields disable.
  6. Data steward assignment — Add/remove stewards → parallel API calls fire in the network tab → on success the steward list re-reads from the refreshed system payload.
  7. Custom fields — Create a system, save it, then re-open → custom fields card appears with persisted values.
  8. Dirty-form guard — Modify a field, then navigate away → "Unsaved Changes / Continue anyway" dialog fires.
  9. Read-only access — As a viewer without SYSTEM_UPDATE, all fields render disabled and the Save button is hidden.

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
  • 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:
    • No migrations
  • Documentation:
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 19, 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 May 20, 2026 8:45pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored May 20, 2026 8:45pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 11%
8.55% (4053/47382) 7.6% (2094/27530) 5.83% (812/13926)
fides-js Coverage: 78%
79.17% (1977/2497) 66.25% (1249/1885) 73.31% (349/476)
privacy-center Coverage: 86%
83.95% (403/480) 81.25% (208/256) 76.13% (67/88)

@jpople
Copy link
Copy Markdown
Contributor

jpople commented May 20, 2026

Running into some issues here:

  1. Submitting is broken entirely, I get an uncaught error:
Screenshot 2026-05-19 at 22 07 29
  1. GVL locking behavior is not working at all, although I checked and it's actually already broken on main so that wasn't introduced here.

  2. The vendor selector/name field isn't showing error messages at all (this is a duplicate name error, it shows up if I console log getFieldsErrors())

Screenshot 2026-05-19 at 22 02 42
  1. Selecting a steward doesn't fire any extra API calls for me

  2. Viewing as a viewer doesn't lock any fields (submitting after making changes appears not to do anything but the button isn't disabled either)

Comment thread clients/admin-ui/src/features/system/SystemInformationForm.tsx Outdated
@gilluminate
Copy link
Copy Markdown
Contributor Author

@jpople

Submitting is broken entirely, I get an uncaught error

for the life of me I cannot reproduce this, might be a stale build or something. At any rate, Claude seems to know the safeguard for it.

GVL locking behavior is not working at all

You are right, this was pre-existing. I'll create a follow-up bug ticket.

isn't showing error messages at all

looks like I missed validation in 2 places. Good catch!

Selecting a steward doesn't fire any extra API calls for me

Is it supposed to?

Viewing as a viewer doesn't lock any fields

This appears to be pre-existing as well. Disabling the button is an easy interim fix I'll do here and address this along with the GVL locking. Seems related.

…ctor to antd

Replaces the Formik/Chakra-based System Information form (30 fields across
5 sections) with a single antd Form. Swaps VendorSelector for the
already-existing antd-native VendorSelectorAnt, rewrites DictSuggestionInputs
on top of Form.useFormInstance/useWatch, ports the async system-name
uniqueness check to a Form.Item validator, and converts SystemFormInputGroup
to an antd Card. CustomFieldsList and FormGuard are also migrated since
SystemInformationForm was their only remaining Formik consumer.
- Merge initialValues with antd onFinish values so unregistered fields
  (privacy_declarations, system_type, etc.) make it into the POST payload
- Drop required-validation rules on conditional fields (reason_for_exemption,
  legal_basis_for_profiling/transfers, dpa_location) since the original Yup
  schema only validated name + privacy_policy. Compass-driven autofill flips
  some toggles on which would otherwise block submit on empty conditional
  fields the user never edited
- Push customFieldValues into the form via useEffect once they finish
  loading async (key-based remount was broken because useCustomFields
  isLoading flips on create flow too)
- Add controlled-select-* testids on Select components to match the
  existing test contract
When picking a "Create new system" option (or any vendor with no
vendor_id), VendorSelector dispatches setSuggestions("hiding") even though
"showing" was never dispatched. The DictSuggestion hook then restored every
field from an uncaptured preSuggestionRef → wrote undefined into
processes_personal_data → conditional Administrative / Cookie sections
unmounted → cypress couldn't find input-legal_name.

- Guard the restore branch with a hasCapturedRef so we only restore when
  we actually snapshotted a pre-suggestion value
- Default Form.useWatch reads to initialValues so transient undefineds
  during the show/hide lifecycle (and on the very first render) don't
  blink conditional sections out

Fixes the CI-only "does not lock editing for a non-GVL vendor" failure
which timed out finding input-legal_name on a 1280x720 viewport.
Two validation regressions from the migration:

1. The system-name uniqueness rule was attached to the hidden Form.Item
   inside VendorSelector. The validator ran and rejected submit, but the
   error message had nowhere to render. Wrap the visible Select / Input
   in a Form.Item shouldUpdate render-prop that reads form.getFieldError("name")
   and passes it through validateStatus + help so the error renders on the
   actual field (matches main).
2. The original Yup schema had `privacy_policy: Yup.string().min(1).url()`.
   I dropped it during the migration. Add a `rules` prop to
   DictSuggestionTextInput and wire `{ type: "url" }` for the privacy
   policy field.

Also makes handleSubmit's privacy_declarations.length check defensive
with optional chaining so edit-flow systems missing the field don't blow
up the handler before the merge with initialValues lands.
Previously the Compass button sat as a sibling of the typeahead Form.Item
in a Flex with align="flex-end". When the name uniqueness error appeared
below the input, the Form.Item grew taller and flex-end pushed the button
down to the bottom of the error. Move the button inside the visible
Form.Item, next to the Select/Input in a center-aligned row, so the
help text renders below the entire row and the button stays put.
1. useSystemFormTabs.handleSuccess passed `message` to notification.success,
   which is deprecated in antd v6 — renamed to `title`.
2. After picking a real option in VendorSelector, searchParam stayed
   populated, so optionsWithCustom kept the synthetic "Create new system
   \"X\"…" entry. Its value matched the now-selected name (also "X"), but
   its label differed, triggering antd's "label of value is not same as
   label in Select options" warning. Reset searchParam in handleChange so
   the synthetic option drops out after selection.
The old <fieldset disabled> wrapper relied on the browser-level disabled
mechanism, which native inputs respect but antd components mostly ignore
visually — viewers saw a fully editable-looking form, could change values
and click an enabled Save button that silently failed.

- Replace the fieldset with antd Form's `disabled` prop. That propagates
  through Form context to every Form.Item-bound control (Selects, Inputs,
  Switches, InputNumber, DictSuggestion* family), disabling them
  consistently with proper antd disabled styling.
- VendorSelector renders the visible Select/Input outside of Form.Item's
  auto-binding (because labelInValue + manual setFieldsValue), so it
  doesn't pick up the Form-context disabled. Plumb an explicit `disabled`
  prop through VendorSelectorProps and OR it into nameFieldLockedForGVL
  plus the Compass button's disabled state.
CustomSelect isn't the culprit — it correctly forwards props to the inner
antd Select. The bug is React's normal prop precedence: passing
\`disabled={false}\` explicitly to an antd input overrides the
DisabledContext set by an ancestor \`<Form disabled>\`.

Most Form.Item-bound inputs in SystemInformationForm carry conditional
disables like \`disabled={lockedForGVL}\`, etc. When those expressions
evaluate to false (the common case in normal view), they hard-set
\`disabled={false}\` and viewers' Form-level disabled never wins.

Convert every conditional \`disabled={someBool}\` to \`disabled={someBool
|| undefined}\` so a false local condition defers to Form context.
Copy link
Copy Markdown
Contributor

@jpople jpople left a comment

Choose a reason for hiding this comment

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

Thanks for the changes, everything looks to be working now and the submit issue is cleared up-- no idea what was going on there. Approving with one last tiny nit, can we adjust the spacing below the name/vendor input here so it's even?

Image

@gilluminate
Copy link
Copy Markdown
Contributor Author

@jpople oh yeah! I keep meaning to do that and keep getting distracted. Thank you for the reminder!

@gilluminate gilluminate enabled auto-merge May 20, 2026 20:43
@gilluminate gilluminate added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit fcfa2c8 May 20, 2026
50 of 51 checks passed
@gilluminate gilluminate deleted the gill/eng-3549/system-info-form-antd branch May 20, 2026 20:58
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