Parent: https://github.com/Expensify/Expensify/issues/625136
Design doc: https://docs.google.com/document/d/1TGdZYwm4_fvPOZtUziiz27bwVRqYm6HuEGopUYWNg8Y/edit
Scope
Make the Workflows page HR-aware so it reflects TriNet (or Gusto) approval rules without becoming the source of truth:
- Locked approvals toggle — toggle shows "on" with a lock icon; tapping shows a "Not so fast..." modal.
- "Configure via TriNet" link — navigates to the HR page.
- Mode-by-mode behavior:
- Basic: single read-only approver chain,
(from TriNet) badge, no "Add approval workflow."
- Manager: read-only synced manager chains,
(from TriNet) badge per row, no "Add approval workflow."
- Custom: toggle still locked, but the approval table is fully editable; "Add approval workflow" available.
- Deep-link guards on Edit / Create / Limit / ExpensesFrom approval pages — redirect back when in read-only Basic/Manager mode.
Depends on: Release 1 issues — specifically isZenefitsConnected, CONST.ZENEFITS.APPROVAL_MODE, and ZenefitsConnectionConfig type.
Implementation notes (from design doc)
src/libs/PolicyUtils.ts — HR workflow helpers
function isHRReadOnlyWorkflowMode(policy: OnyxEntry<Policy>): boolean {
const zenefitsMode = policy?.connections?.zenefits?.config?.approvalMode;
const gustoMode = policy?.connections?.gusto?.config?.approvalMode;
const activeMode = zenefitsMode ?? gustoMode;
return !!activeMode &&
activeMode !== CONST.ZENEFITS.APPROVAL_MODE.CUSTOM &&
activeMode !== CONST.GUSTO.APPROVAL_MODE.CUSTOM;
}
function isHRIntegrationConnected(policy: OnyxEntry<Policy>): boolean {
return isGustoConnected(policy) || isZenefitsConnected(policy);
}
function getHRConnectionDisplayName(
policy: OnyxEntry<Policy>,
translate: LocaleContextProps['translate'],
): string {
if (isZenefitsConnected(policy)) return translate('workspace.hr.zenefits.title');
if (isGustoConnected(policy)) return translate('workspace.hr.gusto.title');
return '';
}
Update getDefaultApprover:
function getDefaultApprover(policy: OnyxEntry<Policy>): string {
const zenefitsConfig = policy?.connections?.zenefits?.config;
if (
zenefitsConfig?.approvalMode &&
zenefitsConfig.approvalMode !== CONST.ZENEFITS.APPROVAL_MODE.CUSTOM &&
zenefitsConfig.finalApprover
) {
return zenefitsConfig.finalApprover;
}
const gustoConfig = policy?.connections?.gusto?.config;
if (
gustoConfig?.approvalMode &&
gustoConfig.approvalMode !== CONST.GUSTO.APPROVAL_MODE.CUSTOM &&
gustoConfig.finalApprover
) {
return gustoConfig.finalApprover;
}
return policy?.approver || policy?.owner || '';
}
This automatically flows through convertPolicyEmployeesToApprovalWorkflows in WorkflowUtils.ts.
src/components/ApprovalWorkflowSection.tsx — Add badgeText and isReadOnly props
type ApprovalWorkflowSectionProps = {
approvalWorkflow: ApprovalWorkflow;
onPress: () => void;
currency?: string;
badgeText?: string;
isReadOnly?: boolean;
};
Changes:
- Disable
onPress when isReadOnly.
- Remove
accessibilityRole="button" when read-only.
- Hide
ArrowRight icon when read-only.
- Show
badgeText as a supporting label.
- Inner
MenuItem onPress handlers also respect isReadOnly.
src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx — Core changes
New imports:
import {
isHRIntegrationConnected,
isHRReadOnlyWorkflowMode,
getHRConnectionDisplayName,
} from '@libs/PolicyUtils';
Derived state:
const isHRConnected = isHRIntegrationConnected(policy);
const isHRReadOnly = isHRReadOnlyWorkflowMode(policy);
const hrDisplayName = isHRConnected ? getHRConnectionDisplayName(policy, translate) : '';
Update approvals toggle in optionItems:
disabled: isSmartLimitEnabled || isDEWEnabled || isHRConnected,
showLockIcon: isHRConnected,
disabledAction: isHRConnected
? () => {
showConfirmModal({
title: translate('workspace.hr.notSoFast'),
prompt: translate('workspace.hr.approvalsLockedByHR', { integration: hrDisplayName }),
confirmText: translate('workspace.hr.configureVia', { integration: hrDisplayName }),
cancelText: translate('common.cancel'),
}).then((result) => {
if (result.action !== ModalActions.CONFIRM) return;
Navigation.navigate(ROUTES.WORKSPACE_HR.getRoute(route.params.policyID));
});
}
: undefined,
Update isActive:
isActive:
isHRConnected ||
isDEWEnabled ||
(([CONST.POLICY.APPROVAL_MODE.BASIC, CONST.POLICY.APPROVAL_MODE.ADVANCED]
.some((approvalMode) => approvalMode === policy?.approvalMode) && !hasApprovalError) ?? false),
Update subtitle:
subtitle:
isHRConnected
? translate('workspace.hr.approvalsConfiguredByHR', { integration: hrDisplayName })
: isSmartLimitEnabled
? translate('workspace.moreFeatures.workflows.disableApprovalPrompt')
: translate('workflowsPage.addApprovalsDescription'),
Update renderOptionItem to forward showLockIcon, disabledAction, disabledText.
Update ApprovalWorkflowSection usage:
<ApprovalWorkflowSection
approvalWorkflow={workflow}
onPress={() =>
Navigation.navigate(
ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(
route.params.policyID,
workflow.approvers.at(0)?.email ?? '',
),
)
}
currency={policy?.outputCurrency}
badgeText={
isHRConnected && isHRReadOnly
? translate('workspace.hr.fromIntegration', { integration: hrDisplayName })
: undefined
}
isReadOnly={isHRConnected && isHRReadOnly}
/>
Conditionally hide "Add approval workflow":
{!(isHRConnected && isHRReadOnly) && (
<MenuItem
title={translate('workflowsPage.addApprovalButton')}
...existing props...
/>
)}
Approval sub-page guards
Apply isHRReadOnlyWorkflowMode(policy) to shouldShowNotFoundView on each of:
WorkspaceWorkflowsApprovalsEditPage.tsx
WorkspaceWorkflowsApprovalsCreatePage.tsx
WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx
WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
These pages are reachable via deep-link and must also block editing in Basic/Manager mode.
src/languages/en.ts (+ other locales) — New strings
notSoFast: 'Not so fast...'
approvalsLockedByHR:
({integration}: {integration: string}) =>
`Approval workflows are currently managed by your ${integration} connection. To change the approval mode or final approver, go to the HR page.`
approvalsConfiguredByHR:
({integration}: {integration: string}) =>
`Approval workflows are configured via ${integration}.`
configureVia:
({integration}: {integration: string}) =>
`Configure via ${integration}`
fromIntegration:
({integration}: {integration: string}) =>
`(from ${integration})`
Offline behavior
- Locked toggle: always visible from cached Onyx; lock icon and modal work offline.
- Read-only chains: visible from cached data.
- "Configure via" navigation: works offline.
- Deep-link guards: work offline because they rely on cached policy data.
Tests
PolicyUtilsTest.ts: cases for isHRReadOnlyWorkflowMode, isHRIntegrationConnected, getHRConnectionDisplayName, and the updated getDefaultApprover (covering Basic/Manager/Custom modes for both Gusto and Zenefits).
WorkflowUtilsTest.ts: cases for final-approver fallback in Basic/Manager modes vs. Custom mode.
ApprovalWorkflowSection component test: badge rendering, read-only state disables onPress.
- Manual: Manual Test Doc Release 3 section in full.
Issue Owner
Current Issue Owner: @ShridharGoel
Parent: https://github.com/Expensify/Expensify/issues/625136
Design doc: https://docs.google.com/document/d/1TGdZYwm4_fvPOZtUziiz27bwVRqYm6HuEGopUYWNg8Y/edit
Scope
Make the Workflows page HR-aware so it reflects TriNet (or Gusto) approval rules without becoming the source of truth:
(from TriNet)badge, no "Add approval workflow."(from TriNet)badge per row, no "Add approval workflow."Depends on: Release 1 issues — specifically
isZenefitsConnected,CONST.ZENEFITS.APPROVAL_MODE, andZenefitsConnectionConfigtype.Implementation notes (from design doc)
src/libs/PolicyUtils.ts— HR workflow helpersUpdate
getDefaultApprover:This automatically flows through
convertPolicyEmployeesToApprovalWorkflowsinWorkflowUtils.ts.src/components/ApprovalWorkflowSection.tsx— AddbadgeTextandisReadOnlypropsChanges:
onPresswhenisReadOnly.accessibilityRole="button"when read-only.ArrowRighticon when read-only.badgeTextas a supporting label.MenuItemonPresshandlers also respectisReadOnly.src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx— Core changesNew imports:
Derived state:
Update approvals toggle in
optionItems:Update
isActive:Update subtitle:
Update
renderOptionItemto forwardshowLockIcon,disabledAction,disabledText.Update
ApprovalWorkflowSectionusage:Conditionally hide "Add approval workflow":
Approval sub-page guards
Apply
isHRReadOnlyWorkflowMode(policy)toshouldShowNotFoundViewon each of:WorkspaceWorkflowsApprovalsEditPage.tsxWorkspaceWorkflowsApprovalsCreatePage.tsxWorkspaceWorkflowsApprovalsApprovalLimitPage.tsxWorkspaceWorkflowsApprovalsExpensesFromPage.tsxThese pages are reachable via deep-link and must also block editing in Basic/Manager mode.
src/languages/en.ts(+ other locales) — New stringsOffline behavior
Tests
PolicyUtilsTest.ts: cases forisHRReadOnlyWorkflowMode,isHRIntegrationConnected,getHRConnectionDisplayName, and the updatedgetDefaultApprover(covering Basic/Manager/Custom modes for both Gusto and Zenefits).WorkflowUtilsTest.ts: cases for final-approver fallback in Basic/Manager modes vs. Custom mode.ApprovalWorkflowSectioncomponent test: badge rendering, read-only state disables onPress.Issue Owner
Current Issue Owner: @ShridharGoel