Skip to content

Commit 7ab470f

Browse files
authored
Merge pull request Expensify#73908 from nkdengineer/revert-73877-revert-72677-follow-up-71057
useSearchSelector hook and remove useless code v3
2 parents ca4289f + 037ff8f commit 7ab470f

2 files changed

Lines changed: 101 additions & 323 deletions

File tree

src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx

Lines changed: 53 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,19 @@ import type {SectionListData} from 'react-native';
44
import Button from '@components/Button';
55
import HeaderWithBackButton from '@components/HeaderWithBackButton';
66
import {useSession} from '@components/OnyxListItemProvider';
7-
import {useOptionsList} from '@components/OptionListContextProvider';
87
import ScreenWrapper from '@components/ScreenWrapper';
98
import SelectionList from '@components/SelectionListWithSections';
109
import InviteMemberListItem from '@components/SelectionListWithSections/InviteMemberListItem';
1110
import type {Section} from '@components/SelectionListWithSections/types';
1211
import Text from '@components/Text';
1312
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
14-
import useDebouncedState from '@hooks/useDebouncedState';
1513
import useLocalize from '@hooks/useLocalize';
1614
import useOnboardingMessages from '@hooks/useOnboardingMessages';
1715
import useOnyx from '@hooks/useOnyx';
1816
import usePermissions from '@hooks/usePermissions';
1917
import usePolicy from '@hooks/usePolicy';
2018
import useResponsiveLayout from '@hooks/useResponsiveLayout';
19+
import useSearchSelector from '@hooks/useSearchSelector';
2120
import useThemeStyles from '@hooks/useThemeStyles';
2221
import {addMembersToWorkspace} from '@libs/actions/Policy/Member';
2322
import {searchInServer} from '@libs/actions/Report';
@@ -26,8 +25,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2625
import HttpUtils from '@libs/HttpUtils';
2726
import {appendCountryCode} from '@libs/LoginUtils';
2827
import {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';
3129
import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber';
3230
import {getIneligibleInvitees, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils';
3331
import type {OptionData} from '@libs/ReportUtils';
@@ -36,10 +34,9 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActio
3634
import CONST from '@src/CONST';
3735
import ONYXKEYS from '@src/ONYXKEYS';
3836
import type {InvitedEmailsToAccountIDs} from '@src/types/onyx';
39-
import {isEmptyObject} from '@src/types/utils/EmptyObject';
4037
import type {BaseOnboardingWorkspaceInviteProps} from './types';
4138

42-
type MembersSection = SectionListData<MemberForList, Section<MemberForList>>;
39+
type Sections = SectionListData<OptionData, Section<OptionData>>;
4340

4441
function 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

Comments
 (0)