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;