Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions src/libs/WorkflowUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
import type {PolicyEmployeeList} from '@src/types/onyx/PolicyEmployee';
import {isBankAccountPartiallySetup} from './BankAccountUtils';
import {convertToDisplayString} from './CurrencyUtils';
import {getDefaultApprover} from './PolicyUtils';
import {getDefaultApprover, isExpensifyTeam} from './PolicyUtils';

const INITIAL_APPROVAL_WORKFLOW: ApprovalWorkflowOnyx = {
members: [],
Expand Down Expand Up @@ -103,6 +103,9 @@ type PolicyConversionParams = {

/** Locale comparison function */
localeCompare: LocaleContextProps['localeCompare'];

/** Current user's login email, used to determine if Expensify team members should be shown */
currentUserLogin?: string;
};

type PolicyConversionResult = {
Expand All @@ -116,11 +119,35 @@ type PolicyConversionResult = {
usedApproverEmails: string[];
};

/**
* Find the first non-Expensify team member in the approval chain.
* Used to skip internal Expensify approvers when displaying workflows to customers.
* Returns undefined if no non-Expensify approver is found in the chain.
*/
function findFirstNonExpensifyApprover(employees: PolicyEmployeeList, startEmail: string): string | undefined {
let email: string | undefined = startEmail;
const visited = new Set<string>();

while (email && !visited.has(email)) {
if (!isExpensifyTeam(email)) {
return email;
}
visited.add(email);
email = employees[email]?.forwardsTo;
}

return undefined;
}

/** Convert a list of policy employees to a list of approval workflows */
function convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, firstApprover, localeCompare}: PolicyConversionParams): PolicyConversionResult {
function convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, firstApprover, localeCompare, currentUserLogin}: PolicyConversionParams): PolicyConversionResult {
const employees = policy?.employeeList ?? {};
const defaultApprover = getDefaultApprover(policy);
const approvalWorkflows: Record<string, ApprovalWorkflow> = {};
const policyOwner = policy?.owner;

// Determine if we should filter Expensify team members (only for non-Expensify customers)
const shouldFilterExpensifyTeam = !!policyOwner && !!currentUserLogin && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this condition intents to filter out Expensify team members, so please update the comment and rename to variable to shouldFilterOutExpensifyTeam


// Keep track of used approver emails to display hints in the UI
const usedApproverEmails = new Set<string>();
Expand All @@ -133,6 +160,11 @@ function convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, fir
continue;
}

// Filter out Expensify team members from appearing as workflow members
if (shouldFilterExpensifyTeam && isExpensifyTeam(email)) {
continue;
}

const member = buildMemberFromEmployee(employee, personalDetailsByEmail, email);

if (pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
Expand All @@ -143,9 +175,19 @@ function convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, fir
continue;
}

if (!approvalWorkflows[submitsTo]) {
const approvers = calculateApprovers({employees, firstEmail: submitsTo, personalDetailsByEmail});
if (submitsTo !== firstApprover) {
// If submitsTo is an Expensify team member, find the first non-Expensify approver in the chain
const effectiveSubmitsTo = shouldFilterExpensifyTeam ? (findFirstNonExpensifyApprover(employees, submitsTo) ?? submitsTo) : submitsTo;

if (!employees[effectiveSubmitsTo]) {
continue;
}

if (!approvalWorkflows[effectiveSubmitsTo]) {
let approvers = calculateApprovers({employees, firstEmail: effectiveSubmitsTo, personalDetailsByEmail});
if (shouldFilterExpensifyTeam) {
approvers = approvers.filter((approver) => !isExpensifyTeam(approver.email));
}
if (effectiveSubmitsTo !== firstApprover) {
for (const approver of approvers) {
usedApproverEmails.add(approver.email);
}
Expand All @@ -156,20 +198,20 @@ function convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, fir
// should not affect the workflow's display state
const workflowPendingAction = pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? pendingAction : undefined;

approvalWorkflows[submitsTo] = {
approvalWorkflows[effectiveSubmitsTo] = {
members: [],
approvers,
isDefault: defaultApprover === submitsTo,
isDefault: defaultApprover === effectiveSubmitsTo,
pendingAction: workflowPendingAction,
};
}

approvalWorkflows[submitsTo].members.push(member);
approvalWorkflows[effectiveSubmitsTo].members.push(member);
// Only propagate ADD/UPDATE pending actions to the workflow, not DELETE
// When a member is being deleted from the workspace, their DELETE pending action
// should not affect the workflow's display state (e.g., strikethrough styling)
if (pendingAction && pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
approvalWorkflows[submitsTo].pendingAction = pendingAction;
approvalWorkflows[effectiveSubmitsTo].pendingAction = pendingAction;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
policy,
personalDetails: personalDetails ?? {},
localeCompare,
currentUserLogin: currentUserPersonalDetails?.login,
});

useEffect(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Section from '@components/Section';
import Text from '@components/Text';
import useCardFeeds from '@hooks/useCardFeeds';
import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -94,10 +95,12 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {approvalWorkflows, availableMembers, usedApproverEmails} = convertPolicyEmployeesToApprovalWorkflows({
policy,
personalDetails: personalDetails ?? {},
localeCompare,
currentUserLogin: currentUserPersonalDetails?.login,
});

const hasValidExistingAccounts = getEligibleExistingBusinessBankAccounts(bankAccountList, policy?.outputCurrency, true).length > 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {ModalActions} from '@components/Modal/Global/ModalContext';
import ScreenWrapper from '@components/ScreenWrapper';
import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -37,6 +38,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
const {translate, localeCompare} = useLocalize();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [approvalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [initialApprovalWorkflow, setInitialApprovalWorkflow] = useState<ApprovalWorkflow | undefined>();
const formRef = useRef<ScrollView>(null);
const {showConfirmModal} = useConfirmModal();
Expand Down Expand Up @@ -87,6 +89,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
personalDetails,
firstApprover,
localeCompare,
currentUserLogin: currentUserPersonalDetails?.login,
});

return {
Expand Down
Loading