Skip to content

Migrate domains/CORS config form to Ant Design#7853

Merged
gilluminate merged 2 commits intomainfrom
gill/ENG-3182/domains-cors-form-antd
Apr 9, 2026
Merged

Migrate domains/CORS config form to Ant Design#7853
gilluminate merged 2 commits intomainfrom
gill/ENG-3182/domains-cors-form-antd

Conversation

@gilluminate
Copy link
Copy Markdown
Contributor

Ticket ENG-3182

Description Of Changes

Migrates the domains/CORS configuration page from Chakra UI + Formik to Ant Design + antd Form. Full Formik-to-antd-Form migration including dynamic array fields and custom validation.

What changed:

  • Replaced Chakra aliases (ChakraBox, ChakraFlex, ChakraText) with Ant Design equivalents (div, Flex, Typography.Paragraph/Typography.Text)
  • Migrated Formik (Formik, FieldArray, Yup validation) to antd Form with Form.useForm, Form.List, and Form.Item rules
  • Extracted URL validation functions (isValidURL, containsNoWildcard, containsNoPath) and corsOriginRules to validation.ts for reuse
  • Replaced CustomTextInput / TextInput with Form.Item + Input
  • Switched submit handler from isErrorResult pattern to .unwrap() + try/catch
  • Added proper dirty state tracking with local baseline for immediate reset after save

Code Changes

  • validation.ts - Added reusable isValidURL, containsNoWildcard, containsNoPath functions and corsOriginRules array
  • domains.tsx - Full Chakra/Formik to antd Form migration, Yup to antd rules, layout to Flex/div + Tailwind

Steps to Confirm

  1. Navigate to Settings > Domains
  2. Verify existing domains are displayed correctly (both api-set and config-set sections)
  3. Click Add domain - verify an empty input appears
  4. Enter an invalid URL (e.g. not-a-url) - verify validation error appears
  5. Enter a URL with a wildcard (e.g. https://*.example.com) - verify validation error
  6. Enter a URL with a path (e.g. https://example.com/path) - verify validation error
  7. Enter a valid URL (e.g. https://example.com) - verify no error
  8. Click the trash icon to remove a domain - verify it's removed
  9. Click Save - verify success message appears
  10. Verify the Save button is disabled when no changes have been made

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • No UX review needed
  • Followup issues:
    • No followup issues
  • Database migrations:
    • No migrations
  • Documentation:
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 7, 2026

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Apr 7, 2026 10:42pm
fides-privacy-center Ignored Ignored Apr 7, 2026 10:42pm

Request Review

gilluminate and others added 2 commits April 7, 2026 16:42
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gilluminate gilluminate force-pushed the gill/ENG-3182/domains-cors-form-antd branch from 5c06e5e to 9894285 Compare April 7, 2026 22:42
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 7%
5.9% (2541/43011) 4.92% (1199/24359) 3.97% (507/12741)
fides-js Coverage: 78%
78.98% (1962/2484) 65.55% (1214/1852) 72.57% (336/463)
privacy-center Coverage: 86%
84.05% (290/345) 79.18% (156/197) 76.56% (49/64)

@gilluminate gilluminate changed the title ENG-3182: Migrate domains/CORS config form to Ant Design Migrate domains/CORS config form to Ant Design Apr 7, 2026
@gilluminate gilluminate marked this pull request as ready for review April 9, 2026 15:02
@gilluminate gilluminate requested a review from a team as a code owner April 9, 2026 15:02
@gilluminate gilluminate requested review from speaker-ender and removed request for a team April 9, 2026 15:02
@gilluminate gilluminate added this pull request to the merge queue Apr 9, 2026
@gilluminate gilluminate removed the request for review from speaker-ender April 9, 2026 15: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

Clean migration from Chakra/Formik to antd Form. The overall structure is correct — Form.useForm, Form.List, Form.Item rules, and .unwrap() + try/catch for the mutation are all idiomatic antd patterns. The extraction of isValidURL, containsNoWildcard, containsNoPath, and corsOriginRules into validation.ts is a good move for reuse and testability.

Findings (all non-blocking):

  1. Potential isDirty flash after saving empty list — if the server normalizes [] to null, there's a brief window post-save where isDirty could incorrectly become true until the form remounts. See inline comment on line 91.

  2. key={JSON.stringify(apiSettings)} as remount key — works correctly here, but discards in-progress edits on any apiSettings change and serializes on every render. See inline comment on line 129.

  3. eslint-disable on the isDirty deps array — valid suppression, but a brief comment explaining why form is omitted would help. See inline comment on line 73.

  4. corsOriginRules bundled with required — the if (!value) return Promise.resolve() guard works correctly with the paired required rule, but couples the two. Fine for this use case. See inline comment on line 56.

  5. Silent .trim() behavior removed — minor behavior change from the Yup schema; failing with a validation error instead of auto-trimming is arguably better for a security field. See inline comment on line 45.

The Cypress test coverage looks solid — it covers loading states, empty state, editing, removing, and all validation branches. Good work overall.

validationSchema={ValidationSchema}
validateOnChange
onFinish={handleSubmit}
key={JSON.stringify(apiSettings)}
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.

Using key={JSON.stringify(apiSettings)} as a full remount trigger has two concerns:

  1. Performance: JSON.stringify runs on every render while apiSettings is being compared for key changes. For this small object it's negligible, but it's a pattern worth calling out.
  2. In-progress edit loss: If apiSettings changes while the user is mid-edit (e.g., a background refetch or polling scenario), the form will silently discard their work. The old enableReinitialize in Formik had the same behavior, but antd's form.setFieldsValue inside the existing useEffect that watches apiSettings would be a softer reset — only updating when !isDirty.

Not a blocking issue for this PR since there's no background polling here, but worth keeping in mind.

try {
await putConfigSettingsTrigger(payload).unwrap();
message.success("Domains saved successfully");
setBaseline({ cors_origins: values.cors_origins ?? 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.

Potential brief isDirty = true flash after saving an empty list.

If the user removes all domains and saves, values.cors_origins is [], so setBaseline({ cors_origins: [] }) is called. That's fine. But when the subsequent refetch resolves, the server may return cors_origins: null (or omit it), causing useEffect(() => setBaseline(apiSettings)) to set baseline = { cors_origins: null }. Meanwhile, form.getFieldsValue(true) returns { cors_origins: [] } for an empty Form.List. isEqual([], null) is false, so isDirty briefly becomes true until the key remount re-initializes the form.

In practice the key remount likely resolves this within the same render cycle, but if the server roundtrip is slow you could briefly see Save re-enabled after a successful save. Could be addressed by normalizing null/undefined/[] to the same representation before comparison, or simply by not calling setBaseline on success and letting the refetch/remount handle it.

() => !isEqual(form.getFieldsValue(true), baseline),
// eslint-disable-next-line react-hooks/exhaustive-deps
[allValues, baseline],
);
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.

The eslint-disable suppression here is valid — form is intentionally omitted from deps because it's a stable ref from Form.useForm(), and allValues is the actual reactive signal used to trigger re-computation. The pattern is correct, but a short comment explaining why form is omitted would help future readers understand this isn't a forgotten dep.

{
validator: (_: unknown, value: string) => {
if (!value) {
return Promise.resolve();
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.

The if (!value) return Promise.resolve() guard is correct for the antd rules pattern — when the field is empty, the { required: true } rule above handles the error display and this custom validator cleanly defers to it.

One thing to be aware of: this means corsOriginRules should always be used with the required rule included. If someone imports just the custom validator from corsOriginRules in a non-required context (e.g., an optional URL field), empty string would silently pass. Since the rules are exported as a bundled array that includes required, this is safe for now, but worth noting in a comment if the intent is to keep this reusable.

}
try {
const url = new URL(value);
return url.pathname === "/" && !value.endsWith("/");
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.

Subtle behavior change from the original Yup schema: the old schema had .trim(), which silently stripped leading/trailing whitespace from values before validation. The new implementation does not trim, so "https://example.com " (trailing space) will fail isValidURL (since new URL rejects invalid hostnames with spaces) with a generic "must be a valid URL" message rather than succeeding silently after trimming.

For a security-sensitive field like CORS origins, rejecting rather than silently normalizing is actually the safer behavior. Just flagging in case the team has an expectation of whitespace tolerance.

Merged via the queue into main with commit 27ba757 Apr 9, 2026
52 checks passed
@gilluminate gilluminate deleted the gill/ENG-3182/domains-cors-form-antd branch April 9, 2026 15:09
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