@@ -4,20 +4,19 @@ import type {SectionListData} from 'react-native';
44import Button from '@components/Button' ;
55import HeaderWithBackButton from '@components/HeaderWithBackButton' ;
66import { useSession } from '@components/OnyxListItemProvider' ;
7- import { useOptionsList } from '@components/OptionListContextProvider' ;
87import ScreenWrapper from '@components/ScreenWrapper' ;
98import SelectionList from '@components/SelectionListWithSections' ;
109import InviteMemberListItem from '@components/SelectionListWithSections/InviteMemberListItem' ;
1110import type { Section } from '@components/SelectionListWithSections/types' ;
1211import Text from '@components/Text' ;
1312import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' ;
14- import useDebouncedState from '@hooks/useDebouncedState' ;
1513import useLocalize from '@hooks/useLocalize' ;
1614import useOnboardingMessages from '@hooks/useOnboardingMessages' ;
1715import useOnyx from '@hooks/useOnyx' ;
1816import usePermissions from '@hooks/usePermissions' ;
1917import usePolicy from '@hooks/usePolicy' ;
2018import useResponsiveLayout from '@hooks/useResponsiveLayout' ;
19+ import useSearchSelector from '@hooks/useSearchSelector' ;
2120import useThemeStyles from '@hooks/useThemeStyles' ;
2221import { addMembersToWorkspace } from '@libs/actions/Policy/Member' ;
2322import { searchInServer } from '@libs/actions/Report' ;
@@ -26,8 +25,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2625import HttpUtils from '@libs/HttpUtils' ;
2726import { appendCountryCode } from '@libs/LoginUtils' ;
2827import { navigateAfterOnboardingWithMicrotaskQueue } from '@libs/navigateAfterOnboarding' ;
29- import type { MemberForList } from '@libs/OptionsListUtils' ;
30- import { filterAndOrderOptions , formatMemberForList , getHeaderMessage , getMemberInviteOptions , getSearchValueForPhoneOrEmail } from '@libs/OptionsListUtils' ;
28+ import { getHeaderMessage } from '@libs/OptionsListUtils' ;
3129import { addSMSDomainIfPhoneNumber , parsePhoneNumber } from '@libs/PhoneNumber' ;
3230import { getIneligibleInvitees , getMemberAccountIDsForWorkspace } from '@libs/PolicyUtils' ;
3331import type { OptionData } from '@libs/ReportUtils' ;
@@ -36,10 +34,9 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActio
3634import CONST from '@src/CONST' ;
3735import ONYXKEYS from '@src/ONYXKEYS' ;
3836import type { InvitedEmailsToAccountIDs } from '@src/types/onyx' ;
39- import { isEmptyObject } from '@src/types/utils/EmptyObject' ;
4037import type { BaseOnboardingWorkspaceInviteProps } from './types' ;
4138
42- type MembersSection = SectionListData < MemberForList , Section < MemberForList > > ;
39+ type Sections = SectionListData < OptionData , Section < OptionData > > ;
4340
4441function BaseOnboardingWorkspaceInvite ( { shouldUseNativeStyles} : BaseOnboardingWorkspaceInviteProps ) {
4542 const styles = useThemeStyles ( ) ;
@@ -51,28 +48,12 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo
5148 // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment
5249 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
5350 const { onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout ( ) ;
54- const [ searchTerm , debouncedSearchTerm , setSearchTerm ] = useDebouncedState ( '' ) ;
55- const [ selectedOptions , setSelectedOptions ] = useState < MemberForList [ ] > ( [ ] ) ;
56- const [ personalDetails , setPersonalDetails ] = useState < OptionData [ ] > ( [ ] ) ;
57- const [ usersToInvite , setUsersToInvite ] = useState < OptionData [ ] > ( [ ] ) ;
5851 const [ didScreenTransitionEnd , setDidScreenTransitionEnd ] = useState ( false ) ;
5952 const [ isSearchingForReports ] = useOnyx ( ONYXKEYS . IS_SEARCHING_FOR_REPORTS , { canBeMissing : true , initWithStoredValues : false } ) ;
60- const [ betas ] = useOnyx ( ONYXKEYS . BETAS , { canBeMissing : false } ) ;
61- const [ nvpDismissedProductTraining ] = useOnyx ( ONYXKEYS . NVP_DISMISSED_PRODUCT_TRAINING , { canBeMissing : true } ) ;
53+ const [ countryCode = CONST . DEFAULT_COUNTRY_CODE ] = useOnyx ( ONYXKEYS . COUNTRY_CODE , { canBeMissing : false } ) ;
6254 const currentUserPersonalDetails = useCurrentUserPersonalDetails ( ) ;
6355 const session = useSession ( ) ;
6456 const { isBetaEnabled} = usePermissions ( ) ;
65- const { options, areOptionsInitialized} = useOptionsList ( {
66- shouldInitialize : didScreenTransitionEnd ,
67- } ) ;
68- const [ countryCode = CONST . DEFAULT_COUNTRY_CODE ] = useOnyx ( ONYXKEYS . COUNTRY_CODE , { canBeMissing : false } ) ;
69-
70- const welcomeNoteSubject = useMemo (
71- ( ) => `# ${ currentUserPersonalDetails ?. displayName ?? '' } invited you to ${ policy ?. name ?? 'a workspace' } ` ,
72- [ policy ?. name , currentUserPersonalDetails ?. displayName ] ,
73- ) ;
74-
75- const welcomeNote = useMemo ( ( ) => translate ( 'workspace.common.welcomeNote' ) , [ translate ] ) ;
7657
7758 const excludedUsers = useMemo ( ( ) => {
7859 const ineligibleInvitees = getIneligibleInvitees ( policy ?. employeeList ) ;
@@ -85,142 +66,65 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo
8566 ) ;
8667 } , [ policy ?. employeeList ] ) ;
8768
88- const defaultOptions = useMemo ( ( ) => {
89- if ( ! areOptionsInitialized ) {
90- return { recentReports : [ ] , personalDetails : [ ] , userToInvite : null , currentUserOption : null } ;
91- }
92-
93- const inviteOptions = getMemberInviteOptions ( options . personalDetails , nvpDismissedProductTraining , betas ?? [ ] , excludedUsers , true ) ;
94-
95- return { ...inviteOptions , recentReports : [ ] , currentUserOption : null } ;
96- } , [ areOptionsInitialized , betas , excludedUsers , nvpDismissedProductTraining , options . personalDetails ] ) ;
69+ const { searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized} = useSearchSelector ( {
70+ selectionMode : CONST . SEARCH_SELECTOR . SELECTION_MODE_MULTI ,
71+ searchContext : CONST . SEARCH_SELECTOR . SEARCH_CONTEXT_MEMBER_INVITE ,
72+ includeUserToInvite : true ,
73+ excludeLogins : excludedUsers ,
74+ includeRecentReports : false ,
75+ shouldInitialize : didScreenTransitionEnd ,
76+ } ) ;
9777
98- const inviteOptions = useMemo (
99- ( ) => filterAndOrderOptions ( defaultOptions , debouncedSearchTerm , countryCode , { excludeLogins : excludedUsers } ) ,
100- [ debouncedSearchTerm , defaultOptions , excludedUsers , countryCode ] ,
78+ const welcomeNoteSubject = useMemo (
79+ ( ) => `# ${ currentUserPersonalDetails ?. displayName ?? '' } invited you to ${ policy ?. name ?? 'a workspace' } ` ,
80+ [ policy ?. name , currentUserPersonalDetails ?. displayName ] ,
10181 ) ;
10282
103- useEffect ( ( ) => {
104- if ( ! areOptionsInitialized ) {
105- return ;
106- }
107-
108- const newUsersToInviteDict : Record < number , OptionData > = { } ;
109- const newPersonalDetailsDict : Record < number , OptionData > = { } ;
110- const newSelectedOptionsDict : Record < number , MemberForList > = { } ;
111-
112- // Update selectedOptions with the latest personalDetails and policyEmployeeList information
113- const detailsMap : Record < string , MemberForList > = { } ;
114- inviteOptions . personalDetails . forEach ( ( detail ) => {
115- if ( ! detail . login ) {
116- return ;
117- }
118-
119- detailsMap [ detail . login ] = formatMemberForList ( detail ) ;
120- } ) ;
121-
122- const newSelectedOptions : MemberForList [ ] = [ ] ;
123- selectedOptions . forEach ( ( option ) => {
124- newSelectedOptions . push ( option . login && option . login in detailsMap ? { ...detailsMap [ option . login ] , isSelected : true } : option ) ;
125- } ) ;
126-
127- const userToInvite = inviteOptions . userToInvite ;
128-
129- // Only add the user to the invitees list if it is valid
130- if ( typeof userToInvite ?. accountID === 'number' ) {
131- newUsersToInviteDict [ userToInvite . accountID ] = userToInvite ;
132- }
133-
134- // Add all personal details to the new dict
135- inviteOptions . personalDetails . forEach ( ( details ) => {
136- if ( typeof details . accountID !== 'number' ) {
137- return ;
138- }
139- newPersonalDetailsDict [ details . accountID ] = details ;
140- } ) ;
141-
142- // Add all selected options to the new dict
143- newSelectedOptions . forEach ( ( option ) => {
144- if ( typeof option . accountID !== 'number' ) {
145- return ;
146- }
147- newSelectedOptionsDict [ option . accountID ] = option ;
148- } ) ;
149-
150- // Strip out dictionary keys and update arrays
151- setUsersToInvite ( Object . values ( newUsersToInviteDict ) ) ;
152- setPersonalDetails ( Object . values ( newPersonalDetailsDict ) ) ;
153- setSelectedOptions ( Object . values ( newSelectedOptionsDict ) ) ;
83+ const welcomeNote = useMemo ( ( ) => translate ( 'workspace.common.welcomeNote' ) , [ translate ] ) ;
15484
155- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change
156- } , [ options . personalDetails , policy ?. employeeList , betas , debouncedSearchTerm , excludedUsers , areOptionsInitialized , inviteOptions . personalDetails , inviteOptions . userToInvite ] ) ;
85+ useEffect ( ( ) => {
86+ searchInServer ( searchTerm ) ;
87+ } , [ searchTerm ] ) ;
15788
158- const sections : MembersSection [ ] = useMemo ( ( ) => {
159- const sectionsArr : MembersSection [ ] = [ ] ;
89+ const sections : Sections [ ] = useMemo ( ( ) => {
90+ const sectionsArr : Sections [ ] = [ ] ;
16091
16192 if ( ! areOptionsInitialized ) {
16293 return [ ] ;
16394 }
16495
165- // Filter all options that is a part of the search term or in the personal details
166- let filterSelectedOptions = selectedOptions ;
167- if ( debouncedSearchTerm !== '' ) {
168- filterSelectedOptions = selectedOptions . filter ( ( option ) => {
169- const accountID = option . accountID ;
170- const isOptionInPersonalDetails = Object . values ( personalDetails ) . some ( ( personalDetail ) => personalDetail . accountID === accountID ) ;
171-
172- const searchValue = getSearchValueForPhoneOrEmail ( debouncedSearchTerm , countryCode ) ;
173-
174- const isPartOfSearchTerm = ! ! option . text ?. toLowerCase ( ) . includes ( searchValue ) || ! ! option . login ?. toLowerCase ( ) . includes ( searchValue ) ;
175- return isPartOfSearchTerm || isOptionInPersonalDetails ;
96+ // Selected options section
97+ if ( selectedOptionsForDisplay . length > 0 ) {
98+ sectionsArr . push ( {
99+ title : undefined ,
100+ data : selectedOptionsForDisplay ,
176101 } ) ;
177102 }
178103
179- sectionsArr . push ( {
180- title : undefined ,
181- data : filterSelectedOptions ,
182- shouldShow : true ,
183- } ) ;
184-
185- // Filtering out selected users from the search results
186- const selectedLoginsSet = new Set ( selectedOptions . map ( ( { login} ) => login ) ) ;
187- const personalDetailsFormatted = Object . values ( personalDetails )
188- . filter ( ( { login} ) => ! selectedLoginsSet . has ( login ?? '' ) )
189- . map ( formatMemberForList ) ;
190-
191- sectionsArr . push ( {
192- title : translate ( 'common.contacts' ) ,
193- data : personalDetailsFormatted ,
194- shouldShow : ! isEmptyObject ( personalDetailsFormatted ) ,
195- } ) ;
196-
197- Object . values ( usersToInvite ) . forEach ( ( userToInvite ) => {
198- const hasUnselectedUserToInvite = ! selectedLoginsSet . has ( userToInvite . login ?? '' ) ;
199-
200- if ( hasUnselectedUserToInvite ) {
201- sectionsArr . push ( {
202- title : undefined ,
203- data : [ formatMemberForList ( userToInvite ) ] ,
204- shouldShow : true ,
205- } ) ;
206- }
207- } ) ;
208-
209- return sectionsArr ;
210- } , [ areOptionsInitialized , selectedOptions , debouncedSearchTerm , personalDetails , translate , usersToInvite , countryCode ] ) ;
211-
212- const toggleOption = ( option : MemberForList ) => {
213- const isOptionInList = selectedOptions . some ( ( selectedOption ) => selectedOption . login === option . login ) ;
104+ // Contacts section
105+ if ( availableOptions . personalDetails . length > 0 ) {
106+ sectionsArr . push ( {
107+ title : translate ( 'common.contacts' ) ,
108+ data : availableOptions . personalDetails ,
109+ } ) ;
110+ }
214111
215- let newSelectedOptions : MemberForList [ ] ;
216- if ( isOptionInList ) {
217- newSelectedOptions = selectedOptions . filter ( ( selectedOption ) => selectedOption . login !== option . login ) ;
218- } else {
219- newSelectedOptions = [ ...selectedOptions , { ...option , isSelected : true } ] ;
112+ // User to invite section
113+ if ( availableOptions . userToInvite ) {
114+ sectionsArr . push ( {
115+ title : undefined ,
116+ data : [ availableOptions . userToInvite ] ,
117+ } ) ;
220118 }
119+ return sectionsArr ;
120+ } , [ areOptionsInitialized , selectedOptionsForDisplay , availableOptions , translate ] ) ;
221121
222- setSelectedOptions ( newSelectedOptions ) ;
223- } ;
122+ const handleToggleSelection = useCallback (
123+ ( option : OptionData ) => {
124+ toggleSelection ( option ) ;
125+ } ,
126+ [ toggleSelection ] ,
127+ ) ;
224128
225129 const completeOnboarding = useCallback (
226130 ( isInvitedAccountant : boolean ) => {
@@ -285,23 +189,19 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo
285189 completeOnboarding ( true ) ;
286190 } , [ completeOnboarding , onboardingPolicyID , policy ?. employeeList , selectedOptions , welcomeNote , welcomeNoteSubject , formatPhoneNumber ] ) ;
287191
288- useEffect ( ( ) => {
289- searchInServer ( debouncedSearchTerm ) ;
290- } , [ debouncedSearchTerm ] ) ;
291-
292192 const headerMessage = useMemo ( ( ) => {
293- const searchValue = debouncedSearchTerm . trim ( ) . toLowerCase ( ) ;
294- if ( usersToInvite . length === 0 && CONST . EXPENSIFY_EMAILS_OBJECT [ searchValue ] ) {
193+ const searchValue = searchTerm . trim ( ) . toLowerCase ( ) ;
194+ if ( ! availableOptions . userToInvite && CONST . EXPENSIFY_EMAILS_OBJECT [ searchValue ] ) {
295195 return translate ( 'messages.errorMessageInvalidEmail' ) ;
296196 }
297197 if (
298- usersToInvite . length === 0 &&
198+ ! availableOptions . userToInvite &&
299199 excludedUsers [ parsePhoneNumber ( appendCountryCode ( searchValue , countryCode ) ) . possible ? addSMSDomainIfPhoneNumber ( appendCountryCode ( searchValue , countryCode ) ) : searchValue ]
300200 ) {
301201 return translate ( 'messages.userIsAlreadyMember' , { login : searchValue , name : policy ?. name ?? '' } ) ;
302202 }
303- return getHeaderMessage ( personalDetails . length !== 0 , usersToInvite . length > 0 , searchValue , countryCode , false ) ;
304- } , [ excludedUsers , translate , debouncedSearchTerm , policy ?. name , usersToInvite , personalDetails . length , countryCode ] ) ;
203+ return getHeaderMessage ( availableOptions . personalDetails . length !== 0 , ! ! availableOptions . userToInvite , searchValue , countryCode , false ) ;
204+ } , [ excludedUsers , translate , searchTerm , policy ?. name , availableOptions . personalDetails . length , availableOptions . userToInvite , countryCode ] ) ;
305205
306206 const footerContent = useMemo (
307207 ( ) => (
@@ -360,7 +260,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo
360260 setSearchTerm ( value ) ;
361261 } }
362262 headerMessage = { headerMessage }
363- onSelectRow = { toggleOption }
263+ onSelectRow = { handleToggleSelection }
364264 onConfirm = { inviteUser }
365265 showScrollIndicator
366266 showLoadingPlaceholder = { ! areOptionsInitialized || ! didScreenTransitionEnd }
0 commit comments