Skip to content

Commit 8ca2f3e

Browse files
committed
feat(SDK-561): wire up HolidaySelectionForm functional component
Move SelectHolidays logic into HolidaySelectionForm and rename presentation/types/tests/stories to follow SDK naming conventions (HolidaySelectionForm* pattern, co-located under HolidaySelectionForm/).
1 parent 4a973b7 commit 8ca2f3e

File tree

10 files changed

+245
-284
lines changed

10 files changed

+245
-284
lines changed

docs/reference/endpoint-inventory.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,17 @@
13151315
"companyUuid",
13161316
"timeOffPolicyUuid"
13171317
]
1318+
},
1319+
"UNSTABLE_TimeOff.HolidaySelectionForm": {
1320+
"endpoints": [
1321+
{
1322+
"method": "POST",
1323+
"path": "/v1/companies/:companyUuid/holiday_pay_policy"
1324+
}
1325+
],
1326+
"variables": [
1327+
"companyUuid"
1328+
]
13181329
}
13191330
},
13201331
"flows": {
@@ -2084,6 +2095,10 @@
20842095
"UNSTABLE_TimeOff.ViewPolicyEmployees"
20852096
],
20862097
"endpoints": [
2098+
{
2099+
"method": "POST",
2100+
"path": "/v1/companies/:companyUuid/holiday_pay_policy"
2101+
},
20872102
{
20882103
"method": "GET",
20892104
"path": "/v1/companies/:companyUuid/time_off_policies"

docs/reference/endpoint-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json'
248248
| **UNSTABLE_TimeOff.PolicyList** | GET | `/v1/companies/:companyUuid/time_off_policies` |
249249
| | PUT | `/v1/time_off_policies/:timeOffPolicyUuid/deactivate` |
250250
| | GET | `/v1/companies/:companyId/employees` |
251+
| **UNSTABLE_TimeOff.HolidaySelectionForm** | POST | `/v1/companies/:companyUuid/holiday_pay_policy` |
251252

252253
## Flows
253254

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { screen, waitFor } from '@testing-library/react'
3+
import userEvent from '@testing-library/user-event'
4+
import { http, HttpResponse } from 'msw'
5+
import { HolidaySelectionForm } from './HolidaySelectionForm'
6+
import { server } from '@/test/mocks/server'
7+
import { componentEvents } from '@/shared/constants'
8+
import { setupApiTestMocks } from '@/test/mocks/apiServer'
9+
import { renderWithProviders } from '@/test-utils/renderWithProviders'
10+
import { API_BASE_URL } from '@/test/constants'
11+
12+
describe('HolidaySelectionForm', () => {
13+
const onEvent = vi.fn()
14+
const user = userEvent.setup()
15+
const defaultProps = {
16+
companyId: 'company-123',
17+
onEvent,
18+
}
19+
20+
beforeEach(() => {
21+
setupApiTestMocks()
22+
onEvent.mockClear()
23+
})
24+
25+
describe('rendering', () => {
26+
it('renders the heading', async () => {
27+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
28+
29+
await waitFor(() => {
30+
expect(screen.getByText('Choose your company holidays')).toBeInTheDocument()
31+
})
32+
})
33+
34+
it('renders all 11 federal holidays', async () => {
35+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
36+
37+
await waitFor(() => {
38+
expect(screen.getByText("New Year's Day")).toBeInTheDocument()
39+
})
40+
expect(screen.getByText('Martin Luther King, Jr. Day')).toBeInTheDocument()
41+
expect(screen.getByText("Presidents' Day")).toBeInTheDocument()
42+
expect(screen.getByText('Memorial Day')).toBeInTheDocument()
43+
expect(screen.getByText('Juneteenth')).toBeInTheDocument()
44+
expect(screen.getByText('Independence Day')).toBeInTheDocument()
45+
expect(screen.getByText('Labor Day')).toBeInTheDocument()
46+
expect(screen.getByText("Columbus Day (Indigenous Peoples' Day)")).toBeInTheDocument()
47+
expect(screen.getByText('Veterans Day')).toBeInTheDocument()
48+
expect(screen.getByText('Thanksgiving')).toBeInTheDocument()
49+
expect(screen.getByText('Christmas Day')).toBeInTheDocument()
50+
})
51+
52+
it('renders observed dates', async () => {
53+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
54+
55+
await waitFor(() => {
56+
expect(screen.getByText('January 1')).toBeInTheDocument()
57+
})
58+
expect(screen.getByText('Third Monday in January')).toBeInTheDocument()
59+
expect(screen.getByText('December 25')).toBeInTheDocument()
60+
})
61+
62+
it('renders next observation dates with year', async () => {
63+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
64+
65+
await waitFor(() => {
66+
expect(screen.getByText("New Year's Day")).toBeInTheDocument()
67+
})
68+
69+
const container = screen.getByTestId('data-view')
70+
expect(container.textContent).toMatch(/\d{4}/)
71+
})
72+
73+
it('renders Back and Continue buttons', async () => {
74+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
75+
76+
await waitFor(() => {
77+
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument()
78+
})
79+
expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument()
80+
})
81+
82+
it('renders all holidays as selected by default', async () => {
83+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
84+
85+
await waitFor(() => {
86+
const checkboxes = screen.getAllByRole('checkbox')
87+
expect(checkboxes.length).toBeGreaterThanOrEqual(11)
88+
})
89+
})
90+
})
91+
92+
describe('selection', () => {
93+
it('can deselect a holiday', async () => {
94+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
95+
96+
await waitFor(() => {
97+
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0)
98+
})
99+
100+
const checkboxes = screen.getAllByRole('checkbox')
101+
await user.click(checkboxes[1]!)
102+
103+
expect(checkboxes[1]).not.toBeChecked()
104+
})
105+
})
106+
107+
describe('actions', () => {
108+
it('calls CANCEL event when Back button is clicked', async () => {
109+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
110+
111+
await waitFor(() => {
112+
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument()
113+
})
114+
115+
await user.click(screen.getByRole('button', { name: 'Back' }))
116+
117+
expect(onEvent).toHaveBeenCalledWith(componentEvents.CANCEL)
118+
})
119+
120+
it('calls POST and emits HOLIDAY_SELECTION_DONE on Continue', async () => {
121+
let postCalled = false
122+
server.use(
123+
http.post(`${API_BASE_URL}/v1/companies/:companyUuid/holiday_pay_policy`, () => {
124+
postCalled = true
125+
return HttpResponse.json({
126+
version: 'abc123',
127+
company_uuid: 'company-123',
128+
federal_holidays: {},
129+
employees: [],
130+
})
131+
}),
132+
)
133+
134+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
135+
136+
await waitFor(() => {
137+
expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument()
138+
})
139+
140+
await user.click(screen.getByRole('button', { name: 'Continue' }))
141+
142+
await waitFor(() => {
143+
expect(onEvent).toHaveBeenCalledWith(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
144+
})
145+
146+
expect(postCalled).toBe(true)
147+
})
148+
149+
it('emits HOLIDAY_SELECTION_DONE after deselecting a holiday and clicking Continue', async () => {
150+
server.use(
151+
http.post(`${API_BASE_URL}/v1/companies/:companyUuid/holiday_pay_policy`, () => {
152+
return HttpResponse.json({
153+
version: 'abc123',
154+
company_uuid: 'company-123',
155+
federal_holidays: {},
156+
employees: [],
157+
})
158+
}),
159+
)
160+
161+
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)
162+
163+
await waitFor(() => {
164+
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0)
165+
})
166+
167+
const checkboxes = screen.getAllByRole('checkbox')
168+
await user.click(checkboxes[1]!)
169+
170+
await user.click(screen.getByRole('button', { name: 'Continue' }))
171+
172+
await waitFor(() => {
173+
expect(onEvent).toHaveBeenCalledWith(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
174+
})
175+
})
176+
})
177+
})

src/components/UNSTABLE_TimeOff/HolidaySelectionForm/HolidaySelectionForm.tsx

Lines changed: 35 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
import { useCallback, useState } from 'react'
2-
import { SelectHolidaysPresentation } from '../TimeOffManagement/SelectHolidays/SelectHolidaysPresentation'
3-
import type { HolidayItem } from '../TimeOffManagement/SelectHolidays/SelectHolidaysTypes'
1+
import { useCallback, useMemo, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { useHolidayPayPoliciesCreateMutation } from '@gusto/embedded-api/react-query/holidayPayPoliciesCreate'
4+
import {
5+
getDefaultHolidayItems,
6+
buildFederalHolidaysPayload,
7+
FEDERAL_HOLIDAY_KEYS,
8+
} from '../shared/holidayHelpers'
9+
import { HolidaySelectionFormPresentation } from './HolidaySelectionFormPresentation'
10+
import type { HolidayItem } from './HolidaySelectionFormTypes'
411
import { BaseComponent, type BaseComponentInterface } from '@/components/Base'
512
import { useBase } from '@/components/Base/useBase'
613
import { componentEvents } from '@/shared/constants'
14+
import { useI18n } from '@/i18n'
715

816
export interface HolidaySelectionFormProps extends BaseComponentInterface {
917
companyId: string
@@ -12,70 +20,21 @@ export interface HolidaySelectionFormProps extends BaseComponentInterface {
1220
export function HolidaySelectionForm(props: HolidaySelectionFormProps) {
1321
return (
1422
<BaseComponent {...props}>
15-
<Root />
23+
<Root companyId={props.companyId} />
1624
</BaseComponent>
1725
)
1826
}
1927

20-
const DEFAULT_HOLIDAYS: HolidayItem[] = [
21-
{ uuid: 'new-years', name: "New Year's Day", observedDate: 'January 1', nextObservation: '' },
22-
{
23-
uuid: 'mlk',
24-
name: 'Martin Luther King, Jr. Day',
25-
observedDate: 'Third Monday in January',
26-
nextObservation: '',
27-
},
28-
{
29-
uuid: 'presidents',
30-
name: "Presidents' Day",
31-
observedDate: 'Third Monday in February',
32-
nextObservation: '',
33-
},
34-
{
35-
uuid: 'memorial',
36-
name: 'Memorial Day',
37-
observedDate: 'Last Monday in May',
38-
nextObservation: '',
39-
},
40-
{ uuid: 'juneteenth', name: 'Juneteenth', observedDate: 'June 19', nextObservation: '' },
41-
{
42-
uuid: 'independence',
43-
name: 'Independence Day',
44-
observedDate: 'July 4',
45-
nextObservation: '',
46-
},
47-
{
48-
uuid: 'labor',
49-
name: 'Labor Day',
50-
observedDate: 'First Monday in September',
51-
nextObservation: '',
52-
},
53-
{
54-
uuid: 'columbus',
55-
name: "Columbus Day (Indigenous Peoples' Day)",
56-
observedDate: 'Second Monday in October',
57-
nextObservation: '',
58-
},
59-
{ uuid: 'veterans', name: 'Veterans Day', observedDate: 'November 11', nextObservation: '' },
60-
{
61-
uuid: 'thanksgiving',
62-
name: 'Thanksgiving',
63-
observedDate: 'Fourth Thursday in November',
64-
nextObservation: '',
65-
},
66-
{
67-
uuid: 'christmas',
68-
name: 'Christmas Day',
69-
observedDate: 'December 25',
70-
nextObservation: '',
71-
},
72-
]
28+
function Root({ companyId }: { companyId: string }) {
29+
useI18n('Company.TimeOff.HolidayPolicy')
30+
const { t } = useTranslation('Company.TimeOff.HolidayPolicy')
31+
const { onEvent, baseSubmitHandler } = useBase()
7332

74-
const ALL_HOLIDAY_UUIDS = new Set(DEFAULT_HOLIDAYS.map(h => h.uuid))
33+
const holidays = useMemo(() => getDefaultHolidayItems(t), [t])
34+
const allKeys = useMemo(() => new Set(FEDERAL_HOLIDAY_KEYS), [])
35+
const [selectedUuids, setSelectedUuids] = useState(allKeys)
7536

76-
function Root() {
77-
const { onEvent } = useBase()
78-
const [selectedUuids, setSelectedUuids] = useState(ALL_HOLIDAY_UUIDS)
37+
const { mutateAsync: createPolicy } = useHolidayPayPoliciesCreateMutation()
7938

8039
const handleSelectionChange = useCallback((item: HolidayItem, selected: boolean) => {
8140
setSelectedUuids(prev => {
@@ -89,17 +48,27 @@ function Root() {
8948
})
9049
}, [])
9150

92-
const handleContinue = useCallback(() => {
93-
onEvent(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
94-
}, [onEvent])
51+
const handleContinue = useCallback(async () => {
52+
await baseSubmitHandler({}, async () => {
53+
await createPolicy({
54+
request: {
55+
companyUuid: companyId,
56+
holidayPayPolicyRequest: {
57+
federalHolidays: buildFederalHolidaysPayload(selectedUuids),
58+
},
59+
},
60+
})
61+
onEvent(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
62+
})
63+
}, [baseSubmitHandler, createPolicy, companyId, selectedUuids, onEvent])
9564

9665
const handleBack = useCallback(() => {
9766
onEvent(componentEvents.CANCEL)
9867
}, [onEvent])
9968

10069
return (
101-
<SelectHolidaysPresentation
102-
holidays={DEFAULT_HOLIDAYS}
70+
<HolidaySelectionFormPresentation
71+
holidays={holidays}
10372
selectedHolidayUuids={selectedUuids}
10473
onSelectionChange={handleSelectionChange}
10574
onContinue={handleContinue}

src/components/UNSTABLE_TimeOff/TimeOffManagement/SelectHolidays/SelectHolidays.stories.tsx renamed to src/components/UNSTABLE_TimeOff/HolidaySelectionForm/HolidaySelectionFormPresentation.stories.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState } from 'react'
22
import { fn } from 'storybook/test'
3-
import { SelectHolidaysPresentation } from './SelectHolidaysPresentation'
4-
import type { HolidayItem } from './SelectHolidaysTypes'
3+
import { HolidaySelectionFormPresentation } from './HolidaySelectionFormPresentation'
4+
import type { HolidayItem } from './HolidaySelectionFormTypes'
55
import { useI18n } from '@/i18n'
66

77
function I18nLoader({ children }: { children: React.ReactNode }) {
@@ -10,7 +10,7 @@ function I18nLoader({ children }: { children: React.ReactNode }) {
1010
}
1111

1212
export default {
13-
title: 'TimeOff/SelectHolidays',
13+
title: 'TimeOff/HolidaySelectionForm',
1414
decorators: [
1515
(Story: React.ComponentType) => (
1616
<I18nLoader>
@@ -111,7 +111,7 @@ function StoryWrapper({ initialSelected }: { initialSelected: Set<string> }) {
111111
}
112112

113113
return (
114-
<SelectHolidaysPresentation
114+
<HolidaySelectionFormPresentation
115115
holidays={holidays}
116116
selectedHolidayUuids={selectedUuids}
117117
onSelectionChange={handleSelectionChange}
@@ -127,4 +127,4 @@ export const PartialSelection = () => <StoryWrapper initialSelected={partialUuid
127127

128128
export const EditMode = () => <StoryWrapper initialSelected={new Set()} />
129129

130-
export const ViewMode = () => <SelectHolidaysPresentation mode="view" holidays={holidays} />
130+
export const ViewMode = () => <HolidaySelectionFormPresentation mode="view" holidays={holidays} />

src/components/UNSTABLE_TimeOff/TimeOffManagement/SelectHolidays/SelectHolidaysPresentation.tsx renamed to src/components/UNSTABLE_TimeOff/HolidaySelectionForm/HolidaySelectionFormPresentation.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { useMemo } from 'react'
22
import { useTranslation } from 'react-i18next'
3-
import type { HolidayItem, SelectHolidaysPresentationProps } from './SelectHolidaysTypes'
3+
import type {
4+
HolidayItem,
5+
HolidaySelectionFormPresentationProps,
6+
} from './HolidaySelectionFormTypes'
47
import { DataView, Flex, ActionsLayout, useDataView } from '@/components/Common'
58
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
69
import { useI18n } from '@/i18n'
710

8-
export function SelectHolidaysPresentation(props: SelectHolidaysPresentationProps) {
11+
export function HolidaySelectionFormPresentation(props: HolidaySelectionFormPresentationProps) {
912
useI18n('Company.TimeOff.HolidayPolicy')
1013
const { t } = useTranslation('Company.TimeOff.HolidayPolicy')
1114
const { Heading, Text, Button } = useComponentContext()

0 commit comments

Comments
 (0)