diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index ec9b8bde2..701981631 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -4376,6 +4376,7 @@ interface PayrollOverviewProps extends BaseComponentInterface<'Payroll.PayrollOv companyId: string; ConfirmWireDetailsComponent?: ConfirmWireDetailsComponentType; payrollId: string; + readOnly?: boolean; withReimbursements?: boolean; } diff --git a/docs/reference/payroll/blocks.md b/docs/reference/payroll/blocks.md index 7837a46ad..7075c17f7 100644 --- a/docs/reference/payroll/blocks.md +++ b/docs/reference/payroll/blocks.md @@ -637,7 +637,8 @@ The payroll referenced by `payrollId` must already be calculated; rendering with uncalculated payroll throws. Unresolved submission blockers (e.g. fast-ACH threshold, wire-in funding) are surfaced inline and the submit action stays disabled until each blocker has a selected unblock option. While the payroll is processing, the component -polls until success or failure and emits the corresponding event. +polls until success or failure and emits the corresponding event. Pass `readOnly` to hide +the edit and cancel actions while keeping submit available.
@@ -655,6 +656,7 @@ Props for [PayrollOverview](#payrolloverview). | `alerts?` | [`PayrollFlowAlert`](#payrollflowalert)[] | Alert banners to display above the payroll summary. | | `ConfirmWireDetailsComponent?` | [`ConfirmWireDetailsComponentType`](#confirmwiredetailscomponenttype) | Custom component to replace the default wire details confirmation UI. | | `dictionary?` | `Record`\<`"en"`, [`DeepPartial`](../Translations/index.md#deeppartial)\<[`PayrollPayrollOverview`](../Translations/index.md#payrollpayrolloverview)\>\> | Overrides for the component's i18n strings. Supply a partial object whose keys match the component's resource namespace — any omitted keys fall back to SDK defaults. See the [Translation guide](https://docs.gusto.com/embedded-payroll/docs/translation) for details. | +| `readOnly?` | `boolean` | Hides the edit and cancel actions, leaving submit and receipt/paystub actions available. Use for a deep link to a specific payroll where editing shouldn't be offered. Defaults to `false`. | | `withReimbursements?` | `boolean` | Whether reimbursement fields are shown in the totals and per-employee tables. Defaults to `true`. | _Inherits `children`, `className`, `defaultValues`, `FallbackComponent`, `LoaderComponent` from [BaseComponentInterface](../blocks.md#basecomponentinterface)._ diff --git a/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx b/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx index 6391e6eea..b834e4d77 100644 --- a/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx +++ b/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx @@ -1109,6 +1109,100 @@ export const PayrollOverviewStory = () => { ) } +export const PayrollOverviewReadOnly = () => { + return ( + + ) +} + export const PayrollOverviewWithWireFunds = () => { return ( ({ payrollsGetPayStub: vi.fn(), })) +vi.mock('../helpers', async importOriginal => { + const actual = await importOriginal() + return { + ...(actual as Record), + canCancelPayroll: vi.fn(), + } +}) + describe('PayrollOverview polling', () => { const mockOnEvent = vi.fn() @@ -369,3 +378,62 @@ describe('PayrollOverview print checks modal', () => { expect(await screen.findByText('Choose check stock')).toBeInTheDocument() }) }) + +describe('PayrollOverview readOnly mode', () => { + const mockOnEvent = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + mockPayrollData = { ...basePayrollData } + mockIsFetching = false + vi.mocked(canCancelPayroll).mockReturnValue(false) + }) + + it('hides Edit but keeps Submit enabled and functional on an unprocessed payroll', async () => { + const user = userEvent.setup() + mockSubmitPayroll.mockResolvedValue({ payrollUuid: 'payroll-uuid' }) + + renderWithProviders( + , + ) + + expect(await screen.findByRole('button', { name: 'Submit' })).toBeEnabled() + expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull() + + await user.click(screen.getByRole('button', { name: 'Submit' })) + + await waitFor(() => { + expect(mockSubmitPayroll).toHaveBeenCalled() + expect(mockOnEvent).toHaveBeenCalledWith( + componentEvents.RUN_PAYROLL_SUBMITTED, + expect.anything(), + ) + }) + }) + + it('hides Cancel on a processed payroll even when the payroll is otherwise cancellable', async () => { + vi.mocked(canCancelPayroll).mockReturnValue(true) + mockPayrollData = { + ...basePayrollData, + processed: true, + processingRequest: { status: 'submit_success', errors: [] }, + } + + renderWithProviders( + , + ) + + expect(await screen.findByRole('button', { name: 'View payroll receipt' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Cancel payroll' })).toBeNull() + }) +}) diff --git a/src/components/Payroll/PayrollOverview/PayrollOverview.tsx b/src/components/Payroll/PayrollOverview/PayrollOverview.tsx index ae25619f1..8737f9520 100644 --- a/src/components/Payroll/PayrollOverview/PayrollOverview.tsx +++ b/src/components/Payroll/PayrollOverview/PayrollOverview.tsx @@ -57,6 +57,12 @@ export interface PayrollOverviewProps extends BaseComponentInterface<'Payroll.Pa withReimbursements?: boolean /** Custom component to replace the default wire details confirmation UI. */ ConfirmWireDetailsComponent?: ConfirmWireDetailsComponentType + /** + * Hides the edit and cancel actions, leaving submit and receipt/paystub actions available. + * Use for a deep link to a specific payroll where editing shouldn't be offered. Defaults to + * `false`. + */ + readOnly?: boolean } const findUnresolvedBlockersWithOptions = ( @@ -96,7 +102,8 @@ const findWireInRequestUuid = ( * uncalculated payroll throws. Unresolved submission blockers (e.g. fast-ACH threshold, * wire-in funding) are surfaced inline and the submit action stays disabled until each * blocker has a selected unblock option. While the payroll is processing, the component - * polls until success or failure and emits the corresponding event. + * polls until success or failure and emits the corresponding event. Pass `readOnly` to hide + * the edit and cancel actions while keeping submit available. * * @events * | Event | Description | Data | @@ -138,6 +145,7 @@ const Root = ({ alerts, withReimbursements = true, ConfirmWireDetailsComponent = ConfirmWireDetails, + readOnly = false, }: PayrollOverviewProps) => { useComponentDictionary('Payroll.PayrollOverview', dictionary) useI18n('Payroll.PayrollOverview') @@ -283,9 +291,11 @@ const Root = ({ content: ( - + {!readOnly && ( + + )} ), }, @@ -305,6 +315,7 @@ const Root = ({ payrollData?.totals?.companyDebit, payrollData?.payrollStatusMeta?.expectedDebitTime, payrollData?.payrollDeadline, + readOnly, ]) const { data: bankAccountData } = useBankAccountsGetSuspense({ @@ -460,7 +471,8 @@ const Root = ({ payrollData.processed === true || payrollData.processingRequest?.status === PAYROLL_PROCESSING_STATUS.submit_success } - canCancel={canCancelPayroll(payrollData)} + canCancel={canCancelPayroll(payrollData) && !readOnly} + canEdit={!readOnly} payrollData={payrollData} bankAccount={bankAccount} taxes={taxes} diff --git a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx index 5688907a3..42e16ddad 100644 --- a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx +++ b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx @@ -665,5 +665,14 @@ describe('PayrollOverviewPresentation', () => { expect(await screen.findByRole('button', { name: /^Edit$/i })).toBeInTheDocument() expect(screen.getByRole('button', { name: /^Submit$/i })).toBeInTheDocument() }) + + it('hides Edit but keeps Submit when canEdit is false on an unprocessed payroll', async () => { + renderWithProviders( + , + ) + + expect(await screen.findByRole('button', { name: /^Submit$/i })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: /^Edit$/i })).not.toBeInTheDocument() + }) }) }) diff --git a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx index 29dfa7c42..94c2880c9 100644 --- a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx +++ b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx @@ -43,6 +43,7 @@ interface PayrollOverviewProps { status?: PayrollOverviewStatus isProcessed: boolean canCancel?: boolean + canEdit?: boolean alerts?: PayrollFlowAlert[] submissionBlockers?: PayrollSubmissionBlockerType[] selectedUnblockOptions?: Record @@ -82,6 +83,7 @@ export const PayrollOverviewPresentation = ({ status = PayrollOverviewStatus.Viewing, isProcessed, canCancel = false, + canEdit = true, alerts = [], submissionBlockers = [], selectedUnblockOptions = {}, @@ -582,9 +584,11 @@ export const PayrollOverviewPresentation = ({ ) : ( <> - + {canEdit && ( + + )}