diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cbf4d7714967..b46d3db8b60d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -465,7 +465,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs | undefined; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string | undefined; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index b8a92820e6c0..be871971ee92 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -55,7 +55,7 @@ type User = { login?: string; /** Element to show on the right side of the item */ - rightElement?: ReactElement; + rightElement?: ReactNode; /** Icons for the user (can be multiple if it's a Workspace) */ icons?: Icon[]; @@ -129,6 +129,9 @@ type Section = { /** Whether this section items disabled for selection */ isDisabled?: boolean; + + /** Whether this section should be shown or not */ + shouldShow?: boolean; }; type BaseSelectionListProps = Partial & { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 44449fecd517..046a66a8d6e0 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -5,6 +5,7 @@ import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; +import type {ReactElement} from 'react'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; @@ -29,7 +30,6 @@ import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; @@ -118,13 +118,13 @@ type GetOptionsConfig = { type MemberForList = { text: string; - alternateText: string | null; - keyForList: string | null; + alternateText: string; + keyForList: string; isSelected: boolean; - isDisabled: boolean | null; - accountID?: number | null; - login: string | null; - rightElement: React.ReactNode | null; + isDisabled: boolean; + accountID?: number; + login: string; + rightElement: ReactElement | null; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; }; @@ -1837,12 +1837,14 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config?: Partial): MemberForList; +function formatMemberForList(member: null | undefined, config?: Partial): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: Partial = {}): MemberForList | undefined { if (!member) { return undefined; } - const accountID = member.accountID; + const accountID = member.accountID ?? undefined; return { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -2022,3 +2024,5 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, }; + +export type {MemberForList}; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 866206895d5e..498ce6918509 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -35,7 +35,19 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Policy, PolicyMember, PolicyTags, RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, Report, ReportAction, Transaction} from '@src/types/onyx'; +import type { + InvitedEmailsToAccountIDs, + PersonalDetailsList, + Policy, + PolicyMember, + PolicyTags, + RecentlyUsedCategories, + RecentlyUsedTags, + ReimbursementAccount, + Report, + ReportAction, + Transaction, +} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {CustomUnit} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -518,7 +530,7 @@ function removeMembers(accountIDs: number[], policyID: string) { * * @returns - object with onyxSuccessData, onyxOptimisticData, and optimisticReportIDs (map login to reportID) */ -function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: Record, hasOutstandingChildRequest = false): WorkspaceMembersChats { +function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, hasOutstandingChildRequest = false): WorkspaceMembersChats { const workspaceMembersChats: WorkspaceMembersChats = { onyxSuccessData: [], onyxOptimisticData: [], @@ -607,7 +619,7 @@ function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: R /** * Adds members to the specified workspace/policyID */ -function addMembersToWorkspace(invitedEmailsToAccountIDs: Record, welcomeNote: string, policyID: string) { +function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string) { const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const; const logins = Object.keys(invitedEmailsToAccountIDs).map((memberLogin) => OptionsListUtils.addSMSDomainIfPhoneNumber(memberLogin)); const accountIDs = Object.values(invitedEmailsToAccountIDs); @@ -1499,7 +1511,7 @@ function openDraftWorkspaceRequest(policyID: string) { API.read(READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST, params); } -function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccountIDs: Record) { +function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs) { Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, invitedEmailsToAccountIDs); } diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index 3f55ad6c0ff7..9c43a1820aa9 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -86,7 +86,7 @@ function RoomInvitePage(props) { // Update selectedOptions with the latest personalDetails information const detailsMap = {}; - _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail, false))); + _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); const newSelectedOptions = []; _.forEach(selectedOptions, (option) => { newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); @@ -142,7 +142,7 @@ function RoomInvitePage(props) { // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); - const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false)); + const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login); sectionsArr.push({ @@ -156,7 +156,7 @@ function RoomInvitePage(props) { if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, - data: [OptionsListUtils.formatMemberForList(userToInvite, false)], + data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, indexOffset, }); diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.tsx similarity index 64% rename from src/pages/workspace/WorkspaceInviteMessagePage.js rename to src/pages/workspace/WorkspaceInviteMessagePage.tsx index bd5de51e0503..2e43708ed3ce 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,130 +1,110 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; +import lodashDebounce from 'lodash/debounce'; import React, {useEffect, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withNavigationFocus from '@components/withNavigationFocus'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as Link from '@userActions/Link'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -const personalDetailsPropTypes = PropTypes.shape({ - /** The accountID of the person */ - accountID: PropTypes.number.isRequired, - - /** The login of the person (either email or phone number) */ - login: PropTypes.string, - - /** The URL of the person's avatar (there should already be a default avatar if - the person doesn't have their own avatar uploaded yet, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); - -const propTypes = { +type WorkspaceInviteMessagePageOnyxProps = { /** All of the personal details for everyone */ - allPersonalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), + allPersonalDetails: OnyxEntry; - /** URL Route params */ - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** policyID passed via route: /workspace/:policyID/invite-message */ - policyID: PropTypes.string, - }), - }).isRequired, + /** An object containing the accountID for every invited user email */ + invitedEmailsToAccountIDsDraft: OnyxEntry; - ...policyPropTypes, + /** Updated workspace invite message */ + workspaceInviteMessageDraft: OnyxEntry; }; -const defaultProps = { - ...policyDefaultProps, - allPersonalDetails: {}, - invitedEmailsToAccountIDsDraft: {}, -}; +type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & + WorkspaceInviteMessagePageOnyxProps & + StackScreenProps; -function WorkspaceInviteMessagePage(props) { +function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, policy, route, allPersonalDetails}: WorkspaceInviteMessagePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [welcomeNote, setWelcomeNote] = useState(); + const [welcomeNote, setWelcomeNote] = useState(); const {inputCallbackRef} = useAutoFocusInput(); const getDefaultWelcomeNote = () => - props.workspaceInviteMessageDraft || + // workspaceInviteMessageDraft can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + workspaceInviteMessageDraft || translate('workspace.inviteMessage.welcomeNote', { - workspaceName: props.policy.name, + workspaceName: policy?.name ?? '', }); useEffect(() => { - if (!_.isEmpty(props.invitedEmailsToAccountIDsDraft)) { + if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { setWelcomeNote(getDefaultWelcomeNote()); return; } - Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID), true); + Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const debouncedSaveDraft = _.debounce((newDraft) => { - Policy.setWorkspaceInviteMessageDraft(props.route.params.policyID, newDraft); + const debouncedSaveDraft = lodashDebounce((newDraft: string) => { + Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft); }); const sendInvitation = () => { Keyboard.dismiss(); - Policy.addMembersToWorkspace(props.invitedEmailsToAccountIDsDraft, welcomeNote, props.route.params.policyID); - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + Policy.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, welcomeNote ?? '', route.params.policyID); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); SearchInputManager.searchInput = ''; // Pop the invite message page before navigating to the members page. Navigation.goBack(); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); }; - /** - * Opens privacy url as an external link - * @param {Object} event - */ - const openPrivacyURL = (event) => { - event.preventDefault(); + /** Opens privacy url as an external link */ + const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { + event?.preventDefault(); Link.openExternalLink(CONST.PRIVACY_URL); }; - const validate = () => { - const errorFields = {}; - if (_.isEmpty(props.invitedEmailsToAccountIDsDraft)) { + const validate = (): Errors => { + const errorFields: Errors = {}; + if (isEmptyObject(invitedEmailsToAccountIDsDraft)) { errorFields.welcomeMessage = 'workspace.inviteMessage.inviteNoMembersError'; } return errorFields; }; - const policyName = lodashGet(props.policy, 'name'); + const policyName = policy?.name; return ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > Navigation.dismissModal()} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))} /> { + onChangeText={(text: string) => { setWelcomeNote(text); debouncedSaveDraft(text); }} - ref={(el) => { - if (!el) { + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } - inputCallbackRef(el); - updateMultilineInputRange(el); + inputCallbackRef(element); + updateMultilineInputRange(element); }} /> @@ -210,13 +194,10 @@ function WorkspaceInviteMessagePage(props) { ); } -WorkspaceInviteMessagePage.propTypes = propTypes; -WorkspaceInviteMessagePage.defaultProps = defaultProps; WorkspaceInviteMessagePage.displayName = 'WorkspaceInviteMessagePage'; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ +export default withPolicyAndFullscreenLoading( + withOnyx({ allPersonalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, @@ -226,6 +207,5 @@ export default compose( workspaceInviteMessageDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${route.params.policyID.toString()}`, }, - }), - withNavigationFocus, -)(WorkspaceInviteMessagePage); + })(WorkspaceInviteMessagePage), +); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.tsx similarity index 57% rename from src/pages/workspace/WorkspaceInvitePage.js rename to src/pages/workspace/WorkspaceInvitePage.tsx index 8fcf425d8f34..ae139752a052 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,98 +1,87 @@ import {useNavigation} from '@react-navigation/native'; +import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import type {Section} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {MemberForList} from '@libs/OptionsListUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Beta, InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -const personalDetailsPropTypes = PropTypes.shape({ - /** The login of the person (either email or phone number) */ - login: PropTypes.string, +type MembersSection = SectionListData>; - /** The URL of the person's avatar (there should already be a default avatar if - the person doesn't have their own avatar uploaded yet, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); +type WorkspaceInvitePageOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; -const propTypes = { /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - /** URL Route params */ - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** policyID passed via route: /workspace/:policyID/invite */ - policyID: PropTypes.string, - }), - }).isRequired, - - isLoadingReportData: PropTypes.bool, - invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), - ...policyPropTypes, -}; + betas: OnyxEntry; -const defaultProps = { - personalDetails: {}, - betas: [], - isLoadingReportData: true, - invitedEmailsToAccountIDsDraft: {}, - ...policyDefaultProps, + /** An object containing the accountID for every invited user email */ + invitedEmailsToAccountIDsDraft: OnyxEntry; }; -function WorkspaceInvitePage(props) { +type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps; + +function WorkspaceInvitePage({ + route, + policyMembers, + personalDetails: personalDetailsProp, + betas, + invitedEmailsToAccountIDsDraft, + policy, + isLoadingReportData = true, +}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); - const [selectedOptions, setSelectedOptions] = useState([]); - const [personalDetails, setPersonalDetails] = useState([]); - const [usersToInvite, setUsersToInvite] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + const [personalDetails, setPersonalDetails] = useState([]); + const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation = useNavigation(); + const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { - const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails); - Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); + Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; useEffect(() => { setSearchTerm(SearchInputManager.searchInput); return () => { - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); }; - }, [props.route.params.policyID]); + }, [route.params.policyID]); useEffect(() => { - Policy.clearErrors(props.route.params.policyID); + Policy.clearErrors(route.params.policyID); openWorkspaceInvitePage(); // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); @@ -111,57 +100,69 @@ function WorkspaceInvitePage(props) { useNetwork({onReconnect: openWorkspaceInvitePage}); - const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); + const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policyMembers, personalDetailsProp), [policyMembers, personalDetailsProp]); useEffect(() => { - const newUsersToInviteDict = {}; - const newPersonalDetailsDict = {}; - const newSelectedOptionsDict = {}; + const newUsersToInviteDict: Record = {}; + const newPersonalDetailsDict: Record = {}; + const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap = {}; - _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); + const detailsMap: Record = {}; + inviteOptions.personalDetails.forEach((detail) => { + if (!detail.login) { + return; + } - const newSelectedOptions = []; - _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => { - if (!_.has(detailsMap, login)) { + detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail); + }); + + const newSelectedOptions: MemberForList[] = []; + Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { + if (!(login in detailsMap)) { return; } newSelectedOptions.push({...detailsMap[login], isSelected: true}); }); - _.each(selectedOptions, (option) => { - newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); + 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 (userToInvite) { + if (typeof userToInvite?.accountID === 'number') { newUsersToInviteDict[userToInvite.accountID] = userToInvite; } // Add all personal details to the new dict - _.each(inviteOptions.personalDetails, (details) => { + inviteOptions.personalDetails.forEach((details) => { + if (typeof details.accountID !== 'number') { + return; + } newPersonalDetailsDict[details.accountID] = details; }); // Add all selected options to the new dict - _.each(newSelectedOptions, (option) => { + newSelectedOptions.forEach((option) => { + if (typeof option.accountID !== 'number') { + return; + } newSelectedOptionsDict[option.accountID] = option; }); // Strip out dictionary keys and update arrays - setUsersToInvite(_.values(newUsersToInviteDict)); - setPersonalDetails(_.values(newPersonalDetailsDict)); - setSelectedOptions(_.values(newSelectedOptionsDict)); + setUsersToInvite(Object.values(newUsersToInviteDict)); + setPersonalDetails(Object.values(newPersonalDetailsDict)); + setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]); + }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); - const sections = useMemo(() => { - const sectionsArr = []; + const sections: MembersSection[] = useMemo(() => { + const sectionsArr: MembersSection[] = []; let indexOffset = 0; if (!didScreenTransitionEnd) { @@ -171,13 +172,13 @@ function WorkspaceInvitePage(props) { // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { - filterSelectedOptions = _.filter(selectedOptions, (option) => { - const accountID = lodashGet(option, 'accountID', null); - const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); + filterSelectedOptions = selectedOptions.filter((option) => { + const accountID = option.accountID; + const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchTerm.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); - const isPartOfSearchTerm = option.text.toLowerCase().includes(searchValue) || option.login.toLowerCase().includes(searchValue); + const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; }); } @@ -191,20 +192,20 @@ function WorkspaceInvitePage(props) { indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results - const selectedLogins = _.map(selectedOptions, ({login}) => login); - const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); - const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList); + const selectedLogins = selectedOptions.map(({login}) => login); + const personalDetailsWithoutSelected = Object.values(personalDetails).filter(({login}) => !selectedLogins.some((selectedLogin) => selectedLogin === login)); + const personalDetailsFormatted = personalDetailsWithoutSelected.map((item) => OptionsListUtils.formatMemberForList(item)); sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, - shouldShow: !_.isEmpty(personalDetailsFormatted), + shouldShow: !isEmptyObject(personalDetailsFormatted), indexOffset, }); indexOffset += personalDetailsFormatted.length; - _.each(usersToInvite, (userToInvite) => { - const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); + Object.values(usersToInvite).forEach((userToInvite) => { + const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); if (hasUnselectedUserToInvite) { sectionsArr.push({ @@ -219,14 +220,14 @@ function WorkspaceInvitePage(props) { return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option) => { - Policy.clearErrors(props.route.params.policyID); + const toggleOption = (option: MemberForList) => { + Policy.clearErrors(route.params.policyID); - const isOptionInList = _.some(selectedOptions, (selectedOption) => selectedOption.login === option.login); + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - let newSelectedOptions; + let newSelectedOptions: MemberForList[]; if (isOptionInList) { - newSelectedOptions = _.reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); + newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; } @@ -234,14 +235,14 @@ function WorkspaceInvitePage(props) { setSelectedOptions(newSelectedOptions); }; - const validate = () => { - const errors = {}; + const validate = (): boolean => { + const errors: Errors = {}; if (selectedOptions.length <= 0) { - errors.noUserSelected = true; + errors.noUserSelected = 'true'; } - Policy.setWorkspaceErrors(props.route.params.policyID, errors); - return _.size(errors) <= 0; + Policy.setWorkspaceErrors(route.params.policyID, errors); + return isEmptyObject(errors); }; const inviteUser = () => { @@ -249,27 +250,24 @@ function WorkspaceInvitePage(props) { return; } - const invitedEmailsToAccountIDs = {}; - _.each(selectedOptions, (option) => { - const login = option.login || ''; - const accountID = lodashGet(option, 'accountID', ''); + const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; + selectedOptions.forEach((option) => { + const login = option.login ?? ''; + const accountID = option.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } invitedEmailsToAccountIDs[login] = Number(accountID); }); - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, invitedEmailsToAccountIDs); - Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(props.route.params.policyID)); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs); + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID)); }; - const [policyName, shouldShowAlertPrompt] = useMemo( - () => [lodashGet(props.policy, 'name'), _.size(lodashGet(props.policy, 'errors', {})) > 0 || lodashGet(props.policy, 'alertMessage', '').length > 0], - [props.policy], - ); + const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); - if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { + if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.some((email) => email === searchValue)) { return translate('messages.errorMessageInvalidEmail'); } if ( @@ -289,8 +287,8 @@ function WorkspaceInvitePage(props) { testID={WorkspaceInvitePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > { - Policy.clearErrors(props.route.params.policyID); + Policy.clearErrors(route.params.policyID); Navigation.goBack(); }} /> @@ -316,7 +314,7 @@ function WorkspaceInvitePage(props) { onSelectRow={toggleOption} onConfirm={inviteUser} showScrollIndicator - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)} + showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} /> @@ -325,7 +323,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={[props.policy.alertMessage, {isTranslated: true}]} + message={[policy?.alertMessage ?? '', {isTranslated: true}]} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter @@ -336,24 +334,18 @@ function WorkspaceInvitePage(props) { ); } -WorkspaceInvitePage.propTypes = propTypes; -WorkspaceInvitePage.defaultProps = defaultProps; WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ +export default withPolicyAndFullscreenLoading( + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, betas: { key: ONYXKEYS.BETAS, }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, invitedEmailsToAccountIDsDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, }, - }), -)(WorkspaceInvitePage); + })(WorkspaceInvitePage), +); diff --git a/src/types/onyx/InvitedEmailsToAccountIDs.ts b/src/types/onyx/InvitedEmailsToAccountIDs.ts new file mode 100644 index 000000000000..929d21746682 --- /dev/null +++ b/src/types/onyx/InvitedEmailsToAccountIDs.ts @@ -0,0 +1,3 @@ +type InvitedEmailsToAccountIDs = Record; + +export default InvitedEmailsToAccountIDs; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index cdc87f5df252..aae3b6f2532a 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -29,6 +29,7 @@ import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; import type Fund from './Fund'; import type IntroSelected from './IntroSelected'; +import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs'; import type IOU from './IOU'; import type LastPaymentMethod from './LastPaymentMethod'; import type Locale from './Locale'; @@ -171,6 +172,7 @@ export type { NewRoomForm, IKnowATeacherForm, IntroSchoolPrincipalForm, + InvitedEmailsToAccountIDs, PrivateNotesForm, ReportFieldEditForm, RoomNameForm,