feat: extract PayrollExecutionFlow from PayrollFlow #1051
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors the payroll “execution” portion (configuration → overview → receipts) into a reusable PayrollExecutionFlow, leaving PayrollFlow as a landing-orchestrator for the upcoming off-cycle payroll work.
Changes:
- Added
PayrollExecutionFlow+payrollExecutionMachineand updatedPayrollFlowto switch between landing/execution flows. - Added
RUN_PAYROLL_DATA_LOADEDevent and auseEmitOnDataReadyhook to emit it when pay period data becomes available. - Slimmed
payrollStateMachine.tsdown to landing-only breadcrumb nodes.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/constants.ts | Adds RUN_PAYROLL_DATA_LOADED event constant. |
| src/hooks/useEmitOnDataReady.ts | New hook to emit an event once when data becomes available. |
| src/components/Payroll/PayrollFlow/payrollStateMachine.ts | Removes execution-machine logic; keeps landing breadcrumb node. |
| src/components/Payroll/PayrollFlow/PayrollFlow.tsx | Orchestrates between landing flow and PayrollExecutionFlow. |
| src/components/Payroll/PayrollExecutionFlow/payrollExecutionMachine.ts | New extracted execution state machine + breadcrumb nodes. |
| src/components/Payroll/PayrollExecutionFlow/index.ts | Barrel exports for the new execution flow/machine. |
| src/components/Payroll/PayrollExecutionFlow/PayrollExecutionFlow.tsx | New flow component that instantiates the execution machine. |
| src/components/Payroll/PayrollConfiguration/PayrollConfiguration.tsx | Emits RUN_PAYROLL_DATA_LOADED once pay period data is ready. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f094b0c to
7767855
Compare
659d275 to
9d4c85d
Compare
serikjensen
left a comment
There was a problem hiding this comment.
Chatted with @jeffredodd about this one. Biggest feedback items
- move the PayrollExecutionFlow into a dedicated contextual component
- add the PayrollExecutionFlowContextual into the PayrollFlow landing transitions and pass through the employee id and company id via the flow context
- update to remove the useEmitOnDataReady operations and just fetch the data when needed within the PayrollExecutionFlow
6b6b028 to
e2d6362
Compare
Latest changes: make PayrollExecutionFlow composableThe latest commit refactors Key changesExecution machine is now parent-agnostic:
Bug fixes:
Submitted states restored to parent:
Composability for future OffCyclePayrollFlowWith these changes, any parent flow can:
|
| namespace: 'Payroll.PayrollBlocker', | ||
| }, | ||
| }, | ||
| submittedOverview: { |
There was a problem hiding this comment.
@serikjensen I realized we probably need these states and this machine at the flow level since Execution shouldn't be responsible for viewing static payroll stuff.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }, | ||
| }), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
RUN_PAYROLL_CANCELLED_ALERT_DISMISSED is emitted by PayrollLanding when the cancelled alert is dismissed, but this state machine no longer handles that event. As a result, showPayrollCancelledAlert will stay true and the alert won't dismiss. Add a landing-state transition for componentEvents.RUN_PAYROLL_CANCELLED_ALERT_DISMISSED that reduces showPayrollCancelledAlert back to false (similar to the previous machine behavior).
| ), | |
| ), | |
| transition( | |
| componentEvents.RUN_PAYROLL_CANCELLED_ALERT_DISMISSED, | |
| 'landing', | |
| reduce( | |
| (ctx: PayrollFlowContextInterface): PayrollFlowContextInterface => ({ | |
| ...ctx, | |
| showPayrollCancelledAlert: false, | |
| }), | |
| ), | |
| ), |
| type LandingEventPayloads = { | ||
| [componentEvents.RUN_PAYROLL_SELECTED]: { | ||
| payrollUuid: string | ||
| } | ||
| [componentEvents.REVIEW_PAYROLL]: { | ||
| payrollUuid: string | ||
| } | ||
| } | ||
|
|
||
| const breadcrumbNavigateTransition = | ||
| createBreadcrumbNavigateTransition<PayrollFlowContextInterface>() | ||
|
|
||
| const landingBreadcrumbNavigateTransition = transition( | ||
| componentEvents.BREADCRUMB_NAVIGATE, | ||
| 'landing', | ||
| guard( | ||
| (_ctx: PayrollFlowContextInterface, ev: { payload: { key: string } }) => | ||
| ev.payload.key === 'landing', | ||
| ), | ||
| reduce(toLandingReducer), | ||
| ) | ||
|
|
||
| function toLandingReducer(ctx: PayrollFlowContextInterface): PayrollFlowContextInterface { | ||
| return { | ||
| ...ctx, | ||
| component: PayrollLandingContextual, | ||
| payrollUuid: undefined, | ||
| progressBarType: null, | ||
| currentBreadcrumbId: 'landing', | ||
| executionInitialState: undefined, | ||
| } | ||
| } | ||
|
|
||
| const toExecutionReducer = ( | ||
| ctx: PayrollFlowContextInterface, | ||
| ev: { payload: { payrollUuid: string } }, | ||
| executionInitialState: PayrollExecutionInitialState, | ||
| ): PayrollFlowContextInterface => ({ | ||
| ...ctx, | ||
| component: PayrollExecutionFlowContextual, | ||
| payrollUuid: ev.payload.payrollUuid, | ||
| showPayrollCancelledAlert: false, | ||
| executionInitialState, | ||
| }) |
There was a problem hiding this comment.
The landing->execution transitions ignore the payPeriod that is currently emitted by PayrollList for RUN_PAYROLL_SELECTED/REVIEW_PAYROLL. The execution flow breadcrumbs (e.g. Payroll.PayrollConfiguration.breadcrumbLabel) interpolate startDate/endDate, so without carrying payPeriod (or otherwise populating it in the execution machine context) the breadcrumb label will render with missing dates. Preserve payPeriod in the event payload and/or populate the execution flow context with payPeriod (or start/end dates) on entry so breadcrumb interpolation works from the first render.
| const executionFlowMachine = useMemo(() => { | ||
| const baseBreadcrumbs = buildBreadcrumbs(payrollExecutionBreadcrumbsNodes) | ||
| const breadcrumbs = Object.fromEntries( | ||
| Object.entries(baseBreadcrumbs).map(([stateKey, trail]) => [ | ||
| stateKey, | ||
| [...prefixBreadcrumbs, ...trail], | ||
| ]), | ||
| ) | ||
|
|
||
| const initialBreadcrumbContext = updateBreadcrumbs(initialState, { | ||
| breadcrumbs, | ||
| } as PayrollFlowContextInterface) | ||
|
|
There was a problem hiding this comment.
The initial breadcrumb context is built via updateBreadcrumbs(initialState, { breadcrumbs }) without any variables (start/end dates), but the configuration breadcrumb label (Payroll.PayrollConfiguration.breadcrumbLabel) requires startDate/endDate interpolation. Unless payPeriod (or variables) are set into the execution machine context before the first render, users will see an incomplete breadcrumb label. Consider accepting initial pay period / start+end date props (or emitting a data-loaded event) and calling updateBreadcrumbs('configuration' | 'overview', ctx, { startDate, endDate }) during initialization.
| blockers: state<MachineTransition>( | ||
| breadcrumbNavigateTransition('landing'), | ||
| transition(componentEvents.PAYROLL_EXIT_FLOW, 'landing', reduce(toLandingReducer)), | ||
| ), | ||
| submittedOverview: state<MachineTransition>( | ||
| breadcrumbNavigateTransition('landing'), | ||
| transition( | ||
| componentEvents.RUN_PAYROLL_RECEIPT_GET, | ||
| 'submittedReceipts', | ||
| reduce( | ||
| (ctx: PayrollFlowContextInterface): PayrollFlowContextInterface => ({ | ||
| ...updateBreadcrumbs('submittedReceipts', ctx, { | ||
| startDate: ctx.payPeriod?.startDate ?? '', | ||
| endDate: ctx.payPeriod?.endDate ?? '', | ||
| }), | ||
| component: PayrollReceiptsContextual, | ||
| progressBarType: 'breadcrumbs', | ||
| alerts: undefined, | ||
| ctaConfig: { | ||
| labelKey: 'exitFlowCta', | ||
| namespace: 'Payroll.PayrollReceipts', | ||
| }, | ||
| }), | ||
| ), | ||
| ), | ||
| transition( | ||
| componentEvents.RUN_PAYROLL_CANCELLED, | ||
| 'landing', | ||
| reduce( | ||
| (ctx: PayrollFlowContextInterface): PayrollFlowContextInterface => ({ | ||
| ...toLandingReducer(ctx), | ||
| showPayrollCancelledAlert: true, | ||
| }), | ||
| ), | ||
| ), | ||
| transition(componentEvents.PAYROLL_EXIT_FLOW, 'landing', reduce(toLandingReducer)), | ||
| ), | ||
| submittedReceipts: state<MachineTransition>( | ||
| breadcrumbNavigateTransition('landing'), | ||
| transition(componentEvents.PAYROLL_EXIT_FLOW, 'landing', reduce(toLandingReducer)), | ||
| ), |
There was a problem hiding this comment.
Breadcrumb navigation back to landing currently uses the generic createBreadcrumbNavigateTransition('landing'), which applies the breadcrumb node’s onNavigate. That onNavigate only swaps component/progressBarType/currentBreadcrumbId, while toLandingReducer also clears payrollUuid and executionInitialState. To avoid divergent landing resets depending on whether the user clicks the breadcrumb vs. exit CTA, consider reusing toLandingReducer for the landing breadcrumb transition in these states (or update the landing breadcrumb onNavigate to mirror toLandingReducer).
| import { createMachine, state, transition, reduce, guard } from 'robot3' | ||
| import { useMemo } from 'react' | ||
| import { payrollFlowBreadcrumbsNodes, payrollMachine } from './payrollStateMachine' | ||
| import { PayrollExecutionFlow, type PayrollExecutionInitialState } from '../PayrollExecutionFlow' | ||
| import { payrollFlowBreadcrumbsNodes } from './payrollStateMachine' | ||
| import type { PayrollFlowProps } from './PayrollFlowComponents' | ||
| import { | ||
| SaveAndExitCta, | ||
| PayrollLandingContextual, | ||
| PayrollBlockerContextual, | ||
| PayrollOverviewContextual, | ||
| PayrollReceiptsContextual, | ||
| type PayrollFlowContextInterface, | ||
| } from './PayrollFlowComponents' | ||
| import { Flow } from '@/components/Flow/Flow' | ||
| import { buildBreadcrumbs } from '@/helpers/breadcrumbHelpers' | ||
| import { useFlow } from '@/components/Flow/useFlow' | ||
| import { buildBreadcrumbs, updateBreadcrumbs } from '@/helpers/breadcrumbHelpers' | ||
| import { ensureRequired } from '@/helpers/ensureRequired' | ||
| import { componentEvents } from '@/shared/constants' | ||
| import type { MachineEventType, MachineTransition } from '@/types/Helpers' | ||
| import { createBreadcrumbNavigateTransition } from '@/components/Common/FlowBreadcrumbs/breadcrumbTransitionHelpers' | ||
|
|
There was a problem hiding this comment.
PR description mentions adding a useEmitOnDataReady hook and a RUN_PAYROLL_DATA_LOADED event constant, but those identifiers do not exist in the updated codebase (search across src returns no matches). Either include the missing hook/constant changes, or update the PR description to reflect what actually landed in this refactor.
|
Ok, I think i've made a lot of updates here that should hopefully make a visual difference. I've also been working ahead to look at how this will interact with Off Cycle and generated this diagram of what I have working between this PR and the other off cycle PR #1055.
|
d46c7a9 to
72758f3
Compare
serikjensen
left a comment
There was a problem hiding this comment.
Thanks for all the updates here!
if there's some way to just make the api call to get the pay period in the parent directly instead of having to use the onPayPeriodReady prop that would be preferable, but going to unblock
05c1edc to
6b6dfcd
Compare
Refactor the payroll flow to separate landing and execution concerns. The execution portion (configuration -> overview -> receipts) is now a standalone PayrollExecutionFlow component that can be composed into any parent flow. PayrollFlow becomes a lightweight orchestrator. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add missing progressBarCta (SaveAndExitCta) to execution flow context - Fix i18n namespaces for submittedOverview/submittedReceipts breadcrumbs - Handle RUN_PAYROLL_CANCELLED to return to landing with cancelled alert - Implement prefixBreadcrumbs prop by prepending to each breadcrumb trail Co-authored-by: Cursor <cursoragent@cursor.com>
Wire PayrollExecutionFlow into PayrollFlow's state machine as a contextual component instead of rendering it directly with props. Remove useEmitOnDataReady data-passing pattern and dataLoadedTransition in favor of fetching pay period data on demand. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…outing
Make PayrollExecutionFlow a parent-agnostic module that any parent flow
(PayrollFlow today, OffCyclePayrollFlow in the future) can host by
rendering it with props and handling bubbled events.
Execution machine changes:
- Remove exit/cancel/processed transitions from execution machine. These
events now pass through unhandled and bubble to the parent, which
decides how to handle them. This makes the module composable -- each
parent can respond differently to these events.
- Remove submittedOverview/submittedReceipts states and breadcrumb nodes
from execution machine. These are post-execution viewing states that
belong at the parent level (original machine had parent: 'landing').
- Remove dead RUN_PAYROLL_SELECTED/REVIEW_PAYROLL payload types that
could never reach the execution machine.
PayrollExecutionFlow component changes:
- Add initialState prop ('configuration' | 'overview') so REVIEW_PAYROLL
can start the execution flow at overview instead of always starting at
configuration. This fixes a behavioral regression where reviewing an
already-calculated payroll would show the configuration screen.
- Fix prefixBreadcrumbs referential instability (new [] on every render)
with a module-level EMPTY_BREADCRUMBS constant.
PayrollFlow parent orchestrator changes:
- Add executionInitialState to context, set by RUN_PAYROLL_SELECTED
('configuration') and REVIEW_PAYROLL ('overview').
- Add BREADCRUMB_NAVIGATE handler in execution state so clicking the
Landing breadcrumb from within execution navigates back to landing.
- Add RUN_PAYROLL_PROCESSED handler in execution state -> submittedOverview.
- Add blockers state to landing machine (was a no-op transition before).
- Add submittedOverview and submittedReceipts states with correct
parent: 'landing' breadcrumb hierarchy.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move parent machine definition from PayrollFlow.tsx into payrollStateMachine.ts and extract shared transitions to reduce duplication. Extract PayrollExecutionFlowContextual into its own file to avoid a circular dependency between PayrollFlowComponents and PayrollExecutionFlow. Bug fixes: - Include payPeriod in RUN_PAYROLL_PROCESSED event from PayrollOverview so parent machine has dates for submitted breadcrumbs - Add RUN_PAYROLL_CANCELLED_ALERT_DISMISSED handler in parent landing state to clear stale alert flag - Align landing breadcrumb onNavigate with toLandingReducer to clear payrollUuid and executionInitialState on navigation - Add missing updateBreadcrumbs call in receiptGetTransition - Add RUN_PAYROLL_DATA_READY event so PayrollConfiguration can update breadcrumb date labels once pay period data loads Add state machine transition tests for both payrollFlowMachine (14 tests) and payrollExecutionMachine (15 tests). Co-authored-by: Cursor <cursoragent@cursor.com>
… ready event Move the RUN_PAYROLL_DATA_READY event emission from a useRef+useEffect pattern in PayrollConfiguration into an onPayPeriodReady callback in usePayrollConfigurationData's queryFn. This co-locates the notification with data fetching, matching the existing onDataReady pattern in usePreparedPayrollData. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
6b6dfcd to
cd2fd72
Compare

Summary
PayrollExecutionFlowcomponentpayrollStateMachine.tsto landing-only breadcrumbsPayrollFlowinto a lightweight orchestrator that delegates toPayrollExecutionFlowuseEmitOnDataReadyhook for flow data communicationRUN_PAYROLL_DATA_LOADEDevent constantThis is a pure refactor to make the execution flow reusable -- no new features. Enables off-cycle payroll support in follow-up PRs.
Test plan
PayrollFlowcorrectly transitions between landing and execution statesMade with Cursor