Skip to content

Commit 66de3d6

Browse files
authored
Merge pull request Expensify#75589 from twilight2294/issue61625
Allow inviting new users in the Add approval workflow flow
2 parents d584922 + 3386129 commit 66de3d6

File tree

6 files changed

+259
-40
lines changed

6 files changed

+259
-40
lines changed

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ const CONST = {
202202
CUSTOM_FIELD_KEYS: {customField1: 'employeeUserID', customField2: 'employeePayrollID'},
203203
ANDROID_PACKAGE_NAME,
204204
WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100,
205+
WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE: 'expenses-from',
205206
ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50,
206207
ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300,
207208
ANIMATED_HIGHLIGHT_START_DELAY: 10,

src/ROUTES.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import type {TupleToUnion, ValueOf} from 'type-fest';
77
import type {UpperCaseCharacters} from 'type-fest/source/internal';
88
import type {SearchFilterKey, SearchQueryString, UserFriendlyKey} from './components/Search/types';
9-
import type CONST from './CONST';
9+
import CONST from './CONST';
1010
import type {IOUAction, IOUType} from './CONST';
1111
import type {ReplacementReason} from './libs/actions/Card';
1212
import type {IOURequestType} from './libs/actions/IOU';
@@ -1727,10 +1727,11 @@ const ROUTES = {
17271727
getRoute: (policyID: string, firstApproverEmail: string) => `workspaces/${policyID}/workflows/approvals/${encodeURIComponent(firstApproverEmail)}/edit` as const,
17281728
},
17291729
WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM: {
1730-
route: 'workspaces/:policyID/workflows/approvals/expenses-from',
1730+
route: `workspaces/:policyID/workflows/approvals/${CONST.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE}`,
17311731

1732-
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
1733-
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/expenses-from` as const, backTo),
1732+
getRoute: (policyID: string, backTo?: string) =>
1733+
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
1734+
getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/${CONST.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE}` as const, backTo),
17341735
},
17351736
WORKSPACE_WORKFLOWS_APPROVALS_APPROVER: {
17361737
route: 'workspaces/:policyID/workflows/approvals/approver',

src/components/ApproverSelectionList.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useMemo} from 'react';
1+
import React, {useCallback, useMemo} from 'react';
22
import useDebouncedState from '@hooks/useDebouncedState';
33
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
44
import useLocalize from '@hooks/useLocalize';
@@ -40,6 +40,7 @@ type ApproverSelectionListPageProps = {
4040
onSelectApprover?: (approvers: SelectionListApprover[]) => void;
4141
shouldShowLoadingPlaceholder?: boolean;
4242
shouldEnableHeaderMaxHeight?: boolean;
43+
onSearchChange?: (searchTerm: string) => void;
4344
shouldUpdateFocusedIndex?: boolean;
4445
};
4546

@@ -66,6 +67,7 @@ function ApproverSelectionList({
6667
onSelectApprover,
6768
shouldShowLoadingPlaceholder,
6869
shouldEnableHeaderMaxHeight,
70+
onSearchChange,
6971
shouldUpdateFocusedIndex = true,
7072
}: ApproverSelectionListPageProps) {
7173
const styles = useThemeStyles();
@@ -75,6 +77,16 @@ function ApproverSelectionList({
7577
const shouldShowTextInput = shouldShowTextInputProp ?? allApprovers?.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
7678
const lazyIllustrations = useMemoizedLazyIllustrations(['TurtleInShell']);
7779

80+
const handleSearchChange = useCallback(
81+
(term: string) => {
82+
setSearchTerm(term);
83+
if (onSearchChange) {
84+
onSearchChange(term);
85+
}
86+
},
87+
[onSearchChange, setSearchTerm],
88+
);
89+
7890
const selectedMembers = useMemo(() => allApprovers.filter((approver) => approver.isSelected), [allApprovers]);
7991

8092
// eslint-disable-next-line rulesdir/no-negated-variables
@@ -126,10 +138,10 @@ function ApproverSelectionList({
126138
() => ({
127139
label: shouldShowListEmptyContent ? undefined : translate('selectionList.findMember'),
128140
value: searchTerm,
129-
onChangeText: setSearchTerm,
141+
onChangeText: handleSearchChange,
130142
headerMessage: searchTerm && !data?.length ? translate('common.noResultsFound') : '',
131143
}),
132-
[shouldShowListEmptyContent, translate, searchTerm, setSearchTerm, data?.length],
144+
[shouldShowListEmptyContent, translate, searchTerm, handleSearchChange, data?.length],
133145
);
134146

135147
return (
@@ -153,9 +165,9 @@ function ApproverSelectionList({
153165
{subtitle}
154166
<SelectionList
155167
data={data}
156-
onSelectRow={toggleApprover}
157168
ListItem={InviteMemberListItem}
158169
textInputOptions={textInputOptions}
170+
onSelectRow={toggleApprover}
159171
canSelectMultiple={allowMultipleSelection}
160172
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
161173
listEmptyContent={listEmptyContent}

src/libs/PolicyUtils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,18 @@ function getIneligibleInvitees(employeeList?: PolicyEmployeeList): string[] {
504504
return memberEmailsToExclude;
505505
}
506506

507+
/**
508+
* Get excluded users as a Record for use in search selector
509+
*/
510+
function getExcludedUsers(employeeList?: PolicyEmployeeList): Record<string, boolean> {
511+
const ineligibleInvitees = getIneligibleInvitees(employeeList);
512+
const result: Record<string, boolean> = {};
513+
for (const login of ineligibleInvitees) {
514+
result[login] = true;
515+
}
516+
return result;
517+
}
518+
507519
function getSortedTagKeys(policyTagList: OnyxEntry<PolicyTagLists>): Array<keyof PolicyTagLists> {
508520
if (isEmptyObject(policyTagList)) {
509521
return [];
@@ -1687,6 +1699,7 @@ export {
16871699
getValidConnectedIntegration,
16881700
getCountOfEnabledTagsOfList,
16891701
getIneligibleInvitees,
1702+
getExcludedUsers,
16901703
getMemberAccountIDsForWorkspace,
16911704
getNumericValue,
16921705
isMultiLevelTags,

src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,30 @@ function WorkspaceInviteMessageComponent({
6666
}: WorkspaceInviteMessageComponentProps) {
6767
const styles = useThemeStyles();
6868
const {translate, formatPhoneNumber} = useLocalize();
69+
const policyName = policy?.name;
70+
71+
const isWorkflowApprovalExpensesFromRoute = useMemo(() => {
72+
if (!backTo || typeof backTo !== 'string') {
73+
return false;
74+
}
75+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
76+
return (backTo as string).includes(CONST.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE);
77+
}, [backTo]);
78+
79+
const headerTitle = useMemo(() => {
80+
if (isWorkflowApprovalExpensesFromRoute) {
81+
return translate('workflowsExpensesFromPage.title');
82+
}
83+
return translate('workspace.inviteMessage.confirmDetails');
84+
}, [isWorkflowApprovalExpensesFromRoute, translate]);
85+
86+
const subtitle = useMemo(() => {
87+
if (isWorkflowApprovalExpensesFromRoute) {
88+
return undefined;
89+
}
90+
return policyName;
91+
}, [isWorkflowApprovalExpensesFromRoute, policyName]);
92+
6993
const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true});
7094
const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
7195

@@ -82,22 +106,25 @@ function WorkspaceInviteMessageComponent({
82106
});
83107
const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID}`, {canBeMissing: true});
84108
const isOnyxLoading = isLoadingOnyxValue(workspaceInviteMessageDraftResult, invitedEmailsToAccountIDsDraftResult, formDataResult);
85-
const personalDetailsOfInvitedEmails = getPersonalDetailsForAccountIDs(Object.values(invitedEmailsToAccountIDsDraft ?? {}), allPersonalDetails ?? {});
86-
const memberNames = Object.values(personalDetailsOfInvitedEmails)
87-
.map((personalDetail) => {
88-
const displayName = getDisplayNameOrDefault(personalDetail, '', false);
89-
if (displayName) {
90-
return displayName;
91-
}
92109

93-
// We don't have login details for users who are not in the database yet
94-
// So we need to fallback to their login from the invitedEmailsToAccountIDsDraft
95-
const accountID = personalDetail.accountID;
96-
const loginFromInviteMap = Object.entries(invitedEmailsToAccountIDsDraft ?? {}).find(([, id]) => id === accountID)?.[0];
110+
const memberNames = useMemo(() => {
111+
const personalDetailsOfInvitedEmails = getPersonalDetailsForAccountIDs(Object.values(invitedEmailsToAccountIDsDraft ?? {}), allPersonalDetails ?? {});
112+
return Object.values(personalDetailsOfInvitedEmails)
113+
.map((personalDetail) => {
114+
const displayName = getDisplayNameOrDefault(personalDetail, '', false);
115+
if (displayName) {
116+
return displayName;
117+
}
118+
119+
// We don't have login details for users who are not in the database yet
120+
// So we need to fallback to their login from the invitedEmailsToAccountIDsDraft
121+
const accountID = personalDetail.accountID;
122+
const loginFromInviteMap = Object.entries(invitedEmailsToAccountIDsDraft ?? {}).find(([, id]) => id === accountID)?.[0];
97123

98-
return loginFromInviteMap;
99-
})
100-
.join(', ');
124+
return loginFromInviteMap;
125+
})
126+
.join(', ');
127+
}, [invitedEmailsToAccountIDsDraft, allPersonalDetails]);
101128

102129
const welcomeNoteSubject = useMemo(
103130
() => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`,
@@ -147,6 +174,21 @@ function WorkspaceInviteMessageComponent({
147174
return;
148175
}
149176

177+
// If backTo is provided and it's the expenses-from route, navigate to approver screen
178+
if (isWorkflowApprovalExpensesFromRoute) {
179+
// Check if it's initial creation flow (backTo doesn't have a nested backTo param)
180+
const backToStr = typeof backTo === 'string' ? backTo : '';
181+
const isInitialCreationFlow = !backToStr.includes('/expenses-from/');
182+
if (isInitialCreationFlow) {
183+
// Navigate to approver screen for initial creation flow
184+
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, 0));
185+
} else {
186+
// For edit flow, just go back
187+
Navigation.goBack();
188+
}
189+
return;
190+
}
191+
150192
if ((backTo as string)?.endsWith('members')) {
151193
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal());
152194
return;
@@ -176,7 +218,6 @@ function WorkspaceInviteMessageComponent({
176218
return errorFields;
177219
};
178220

179-
const policyName = policy?.name;
180221
const invitingMemberEmail = Object.keys(invitedEmailsToAccountIDsDraft ?? {}).at(0) ?? '';
181222
const invitingMemberDetails = getPersonalDetailByEmail(invitingMemberEmail);
182223
const invitingMemberName = Str.removeSMSDomain(invitingMemberDetails?.displayName ?? '');
@@ -201,8 +242,8 @@ function WorkspaceInviteMessageComponent({
201242
>
202243
{shouldShowBackButton && (
203244
<HeaderWithBackButton
204-
title={translate('workspace.inviteMessage.confirmDetails')}
205-
subtitle={policyName}
245+
title={headerTitle}
246+
subtitle={subtitle}
206247
shouldShowBackButton
207248
onCloseButtonPress={() => Navigation.dismissModal()}
208249
onBackButtonPress={() => Navigation.goBack(backTo)}
@@ -218,7 +259,9 @@ function WorkspaceInviteMessageComponent({
218259
shouldHideFixErrorsAlert
219260
addBottomSafeAreaPadding
220261
>
221-
{isInviteNewMemberStep && <Text style={[styles.textHeadlineLineHeightXXL, styles.mv3]}>{translate('workspace.card.issueNewCard.inviteNewMember')}</Text>}
262+
{(isInviteNewMemberStep || isWorkflowApprovalExpensesFromRoute) && (
263+
<Text style={[styles.textHeadlineLineHeightXXL, styles.mv3]}>{translate('workspace.card.issueNewCard.inviteNewMember')}</Text>
264+
)}
222265
<View style={[styles.mv4, styles.justifyContentCenter, styles.alignItemsCenter]}>
223266
<ReportActionAvatars
224267
size={CONST.AVATAR_SIZE.LARGE}

0 commit comments

Comments
 (0)