From de0e9d6ccaf2f624500b08c40066099816705f72 Mon Sep 17 00:00:00 2001 From: Samuel Herodotou Date: Sat, 7 Oct 2023 00:28:23 +0100 Subject: [PATCH 1/6] Allow multiple emails to be added when inviting members to workspace --- src/pages/workspace/WorkspaceInvitePage.js | 82 +++++++++++++++------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 39495911b8dc..29821a74a857 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -66,7 +66,7 @@ function WorkspaceInvitePage(props) { const [searchTerm, setSearchTerm] = useState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); - const [userToInvite, setUserToInvite] = useState(null); + const [usersToInvite, setUsersToInvite] = useState([]); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails); Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); @@ -83,19 +83,48 @@ function WorkspaceInvitePage(props) { const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); - - // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap = {}; - _.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); + let emails = searchTerm.replace(/,\s/g, ',').split(','); + emails = _.map(emails, word => word.trim()); + + const newUsersToInviteDict = {}; + const newPersonalDetailsDict = {}; + const newSelectedOptionsDict = {}; + + emails.forEach((email) => { + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers); + + // Update selectedOptions with the latest personalDetails and policyMembers information + const detailsMap = {}; + _.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); + }); + + const userToInvite = inviteOptions.userToInvite; + + // Only add the user to the invites list if it is valid + if (userToInvite) { + newUsersToInviteDict[userToInvite.accountID] = userToInvite; + } + + // Add all personal details to the new dict + _.forEach(inviteOptions.personalDetails, (details) => { + newPersonalDetailsDict[details.accountID] = details; + }); + + // Add all selected options to the new dict + _.forEach(newSelectedOptions, (option) => { + newSelectedOptionsDict[option.accountID] = option; + }); }); + + // Strip out dictionary keys and update arrays + setUsersToInvite(_.map(newUsersToInviteDict, (v) => v)); + setPersonalDetails(_.map(newPersonalDetailsDict, (v) => v)); + setSelectedOptions(_.map(newSelectedOptionsDict, (v) => v)); - setUserToInvite(inviteOptions.userToInvite); - setPersonalDetails(inviteOptions.personalDetails); - setSelectedOptions(newSelectedOptions); // 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]); @@ -115,7 +144,6 @@ function WorkspaceInvitePage(props) { const selectedLogins = _.map(selectedOptions, ({login}) => login); const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList); - const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login); sections.push({ title: translate('common.contacts'), @@ -125,14 +153,18 @@ function WorkspaceInvitePage(props) { }); indexOffset += personalDetailsFormatted.length; - if (hasUnselectedUserToInvite) { - sections.push({ - title: undefined, - data: [OptionsListUtils.formatMemberForList(userToInvite)], - shouldShow: true, - indexOffset, - }); - } + _.forEach(usersToInvite, (userToInvite) => { + const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); + + if (hasUnselectedUserToInvite) { + sections.push({ + title: undefined, + data: [OptionsListUtils.formatMemberForList(userToInvite)], + shouldShow: true, + indexOffset: indexOffset++, + }); + } + }); return sections; }; @@ -187,14 +219,14 @@ function WorkspaceInvitePage(props) { const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); - if (!userToInvite && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { + if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { return translate('messages.errorMessageInvalidEmail'); } - if (!userToInvite && excludedUsers.includes(searchValue)) { + if (usersToInvite.length === 0 && excludedUsers.includes(searchValue)) { return translate('messages.userIsAlreadyMemberOfWorkspace', {login: searchValue, workspace: policyName}); } - return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, Boolean(userToInvite), searchValue); - }, [excludedUsers, translate, searchTerm, policyName, userToInvite, personalDetails]); + return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue); + }, [excludedUsers, translate, searchTerm, policyName, usersToInvite, personalDetails]); return ( Date: Wed, 11 Oct 2023 10:12:05 +0100 Subject: [PATCH 2/6] Increased strictness of email parsing --- src/pages/workspace/WorkspaceInvitePage.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 29821a74a857..bed146528610 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -83,14 +83,18 @@ function WorkspaceInvitePage(props) { const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); useEffect(() => { - let emails = searchTerm.replace(/,\s/g, ',').split(','); + let emails = searchTerm.replace(/\s,\s/g, ',').split(','); emails = _.map(emails, word => word.trim()); const newUsersToInviteDict = {}; const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; - - emails.forEach((email) => { + + _.forEach(emails, (email) => { + if (email === '') { + return; + } + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers); // Update selectedOptions with the latest personalDetails and policyMembers information From 7254e5ac2fcd8e777d04aa5782f9ac2b0dba67ce Mon Sep 17 00:00:00 2001 From: Samuel Herodotou Date: Wed, 11 Oct 2023 18:19:04 +0100 Subject: [PATCH 3/6] Combined map and filter --- src/pages/workspace/WorkspaceInvitePage.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index bed146528610..8986ddccc0e3 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -84,17 +84,13 @@ function WorkspaceInvitePage(props) { useEffect(() => { let emails = searchTerm.replace(/\s,\s/g, ',').split(','); - emails = _.map(emails, word => word.trim()); + emails = _.filter(_.map(emails, word => word.trim()), email => email !== ''); const newUsersToInviteDict = {}; const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; _.forEach(emails, (email) => { - if (email === '') { - return; - } - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers); // Update selectedOptions with the latest personalDetails and policyMembers information From 095359dcebb70544a8fdee5ada1af3e565938d7e Mon Sep 17 00:00:00 2001 From: Samuel Herodotou Date: Wed, 11 Oct 2023 23:49:45 +0100 Subject: [PATCH 4/6] Prettier --- src/pages/workspace/WorkspaceInvitePage.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 8986ddccc0e3..2c75728e1267 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -84,7 +84,10 @@ function WorkspaceInvitePage(props) { useEffect(() => { let emails = searchTerm.replace(/\s,\s/g, ',').split(','); - emails = _.filter(_.map(emails, word => word.trim()), email => email !== ''); + emails = _.filter( + _.map(emails, (word) => word.trim()), + (email) => email !== '', + ); const newUsersToInviteDict = {}; const newPersonalDetailsDict = {}; @@ -92,23 +95,23 @@ function WorkspaceInvitePage(props) { _.forEach(emails, (email) => { const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers); - + // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap = {}; _.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); }); - + const userToInvite = inviteOptions.userToInvite; // Only add the user to the invites list if it is valid - if (userToInvite) { + if (userToInvite) { newUsersToInviteDict[userToInvite.accountID] = userToInvite; } - + // Add all personal details to the new dict _.forEach(inviteOptions.personalDetails, (details) => { newPersonalDetailsDict[details.accountID] = details; @@ -119,7 +122,7 @@ function WorkspaceInvitePage(props) { newSelectedOptionsDict[option.accountID] = option; }); }); - + // Strip out dictionary keys and update arrays setUsersToInvite(_.map(newUsersToInviteDict, (v) => v)); setPersonalDetails(_.map(newPersonalDetailsDict, (v) => v)); From 8166fe3905968f6c9082f6473de951606c247615 Mon Sep 17 00:00:00 2001 From: Samuel Herodotou Date: Wed, 11 Oct 2023 23:56:07 +0100 Subject: [PATCH 5/6] Cleaner dictionary handling --- src/pages/workspace/WorkspaceInvitePage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 2c75728e1267..2a696d7548eb 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -124,9 +124,9 @@ function WorkspaceInvitePage(props) { }); // Strip out dictionary keys and update arrays - setUsersToInvite(_.map(newUsersToInviteDict, (v) => v)); - setPersonalDetails(_.map(newPersonalDetailsDict, (v) => v)); - setSelectedOptions(_.map(newSelectedOptionsDict, (v) => v)); + setUsersToInvite(_.values(newUsersToInviteDict)); + setPersonalDetails(_.values(newPersonalDetailsDict)); + setSelectedOptions(_.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]); From 268be0749f5e94deec2476cc90398b01156bde7a Mon Sep 17 00:00:00 2001 From: Samuel Herodotou Date: Thu, 12 Oct 2023 00:43:55 +0100 Subject: [PATCH 6/6] Cleaner search term handling Use `each` instead of `forEach` --- src/pages/workspace/WorkspaceInvitePage.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 2a696d7548eb..23625e2b5d4e 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -83,25 +83,26 @@ function WorkspaceInvitePage(props) { const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); useEffect(() => { - let emails = searchTerm.replace(/\s,\s/g, ',').split(','); - emails = _.filter( - _.map(emails, (word) => word.trim()), - (email) => email !== '', + const emails = _.compact( + searchTerm + .trim() + .replace(/\s*,\s*/g, ',') + .split(','), ); const newUsersToInviteDict = {}; const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; - _.forEach(emails, (email) => { + _.each(emails, (email) => { const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap = {}; - _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); + _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); const newSelectedOptions = []; - _.forEach(selectedOptions, (option) => { + _.each(selectedOptions, (option) => { newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); }); @@ -113,12 +114,12 @@ function WorkspaceInvitePage(props) { } // Add all personal details to the new dict - _.forEach(inviteOptions.personalDetails, (details) => { + _.each(inviteOptions.personalDetails, (details) => { newPersonalDetailsDict[details.accountID] = details; }); // Add all selected options to the new dict - _.forEach(newSelectedOptions, (option) => { + _.each(newSelectedOptions, (option) => { newSelectedOptionsDict[option.accountID] = option; }); }); @@ -156,7 +157,7 @@ function WorkspaceInvitePage(props) { }); indexOffset += personalDetailsFormatted.length; - _.forEach(usersToInvite, (userToInvite) => { + _.each(usersToInvite, (userToInvite) => { const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); if (hasUnselectedUserToInvite) {