Skip to content

refactor(ui): migrate console forms to react-hook-form (team#152)#6619

Open
luannmoreira wants to merge 13 commits into
masterfrom
refactor/adopt-react-hook-form
Open

refactor(ui): migrate console forms to react-hook-form (team#152)#6619
luannmoreira wants to merge 13 commits into
masterfrom
refactor/adopt-react-hook-form

Conversation

@luannmoreira

@luannmoreira luannmoreira commented Jul 7, 2026

Copy link
Copy Markdown
Member

What

Migrates every console form to a single react-hook-form + Zod reference
architecture
(tracked in shellhub-io/team#152). What began as an incremental
RHF migration is now a full standardization: all in-scope forms share one
validation, submit, reset, and error-handling model, and the legacy
makeRhfResolver adapter is gone.

The pilot (SignUp) and the auth/onboarding forms were merged separately and are
intentionally left on their existing resolvers (converted in a later PR).

Architecture — four layers

  1. Field wrappers (components/common/fields/rhf/*) — thin useController
    bridges over the design-system inputs (FormInputField, FormPasswordField,
    FormNumericInput, FormCheckboxField, FormRadioGroupField,
    FormTextareaField, FormToggleField). FormToggleField now renders its
    validation error like the other fields.
  2. Schema + business module — one colocated *Schema.ts per form as the
    single source of truth: a Zod schema (a schema(mode) factory where rules are
    mode-dependent), type X = z.infer<...>, buildXDefaults, and buildXBody.
    All business logic (trimming, parseInt, payload shaping, SAML cert
    normalization) lives here, not in components.
  3. State + lifecycleuseDrawerForm(open, schema, defaults) wraps
    useForm({ resolver: zodResolver(schema), mode: "onChange" }) and standardizes
    reset-on-open.
  4. UI plumbingFormDrawer owns the Drawer, <form>, Cancel/Submit
    footer (disabled on !isValid || isSubmitting, optional requireDirty), and
    the shared FormRootError.

A new form = write xSchema.ts + a drawer that calls useDrawerForm and renders
<FormDrawer> with field wrappers. No hand-rolled resolver, reset, submit, or
root-error code. This is documented in
components/common/fields/rhf/README.md.

Migrated (team#152 checklist)

Account & settings

  • Profile — edit-profile + change-password
  • Settings — namespace rename
  • SAML config (business logic moved into samlSchema.ts)

Resources

  • Firewall rule
  • Public key
  • Secure vault key

Admin

  • Create / edit user (bespoke useUserForm hook removed)
  • Edit namespace
  • Announcements — New / Edit (full-page forms: useForm + zodResolver
    directly, no FormDrawer)

Team

  • Add / edit member
  • Invitations — send / edit
  • API keys — generate / edit

Two dual-mode drawers (Invitation, Generate Key) swap to a result view after
submit, so they compose useDrawerForm + field wrappers + FormRootError under
a raw Drawer rather than FormDrawer.

Cleanup

  • Removed makeRhfResolver and its test, and the per-form resolver adapters —
    zodResolver(schema) inside useDrawerForm is now the only resolver.
  • Resolver unit tests converted to schema.safeParse tests with equivalent
    coverage.

Behavior change

FormDrawer gates the submit button on isValid, so an invalid form now keeps
submit disabled rather than enabled-but-blocked-on-click — a stronger
guarantee. A few drawer/page assertions were updated accordingly (async enable
via waitFor; the invitation email field flags itself invalid inline as typed).

Testing

npm run build (tsc + vite), npm run lint (0 errors), and the full vitest
console suite (2894 tests) all green in the ui container.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Claude finished @luannmoreira's task in 8m 26s —— View job


Code Review Complete

Reviewed 17 files across code quality, security, testing, language patterns, and architecture. 1 inline issue posted (1 medium).

Additional notes

  • EditProfileDrawer now validates all fields unconditionally (pages/Profile.tsx:302, pages/profile/editProfileResolver.ts). The old form only validated a field when it diverged from its saved value, so a stored value that fails the stricter client-side rules (e.g. a legacy mixed-case username failing validateUsername's lowercase rule) didn't block editing other fields. Now disabled={!isDirty || !isValid ...} gates Save on all fields passing, so such an account could be permanently blocked from saving any change. Not posted inline because the impact depends on whether the backend can actually produce values that fail these client regexes — worth confirming, and if possible, scoping resolver validation (or the isValid gate) to dirty fields to preserve the previous UX.

Otherwise the migration is solid: the resolver values: {}-on-error shape matches RHF's Resolver contract, FormTextareaField mirrors the existing rhf/ wrappers correctly, submit payloads (Profile, Settings, SAML both modes, ChangePassword) are preserved verbatim, no any introduced, no security-relevant sinks/secrets, and the new resolver + drawer tests assert real behavior. No cross-repo (cloud/) impact — the new files aren't consumed there.


If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui/apps/console/src/pages/Profile.tsx Outdated
@luannmoreira luannmoreira self-assigned this Jul 7, 2026
@luannmoreira luannmoreira force-pushed the refactor/adopt-react-hook-form branch 2 times, most recently from 70c7ed4 to 443179b Compare July 8, 2026 19:49
@otavio

otavio commented Jul 9, 2026

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @otavio's task in 19m 54s —— View job


Code Review Complete

Reviewed the current PR head (c9b603e, 19 commits) across code quality, security, testing, TypeScript/React patterns, and architecture/cross-repo. 3 inline issues posted (1 medium, 2 low).

⚠️ Note: the branch advanced well beyond the snapshot in the last automated review — it now completes the full migration (~70 files), switches resolvers to zod schemas (*Schema.ts), and centralizes the form-reset lifecycle in the new useDrawerForm hook. This review is against the current head. The previous review's reset()-on-prop-change concern (Profile/Settings/SAML/etc.) is resolved: useDrawerForm resets only on the closed→open transition via a wasOpen ref, so it no longer wipes in-progress edits.

Findings posted inline:

  • ruleSchema.ts:29-31 (medium) — priority validation loosened; the old form rejected priority <= 0 ("must be a positive integer"), the schema now accepts 0/negatives. The field hint still says "Must be greater than 0." and a new test locks in the "0" behavior.
  • RuleDrawer.test.tsx:143-169 (low/medium) — the test "keeps submit disabled when tags mode has more than 3 tags" selects only 3 tags and asserts the button is enabled, contradicting its title; the > 3 rejection branch has no drawer-level coverage (only unit coverage in ruleSchema.test.ts).
  • secure-vault/KeyDrawer.tsx:49 (low) — setValue("passphrase", "") omits { shouldValidate: true } (unlike every sibling call), leaving isValid/errors.passphrase stale when swapping encrypted keys.

Additional notes

  • Cross-repo: no impact — cloud/ is Go-only and consumes none of these files. The one modified shared component (RadioGroupField.tsx, +6) is purely additive (optional error prop; FieldError renders null when empty), so existing consumers are unaffected. New deps (@hookform/resolvers, zod) are consistent with the lockfile.
  • Security: no exploitable issues. Every zod schema was compared against the base validators — samlSchema, userSchema (blank-password-keeps-current preserved), keySchema/vaultKeySchema, team/profile/password schemas all faithfully reproduce prior validation and submit payloads; editProfileSchema still submits only dirty fields. Minor non-security note: buildRuleBody no longer .trim()s source_ip/username (cosmetic payload change).

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui/apps/console/src/pages/firewall-rules/__tests__/RuleDrawer.test.tsx Outdated
@luannmoreira luannmoreira force-pushed the refactor/adopt-react-hook-form branch from c9b603e to 34f31d7 Compare July 9, 2026 14:51
Comment thread ui/apps/console/src/pages/firewall-rules/ruleSchema.ts
Comment thread ui/apps/console/src/pages/secure-vault/KeyDrawer.tsx Outdated
luannmoreira added a commit that referenced this pull request Jul 9, 2026
- Restore the positive-integer rule for firewall priority (reject 0 and
  negatives), matching the field hint and pre-migration behavior.
- Validate on clearing the secure-vault passphrase so isValid/errors stay
  in sync when swapping in a different encrypted key.
- Rework the firewall tags test to verify the 3-tag selector cap instead of
  passing on a valid 3-tag selection.
@luannmoreira luannmoreira force-pushed the refactor/adopt-react-hook-form branch from 34f31d7 to 797df19 Compare July 9, 2026 15:17
@otavio

otavio commented Jul 9, 2026

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @otavio's task in 12m 43s —— View job


Code Review Complete

Reviewed 69 files across code quality, security, testing, TypeScript/React patterns, and architecture/cross-repo — no new issues found. The code looks good as-is.

The four issues from the previous review rounds are all confirmed fixed in 797df19:

  • ruleSchema.ts — positive-integer priority rule restored (rejects 0/negatives).
  • secure-vault/KeyDrawer.tsxsetValue("passphrase", "", { shouldValidate: true }).
  • RuleDrawer.test.tsx — reworked to verify the 3-tag selector cap.
  • Profile/Settings reset-on-prop-change — resolved by useDrawerForm's wasOpen guard.

Spot-checks that came back clean: removed symbols (makeRhfResolver, useUserForm) have no remaining importers; RadioGroupField's new optional error prop is backward-compatible with all 8 existing consumers; no cloud/ cross-repo impact (Go-only); submit payloads (buildUserPayload, buildKeyBody, buildRuleBody, SAML both modes, invitations/keys) faithfully match the pre-migration shapes; no any introduced and no XSS/secret-handling regressions; the two dual-mode drawers (Invitation, GenerateKey) correctly reimplement the reset/submit lifecycle outside FormDrawer.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

…rimitives

Introduce the shared foundation for the console form standardization
(shellhub-io/team#152): FormDrawer owns the Drawer, form element, and
Cancel/Submit footer; useDrawerForm wraps useForm with zodResolver and
reset-on-open; FormRootError renders form-level server errors; and the
react-hook-form field wrappers (FormNumericInput, FormRadioGroupField,
FormTagsSelector, FormTextareaField, FormToggleField) bridge useController
over the design-system inputs. Adds the react-hook-form and zod dependencies.
Migrate the edit-profile and change-password drawers to the shared
react-hook-form + zod architecture (shellhub-io/team#152). Validation moves
into editProfileSchema.ts and changePasswordSchema.ts as the single source of
truth; required fields validate unconditionally and the recovery email stays
optional with its cross-field rule re-running when the primary email changes.
Form-level server errors use RHF root errors.
Migrate the Settings rename-namespace drawer to react-hook-form + zod
(shellhub-io/team#152). The namespace-name rule lives in
namespaceRenameSchema.ts and the rename error surfaces through an RHF root
error. The session-recording and device-auto-accept toggles and the
delete/leave dialogs are left as-is.
Migrate the admin SAML configuration drawer to react-hook-form + zod
(shellhub-io/team#152). All business logic (certificate normalization, payload
shaping) moves into samlSchema.ts, with the drawer reduced to field wrappers
over the schema.
Migrate the firewall rule drawer to react-hook-form + zod
(shellhub-io/team#152). ruleSchema.ts replaces the hand-rolled resolver as the
single source of truth, and the drawer composes FormDrawer with the shared
field wrappers.
Migrate the public key drawer to react-hook-form + zod (shellhub-io/team#152).
keySchema.ts owns validation and payload shaping; the drawer composes
FormDrawer with the shared field wrappers.
Migrate the secure vault key drawer to react-hook-form + zod
(shellhub-io/team#152). vaultKeySchema.ts owns validation, and the drawer
composes FormDrawer with the shared field wrappers.
Migrate the admin create/edit user drawers to react-hook-form + zod
(shellhub-io/team#152). userSchema.ts becomes the single source of truth and
the bespoke useUserForm hook is removed; the drawers compose FormDrawer with
the shared field wrappers.
…m + zod

Migrate the admin edit-namespace drawer to react-hook-form + zod
(shellhub-io/team#152). editNamespaceSchema.ts owns validation; the drawer
composes FormDrawer with the shared field wrappers.
…+ zod

Migrate the New / Edit announcement full-page forms to react-hook-form + zod
(shellhub-io/team#152). announcementSchema.ts owns validation; the pages use
useForm with zodResolver directly rather than FormDrawer.
…+ zod

Migrate the add-member and edit-member-role drawers to react-hook-form + zod
(shellhub-io/team#152). The add-member and edit-role schemas live in
team/schemas.ts, and constants.tsx gains FormRoleSelector — a react-hook-form
binding over the shared RoleSelector used across the team drawers.
Migrate the send-invitation and edit-invitation drawers to react-hook-form +
zod (shellhub-io/team#152). The invitation and edit-role schemas live in
team/schemas.ts. The send-invitation drawer swaps to a result view after
submit, so it composes useDrawerForm + field wrappers + FormRootError under a
raw Drawer rather than FormDrawer.
…-form + zod

Migrate the generate-key and edit-key drawers to react-hook-form + zod
(shellhub-io/team#152). The generate-key and edit-key schemas live in
team/schemas.ts, including the string-bound expiry that is converted to the
numeric API value in buildGenerateKeyBody. The generate-key drawer swaps to a
result view after submit, so it composes useDrawerForm + field wrappers +
FormRootError under a raw Drawer rather than FormDrawer.
@luannmoreira luannmoreira force-pushed the refactor/adopt-react-hook-form branch from 797df19 to 3aafeb6 Compare July 9, 2026 22:02
@otavio

otavio commented Jul 9, 2026

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @otavio's task in 6m 39s —— View job


Code Review Complete

Reviewed 69 files across code quality, security, testing, TypeScript/React patterns, and architecture/cross-repo — no new issues found. The code looks good as-is.

All four issues from previous review rounds remain resolved at the current head (3aafeb6):

  • ruleSchema.ts — positive-integer priority rule restored (rejects 0/negatives).
  • secure-vault/KeyDrawer.tsxsetValue("passphrase", "", { shouldValidate: true }).
  • RuleDrawer.test.tsx — reworked to verify the TagsSelector 3-tag cap (4th click ignored → 3 chips).
  • Profile/Settings reset-on-prop-change — guarded by useDrawerForm's wasOpen closed→open ref.

Spot-checks that came back clean:

  • Schemas faithfully port (or strengthen) prior validationsamlSchema, userSchema (blank-password-keeps-current preserved), keySchema (now adds 64-char max), ruleSchema (now adds isValidRegex on IP/username/hostname), vaultKeySchema, editProfileSchema/changePasswordSchema, team/schemas (roleField excludes owner), editNamespaceSchema/namespaceRenameSchema/announcementSchema. No loosened validation, no XSS/secret-handling regressions.
  • Removed symbols (makeRhfResolver, useUserForm) have zero remaining importers — no build break.
  • RadioGroupField's new optional error prop is backward-compatible with all existing consumers (FieldError renders null on empty).
  • Dual-mode drawers (InvitationDrawer, GenerateKeyDrawer) correctly reuse useDrawerForm + useResetOnOpen and match FormDrawer's submit gating.
  • No cloud/ cross-repo impact (Go-only, consumes none of these files); new deps (@hookform/resolvers, zod) are consistent across package.json and package-lock.json.
  • No any introduced, no leftover console.log/debugger/TODO; test titles match their assertions.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.
refactor/adopt-react-hook-form

@luannmoreira luannmoreira force-pushed the refactor/adopt-react-hook-form branch from 3aafeb6 to 5a16b59 Compare July 10, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants