From fefddd2e05db64ff6f590fb6d104f5bc26c3327c Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 14 Jun 2026 18:50:31 +0530 Subject: [PATCH] Migrate useSearchSelector.base.ts from useOptionsList to usePersonalDetailOptions (part 4) --- ...BaseVacationDelegateSelectionComponent.tsx | 141 +++++++------- .../Security/AddDelegate/AddDelegatePage.tsx | 86 +++++---- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 108 +++++------ ...WorkspaceConfirmationOwnerSelectorPage.tsx | 177 +++++++----------- 4 files changed, 249 insertions(+), 263 deletions(-) diff --git a/src/components/BaseVacationDelegateSelectionComponent.tsx b/src/components/BaseVacationDelegateSelectionComponent.tsx index 6879ef14ae84..4d8a5aa5c3fe 100644 --- a/src/components/BaseVacationDelegateSelectionComponent.tsx +++ b/src/components/BaseVacationDelegateSelectionComponent.tsx @@ -3,11 +3,11 @@ import {View} from 'react-native'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSearchSelector from '@hooks/useSearchSelector'; +import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchUserInServer} from '@libs/actions/Report'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; -import {getHeaderMessage} from '@libs/OptionsListUtils'; +import {filterOption, getHeaderMessage} from '@libs/PersonalDetailOptionsListUtils'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -67,95 +67,107 @@ function BaseVacationDelegateSelectionComponent({ ...additionalExcludeLogins, }; - const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized, onListEndReached} = useSearchSelector({ + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized} = usePersonalDetailSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, excludeLogins, + includeUserToInvite: true, includeRecentReports: true, - getValidOptionsConfig: { - excludeLogins, - includeCurrentUser, - }, + includeCurrentUser, }); useEffect(() => { searchUserInServer(debouncedSearchTerm); }, [debouncedSearchTerm]); - const sectionsList = []; - - if (currentVacationDelegate && delegatePersonalDetails) { - sectionsList.push({ - title: undefined, - sectionIndex: 0, - data: [ - { - ...delegatePersonalDetails, - text: delegatePersonalDetails?.displayName ?? currentVacationDelegate, - alternateText: delegatePersonalDetails?.login ?? currentVacationDelegate, - login: delegatePersonalDetails.login ?? currentVacationDelegate, - keyForList: `vacationDelegate-${delegatePersonalDetails.login}`, - isDisabled: false, - isSelected: true, - shouldShowSubscript: undefined, - icons: [ - { - source: delegatePersonalDetails?.avatar ?? icons.FallbackAvatar, - name: formatPhoneNumber(delegatePersonalDetails?.login ?? ''), - type: CONST.ICON_TYPE_AVATAR, - id: delegatePersonalDetails?.accountID, - }, - ], - }, - ], - }); - } - - sectionsList.push({ - title: translate('common.recents'), - sectionIndex: 1, - data: availableOptions.recentReports, - }); - sectionsList.push({ - title: translate('common.contacts'), - sectionIndex: 2, - data: availableOptions.personalDetails, - }); - - if (availableOptions.userToInvite) { - sectionsList.push({ - title: undefined, - sectionIndex: 3, - data: [availableOptions.userToInvite], - }); - } + const sectionsList = (() => { + const list = []; + + const delegateOption = + currentVacationDelegate && delegatePersonalDetails + ? { + ...delegatePersonalDetails, + text: delegatePersonalDetails?.displayName ?? currentVacationDelegate, + alternateText: delegatePersonalDetails?.login ?? currentVacationDelegate, + login: delegatePersonalDetails.login ?? currentVacationDelegate, + keyForList: `vacationDelegate-${delegatePersonalDetails.login}`, + isDisabled: false, + isSelected: true, + shouldShowSubscript: undefined, + icons: [ + { + source: delegatePersonalDetails?.avatar ?? icons.FallbackAvatar, + name: formatPhoneNumber(delegatePersonalDetails?.login ?? ''), + type: CONST.ICON_TYPE_AVATAR, + id: delegatePersonalDetails?.accountID, + }, + ], + } + : undefined; + + // Only pin the current delegate when it matches the search term, mirroring how the hook filters the other sections + if (delegateOption && filterOption(delegateOption, debouncedSearchTerm)) { + list.push({ + title: undefined, + sectionIndex: 0, + data: [delegateOption], + }); + } + + if (availableOptions.recentOptions.length) { + list.push({ + title: translate('common.recents'), + sectionIndex: 1, + data: availableOptions.recentOptions, + }); + } + + if (availableOptions.personalDetails.length) { + list.push({ + title: translate('common.contacts'), + sectionIndex: 2, + data: availableOptions.personalDetails, + }); + } + + if (availableOptions.userToInvite) { + list.push({ + title: undefined, + sectionIndex: 3, + data: [availableOptions.userToInvite], + }); + } + + return list; + })(); const sections = sectionsList.map((section) => ({ ...section, data: (section.data ?? []).map((option) => ({ ...option, - text: option.text ?? option.displayName ?? '', + text: option.text ?? '', alternateText: option.alternateText ?? option.login ?? undefined, keyForList: option.keyForList ?? '', isDisabled: option.isDisabled ?? undefined, isSelected: option.isSelected ?? undefined, login: option.login ?? undefined, - shouldShowSubscript: option.shouldShowSubscript ?? undefined, + shouldShowSubscript: undefined, })), })); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); + const headerMessage = (() => { + if (sections.length > 0) { + return ''; + } + return getHeaderMessage(translate, searchValue, countryCode); + })(); + const textInputOptions = { value: searchTerm, onChangeText: setSearchTerm, label: translate('selectionList.nameEmailOrPhoneNumber'), - headerMessage: getHeaderMessage( - (availableOptions.recentReports?.length || 0) + (availableOptions.personalDetails?.length || 0) !== 0, - !!availableOptions.userToInvite, - debouncedSearchTerm.trim(), - countryCode, - false, - ), + headerMessage, }; return ( @@ -186,7 +198,6 @@ function BaseVacationDelegateSelectionComponent({ textInputOptions={textInputOptions} shouldShowLoadingPlaceholder={!areOptionsInitialized} isLoadingNewOptions={!!isSearchingForReports} - onEndReached={onListEndReached} shouldSingleExecuteRowSelect shouldShowTextInput /> diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 816bc38fcf83..6945afc2e623 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -7,12 +7,12 @@ import UserListItem from '@components/SelectionList/ListItem/UserListItem'; import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSearchSelector from '@hooks/useSearchSelector'; +import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchUserInServer} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; -import {getHeaderMessage} from '@libs/OptionsListUtils'; -import type {OptionData} from '@libs/ReportUtils'; +import {getHeaderMessage} from '@libs/PersonalDetailOptionsListUtils'; +import type {OptionData} from '@libs/PersonalDetailOptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -33,47 +33,60 @@ function AddDelegatePage() { {} as Record, ) ?? {}; - const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized, setSelectedOptions, onListEndReached} = useSearchSelector({ + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedNonExistingOptions, areOptionsInitialized, toggleSelection} = usePersonalDetailSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, includeUserToInvite: true, excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, includeRecentReports: true, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, shouldKeepSelectedInAvailableOptions: true, + shouldUpdateSelectedOptionsOnSingleSelect: true, }); const handleSelectRow = (option: OptionData) => { - setSelectedOptions([option]); + // toggleSelection would deselect an already-selected row on re-tap, so only select when it isn't selected yet + if (!option.isSelected) { + toggleSelection(option); + } Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(option.login ?? '')); }; - const headerMessage = getHeaderMessage( - (availableOptions.recentReports?.length || 0) + (availableOptions.personalDetails?.length || 0) !== 0, - !!availableOptions.userToInvite, - debouncedSearchTerm, - countryCode, - ); - const sectionsList = [ - { - title: translate('common.recents'), - sectionIndex: 0, - data: availableOptions.recentReports, - }, - { - title: translate('common.contacts'), - sectionIndex: 1, - data: availableOptions.personalDetails, - }, - ]; + const sectionsList = (() => { + const list = []; + if (selectedNonExistingOptions.length > 0) { + list.push({ + title: undefined, + sectionIndex: 0, + data: selectedNonExistingOptions, + }); + } + + if (availableOptions.recentOptions?.length) { + list.push({ + title: translate('common.recents'), + sectionIndex: 1, + data: availableOptions.recentOptions, + }); + } + + if (availableOptions.personalDetails?.length) { + list.push({ + title: translate('common.contacts'), + sectionIndex: 2, + data: availableOptions.personalDetails, + }); + } - if (availableOptions.userToInvite) { - sectionsList.push({ - sectionIndex: 2, - title: '', - data: [availableOptions.userToInvite], - }); - } + if (availableOptions.userToInvite) { + list.push({ + sectionIndex: 3, + title: '', + data: [availableOptions.userToInvite], + }); + } + + return list; + })(); const sections = sectionsList.map((section) => ({ ...section, @@ -84,10 +97,18 @@ function AddDelegatePage() { keyForList: `${option.keyForList}-${index}`, isDisabled: option.isDisabled ?? undefined, login: option.login ?? undefined, - shouldShowSubscript: option.shouldShowSubscript ?? undefined, + shouldShowSubscript: undefined, })), })); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); + const headerMessage = (() => { + if (sections.length > 0) { + return ''; + } + return getHeaderMessage(translate, searchValue, countryCode); + })(); + useEffect(() => { searchUserInServer(debouncedSearchTerm); }, [debouncedSearchTerm]); @@ -117,7 +138,6 @@ function AddDelegatePage() { shouldShowLoadingPlaceholder={!areOptionsInitialized} isLoadingNewOptions={!!isSearchingForReports} shouldShowTextInput - onEndReached={onListEndReached} /> diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 71d808e8c11f..3f29c634a773 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -10,15 +10,13 @@ import ScreenWrapper from '@components/ScreenWrapper'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import type {ListItem} from '@components/SelectionList/types'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useHasOutstandingChildTask from '@hooks/useHasOutstandingChildTask'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchUserInServer} from '@libs/actions/Report'; import {canModifyTask, editTaskAssignee, setAssigneeValue} from '@libs/actions/Task'; @@ -26,9 +24,8 @@ import {READ_COMMANDS} from '@libs/API/types'; import HttpUtils from '@libs/HttpUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import {getHeaderMessage, isCurrentUser} from '@libs/OptionsListUtils'; +import {getHeaderMessage} from '@libs/PersonalDetailOptionsListUtils'; import {isOpenTaskReport, isTaskReport} from '@libs/ReportUtils'; -import {expensifyLoginsSelector} from '@libs/UserUtils'; import type {NewTaskNavigatorParamList, TaskDetailsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -52,39 +49,16 @@ function TaskAssigneeSelectorModal() { const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserEmail = currentUserPersonalDetails.email ?? ''; - const [loginList] = useOnyx(ONYXKEYS.LOGINS, {selector: expensifyLoginsSelector}); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); - const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized} = useSearchSelector({ + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized} = usePersonalDetailSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, includeUserToInvite: true, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - getValidOptionsConfig: { - includeCurrentUser: true, - }, + includeRecentReports: true, }); - const optionsWithoutCurrentUser = !currentUserPersonalDetails?.accountID - ? availableOptions - : { - ...availableOptions, - personalDetails: availableOptions.personalDetails.filter((detail) => detail.accountID !== currentUserPersonalDetails.accountID), - recentReports: availableOptions.recentReports.filter((report) => report.accountID !== currentUserPersonalDetails.accountID), - }; - - const recentReportsLength = optionsWithoutCurrentUser.recentReports?.length || 0; - const personalDetailsLength = optionsWithoutCurrentUser.personalDetails?.length || 0; - - const headerMessage = getHeaderMessage( - recentReportsLength + personalDetailsLength !== 0 || !!optionsWithoutCurrentUser.currentUserOption, - !!optionsWithoutCurrentUser.userToInvite, - debouncedSearchTerm, - countryCode, - false, - ); - const allPersonalDetails = usePersonalDetails(); const report: OnyxEntry = (() => { @@ -104,35 +78,43 @@ function TaskAssigneeSelectorModal() { const hasOutstandingChildTask = useHasOutstandingChildTask(report); - const sectionsList = []; + const sectionsList = (() => { + const list = []; - if (optionsWithoutCurrentUser.currentUserOption) { - sectionsList.push({ - title: translate('newTaskPage.assignMe'), - data: [optionsWithoutCurrentUser.currentUserOption], - sectionIndex: 0, - }); - } + if (availableOptions.currentUserOption) { + list.push({ + title: translate('newTaskPage.assignMe'), + data: [availableOptions.currentUserOption], + sectionIndex: 0, + }); + } - sectionsList.push({ - title: translate('common.recents'), - data: optionsWithoutCurrentUser.recentReports, - sectionIndex: 1, - }); + if (availableOptions.recentOptions.length) { + list.push({ + title: translate('common.recents'), + data: availableOptions.recentOptions, + sectionIndex: 1, + }); + } - sectionsList.push({ - title: translate('common.contacts'), - data: optionsWithoutCurrentUser.personalDetails, - sectionIndex: 2, - }); + if (availableOptions.personalDetails.length) { + list.push({ + title: translate('common.contacts'), + data: availableOptions.personalDetails, + sectionIndex: 2, + }); + } - if (optionsWithoutCurrentUser.userToInvite) { - sectionsList.push({ - title: '', - data: [optionsWithoutCurrentUser.userToInvite], - sectionIndex: 3, - }); - } + if (availableOptions.userToInvite) { + list.push({ + title: '', + data: [availableOptions.userToInvite], + sectionIndex: 3, + }); + } + + return list; + })(); const sections = sectionsList.map((section) => ({ ...section, @@ -143,7 +125,7 @@ function TaskAssigneeSelectorModal() { keyForList: option.keyForList ?? '', isDisabled: option.isDisabled ?? undefined, login: option.login ?? undefined, - shouldShowSubscript: option.shouldShowSubscript ?? undefined, + shouldShowSubscript: undefined, isSelected: task?.assigneeAccountID === option.accountID || task?.report?.managerID === option.accountID, })), })); @@ -171,7 +153,7 @@ function TaskAssigneeSelectorModal() { assigneePersonalDetails, report.reportID, undefined, // passing null as report because for editing task the report will be task details report page not the actual report where task was created - isCurrentUser({...option, accountID: option?.accountID ?? CONST.DEFAULT_NUMBER_ID, login: option?.login ?? ''}, loginList, currentUserEmail), + option.accountID === currentUserPersonalDetails.accountID, ); // Pass through the selected assignee editTaskAssignee({ @@ -196,7 +178,7 @@ function TaskAssigneeSelectorModal() { assigneePersonalDetails, task?.shareDestination ?? '', undefined, // passing null as report is null in this condition - isCurrentUser({...option, accountID: option?.accountID ?? CONST.DEFAULT_NUMBER_ID, login: option?.login ?? undefined}, loginList, currentUserEmail), + option.accountID === currentUserPersonalDetails.accountID, ); Navigation.goBack(ROUTES.NEW_TASK.getRoute(backTo)); } @@ -219,6 +201,14 @@ function TaskAssigneeSelectorModal() { searchUserInServer(debouncedSearchTerm); }, [debouncedSearchTerm]); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); + const headerMessage = (() => { + if (sections.length > 0) { + return ''; + } + return getHeaderMessage(translate, searchValue, countryCode); + })(); + const textInputOptions = { value: searchTerm, onChangeText: setSearchTerm, @@ -256,4 +246,4 @@ function TaskAssigneeSelectorModal() { ); } -export default withNavigationTransitionEnd(withCurrentUserPersonalDetails(TaskAssigneeSelectorModal)); +export default TaskAssigneeSelectorModal; diff --git a/src/pages/workspace/WorkspaceConfirmationOwnerSelectorPage.tsx b/src/pages/workspace/WorkspaceConfirmationOwnerSelectorPage.tsx index 1d1379eca10c..37fddd33130c 100644 --- a/src/pages/workspace/WorkspaceConfirmationOwnerSelectorPage.tsx +++ b/src/pages/workspace/WorkspaceConfirmationOwnerSelectorPage.tsx @@ -1,132 +1,96 @@ -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSearchSelector from '@hooks/useSearchSelector'; +import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftValues} from '@libs/actions/FormActions'; import {searchInServer} from '@libs/actions/Report'; -import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; -import {getHeaderMessage} from '@libs/OptionsListUtils'; -import type {OptionWithKey} from '@libs/OptionsListUtils/types'; +import {getHeaderMessage, getUserToInviteOption} from '@libs/PersonalDetailOptionsListUtils'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; -import {generateAccountID} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; import type {Participant} from '@src/types/onyx/IOU'; -import type IconAsset from '@src/types/utils/IconAsset'; - -/** - * Helper function to create a formatted user list item - */ -function createUserListItem( - personalDetails: ReturnType, - login: string, - keyPrefix: string, - isSelected: boolean, - fallBackAvatarIcon: IconAsset, -): OptionWithKey { - const accountID = personalDetails?.accountID ?? generateAccountID(login); - return { - ...(personalDetails ?? {}), - text: personalDetails?.displayName ?? login, - alternateText: personalDetails?.login ?? login, - login: personalDetails?.login ?? login, - keyForList: `${keyPrefix}-${personalDetails?.login ?? login}`, - accountID, - isSelected, - shouldShowSubscript: undefined, - icons: [ - { - source: personalDetails?.avatar ?? fallBackAvatarIcon, - name: formatPhoneNumber(personalDetails?.login ?? login), - type: CONST.ICON_TYPE_AVATAR, - id: accountID, - }, - ], - }; -} +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -function WorkspaceConfirmationOwnerSelectorPage() { - const {translate} = useLocalize(); +type WorkspaceConfirmationOwnerSelectorPageContentProps = { + /** The currently selected workspace owner login (from the draft, falling back to the current user) */ + currentOwner: string; +}; + +function WorkspaceConfirmationOwnerSelectorPageContent({currentOwner}: WorkspaceConfirmationOwnerSelectorPageContentProps) { + const {translate, formatPhoneNumber} = useLocalize(); const styles = useThemeStyles(); - const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); - const {login: currentUserLogin} = useCurrentUserPersonalDetails(); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); - const [isSearchingForReports] = useOnyx(ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS); - const [draftValues] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM_DRAFT); - const currentOwner = draftValues?.owner ?? currentUserLogin ?? ''; - const ownerPersonalDetails = getPersonalDetailByEmail(currentOwner); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.OWNER_SELECTOR.path); - const excludeLogins = useMemo( - () => ({ - ...CONST.EXPENSIFY_EMAILS_OBJECT, - }), - [], - ); + const ownerPersonalDetails = getPersonalDetailByEmail(currentOwner); - const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized, onListEndReached} = useSearchSelector({ + // When the current owner isn't in the personal details list (e.g. an external email), build an optimistic option to seed the selection + const ownerExtraOption = + !currentOwner || ownerPersonalDetails ? undefined : (getUserToInviteOption({searchValue: currentOwner, countryCode, formatPhoneNumber, loginList: {}}) ?? undefined); + + const ownerAccountID = ownerPersonalDetails?.accountID ?? ownerExtraOption?.accountID; + const initialSelected = new Set(ownerAccountID ? [String(ownerAccountID)] : []); + const initialExtraOptions = ownerExtraOption ? [{...ownerExtraOption, isSelected: true}] : []; + + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized} = usePersonalDetailSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, - excludeLogins, + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeRecentReports: true, - getValidOptionsConfig: { - excludeLogins, - }, + includeUserToInvite: true, + initialSelected, + initialExtraOptions, }); - const sections = useMemo(() => { + const sections = (() => { const sectionsList = []; - const currentUserPersonalDetails = getPersonalDetailByEmail(currentUserLogin ?? ''); - if (currentOwner) { - const ownerItem = createUserListItem(ownerPersonalDetails, currentOwner, 'currentOwner', true, icons.FallbackAvatar); + // Current owner (always pinned at the top) — sourced from the hook's selected option + if (availableOptions.selectedOptions.length > 0) { sectionsList.push({ - data: [ownerItem], + data: availableOptions.selectedOptions, sectionIndex: 0, }); } - if (currentUserLogin && currentUserLogin !== currentOwner) { - const currentUserItem = createUserListItem(currentUserPersonalDetails, currentUserLogin, 'currentUser', false, icons.FallbackAvatar); + // "Switch back to me" quick-pick — only when the current user isn't already the owner + if (availableOptions.currentUserOption) { sectionsList.push({ - data: [currentUserItem], + data: [availableOptions.currentUserOption], sectionIndex: 1, }); } - const filteredRecentReports = availableOptions.recentReports?.filter((report) => report.login !== currentOwner) ?? []; - if (filteredRecentReports.length > 0) { + if (availableOptions.recentOptions.length > 0) { sectionsList.push({ title: translate('common.recents'), - data: filteredRecentReports, + data: availableOptions.recentOptions, sectionIndex: 2, }); } - const filteredPersonalDetails = availableOptions.personalDetails?.filter((contact) => contact.login !== currentOwner) ?? []; - if (filteredPersonalDetails.length > 0) { + if (availableOptions.personalDetails.length > 0) { sectionsList.push({ title: translate('common.contacts'), - data: filteredPersonalDetails, + data: availableOptions.personalDetails, sectionIndex: 3, }); } - if (availableOptions.userToInvite && availableOptions.userToInvite.login !== currentOwner) { + if (availableOptions.userToInvite) { sectionsList.push({ data: [availableOptions.userToInvite], sectionIndex: 4, @@ -134,47 +98,37 @@ function WorkspaceConfirmationOwnerSelectorPage() { } return sectionsList; - }, [ - currentOwner, - currentUserLogin, - ownerPersonalDetails, - translate, - availableOptions.recentReports, - availableOptions.personalDetails, - availableOptions.userToInvite, - icons.FallbackAvatar, - ]); - - const onSelectRow = useCallback( - (option: Participant) => { - // Clear search to prevent "No results found" after selection - setSearchTerm(''); - - setDraftValues(ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM, { - [INPUT_IDS.OWNER]: option?.login, - }); + })(); - // Navigate back to the confirmation form - Navigation.goBack(backPath); - }, - [setSearchTerm, backPath], - ); + const onSelectRow = (option: Participant) => { + // Clear search to prevent "No results found" after selection + setSearchTerm(''); + + setDraftValues(ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM, { + [INPUT_IDS.OWNER]: option?.login, + }); + + // Navigate back to the confirmation form + Navigation.goBack(backPath); + }; useEffect(() => { searchInServer(debouncedSearchTerm); }, [debouncedSearchTerm]); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); + const headerMessage = (() => { + if (sections.length > 0) { + return ''; + } + return getHeaderMessage(translate, searchValue, countryCode); + })(); + const textInputOptions = { onChangeText: setSearchTerm, value: searchTerm, label: translate('selectionList.nameEmailOrPhoneNumber'), - headerMessage: getHeaderMessage( - (availableOptions.recentReports?.length || 0) + (availableOptions.personalDetails?.length || 0) !== 0, - !!availableOptions.userToInvite, - debouncedSearchTerm.trim(), - countryCode, - false, - ), + headerMessage, }; return ( @@ -195,7 +149,6 @@ function WorkspaceConfirmationOwnerSelectorPage() { textInputOptions={textInputOptions} shouldShowLoadingPlaceholder={!areOptionsInitialized} isLoadingNewOptions={!!isSearchingForReports} - onEndReached={onListEndReached} shouldSingleExecuteRowSelect /> @@ -203,4 +156,16 @@ function WorkspaceConfirmationOwnerSelectorPage() { ); } +function WorkspaceConfirmationOwnerSelectorPage() { + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + const [draftValues, draftValuesMetadata] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM_DRAFT); + + // Wait for the draft to load so the initial owner selection is seeded correctly on mount + if (isLoadingOnyxValue(draftValuesMetadata)) { + return ; + } + + return ; +} + export default WorkspaceConfirmationOwnerSelectorPage;