Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3e97009
Allow inviting new users in the Add approval workflow flow
twilight2294 Nov 19, 2025
c9c8dcb
Merge branch 'Expensify:main' into issue61625
twilight2294 Nov 26, 2025
b3d9a0a
address AI reviewer comments
twilight2294 Nov 26, 2025
e690024
fix changed files eslint
twilight2294 Nov 26, 2025
d03797d
Merge branch 'main' into issue61625
twilight2294 Dec 4, 2025
ebbae73
apply reviewer suggestions
twilight2294 Dec 4, 2025
e317407
Merge branch 'main' into issue61625
twilight2294 Dec 8, 2025
96acb93
fix workflow failures
twilight2294 Dec 8, 2025
6dce4df
Merge branch 'Expensify:main' into issue61625
twilight2294 Dec 11, 2025
f0d84e9
Update ROUTES.ts
twilight2294 Dec 11, 2025
05047dc
fix reinvite flow
twilight2294 Dec 11, 2025
92dadfb
Merge branch 'Expensify:main' into issue61625
twilight2294 Dec 12, 2025
e45bde0
Merge branch 'Expensify:main' into issue61625
twilight2294 Dec 15, 2025
9ccdb59
fix compiler error
twilight2294 Dec 15, 2025
90e6163
Merge branch 'main' into issue61625
twilight2294 Dec 18, 2025
a919a8f
address reviewers comments
twilight2294 Dec 18, 2025
7c2da9b
Merge branch 'main' into issue61625
twilight2294 Dec 22, 2025
630b602
Update WorkspaceInviteMessageComponent.tsx
twilight2294 Dec 22, 2025
568ab87
Update WorkspaceInviteMessageComponent.tsx
twilight2294 Dec 22, 2025
0e74b51
Update WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
twilight2294 Dec 22, 2025
0b59816
Update WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
twilight2294 Dec 22, 2025
7bf3282
Apply suggestion from @twilight2294
twilight2294 Dec 22, 2025
f5e6bad
Apply suggestion from @twilight2294
twilight2294 Dec 22, 2025
f9a0a34
Apply suggestion from @twilight2294
twilight2294 Dec 22, 2025
eb3fa72
Update WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
twilight2294 Dec 22, 2025
7eeaa76
Merge branch 'Expensify:main' into issue61625
twilight2294 Dec 27, 2025
312682b
Apply suggestions from code review
twilight2294 Dec 30, 2025
5ec4d9f
Apply suggestions from code review
twilight2294 Dec 30, 2025
3386129
Update WorkspaceInviteMessageComponent.tsx
twilight2294 Dec 30, 2025
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
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const CONST = {
CUSTOM_FIELD_KEYS: {customField1: 'employeeUserID', customField2: 'employeePayrollID'},
ANDROID_PACKAGE_NAME,
WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100,
WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE: 'expenses-from',
ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50,
ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300,
ANIMATED_HIGHLIGHT_START_DELAY: 10,
Expand Down
9 changes: 5 additions & 4 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type {TupleToUnion, ValueOf} from 'type-fest';
import type {UpperCaseCharacters} from 'type-fest/source/internal';
import type {SearchFilterKey, SearchQueryString, UserFriendlyKey} from './components/Search/types';
import type CONST from './CONST';
import CONST from './CONST';
import type {IOUAction, IOUType} from './CONST';
import type {ReplacementReason} from './libs/actions/Card';
import type {IOURequestType} from './libs/actions/IOU';
Expand Down Expand Up @@ -1733,10 +1733,11 @@ const ROUTES = {
getRoute: (policyID: string, firstApproverEmail: string) => `workspaces/${policyID}/workflows/approvals/${encodeURIComponent(firstApproverEmail)}/edit` as const,
},
WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM: {
route: 'workspaces/:policyID/workflows/approvals/expenses-from',
route: `workspaces/:policyID/workflows/approvals/${CONST.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE}`,

// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/expenses-from` as const, backTo),
getRoute: (policyID: string, backTo?: string) =>
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/${CONST.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE}` as const, backTo),
},
WORKSPACE_WORKFLOWS_APPROVALS_APPROVER: {
route: 'workspaces/:policyID/workflows/approvals/approver',
Expand Down
20 changes: 16 additions & 4 deletions src/components/ApproverSelectionList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useMemo} from 'react';
import React, {useCallback, useMemo} from 'react';
import useDebouncedState from '@hooks/useDebouncedState';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
Expand Down Expand Up @@ -40,6 +40,7 @@ type ApproverSelectionListPageProps = {
onSelectApprover?: (approvers: SelectionListApprover[]) => void;
shouldShowLoadingPlaceholder?: boolean;
shouldEnableHeaderMaxHeight?: boolean;
onSearchChange?: (searchTerm: string) => void;
shouldUpdateFocusedIndex?: boolean;
};

Expand All @@ -66,6 +67,7 @@ function ApproverSelectionList({
onSelectApprover,
shouldShowLoadingPlaceholder,
shouldEnableHeaderMaxHeight,
onSearchChange,
shouldUpdateFocusedIndex = true,
}: ApproverSelectionListPageProps) {
const styles = useThemeStyles();
Expand All @@ -75,6 +77,16 @@ function ApproverSelectionList({
const shouldShowTextInput = shouldShowTextInputProp ?? allApprovers?.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
const lazyIllustrations = useMemoizedLazyIllustrations(['TurtleInShell']);

const handleSearchChange = useCallback(
(term: string) => {
setSearchTerm(term);
if (onSearchChange) {
onSearchChange(term);
}
},
[onSearchChange, setSearchTerm],
);

const selectedMembers = useMemo(() => allApprovers.filter((approver) => approver.isSelected), [allApprovers]);

// eslint-disable-next-line rulesdir/no-negated-variables
Expand Down Expand Up @@ -126,10 +138,10 @@ function ApproverSelectionList({
() => ({
label: shouldShowListEmptyContent ? undefined : translate('selectionList.findMember'),
value: searchTerm,
onChangeText: setSearchTerm,
onChangeText: handleSearchChange,
headerMessage: searchTerm && !data?.length ? translate('common.noResultsFound') : '',
}),
[shouldShowListEmptyContent, translate, searchTerm, setSearchTerm, data?.length],
[shouldShowListEmptyContent, translate, searchTerm, handleSearchChange, data?.length],
);

return (
Expand All @@ -153,9 +165,9 @@ function ApproverSelectionList({
{subtitle}
<SelectionList
data={data}
onSelectRow={toggleApprover}
ListItem={InviteMemberListItem}
textInputOptions={textInputOptions}
onSelectRow={toggleApprover}
canSelectMultiple={allowMultipleSelection}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
listEmptyContent={listEmptyContent}
Expand Down
13 changes: 13 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

let allPolicies: OnyxCollection<Policy>;

Onyx.connect({

Check warning on line 63 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand Down Expand Up @@ -504,6 +504,18 @@
return memberEmailsToExclude;
}

/**
* Get excluded users as a Record for use in search selector
*/
function getExcludedUsers(employeeList?: PolicyEmployeeList): Record<string, boolean> {
const ineligibleInvitees = getIneligibleInvitees(employeeList);
const result: Record<string, boolean> = {};
for (const login of ineligibleInvitees) {
result[login] = true;
}
return result;
}

function getSortedTagKeys(policyTagList: OnyxEntry<PolicyTagLists>): Array<keyof PolicyTagLists> {
if (isEmptyObject(policyTagList)) {
return [];
Expand Down Expand Up @@ -1697,6 +1709,7 @@
getValidConnectedIntegration,
getCountOfEnabledTagsOfList,
getIneligibleInvitees,
getExcludedUsers,
getMemberAccountIDsForWorkspace,
getNumericValue,
isMultiLevelTags,
Expand Down
79 changes: 61 additions & 18 deletions src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,30 @@ function WorkspaceInviteMessageComponent({
}: WorkspaceInviteMessageComponentProps) {
const styles = useThemeStyles();
const {translate, formatPhoneNumber} = useLocalize();
const policyName = policy?.name;

const isWorkflowApprovalExpensesFromRoute = useMemo(() => {
if (!backTo || typeof backTo !== 'string') {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return (backTo as string).includes(CONST.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM_ROUTE);
}, [backTo]);

const headerTitle = useMemo(() => {
if (isWorkflowApprovalExpensesFromRoute) {
return translate('workflowsExpensesFromPage.title');
}
return translate('workspace.inviteMessage.confirmDetails');
}, [isWorkflowApprovalExpensesFromRoute, translate]);

const subtitle = useMemo(() => {
if (isWorkflowApprovalExpensesFromRoute) {
return undefined;
}
Comment thread
twilight2294 marked this conversation as resolved.
return policyName;
}, [isWorkflowApprovalExpensesFromRoute, policyName]);

const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true});
const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});

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

// We don't have login details for users who are not in the database yet
// So we need to fallback to their login from the invitedEmailsToAccountIDsDraft
const accountID = personalDetail.accountID;
const loginFromInviteMap = Object.entries(invitedEmailsToAccountIDsDraft ?? {}).find(([, id]) => id === accountID)?.[0];
const memberNames = useMemo(() => {
const personalDetailsOfInvitedEmails = getPersonalDetailsForAccountIDs(Object.values(invitedEmailsToAccountIDsDraft ?? {}), allPersonalDetails ?? {});
return Object.values(personalDetailsOfInvitedEmails)
.map((personalDetail) => {
const displayName = getDisplayNameOrDefault(personalDetail, '', false);
if (displayName) {
return displayName;
}

// We don't have login details for users who are not in the database yet
// So we need to fallback to their login from the invitedEmailsToAccountIDsDraft
const accountID = personalDetail.accountID;
const loginFromInviteMap = Object.entries(invitedEmailsToAccountIDsDraft ?? {}).find(([, id]) => id === accountID)?.[0];

return loginFromInviteMap;
})
.join(', ');
return loginFromInviteMap;
})
.join(', ');
}, [invitedEmailsToAccountIDsDraft, allPersonalDetails]);

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

// If backTo is provided and it's the expenses-from route, navigate to approver screen
if (isWorkflowApprovalExpensesFromRoute) {
// Check if it's initial creation flow (backTo doesn't have a nested backTo param)
const backToStr = typeof backTo === 'string' ? backTo : '';
const isInitialCreationFlow = !backToStr.includes('/expenses-from/');
if (isInitialCreationFlow) {
// Navigate to approver screen for initial creation flow
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, 0));
} else {
// For edit flow, just go back
Navigation.goBack();
}
return;
}

if ((backTo as string)?.endsWith('members')) {
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal());
return;
Expand Down Expand Up @@ -176,7 +218,6 @@ function WorkspaceInviteMessageComponent({
return errorFields;
};

const policyName = policy?.name;
const invitingMemberEmail = Object.keys(invitedEmailsToAccountIDsDraft ?? {}).at(0) ?? '';
const invitingMemberDetails = getPersonalDetailByEmail(invitingMemberEmail);
const invitingMemberName = Str.removeSMSDomain(invitingMemberDetails?.displayName ?? '');
Expand All @@ -201,8 +242,8 @@ function WorkspaceInviteMessageComponent({
>
{shouldShowBackButton && (
<HeaderWithBackButton
title={translate('workspace.inviteMessage.confirmDetails')}
subtitle={policyName}
title={headerTitle}
subtitle={subtitle}
shouldShowBackButton
onCloseButtonPress={() => Navigation.dismissModal()}
onBackButtonPress={() => Navigation.goBack(backTo)}
Expand All @@ -218,7 +259,9 @@ function WorkspaceInviteMessageComponent({
shouldHideFixErrorsAlert
addBottomSafeAreaPadding
>
{isInviteNewMemberStep && <Text style={[styles.textHeadlineLineHeightXXL, styles.mv3]}>{translate('workspace.card.issueNewCard.inviteNewMember')}</Text>}
{(isInviteNewMemberStep || isWorkflowApprovalExpensesFromRoute) && (
<Text style={[styles.textHeadlineLineHeightXXL, styles.mv3]}>{translate('workspace.card.issueNewCard.inviteNewMember')}</Text>
)}
<View style={[styles.mv4, styles.justifyContentCenter, styles.alignItemsCenter]}>
<ReportActionAvatars
size={CONST.AVATAR_SIZE.LARGE}
Expand Down
Loading
Loading