diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index 2f11adbd48de..f02d6f6f1866 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -4,20 +4,19 @@ import type {SectionListData} from 'react-native'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useSession} from '@components/OnyxListItemProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionListWithSections'; import InviteMemberListItem from '@components/SelectionListWithSections/InviteMemberListItem'; import type {Section} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useOnboardingMessages from '@hooks/useOnboardingMessages'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {addMembersToWorkspace} from '@libs/actions/Policy/Member'; import {searchInServer} from '@libs/actions/Report'; @@ -26,8 +25,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import HttpUtils from '@libs/HttpUtils'; import {appendCountryCode} from '@libs/LoginUtils'; import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; -import type {MemberForList} from '@libs/OptionsListUtils'; -import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; +import {getHeaderMessage} from '@libs/OptionsListUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -36,10 +34,9 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActio import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {BaseOnboardingWorkspaceInviteProps} from './types'; -type MembersSection = SectionListData>; +type Sections = SectionListData>; function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWorkspaceInviteProps) { const styles = useThemeStyles(); @@ -51,27 +48,11 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout(); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [selectedOptions, setSelectedOptions] = useState([]); - const [personalDetails, setPersonalDetails] = useState([]); - const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {canBeMissing: true, initWithStoredValues: false}); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const session = useSession(); const {isBetaEnabled} = usePermissions(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); - - const welcomeNoteSubject = useMemo( - () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, - [policy?.name, currentUserPersonalDetails?.displayName], - ); - - const welcomeNote = useMemo(() => translate('workspace.common.welcomeNote'), [translate]); const excludedUsers = useMemo(() => { const ineligibleInvitees = getIneligibleInvitees(policy?.employeeList); @@ -84,142 +65,65 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo ); }, [policy?.employeeList]); - const defaultOptions = useMemo(() => { - if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null}; - } - - const inviteOptions = getMemberInviteOptions(options.personalDetails, betas ?? [], excludedUsers, true); - - return {...inviteOptions, recentReports: [], currentUserOption: null}; - }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); + const {searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized} = useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, + includeUserToInvite: true, + excludeLogins: excludedUsers, + includeRecentReports: true, + shouldInitialize: didScreenTransitionEnd, + }); - const inviteOptions = useMemo( - () => filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, {excludeLogins: excludedUsers}), - [debouncedSearchTerm, defaultOptions, excludedUsers, countryCode], + const welcomeNoteSubject = useMemo( + () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, + [policy?.name, currentUserPersonalDetails?.displayName], ); - useEffect(() => { - if (!areOptionsInitialized) { - return; - } - - const newUsersToInviteDict: Record = {}; - const newPersonalDetailsDict: Record = {}; - const newSelectedOptionsDict: Record = {}; - - // Update selectedOptions with the latest personalDetails and policyEmployeeList information - const detailsMap: Record = {}; - inviteOptions.personalDetails.forEach((detail) => { - if (!detail.login) { - return; - } - - detailsMap[detail.login] = formatMemberForList(detail); - }); - - const newSelectedOptions: MemberForList[] = []; - selectedOptions.forEach((option) => { - newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); - }); - - const userToInvite = inviteOptions.userToInvite; - - // Only add the user to the invitees list if it is valid - if (typeof userToInvite?.accountID === 'number') { - newUsersToInviteDict[userToInvite.accountID] = userToInvite; - } - - // Add all personal details to the new dict - inviteOptions.personalDetails.forEach((details) => { - if (typeof details.accountID !== 'number') { - return; - } - newPersonalDetailsDict[details.accountID] = details; - }); - - // Add all selected options to the new dict - newSelectedOptions.forEach((option) => { - if (typeof option.accountID !== 'number') { - return; - } - newSelectedOptionsDict[option.accountID] = option; - }); - - // Strip out dictionary keys and update arrays - setUsersToInvite(Object.values(newUsersToInviteDict)); - setPersonalDetails(Object.values(newPersonalDetailsDict)); - setSelectedOptions(Object.values(newSelectedOptionsDict)); + const welcomeNote = useMemo(() => translate('workspace.common.welcomeNote'), [translate]); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [options.personalDetails, policy?.employeeList, betas, debouncedSearchTerm, excludedUsers, areOptionsInitialized, inviteOptions.personalDetails, inviteOptions.userToInvite]); + useEffect(() => { + searchInServer(searchTerm); + }, [searchTerm]); - const sections: MembersSection[] = useMemo(() => { - const sectionsArr: MembersSection[] = []; + const sections: Sections[] = useMemo(() => { + const sectionsArr: Sections[] = []; if (!areOptionsInitialized) { return []; } - // Filter all options that is a part of the search term or in the personal details - let filterSelectedOptions = selectedOptions; - if (debouncedSearchTerm !== '') { - filterSelectedOptions = selectedOptions.filter((option) => { - const accountID = option.accountID; - const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode); - - const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); - return isPartOfSearchTerm || isOptionInPersonalDetails; + // Selected options section + if (selectedOptionsForDisplay.length > 0) { + sectionsArr.push({ + title: undefined, + data: selectedOptionsForDisplay, }); } - sectionsArr.push({ - title: undefined, - data: filterSelectedOptions, - shouldShow: true, - }); - - // Filtering out selected users from the search results - const selectedLoginsSet = new Set(selectedOptions.map(({login}) => login)); - const personalDetailsFormatted = Object.values(personalDetails) - .filter(({login}) => !selectedLoginsSet.has(login ?? '')) - .map(formatMemberForList); - - sectionsArr.push({ - title: translate('common.contacts'), - data: personalDetailsFormatted, - shouldShow: !isEmptyObject(personalDetailsFormatted), - }); - - Object.values(usersToInvite).forEach((userToInvite) => { - const hasUnselectedUserToInvite = !selectedLoginsSet.has(userToInvite.login ?? ''); - - if (hasUnselectedUserToInvite) { - sectionsArr.push({ - title: undefined, - data: [formatMemberForList(userToInvite)], - shouldShow: true, - }); - } - }); - - return sectionsArr; - }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, personalDetails, translate, usersToInvite, countryCode]); - - const toggleOption = (option: MemberForList) => { - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); + // Contacts section + if (availableOptions.personalDetails.length > 0) { + sectionsArr.push({ + title: translate('common.contacts'), + data: availableOptions.personalDetails, + }); + } - let newSelectedOptions: MemberForList[]; - if (isOptionInList) { - newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); - } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; + // User to invite section + if (availableOptions.userToInvite) { + sectionsArr.push({ + title: undefined, + data: [availableOptions.userToInvite], + }); } + return sectionsArr; + }, [areOptionsInitialized, selectedOptionsForDisplay, availableOptions, translate]); - setSelectedOptions(newSelectedOptions); - }; + const handleToggleSelection = useCallback( + (option: OptionData) => { + toggleSelection(option); + }, + [toggleSelection], + ); const completeOnboarding = useCallback( (isInvitedAccountant: boolean) => { @@ -284,23 +188,19 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo completeOnboarding(true); }, [completeOnboarding, onboardingPolicyID, policy?.employeeList, selectedOptions, welcomeNote, welcomeNoteSubject, formatPhoneNumber]); - useEffect(() => { - searchInServer(debouncedSearchTerm); - }, [debouncedSearchTerm]); - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); - if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { + const searchValue = searchTerm.trim().toLowerCase(); + if (!availableOptions.userToInvite && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { return translate('messages.errorMessageInvalidEmail'); } if ( - usersToInvite.length === 0 && + !availableOptions.userToInvite && excludedUsers[parsePhoneNumber(appendCountryCode(searchValue)).possible ? addSMSDomainIfPhoneNumber(appendCountryCode(searchValue)) : searchValue] ) { return translate('messages.userIsAlreadyMember', {login: searchValue, name: policy?.name ?? ''}); } - return getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue); - }, [excludedUsers, translate, debouncedSearchTerm, policy?.name, usersToInvite, personalDetails.length]); + return getHeaderMessage(availableOptions.personalDetails.length !== 0, !!availableOptions.userToInvite, searchValue); + }, [excludedUsers, translate, searchTerm, policy?.name, availableOptions.personalDetails.length, availableOptions.userToInvite]); const footerContent = useMemo( () => ( @@ -359,7 +259,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo setSearchTerm(value); }} headerMessage={headerMessage} - onSelectRow={toggleOption} + onSelectRow={handleToggleSelection} onConfirm={inviteUser} showScrollIndicator showLoadingPlaceholder={!areOptionsInitialized || !didScreenTransitionEnd} diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 6d1b6a259c4c..3d649c45ac80 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,18 +1,17 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionListWithSections'; import InviteMemberListItem from '@components/SelectionListWithSections/InviteMemberListItem'; import type {Section} from '@components/SelectionListWithSections/types'; import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; -import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceInviteMembersDraft} from '@libs/actions/Policy/Member'; import {clearErrors, openWorkspaceInvitePage as policyOpenWorkspaceInvitePage, setWorkspaceErrors} from '@libs/actions/Policy/Policy'; @@ -23,8 +22,7 @@ import HttpUtils from '@libs/HttpUtils'; import {appendCountryCode} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail, getUserToInviteOption} from '@libs/OptionsListUtils'; -import type {MemberForList} from '@libs/OptionsListUtils'; +import {getHeaderMessage} from '@libs/OptionsListUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -40,7 +38,7 @@ import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -type MembersSection = SectionListData>; +type Sections = SectionListData>; type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WithNavigationTransitionEndProps & @@ -49,24 +47,13 @@ type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [selectedOptions, setSelectedOptions] = useState([]); - const [personalDetails, setPersonalDetails] = useState([]); - const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const firstRenderRef = useRef(true); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); - const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, {canBeMissing: true}); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); policyOpenWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); useEffect(() => { clearErrors(route.params.policyID); @@ -87,165 +74,55 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { ); }, [policy?.employeeList]); - const defaultOptions = useMemo(() => { - if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null}; - } - - const inviteOptions = getMemberInviteOptions(options.personalDetails, betas ?? [], excludedUsers, true); - - return {...inviteOptions, recentReports: [], currentUserOption: null}; - }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); + const {searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized} = useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, + includeUserToInvite: true, + excludeLogins: excludedUsers, + includeRecentReports: true, + shouldInitialize: didScreenTransitionEnd, + }); - const inviteOptions = useMemo( - () => filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, {excludeLogins: excludedUsers}), - [debouncedSearchTerm, defaultOptions, excludedUsers, countryCode], - ); + const sections: Sections[] = useMemo(() => { + const sectionsArr: Sections[] = []; - useEffect(() => { if (!areOptionsInitialized) { - return; + return []; } - const newUsersToInviteDict: Record = {}; - const newPersonalDetailsDict: Record = {}; - const newSelectedOptionsDict: Record = {}; - - // Update selectedOptions with the latest personalDetails and policyEmployeeList information - const detailsMap: Record = {}; - inviteOptions.personalDetails.forEach((detail) => { - if (!detail.login) { - return; - } - - detailsMap[detail.login] = formatMemberForList(detail); - }); - - const newSelectedOptions: MemberForList[] = []; - if (firstRenderRef.current) { - // We only want to add the saved selected user on first render - firstRenderRef.current = false; - Object.entries(invitedEmailsToAccountIDsDraft ?? {}).forEach(([login, accountID]) => { - if (login in detailsMap) { - newSelectedOptions.push({...detailsMap[login], isSelected: true}); - } else { - const optimisticOption = getUserToInviteOption({ - searchValue: login, - }); - if (optimisticOption) { - optimisticOption.accountID = Number(accountID); - const memberOption = formatMemberForList({ - ...optimisticOption, - accountID: Number(accountID), - isSelected: true, - }); - newSelectedOptions.push(memberOption); - } - } + // Selected options section + if (selectedOptionsForDisplay.length > 0) { + sectionsArr.push({ + title: undefined, + data: selectedOptionsForDisplay, }); } - selectedOptions.forEach((option) => { - newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); - }); - - const userToInvite = inviteOptions.userToInvite; - - // Only add the user to the invites list if it is valid - if (typeof userToInvite?.accountID === 'number') { - newUsersToInviteDict[userToInvite.accountID] = userToInvite; - } - // Add all personal details to the new dict - inviteOptions.personalDetails.forEach((details) => { - if (typeof details.accountID !== 'number') { - return; - } - newPersonalDetailsDict[details.accountID] = details; - }); - - // Add all selected options to the new dict - newSelectedOptions.forEach((option) => { - if (typeof option.accountID !== 'number') { - return; - } - newSelectedOptionsDict[option.accountID] = option; - }); - - // Strip out dictionary keys and update arrays - setUsersToInvite(Object.values(newUsersToInviteDict)); - setPersonalDetails(Object.values(newPersonalDetailsDict)); - setSelectedOptions(Object.values(newSelectedOptionsDict)); - - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [options.personalDetails, policy?.employeeList, betas, debouncedSearchTerm, excludedUsers, areOptionsInitialized, inviteOptions.personalDetails, inviteOptions.userToInvite]); - - const sections: MembersSection[] = useMemo(() => { - const sectionsArr: MembersSection[] = []; - - if (!areOptionsInitialized) { - return []; + // Contacts section + if (availableOptions.personalDetails.length > 0) { + sectionsArr.push({ + title: translate('common.contacts'), + data: availableOptions.personalDetails, + }); } - // Filter all options that is a part of the search term or in the personal details - let filterSelectedOptions = selectedOptions; - if (debouncedSearchTerm !== '') { - filterSelectedOptions = selectedOptions.filter((option) => { - const accountID = option.accountID; - const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode); - - const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); - return isPartOfSearchTerm || isOptionInPersonalDetails; + // User to invite section + if (availableOptions.userToInvite) { + sectionsArr.push({ + title: undefined, + data: [availableOptions.userToInvite], }); } - sectionsArr.push({ - title: undefined, - data: filterSelectedOptions, - shouldShow: true, - }); - - // Filtering out selected users from the search results - const selectedLogins = selectedOptions.map(({login}) => login); - const personalDetailsWithoutSelected = Object.values(personalDetails).filter(({login}) => !selectedLogins.some((selectedLogin) => selectedLogin === login)); - const personalDetailsFormatted = personalDetailsWithoutSelected.map((item) => formatMemberForList(item)); - - sectionsArr.push({ - title: translate('common.contacts'), - data: personalDetailsFormatted, - shouldShow: !isEmptyObject(personalDetailsFormatted), - }); - - Object.values(usersToInvite).forEach((userToInvite) => { - const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); - - if (hasUnselectedUserToInvite) { - sectionsArr.push({ - title: undefined, - data: [formatMemberForList(userToInvite)], - shouldShow: true, - }); - } - }); - return sectionsArr; - }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, personalDetails, translate, usersToInvite, countryCode]); + }, [areOptionsInitialized, selectedOptionsForDisplay, availableOptions, translate]); - const toggleOption = (option: MemberForList) => { - clearErrors(route.params.policyID); - - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - - let newSelectedOptions: MemberForList[]; - if (isOptionInList) { - newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); - } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; - } - - setSelectedOptions(newSelectedOptions); - }; + const handleToggleSelection = useCallback( + (option: OptionData) => { + toggleSelection(option); + }, + [toggleSelection], + ); const inviteUser = useCallback(() => { const errors: Errors = {}; @@ -277,18 +154,18 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]); const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); - if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { + const searchValue = searchTerm.trim().toLowerCase(); + if (!availableOptions.userToInvite && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { return translate('messages.errorMessageInvalidEmail'); } if ( - usersToInvite.length === 0 && + !availableOptions.userToInvite && excludedUsers[parsePhoneNumber(appendCountryCode(searchValue)).possible ? addSMSDomainIfPhoneNumber(appendCountryCode(searchValue)) : searchValue] ) { return translate('messages.userIsAlreadyMember', {login: searchValue, name: policyName}); } - return getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue); - }, [excludedUsers, translate, debouncedSearchTerm, policyName, usersToInvite, personalDetails.length]); + return getHeaderMessage(availableOptions.personalDetails.length !== 0, !!availableOptions.userToInvite, searchValue); + }, [excludedUsers, translate, searchTerm, policyName, availableOptions.personalDetails.length, availableOptions.userToInvite]); const footerContent = useMemo( () => ( @@ -306,8 +183,8 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { ); useEffect(() => { - searchInServer(debouncedSearchTerm); - }, [debouncedSearchTerm]); + searchInServer(searchTerm); + }, [searchTerm]); return (