Skip to content

refactor(sdk-app): split design components into View/Container layers#1986

Open
aaronlee777 wants to merge 18 commits into
mainfrom
al/feat/compensation-history
Open

refactor(sdk-app): split design components into View/Container layers#1986
aaronlee777 wants to merge 18 commits into
mainfrom
al/feat/compensation-history

Conversation

@aaronlee777
Copy link
Copy Markdown
Contributor

@aaronlee777 aaronlee777 commented Jun 1, 2026

Summary

Split the design app's prototype layer into pure Views (presentational, prop-driven) and thin Containers (data fetching + event dispatch + route wiring). Once that split exists, a per-prototype Component States viewer can render any View in isolation with mock data — no MSW, no live API.

Migrated CompensationHistory, all of contractor-management, and all of contractor-self-onboarding to the new pattern. Three prototype state viewers ship as part of this change.

Final layout

sdk-app/src/design/
  components/                            ← Views (pure presentational)
    common/
    contractor/
      management/                          ContractorList, ContractorDetails,
                                            ContractorAddress, ContractorPay,
                                            ContractorDocuments, ContractorDismissalForm,
                                            ContractorRehireForm, +Form variants…
      self-onboarding/                     ContractorWelcome, ContractorDocumentList,
                                            ContractorDocumentSignature, ContractorOnboardingComplete
      shared/                              AddressForm, PaymentMethodForm, ProfileDetailsForm
    employee/
      management/
        CompensationHistory/

  prototypes/                            ← Containers (data + routing)
    contractor-management/                 ContractorList, ContractorDismiss, ContractorRehire,
                                           ContractorManagementFlow, ContractorProfile/,
                                           AddContractor/, states.tsx, index.tsx
    contractor-self-onboarding/            ContractorLanding, ContractorSelfProfile,
                                           ContractorSelfAddressForm, ContractorSelfPaymentMethodForm,
                                           ContractorDocumentSigner, ContractorSignatureForm,
                                           ContractorOnboardingSummary,
                                           ContractorSelfOnboardingWizard,
                                           states.tsx, index.tsx
    employee-management/CompensationHistory/  CompensationHistory.tsx (container),
                                              states.tsx, index.tsx

Naming rule: single file per route container when one file is enough; folder when multiple. No Route suffix anywhere — the folder location already says it. Views and containers can share a name (e.g. ContractorList); when both end up in the same file the View is aliased on import (as ContractorListView).

What this PR delivers

Structure

  • Pure Views relocated to design/components/{contractor,employee}/{management,self-onboarding,shared}/.
  • Thin containers in design/prototypes/<prototype>/ that own data fetching, mutations, and event dispatch.
  • self-onboarding/ lifted from inside contractor-management/ to a sibling prototype (prototypes/contractor-self-onboarding/).
  • contractor-profile/ subfolder flattened — ContractorManagementFlow and ContractorProfile/ now sit at the prototype root.
  • Route suffix dropped from ContractorList, ContractorDismiss, ContractorRehire.
  • AddContractor/ wizard flattened to prototypes/contractor-management/AddContractor/.

Component States viewers

Three live viewers, each with a right-rail sidebar that lists Views and per-View configurations:

  • /design/employee-compensation-history/component-states/ — CompensationHistory (4 configs).
  • /design/contractor-management/component-states/ — ContractorList (8 configs), ContractorDismissalForm (4), ContractorDetails (4), ProfileDetailsForm (2), AddressForm (2), PaymentMethodForm (2).
  • /design/contractor-self-onboarding/component-states/ — ContractorWelcome (3), ContractorDocumentList (4), ContractorDocumentSignature (2), ContractorOnboardingComplete (1), ProfileDetailsForm (3), AddressForm (2), PaymentMethodForm (3).

Shared-form viewers in both contractor surfaces so the same Views can be inspected with management-context copy or onboarding-context copy.

Shared form refactor

  • PaymentMethodForm (shared) was a container masquerading as a View — it did its own suspense queries, mutations, and event dispatch. Split into a pure View (just fields + form mechanics) and lifted the data wiring into each consumer (self-onboarding container + AddContractor wizard step). Matches AddressForm / ProfileDetailsForm shape. Also fixed a stale requestBody field name (the SDK type expects contractorBankAccountCreateRequestBody).

Dead-code removal

  • Deleted MockedRender, MockedEntitiesOutlet, sdk-app/src/mocks/ (browser worker + useMockHandlers), and sdk-app/public/mockServiceWorker.js. With every state demo passing data via props directly, the MSW machinery wasn't earning its keep.
  • Removed the handlers: RequestHandler[] field from PrototypeConfiguration and every handlers: [] line in states files.
  • ComponentStatesPage now calls configuration.render() directly.

Other small wins

  • Local W-9 PDF bundled at sdk-app/public/sample-documents/w9.pdf so the DocumentSignature demo's DocumentViewer actually renders something (irs.gov refuses to be embedded).
  • Registry sidebar now lists Prototype / Component states children under Contractor Self-Onboarding.

What's not in this PR

  • The wizard's AddContractor/ContractorProfile.tsx step (with the self-onboarding toggle + classification fields) is intentionally kept separate from the shared ProfileDetailsForm. The forms are structurally different and consolidating would mean ~8 toggle props on the shared View.
  • Unit tests for the newly-extracted self-onboarding Views — the existing CompensationHistory test was migrated and still passes; adding tests for the rest can come in a follow-up.

Test plan

  • npm run sdk-app — open the design app.
  • Sidebar shows three prototypes with Prototype / Component states children: Compensation History, Contractor Management, Contractor Self-Onboarding.
  • Compensation History → Component states → step through Single job / Multiple jobs / Mixed wages / Empty.
  • Contractor Management → Component states → step through every component: List (8 configs), Dismissal Form (4), Basic Details (4), Profile Details Form (2), Address Form (2), Payment Method Form (2).
  • Contractor Self-Onboarding → Component states → step through Welcome (3), Profile Details (3), Address Form (2), Payment Method Form (3), Document List (4), Document Signature (2), Onboarding Complete (1). The Document Signature default config should render a W-9 in the DocumentViewer.
  • Live prototype routes still work: /design/employee-compensation-history/prototype, /design/contractor-management, /design/contractor-self-onboarding.
  • npx tsc --noEmit clean.
  • npm run test -- --run sdk-app/src/design/components/employee/management/CompensationHistory/CompensationHistory.test.tsx passes (4 tests).

🤖 Generated with Claude Code

aaronlee777 and others added 3 commits June 1, 2026 11:18
Adds a read-only CompensationHistory component that lists every job
with its full effective-dated compensation history (effective date,
employee type, wage + frequency). Surfaced via a new tertiary
"View history" button in the Job & pay compensation card, wired
through the dashboard state machine alongside AddAnotherJob and
EditCompensation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…able single view

- Render a single combined DataView (with Job title column) when the
  employee has 2+ jobs, sorted by effective date descending across jobs.
- Add a Job filter Select beside the heading that defaults to "All jobs"
  and narrows rows to the chosen job.
- Keep the per-job heading + 3-column DataView when there's only one job.
- Show an empty state when the employee has no jobs.
- Use useContainerBreakpoints + FlexItem to stack the heading and Select
  in narrow containers (matches the PayrollConfiguration header pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FlexItem with no props is a div with flex-grow: initial, which is the
default for any flex child — the wrappers were not load-bearing. Heading
and the .jobFilter div behave identically as direct Flex children.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aaronlee777 aaronlee777 changed the title feat: combine multi-job compensation history into a filterable single view feat: add compensation history component Jun 1, 2026
aaronlee777 and others added 3 commits June 1, 2026 17:36
Move the read-only CompensationHistory component out of the SDK surface
and into sdk-app/src/design/prototypes/employee-management/CompensationHistory.
Strings are hardcoded (no i18n dependency) since prototypes don't use
the SDK's translation system.

Revert all SDK wiring back to main:
- Remove CompensationHistoryContextual from DashboardComponents
- Remove compensationHistory state + EMPLOYEE_COMPENSATION_HISTORY_VIEW
  transition from dashboardStateMachine
- Drop the "View history" tertiary button from JobAndPayView
- Remove handleViewCompensationHistory wiring from Dashboard
- Remove the export from Compensation/management/index.ts
- Remove the EMPLOYEE_COMPENSATION_HISTORY_VIEW event constant
- Remove the history.* translation keys and viewHistoryCta

Wire the prototype into the design app, matching the existing
contractor-management folder/index.tsx convention:
- index.tsx reads employeeId from outlet context (optional prop kept
  for testability)
- New route /design/employee-compensation-history in sdk-app main router
- Register the prototype under the Employees category in design/registry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a Storybook-lite viewer for browsing prototype component states with
mocked data, alongside the existing live-data prototype view.

Each prototype declares its own `states.tsx` listing components and
configurations (slug, name, MSW handlers, render fn). The new
ComponentStatesPage renders a right-rail sidebar of those configurations
and uses MSW (browser worker) to intercept SDK API calls so each
configuration renders with deterministic mock data. The browser worker
lazy-starts on first selection and uses `onUnhandledRequest: 'bypass'`
so live routes are unaffected.

CompensationHistory is the first migrated prototype. Its sidebar entry
now nests two children — Prototype (live) and Component states — and
the latter exposes Single job, Multiple jobs, Mixed wages, and Empty
configurations built from the shared `buildJob` factory primitive.
Empty states render via DataView's emptyState prop instead of bare text.

Misc:
- DesignLayout becomes a flex shell with a portal-based right rail so
  the component-states sidebar can sit flush against the viewport edge.
- Left sidebar: tighter active treatment (color only, no border/bg),
  uppercase tiny category labels, and a font-weight bump on top-level
  items so nested children read as a clear visual hierarchy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorganize the design app so design primitives are clearly separated
from prototype wiring. Establishes a pattern any future prototype work
can follow.

New structure:

  sdk-app/src/design/
    components/                  Views — domain-organized presentational
      common/                    primitives. Anywhere can import.
        Skeleton.tsx
        contractorName.ts
      contractor/
        ContractorList/
          ContractorList.tsx              (View)
          ContractorListStates.tsx        (state-demo helper)
          SkeletonDataView.tsx
        ContractorDismissalForm/
          ContractorDismissalForm.tsx
          ContractorDismissalFormStates.tsx
    prototypes/                  Containers + route assemblies.
      contractor-management/
        ContractorListRoute.tsx
        ContractorDismissRoute.tsx
        states.tsx                        (manifest of which component
                                           states show under this proto)
      employee-management/
        CompensationHistory/

Naming convention:
- View         -> bare name (ContractorList)
- Route/Container -> suffix Route (ContractorListRoute)
- State demos  -> suffix States (ContractorListStates)

What this work shipped along the way:
- A right-rail Component States viewer per prototype, fed by MSW or by
  passing mock data directly to a View
- CompensationHistory as the first View-only prototype
- ContractorList split into View + Container, with mock data passed to
  the View directly (no MSW needed for state demos)
- ContractorDismissalForm extracted as a stand-alone View with its own
  state demos

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aaronlee777 aaronlee777 changed the title feat: add compensation history component refactor(sdk-app): split design components into View/Container layers Jun 2, 2026
aaronlee777 and others added 9 commits June 2, 2026 14:41
Reorganize design/components/contractor/ into a management/ subfolder
so future onboarding-specific contractor Views can live alongside as
contractor/onboarding/ without colliding.

  Before: design/components/contractor/<Name>/
  After:  design/components/contractor/management/<Name>/

All 15 contractor Views moved, internal common-imports updated to the
new depth, and every external consumer's import path patched to inject
management/ after components/contractor/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same shape as ContractorDismissRoute and ContractorListRoute: move the
container out of contractor-list/ContractorRehire/index.tsx to a flat
ContractorRehireRoute.tsx alongside its siblings. Rename the export to
ContractorRehireRoute and update main.tsx accordingly. The View
(ContractorRehireForm) was already in
design/components/contractor/management/ from earlier rounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ctor

- Move AddressForm, PaymentMethodForm, ProfileDetailsForm from
  design/components/contractor/management/ to .../contractor/shared/
  so they can serve both management and self-onboarding consumers
  without implying a management-only home.
- Flatten AddContractor wizard from
  prototypes/contractor-management/contractor-list/AddContractor/ to
  prototypes/contractor-management/AddContractor/, matching the flat
  layout used by the other prototype routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Lift self-onboarding to its own sibling prototype at
  prototypes/contractor-self-onboarding/ (it was nested under
  contractor-management/ for historical reasons, but is a separate
  contractor-facing experience with its own /design route).
- Flatten contractor-profile/ subfolder: ContractorManagementFlow
  (the outer routing shell for the whole prototype) becomes a flat
  file, and ContractorProfile/ moves up to live alongside the other
  route containers.
- Drop the Route suffix from single-file route containers
  (ContractorListRoute -> ContractorList, etc.) so the naming rule
  is consistent: single file when one file, folder when more, no
  suffix in either case. Alias the ContractorList View on import in
  the container to avoid the local name collision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shared PaymentMethodForm was inconsistent with its siblings in
shared/ — AddressForm and ProfileDetailsForm are pure Views with no
data fetching, but PaymentMethodForm did its own suspense queries,
mutations, and event dispatching. That made it impossible to render
in a component-states preview and welded the View to the API layer.

- shared/PaymentMethodForm/PaymentMethodForm.tsx is now a pure View:
  takes defaultValues, isPending, onSubmit. Internally runs zod
  validation plus the lazy accountNumber regex check (only when the
  user actually changed the value vs. the masked default).
- Self-onboarding container (ContractorSelfPaymentMethodForm) and
  AddContractor wizard step (ContractorPaymentMethod) now own the
  fetch + mutate + event-dispatch wiring, matching the
  AddressForm / ProfileDetailsForm pattern.
- Fix stale field name: createBankAccount request uses
  contractorBankAccountCreateRequestBody, not requestBody (the
  previous shared form was using the old field name and only worked
  because the type checker had not flagged it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…history

# Conflicts:
#	sdk-app/src/Sidebar.tsx
#	sdk-app/src/design/components/contractor/management/ContractorAddressForm/ContractorAddressForm.tsx
#	sdk-app/src/design/components/contractor/management/ContractorDetailsForm/ContractorDetailsForm.tsx
#	sdk-app/src/design/components/contractor/management/ContractorList/ContractorList.tsx
#	sdk-app/src/design/components/contractor/management/ContractorPaymentMethod/ContractorPaymentMethod.tsx
#	sdk-app/src/design/prototypes/contractor-management/AddContractor/ContractorAddress.tsx
#	sdk-app/src/design/prototypes/contractor-management/ContractorDismiss.tsx
#	sdk-app/src/design/prototypes/contractor-management/ContractorRehire.tsx
#	sdk-app/src/design/prototypes/contractor-management/common/PaymentMethodForm.tsx
#	sdk-app/src/design/prototypes/contractor-self-onboarding/ContractorSelfAddressForm.tsx
#	sdk-app/src/design/prototypes/contractor-self-onboarding/ContractorSelfProfile.tsx
Apply the View/Container pattern (shared with contractor-management) to
the CompensationHistory prototype.

- New pure View at design/components/employee/management/CompensationHistory/.
  Takes jobs as a prop. Owns the single-vs-combined branching, the local
  job filter state, and the container-breakpoints responsive header.
- Container at prototypes/employee-management/CompensationHistory/CompensationHistory.tsx
  (renamed from CompensationHistoryComponent — drop the Component suffix
  to match the contractor pattern). Fetches via useJobsAndCompensationsGetJobs,
  wraps the View with BaseBoundaries + BaseLayout. Imports the View as
  CompensationHistoryView to disambiguate.
- states.tsx now renders the View directly with camelCase Job mocks
  — no MSW handlers needed.
- Test moves next to the View and exercises the View by passing jobs
  as a prop, dropping the MSW + buildEmployeeWithJobs setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that every state demo renders a pure View directly with mock props,
the MSW-based mock-data infrastructure no longer pulls its weight.

Deleted:
- sdk-app/src/design/prototypes/MockedRender.tsx
  (replaced by an inline configuration.render() call in
  ComponentStatesPage — the nested GustoProvider it set up to point at
  https://api.gusto.com/ was only there for MSW to intercept, which
  no current demo needs)
- sdk-app/src/design/prototypes/MockedEntitiesOutlet.tsx
  (no View reads outlet entities; Views take data via props)
- sdk-app/src/mocks/browser.ts + useMockHandlers.ts (only consumer
  was MockedRender)
- sdk-app/public/mockServiceWorker.js (gitignored, removed from disk)
- The `handlers: RequestHandler[]` field on PrototypeConfiguration and
  every `handlers: []` line in the contractor + compensation-history
  states files

main.tsx no longer wraps the contractor-management component-states
route in MockedEntitiesOutlet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ding

Apply the View/Container split to the remaining mixed self-onboarding
files and wire up a component-states viewer so each screen can be
previewed in isolation with mock data.

Views extracted into design/components/contractor/self-onboarding/:
- ContractorWelcome (from ContractorLanding) — takes contractorName,
  companyName, onStart
- ContractorDocumentList (from ContractorDocumentSigner) — takes
  documents, onRequestSign, onContinue
- ContractorSignaturePad (from ContractorSignatureForm) — takes title,
  description, pdfUrl, isPending, onSubmit, onBack
- ContractorOnboardingComplete (from ContractorOnboardingSummary, which
  was already pure presentation) — takes onDone

Containers in prototypes/contractor-self-onboarding/ slimmed to fetch
data + dispatch events + render the View.

New states viewer at /design/contractor-self-onboarding/component-states/
listing all four self-onboarding Views plus the three shared forms
(Profile Details, Address, Payment Method) with onboarding-context
copy. The same three shared forms are also added to the
contractor-management states viewer with management-context copy so
both flows surface them.

registry.ts gains Prototype / Component states children under
Contractor Self-Onboarding to expose the new route in the sidebar.

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

@boostsecurity-io-ai boostsecurity-io-ai Bot left a comment

Choose a reason for hiding this comment

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

⚠️  1 New Security Finding

The latest commit contains 1 new security finding.

Findings Note: 1 finding is displayed as an inline comment.

Not a finding? Ignore it by adding a comment on the line with just the word noboost.

Scanner: boostsecurity - Semgrep


function buildDocument(overrides: Partial<Document>): Document {
return {
uuid: `doc-${Math.random().toString(36).slice(2, 8)}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
Original Rule ID: rules_lgpl_javascript_crypto_rule-node-insecure-random-generator

Details

The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.

The application uses cryptographically weak random number generators such as
crypto.pseudoRandomBytes() or Math.random() for security-sensitive
operations. The crypto.pseudoRandomBytes() function uses a non-cryptographic
PRNG that may be predictable, while Math.random() is explicitly designed for
general-purpose use and is completely unsuitable for cryptographic purposes.
Using these functions for generating session tokens, CSRF tokens, password reset
tokens, encryption keys, or other security-critical values allows attackers to
predict future outputs by observing previous values. This predictability can lead
to session hijacking, authentication bypass, or complete compromise of
cryptographic protections.
 📘 Learn More

AI Remediation

The code previously used Math.random() to generate part of a document UUID, which is not cryptographically secure and could allow an attacker to predict values. The fix replaces it with crypto.randomBytes(), which uses the operating system's secure random number generator to provide unpredictability and improved security for generated identifiers.

Suggested change
uuid: `doc-${Math.random().toString(36).slice(2, 8)}`,
uuid: `doc-${require('crypto').randomBytes(4).toString('hex')}`,

aaronlee777 and others added 3 commits June 2, 2026 17:26
…Signature

The "SignaturePad" name implied a drawing canvas, but the View is just
a "type your full legal name + agree checkbox" form. Renaming to
ContractorDocumentSignature so the name reflects what it actually does.

Also point the demo configurations at a real W-9 PDF
(https://www.irs.gov/pub/irs-pdf/fw9.pdf) instead of passing pdfUrl=null,
so the DocumentViewer actually renders a usable View document link in
the state-viewer preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
irs.gov refuses to be embedded (sets X-Frame-Options or equivalent),
so the DocumentViewer's <embed> tag was failing with a "refused to
connect" error in the component-states preview. Bundle the W-9 PDF
at sdk-app/public/sample-documents/w9.pdf and point the demo
configurations at the relative path so the preview actually renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aaronlee777 aaronlee777 marked this pull request as ready for review June 2, 2026 21:39
@aaronlee777 aaronlee777 requested a review from a team as a code owner June 2, 2026 21:39
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.

1 participant