Skip to content

feat!: split useCompensationForm into useJobForm + useCompensationForm hooks#1735

Open
serikjensen wants to merge 5 commits intomainfrom
refactor/job-compensation-hooks-split
Open

feat!: split useCompensationForm into useJobForm + useCompensationForm hooks#1735
serikjensen wants to merge 5 commits intomainfrom
refactor/job-compensation-hooks-split

Conversation

@serikjensen
Copy link
Copy Markdown
Member

Summary

Decomposes the onboarding-only useCompensationForm into focused, steady-state hooks so partners can drive job and compensation lifecycles independently or together.

  • New useJobForm / useCurrentJobFormcreate | update routing, Title, HireDate, TwoPercentShareholder, StateWcCovered, StateWcClassCode fields, S-Corp + WA workers' comp visibility helpers.
  • Refactored useCompensationForm + new useCurrentCompensationForm — single create | update path covering both the onboarding stub-update case (POST /v1/employees/:id/jobs returns a stub compensation that gets PUT-updated) and steady-state effective-dated compensations (POST /v1/jobs/:jobId/compensations), with optimistic-locking via version. Adds EffectiveDate field and derived canTriggerCarveOut / minimumEffectiveDate / maximumEffectiveDate / hasPendingFutureCompensation helpers.
  • Composition recipe — adds composeSubmitHandler examples for the onboarding chain (POST job → PUT stub compensation in one submit) and steady-state edits.
  • title semantics clarifieduseJobForm.Fields.Title for job creation / immediate active-role rename; useCompensationForm.Fields.Title for changes scoped to a specific compensation effectiveDate. Documented across both hook docs and the schema.

Breaking changes

useCompensationForm.actions.onSubmit signature changed from (callbacks?, options?) to (options?). The CompensationSubmitCallbacks type is removed from the public exports. Read the saved compensation from the awaited HookSubmitResult<Compensation> (result.data) instead of wiring onCompensationCreated / onCompensationUpdated.

This aligns useCompensationForm with the rest of the SDK convention: hooks expose submit callbacks only when they chain multiple internal API calls (e.g. useEmployeeDetailsForm's onboarding-status update). Single-mutation hooks return their result directly. Updated docs and the migrate-sdk-component-to-hooks skill to reflect this rule.

Testing infrastructure

  • New compose.test.tsx covering the onboarding chain through composeSubmitHandler and steady-state composition.
  • Migrated Compensation.test.tsx and the new hook test suites off the bespoke apiSpy utility onto vi.fn<HttpResponseResolver>(...) resolvers + mock.invocationCallOrder for sequencing assertions. Deleted src/test/spies/apiSpy.ts.
  • Documented the recommended HTTP-assertion pattern in CLAUDE.md under "Asserting on HTTP requests" so future agents converge on the same approach.
  • Added src/test/factories/jobsAndCompensations.ts with fixtures parsed through Job\$inboundSchema / Compensation\$inboundSchema (jobFromJSON / compensationFromJSON) at construction time so wire-shape drift fails fast.
  • Added two missing acceptance-criteria cases (EMF-CO3, EMF-M03) to EditCompensationPresentation.test.tsx.

Docs touched

  • New: docs/hooks/useJobForm.md
  • Refreshed: docs/hooks/useCompensationForm.md, docs/hooks/hooks.md (Composing Job + Compensation section, updated table)
  • Internal: CLAUDE.md, .claude/hooks-implementation.md, .claude/skills/migrate-sdk-component-to-hooks/SKILL.md

Component migration of Employee.Compensation (the connected component) onto the new public hooks is intentionally out of scope of this PR and will land as a follow-up so we can keep this change to types → hooks → tests → docs without churning the presentation layer.

Test plan

  • `npm run test -- --run src/components/Employee/Compensation` — 110 passing locally post-rebase.
  • `npm run build` clean.
  • CI green.
  • Spot-check via Storybook: hook-driven stories in `shared/useJobForm` and `shared/useCompensationForm` (if/when added) render and submit cleanly. (Connected-component migration deferred — `Compensation` flow regression covered by `Compensation.test.tsx`.)
  • Cross-form focus management: composing `useJobForm` + `useCompensationForm` via `composeSubmitHandler` focuses the visually-first invalid field on submit (covered by `composeSubmitHandler.focus.test.tsx` infra; new hooks integrate via `useHookFormInternals`).

Made with Cursor

…m hooks

Decompose the monolithic onboarding-only useCompensationForm into focused,
steady-state hooks so partners can drive job and compensation lifecycles
independently or together.

- Add useJobForm / useCurrentJobForm: create | update routing, error handling,
  and POST/PUT/DELETE wiring for the Jobs API.
- Refactor useCompensationForm + add useCurrentCompensationForm: single
  create | update path covering both the onboarding stub-update case and
  steady-state effective-dated compensations, with optimistic-locking via
  version.
- Drop CompensationSubmitCallbacks (and the never-shipped JobSubmitCallbacks
  scaffolding); per the SDK convention, only hooks chaining multiple internal
  API calls expose submit callbacks. Partners read the saved entity from the
  awaited HookSubmitResult instead.
- Document the title field's effective-dating semantics across the two hooks
  (useJobForm.Fields.Title for job-creation / immediate active-role rename
  vs useCompensationForm.Fields.Title for changes scoped to a specific
  compensation effectiveDate).
- Add compose.test.tsx covering the onboarding chain
  (POST job -> PUT stub compensation) and steady-state composition through
  composeSubmitHandler.
- Migrate Compensation.test.tsx + new hook tests off the bespoke apiSpy
  utility onto vi.fn<HttpResponseResolver> resolvers and
  mock.invocationCallOrder; delete the spy. Document the pattern in CLAUDE.md
  under "Asserting on HTTP requests".
- Add src/test/factories/jobsAndCompensations.ts with fixtures parsed
  through Job\$inboundSchema / Compensation\$inboundSchema at construction
  time so wire shape drift fails fast.
- Update docs/hooks/hooks.md (Composing Job + Compensation), useJobForm.md,
  and useCompensationForm.md; refresh hooks-implementation.md and the
  migrate-sdk-component-to-hooks SKILL to reflect the no-callbacks rule.

BREAKING CHANGE: useCompensationForm.actions.onSubmit changed from
(callbacks?, options?) to (options?). The CompensationSubmitCallbacks export
is removed. Read the saved compensation from the awaited HookSubmitResult's
data field instead of wiring onCompensationCreated / onCompensationUpdated.

Co-authored-by: Cursor <cursoragent@cursor.com>
serikjensen and others added 2 commits May 7, 2026 17:55
The hook documented (in useJobForm.md) that Fields.StateWcClassCode is
hidden until the user marks the role as covered, but the implementation
was only gating on showStateWc (state === 'WA'), leaving the class-code
field visible in WA whether or not coverage was elected.

Watch stateWcCovered in the hook and gate the field accordingly so the
hook owns visibility (matching the migrate-sdk-component-to-hooks rule
"the hook controls field visibility"). The schema's conditional
required-ness was already correct and is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
…h screen UX

- Drop the eager `flsaStatus: NONEXEMPT` fallback so the field renders the
  empty "Select an item" placeholder when nothing is provided. Adds a
  `primaryFlsaStatus` derivation that inherits from the employee's primary
  job's current compensation FLSA when adding a secondary job. Schema still
  enforces requiredness on submit in create mode; partners can seed via
  `defaultValues.flsaStatus` for create flows that have a known
  classification up-front. `flsaStatus` is now `.optional()` at the
  validator level so the type honestly reflects the runtime contract.

- Reorder the FLSA rate validations: surface RATE_EXEMPT_THRESHOLD before
  RATE_MINIMUM so a partner setting rate=0 with EXEMPT sees the more
  actionable "must meet salary threshold" message rather than the generic
  "amount must be at least \$1.00".

Updates two unit tests that relied on the eager NONEXEMPT default to set
`flsaStatus: 'Nonexempt'` in their `defaultValues`.

Surfaced while migrating EditCompensation onto the hooks; the existing
integration test asserts both the placeholder rendering and the EXEMPT
threshold message ordering.

Co-authored-by: Cursor <cursoragent@cursor.com>
serikjensen and others added 2 commits May 7, 2026 19:19
… and split job+comp doc

The carve-out flag previously lived under `data.canTriggerCarveOut`, but
`data.*` was otherwise reserved for fetched entities and static
entity-derived facts; this one boolean was the only `useWatch`-driven
reactive value mixed in. Move it to `status.*` alongside `isPending`
and `mode`, and rename it to describe the real server-side effect
(the carve-out path deletes the employee's secondary jobs, per the
shipped i18n string and the gws-flows reference) so partners don't
need Gusto-internal vocabulary to interpret the flag.

Lift the lengthy "Composing Job + Compensation" section out of
hooks.md into a dedicated docs/hooks/jobs-and-compensations.md, with
see-also links from useJobForm.md and useCompensationForm.md and a
one-line pointer left at the original anchor in hooks.md (preserves
inbound deep links). Steady-state example switched from a
window.confirm at submit time to a render-time inline alert keyed off
the renamed flag, which mirrors the existing EditCompensation UX and
exercises the now-correctly-placed reactive value.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rtners"

The hooks docs are partner-facing, so third-person framings like
"partners typically render…", "partners pass this as `min`…", or
"no extra `useWatch` on the partner side" read as if the reader is a
bystander. Flip those to second-person/imperative voice so the docs
speak directly to the developer reading them. Also rename the
"Partner-configurable?" column header in the requiredness tables to
"Configurable?" for the same reason.

Co-authored-by: Cursor <cursoragent@cursor.com>
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