Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
70 changes: 38 additions & 32 deletions src/components/ApprovalWorkflowSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ function ApprovalWorkflowSection({

const sortedMembers = approvalWorkflow.isDefault ? [] : sortAlphabetically(approvalWorkflow.members, 'displayName', localeCompare);

// Mirror the approver row's pending opacity on the "Expenses from" row so both fade together while
// the workflow (e.g. a member invited offline) is still pending server confirmation.
const membersPendingAction = sortedMembers.find((member) => !!member.pendingFields?.submitsTo)?.pendingFields?.submitsTo;

const members = approvalWorkflow.isDefault ? translate('workspace.common.everyone') : sortedMembers.map((m) => Str.removeSMSDomain(m.displayName)).join(', ');

const memberPills = sortedMembers.map((m) => ({
Expand Down Expand Up @@ -115,38 +119,40 @@ function ApprovalWorkflowSection({
</Text>
</View>
)}
<MenuItem
title={translate('workflowsExpensesFromPage.title')}
style={styles.p0}
titleStyle={styles.textLabelSupportingNormal}
descriptionTextStyle={[styles.textNormalThemeText, styles.lineHeightXLarge]}
description={approvalWorkflow.isDefault ? members : undefined}
numberOfLinesDescription={4}
shouldBeAccessible={false}
tabIndex={-1}
icon={icons.Users}
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
onPress={pressAction}
shouldRemoveBackground
titleComponent={
!approvalWorkflow.isDefault ? (
<View style={styles.ml3}>
{!isDisabled && onShowAllMembersPress ? (
<UserPills
users={memberPills}
onShowAllPress={onShowAllMembersPress}
showAllSentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.APPROVAL_SECTION_SHOW_ALL_MEMBERS}
/>
) : (
<UserPills users={memberPills} />
)}
</View>
) : undefined
}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.APPROVAL_SECTION_EXPENSES_FROM}
/>
<OfflineWithFeedback pendingAction={membersPendingAction}>
<MenuItem
title={translate('workflowsExpensesFromPage.title')}
style={styles.p0}
titleStyle={styles.textLabelSupportingNormal}
descriptionTextStyle={[styles.textNormalThemeText, styles.lineHeightXLarge]}
description={approvalWorkflow.isDefault ? members : undefined}
numberOfLinesDescription={4}
shouldBeAccessible={false}
tabIndex={-1}
icon={icons.Users}
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
onPress={pressAction}
shouldRemoveBackground
titleComponent={
!approvalWorkflow.isDefault ? (
<View style={styles.ml3}>
{!isDisabled && onShowAllMembersPress ? (
<UserPills
users={memberPills}
onShowAllPress={onShowAllMembersPress}
showAllSentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.APPROVAL_SECTION_SHOW_ALL_MEMBERS}
/>
) : (
<UserPills users={memberPills} />
)}
</View>
) : undefined
}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.APPROVAL_SECTION_EXPENSES_FROM}
/>
</OfflineWithFeedback>

{approvalWorkflow.approvers.map((approver, index) => (
<OfflineWithFeedback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ function WorkspaceInviteMessageComponent({
if (nestedBackTo) {
Navigation.goBack(nestedBackTo as Routes);
} else {
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, 0));
// forceReplace so the invite page is removed from the stack. Otherwise it stays
// underneath the Approver page and an iOS swipe-back reopens the invite confirm page.
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, 0), {forceReplace: true});
}
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import {sortAlphabetically} from '@libs/OptionsListUtils';
import {isControlPolicy} from '@libs/PolicyUtils';
import {getDefaultAvatarURL} from '@libs/UserAvatarUtils';
import {getApprovalLimitDescription} from '@libs/WorkflowUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -92,7 +93,9 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic
const memberPills = useMemo(
() =>
sortedMembers.map((m) => ({
avatar: m.avatar,
// A just-invited member is stored without an avatar, so fall back to the email-based default
// avatar instead of the generic fallback icon.
avatar: m.avatar ?? getDefaultAvatarURL({accountEmail: m.email}),
displayName: m.displayName,
email: m.email,
})),
Expand Down Expand Up @@ -133,7 +136,13 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic

const handleExpensesFromPress = useCallback(() => {
const firstApproverEmail = approvalWorkflow.approvers?.at(0)?.email ?? '';
const backTo = approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.EDIT ? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, firstApproverEmail) : undefined;
// Always pass a backTo so that after editing expenses-from (including the invite-a-member detour),
// we return to the page we came from. For EDIT that's the edit page; for CREATE we're on the
// confirm (new) page, so return there instead of falling through to the Approver step.
const backTo =
approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.EDIT
? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, firstApproverEmail)
: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID);
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(policyID, backTo));
}, [approvalWorkflow.action, approvalWorkflow.approvers, policyID]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {isAnyHRReadOnlyWorkflowMode} from '@libs/HRUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
import {getDefaultApprover, getMemberAccountIDsForWorkspace, isExpensifyTeam, shouldFilterExpensifyTeam} from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import MemberRightIcon from '@pages/workspace/MemberRightIcon';
Expand Down Expand Up @@ -172,7 +173,10 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa
return;
}

const newSelectedEmail = approver?.login ?? '';
// Canonicalize phone logins (e164@expensify.sms) so the approver email stored as submitsTo
// matches the canonical key the invite writes into employeeList. Without this, a phone approver
// invited offline stops resolving once online and the workflow disappears. No-op for emails.
const newSelectedEmail = addSMSDomainIfPhoneNumber(approver?.login ?? '');
const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(employeeList);
const accountID = Number(newSelectedEmail ? policyMemberEmailsToAccountIDs[newSelectedEmail] : '');
const {avatar, displayName = newSelectedEmail} = personalDetails?.[accountID] ?? {};
Expand Down
Loading
Loading