-
Notifications
You must be signed in to change notification settings - Fork 1
Introduce PayrollLanding component as first step in RunPayrollFlow #586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
<Flex flexDirection="column" alignItems="center" gap={24}> | ||
<div className={styles.doneIcon}>✓</div> | ||
<Text>{t('emptyState')}</Text> | ||
<Flex flexDirection="column" gap={16}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapped this in the same flex layout I've been using in Payroll History for consistent spacing
@@ -0,0 +1,18 @@ | |||
import { I18nextProvider } from 'react-i18next' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seemed like the best way to bring app i18n context to a story that had compound i18n dictionaries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way we can set this on all stories? i thought we already had that configured somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a centralized Payroll Landing component that consolidates the Payroll History and Run Payroll flows into a tabbed navigation interface. It also fixes type safety issues in the PayrollListPresentation
component where nullable date fields from the embedded API model were being passed to functions expecting non-null strings.
- Centralizes payroll flows into a new tabbed interface structure
- Resolves type errors by adding null checks before date parsing operations
- Updates component exports and entry points to use the new landing component
Reviewed Changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
src/i18n/en/Payroll.PayrollList.json | Adds "title" translation for upcoming payroll section |
src/i18n/en/Payroll.PayrollLanding.json | New translation file for tab labels and accessibility text |
src/components/Payroll/index.ts | Exports the new PayrollLanding component |
src/components/Payroll/RunPayrollFlow/RunPayrollFlow.tsx | Simplifies to use PayrollLanding instead of direct RunPayroll |
src/components/Payroll/PayrollList/PayrollListPresentation.tsx | Fixes type safety with null checks for date parsing |
src/components/Payroll/PayrollLanding/PayrollLanding.tsx | New tabbed interface component for payroll flows |
src/components/Payroll/PayrollLanding/PayrollLanding.test.tsx | Comprehensive test coverage for the new component |
src/components/Payroll/PayrollLanding/PayrollLanding.stories.tsx | Storybook implementation for design system integration |
src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx | Minor layout alignment adjustment |
src/components/Payroll/PayrollHistory/PayrollHistory.stories.tsx | Adds I18n wrapper for proper localization |
.ladle/helpers/I18nWrapper.tsx | New helper component for storybook internationalization |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! biggest thing is replacing RunPayroll
in PayrollLanding
with the PayrollList
component and then replacing PayrollList
in RunPayroll
with the PayrollLanding
component
@@ -0,0 +1,18 @@ | |||
import { I18nextProvider } from 'react-i18next' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way we can set this on all stories? i thought we already had that configured somewhere?
const PayrollLandingStoryContent = () => { | ||
const [selectedTab, setSelectedTab] = useState('run-payroll') | ||
const { Tabs } = useComponentContext() | ||
|
||
const tabs = [ | ||
{ | ||
id: 'run-payroll', | ||
label: 'Run payroll', | ||
content: ( | ||
<PayrollListPresentation | ||
payrolls={mockPayrolls} | ||
paySchedules={mockPaySchedules} | ||
onRunPayroll={action('onRunPayroll')} | ||
/> | ||
), | ||
}, | ||
{ | ||
id: 'payroll-history', | ||
label: 'Payroll history', | ||
content: ( | ||
<PayrollHistoryPresentation | ||
payrollHistory={mockPayrollHistory} | ||
selectedTimeFilter="3months" | ||
onTimeFilterChange={action('onTimeFilterChange')} | ||
onViewSummary={action('onViewSummary')} | ||
onViewReceipt={action('onViewReceipt')} | ||
onCancelPayroll={action('onCancelPayroll')} | ||
/> | ||
), | ||
}, | ||
] | ||
|
||
return ( | ||
<Tabs | ||
tabs={tabs} | ||
selectedId={selectedTab} | ||
onSelectionChange={setSelectedTab} | ||
aria-label="Payroll navigation" | ||
/> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this should actually be importing the PayrollLanding
component instead of recreating the tabs here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can nuke this, this was an attempt at mocking since we can't run live code in ladle
Would you prefer I rename it to 'mock' and we keep it OR should we 🪓
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just remove or bring in the actual component!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went the removal path :)
// Mock the child components | ||
vi.mock('../RunPayrollFlow/RunPayroll', () => ({ | ||
RunPayroll: ({ | ||
companyId, | ||
onEvent, | ||
}: { | ||
companyId: string | ||
onEvent: (event: string, payload: unknown) => void | ||
}) => ( | ||
<div data-testid="run-payroll"> | ||
<h2>Run Payroll</h2> | ||
<p>Company ID: {companyId}</p> | ||
<button | ||
onClick={() => { | ||
onEvent('test-event', {}) | ||
}} | ||
> | ||
Test Event | ||
</button> | ||
</div> | ||
), | ||
})) | ||
|
||
vi.mock('../PayrollHistory/PayrollHistory', () => ({ | ||
PayrollHistory: ({ | ||
companyId, | ||
onEvent, | ||
}: { | ||
companyId: string | ||
onEvent: (event: string, payload: unknown) => void | ||
}) => ( | ||
<div data-testid="payroll-history"> | ||
<h2>Payroll History</h2> | ||
<p>Company ID: {companyId}</p> | ||
<button | ||
onClick={() => { | ||
onEvent('test-event', {}) | ||
}} | ||
> | ||
Test Event | ||
</button> | ||
</div> | ||
), | ||
})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it preferable to mock these? i wonder if it would be more aligned with our testing approach to configure the test fixtures so they could get mocked with msw
describe('internationalization', () => { | ||
it('uses correct translation keys for tab labels', async () => { | ||
renderWithProviders(<PayrollLanding {...defaultProps} />) | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('run-payroll')).toBeInTheDocument() | ||
}) | ||
|
||
// Check that translated tab labels are used | ||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument() | ||
expect(screen.getByRole('tab', { name: 'Payroll history' })).toBeInTheDocument() | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this kinda gets tested implicitly already with the above since we query by these to select the tabs
<RunPayroll | ||
companyId={companyId} | ||
Configuration={PayrollConfiguration} | ||
List={PayrollList} | ||
Overview={PayrollOverview} | ||
EditEmployee={PayrollEditEmployee} | ||
onEvent={onEvent} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to cause an issue where the entire payroll experience is in a single tab. We actually just want to render PayrollList
for the content here.
Then, we would update the RunPayroll component to use this PayrollLanding
component instead. This will emit events to RunPayroll which can manage transitions between the blocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! left some optional follow ups
describe('event handling', () => { | ||
it('renders child components with proper props', async () => { | ||
renderWithProviders(<PayrollLanding {...defaultProps} />) | ||
|
||
await waitFor(() => { | ||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument() | ||
}) | ||
|
||
// Verify PayrollList is rendered with data | ||
await waitFor(() => { | ||
expect(screen.getByRole('heading', { name: 'Upcoming payroll' })).toBeInTheDocument() | ||
}) | ||
|
||
// Verify both tabs are available, indicating child components are receiving proper props | ||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument() | ||
expect(screen.getByRole('tab', { name: 'Payroll history' })).toBeInTheDocument() | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this says event handling and then doesn't actually verify events? current test looks redundant here?
it('switches to payroll history component when tab is clicked', async () => { | ||
const user = userEvent.setup() | ||
renderWithProviders(<PayrollLanding {...defaultProps} />) | ||
|
||
// Wait for initial render | ||
await waitFor(() => { | ||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument() | ||
}) | ||
|
||
// Click on payroll history tab | ||
const payrollHistoryTab = screen.getByRole('tab', { name: 'Payroll history' }) | ||
await user.click(payrollHistoryTab) | ||
|
||
// Check that payroll history tab is now selected | ||
await waitFor(() => { | ||
expect(screen.getByRole('tab', { name: 'Payroll history' })).toHaveAttribute( | ||
'aria-selected', | ||
'true', | ||
) | ||
}) | ||
|
||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toHaveAttribute( | ||
'aria-selected', | ||
'false', | ||
) | ||
|
||
// Verify PayrollHistory component content is now visible | ||
await waitFor(() => { | ||
expect(screen.getByRole('heading', { name: 'Payroll history' })).toBeInTheDocument() | ||
}) | ||
expect(screen.queryByRole('heading', { name: 'Run payroll' })).not.toBeInTheDocument() | ||
}) | ||
|
||
it('switches back to run payroll component when tab is clicked', async () => { | ||
const user = userEvent.setup() | ||
renderWithProviders(<PayrollLanding {...defaultProps} />) | ||
|
||
// Wait for initial render | ||
await waitFor(() => { | ||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument() | ||
}) | ||
|
||
// Switch to payroll history tab | ||
const payrollHistoryTab = screen.getByRole('tab', { name: 'Payroll history' }) | ||
await user.click(payrollHistoryTab) | ||
|
||
await waitFor(() => { | ||
expect(screen.getByRole('tab', { name: 'Payroll history' })).toHaveAttribute( | ||
'aria-selected', | ||
'true', | ||
) | ||
}) | ||
|
||
// Switch back to run payroll tab | ||
const runPayrollTab = screen.getByRole('tab', { name: 'Run payroll' }) | ||
await user.click(runPayrollTab) | ||
|
||
// Check that run payroll tab is selected again | ||
await waitFor(() => { | ||
expect(screen.getByRole('tab', { name: 'Run payroll' })).toHaveAttribute( | ||
'aria-selected', | ||
'true', | ||
) | ||
}) | ||
|
||
expect(screen.getByRole('tab', { name: 'Payroll history' })).toHaveAttribute( | ||
'aria-selected', | ||
'false', | ||
) | ||
|
||
// Verify PayrollList component content is visible again | ||
await waitFor(() => { | ||
expect(screen.getByRole('heading', { name: 'Upcoming payroll' })).toBeInTheDocument() | ||
}) | ||
expect(screen.queryByRole('heading', { name: 'Payroll history' })).not.toBeInTheDocument() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ends up being a pretty large couple of tests to verify tab clicking behavior. is it sufficient to have this covered by a single test that verifies the navigation?
companyId, | ||
Configuration, | ||
Landing, | ||
List, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we should be able to remove List from the component now since that is no longer being used?
- Add PayrollLanding component with RunPayroll and PayrollHistory tabs - Fix TypeScript interface to include resource key generic - Add comprehensive unit tests focused on component orchestration - Add translation support with Payroll.PayrollLanding namespace - Fix ESLint import order in PayrollHistory.stories.tsx - Fix failing test in PayrollHistoryPresentation with waitFor - Add demo story for PayrollLanding (uses presentation components directly) - Update PayrollLanding to use proper BaseComponentInterface typing
967ce6e
to
4603a73
Compare
Summary
Introduces a Payroll Landing component that centralizes Payroll History and Run Payroll flows inside tabbed navigation.
Also resolves type errors in the
PayrollListPresentation
component.These errors occurred because the embedded API model now exposes nullable date fields, but
parseDateStringToLocal
expects string parameters.Changes
Screenshots