Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
204 changes: 52 additions & 152 deletions src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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<MemberForList, Section<MemberForList>>;
type Sections = SectionListData<OptionData, Section<OptionData>>;

function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWorkspaceInviteProps) {
const styles = useThemeStyles();
Expand All @@ -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<MemberForList[]>([]);
const [personalDetails, setPersonalDetails] = useState<OptionData[]>([]);
const [usersToInvite, setUsersToInvite] = useState<OptionData[]>([]);
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);
Expand All @@ -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<number, OptionData> = {};
const newPersonalDetailsDict: Record<number, OptionData> = {};
const newSelectedOptionsDict: Record<number, MemberForList> = {};

// Update selectedOptions with the latest personalDetails and policyEmployeeList information
const detailsMap: Record<string, MemberForList> = {};
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) => {
Expand Down Expand Up @@ -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(
() => (
Expand Down Expand Up @@ -359,7 +259,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo
setSearchTerm(value);
}}
headerMessage={headerMessage}
onSelectRow={toggleOption}
onSelectRow={handleToggleSelection}
onConfirm={inviteUser}
showScrollIndicator
showLoadingPlaceholder={!areOptionsInitialized || !didScreenTransitionEnd}
Expand Down
Loading
Loading