diff --git a/src/components/UNSTABLE_TimeOff/PolicyList/PolicyList.tsx b/src/components/UNSTABLE_TimeOff/PolicyList/PolicyList.tsx index 86c86c6dd..10fd94a9d 100644 --- a/src/components/UNSTABLE_TimeOff/PolicyList/PolicyList.tsx +++ b/src/components/UNSTABLE_TimeOff/PolicyList/PolicyList.tsx @@ -12,7 +12,6 @@ import { invalidateAllHolidayPayPoliciesGet, } from '@gusto/embedded-api/react-query/holidayPayPoliciesGet' import { useHolidayPayPoliciesDeleteMutation } from '@gusto/embedded-api/react-query/holidayPayPoliciesDelete' -import { GustoEmbeddedError } from '@gusto/embedded-api/models/errors/gustoembeddederror' import type { TimeOffPolicy } from '@gusto/embedded-api/models/components/timeoffpolicy' import { PolicyListPresentation } from './PolicyListPresentation' import type { PolicyListItem } from './PolicyListTypes' @@ -56,18 +55,13 @@ function Root({ companyId, onEvent }: PolicyListProps) { }) const timeOffPolicies = (policiesData.timeOffPolicies ?? []).filter(policy => policy.isActive) + // Holiday pay policy is auxiliary to the main time-off list; never crash the + // boundary on its failure. composeErrorHandler below surfaces the error as + // an inline alert via BaseLayout when it isn't an expected 204/404. const holidayQuery = useHolidayPayPoliciesGet( { companyUuid: companyId }, { - throwOnError: (error: Error) => { - if (error instanceof GustoEmbeddedError) { - const status = error.httpMeta.response.status - if (status === 204 || status === 404) { - return false - } - } - return true - }, + throwOnError: () => false, }, ) const holidayPayPolicy = holidayQuery.data?.holidayPayPolicy diff --git a/src/components/UNSTABLE_TimeOff/PolicyList/PolicyListPresentation.tsx b/src/components/UNSTABLE_TimeOff/PolicyList/PolicyListPresentation.tsx index 46e5d493d..e52fd16d7 100644 --- a/src/components/UNSTABLE_TimeOff/PolicyList/PolicyListPresentation.tsx +++ b/src/components/UNSTABLE_TimeOff/PolicyList/PolicyListPresentation.tsx @@ -12,6 +12,7 @@ import { } from '@/components/Common' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' import { useI18n } from '@/i18n' +import { isEditableTimeOffPolicyType } from '@/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffPolicyTypes' export function PolicyListPresentation({ policies, @@ -67,10 +68,35 @@ export function PolicyListPresentation({ ], itemMenu: (policy: PolicyListItem) => { const isDeleting = isDeletingPolicyId === policy.uuid + const isEditable = isEditableTimeOffPolicyType(policy.policyType) + + const menuItems = isEditable + ? [ + { + label: t('actions.editPolicy'), + onClick: () => { + onEditPolicy(policy) + }, + }, + { + label: t('actions.deletePolicy'), + onClick: () => { + handleOpenDeleteDialog(policy) + }, + }, + ] + : [ + { + label: t('actions.viewPolicy'), + onClick: () => { + onEditPolicy(policy) + }, + }, + ] return (
- {!policy.isComplete && ( + {isEditable && !policy.isComplete && ( )} - { - onEditPolicy(policy) - }, - }, - { - label: t('actions.deletePolicy'), - onClick: () => { - handleOpenDeleteDialog(policy) - }, - }, - ]} - /> +
) }, diff --git a/src/components/UNSTABLE_TimeOff/TimeOffFlow/TimeOffFlowComponents.tsx b/src/components/UNSTABLE_TimeOff/TimeOffFlow/TimeOffFlowComponents.tsx index 2ae0b5ab5..e8bf26867 100644 --- a/src/components/UNSTABLE_TimeOff/TimeOffFlow/TimeOffFlowComponents.tsx +++ b/src/components/UNSTABLE_TimeOff/TimeOffFlow/TimeOffFlowComponents.tsx @@ -53,6 +53,11 @@ export function SelectPolicyTypeContextual() { const { onEvent, companyId, policyType, alerts } = useFlow() const { Alert } = useComponentContext() + const selectorDefault = + policyType === 'sick' || policyType === 'vacation' || policyType === 'holiday' + ? policyType + : undefined + return ( {alerts?.map((alert, index) => ( @@ -63,7 +68,7 @@ export function SelectPolicyTypeContextual() { ) diff --git a/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffPolicyTypes.ts b/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffPolicyTypes.ts index 0de31e60a..d47f8c7a7 100644 --- a/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffPolicyTypes.ts +++ b/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffPolicyTypes.ts @@ -5,7 +5,17 @@ import type { PolicyType } from '@gusto/embedded-api/models/components/timeoffpo // Holiday is a distinct concept routed through @gusto/embedded-api's // holidayPayPolicies* hooks against a different endpoint family. export type CreatableTimeOffPolicyType = Extract -export type TimeOffPolicyType = CreatableTimeOffPolicyType | 'holiday' +export type TimeOffPolicyType = PolicyType | 'holiday' + +export const EDITABLE_TIME_OFF_POLICY_TYPES = ['sick', 'vacation', 'holiday'] as const + +export type EditableTimeOffPolicyType = (typeof EDITABLE_TIME_OFF_POLICY_TYPES)[number] + +export function isEditableTimeOffPolicyType( + policyType: string | null | undefined, +): policyType is EditableTimeOffPolicyType { + return EDITABLE_TIME_OFF_POLICY_TYPES.includes(policyType as EditableTimeOffPolicyType) +} export function assertCreatablePolicyType( policyType: TimeOffPolicyType, diff --git a/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.test.ts b/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.test.ts index ecd782daa..dbd8f0bfb 100644 --- a/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.test.ts +++ b/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.test.ts @@ -118,6 +118,22 @@ describe('timeOffStateMachine', () => { expect(service.context.policyType).toBe('holiday') expect(service.context.alerts).toBeUndefined() }) + + it.each(['custom', 'bereavement', 'parental_leave', 'jury_duty', 'volunteer'])( + 'transitions to viewTimeOffPolicyDetail on TIME_OFF_VIEW_POLICY with non-holiday type %s', + policyType => { + const service = createService() + + send(service, componentEvents.TIME_OFF_VIEW_POLICY, { + policyId: `policy-${policyType}`, + policyType, + }) + + expect(service.machine.current).toBe('viewTimeOffPolicyDetail') + expect(service.context.policyId).toBe(`policy-${policyType}`) + expect(service.context.policyType).toBe(policyType) + }, + ) }) describe('policyTypeSelector state', () => { diff --git a/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.ts b/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.ts index 57480983e..67dcc8af8 100644 --- a/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.ts +++ b/src/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffStateMachine.ts @@ -13,13 +13,14 @@ import { type TimeOffFlowContextInterface, type TimeOffFlowAlert, } from './TimeOffFlowComponents' +import type { TimeOffPolicyType } from './timeOffPolicyTypes' import { componentEvents } from '@/shared/constants' import type { MachineTransition } from '@/types/Helpers' type PolicyTypePayload = { policyType: 'sick' | 'vacation' | 'holiday' } type PolicyCreatedPayload = { policyId: string; accrualMethod?: string } type ErrorPayload = { alert?: TimeOffFlowAlert } -type ViewPolicyPayload = { policyId: string; policyType: 'sick' | 'vacation' | 'holiday' } +type ViewPolicyPayload = { policyId: string; policyType: TimeOffPolicyType } type PolicyIdPayload = { policyId: string } function isSickOrVacation(_ctx: TimeOffFlowContextInterface, ev: { payload: PolicyTypePayload }) { @@ -30,11 +31,8 @@ function isHoliday(_ctx: TimeOffFlowContextInterface, ev: { payload: PolicyTypeP return ev.payload.policyType === 'holiday' } -function isSickOrVacationView( - _ctx: TimeOffFlowContextInterface, - ev: { payload: ViewPolicyPayload }, -) { - return ev.payload.policyType === 'sick' || ev.payload.policyType === 'vacation' +function isNonHolidayView(_ctx: TimeOffFlowContextInterface, ev: { payload: ViewPolicyPayload }) { + return ev.payload.policyType !== 'holiday' } function isHolidayView(_ctx: TimeOffFlowContextInterface, ev: { payload: ViewPolicyPayload }) { @@ -88,7 +86,7 @@ export const timeOffMachine = { transition( componentEvents.TIME_OFF_VIEW_POLICY, 'viewTimeOffPolicyDetail', - guard(isSickOrVacationView), + guard(isNonHolidayView), reduce( ( ctx: TimeOffFlowContextInterface, diff --git a/src/components/UNSTABLE_TimeOff/TimeOffPolicyDetail/TimeOffPolicyDetail.tsx b/src/components/UNSTABLE_TimeOff/TimeOffPolicyDetail/TimeOffPolicyDetail.tsx index fe6b840cd..0876e1f29 100644 --- a/src/components/UNSTABLE_TimeOff/TimeOffPolicyDetail/TimeOffPolicyDetail.tsx +++ b/src/components/UNSTABLE_TimeOff/TimeOffPolicyDetail/TimeOffPolicyDetail.tsx @@ -21,6 +21,7 @@ import { useBase } from '@/components/Base/useBase' import { componentEvents } from '@/shared/constants' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' import { useI18n } from '@/i18n' +import { isEditableTimeOffPolicyType } from '@/components/UNSTABLE_TimeOff/TimeOffFlow/timeOffPolicyTypes' import EditIcon from '@/assets/icons/edit-02.svg?react' import TrashCanSvg from '@/assets/icons/trashcan.svg?react' import PlusCircleIcon from '@/assets/icons/plus-circle.svg?react' @@ -143,6 +144,8 @@ function Root({ policyId }: TimeOffPolicyDetailProps) { const policy = policyResponse.timeOffPolicy if (!policy) throw new Error('Unexpected response: missing timeOffPolicy') + const isEditable = isEditableTimeOffPolicyType(policy.policyType) + const { data: employeesData } = useEmployeesListSuspense({ companyId: policy.companyUuid, terminated: false, @@ -265,8 +268,9 @@ function Root({ policyId }: TimeOffPolicyDetailProps) { [selectedEmployeeUuids], ) - const actions = useMemo( - () => [ + const actions = useMemo(() => { + if (!isEditable) return undefined + return [ , - ], - [Button, onEvent, policyId, t], - ) + ] + }, [Button, onEvent, policyId, t, isEditable]) const itemMenu = useCallback( (employee: TimeOffPolicyDetailEmployee) => { @@ -345,9 +348,11 @@ function Root({ policyId }: TimeOffPolicyDetailProps) { : { policyDetails, policySettings: policySettings!, - onChangeSettings: () => { - onEvent(componentEvents.TIME_OFF_CHANGE_SETTINGS, { policyId }) - }, + onChangeSettings: isEditable + ? () => { + onEvent(componentEvents.TIME_OFF_CHANGE_SETTINGS, { policyId }) + } + : undefined, } return ( @@ -370,12 +375,16 @@ function Root({ policyId }: TimeOffPolicyDetailProps) { onSearchClear: () => { setSearchValue('') }, - itemMenu, - selectionMode: 'multiple', - onSelect: handleSelect, - onSelectAll: handleSelectAll, - getIsItemSelected, - footer, + ...(isEditable + ? { + itemMenu, + selectionMode: 'multiple', + onSelect: handleSelect, + onSelectAll: handleSelectAll, + getIsItemSelected, + footer, + } + : {}), }} removeDialog={{ isOpen: removeTarget !== null, diff --git a/src/i18n/en/Company.TimeOff.TimeOffPolicies.json b/src/i18n/en/Company.TimeOff.TimeOffPolicies.json index 877c0d77a..006a1befa 100644 --- a/src/i18n/en/Company.TimeOff.TimeOffPolicies.json +++ b/src/i18n/en/Company.TimeOff.TimeOffPolicies.json @@ -8,6 +8,7 @@ "tableLabel": "Time off policies", "actions": { "editPolicy": "Edit policy", + "viewPolicy": "View policy", "deletePolicy": "Delete policy" }, "finishSetupCta": "Finish setup", diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index f2d037b0d..f21bc1bea 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -615,6 +615,7 @@ export interface CompanyTimeOffTimeOffPolicies{ "tableLabel":string; "actions":{ "editPolicy":string; +"viewPolicy":string; "deletePolicy":string; }; "finishSetupCta":string;