-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Feat : show Invite button on Members page for money request reports #88459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e389f17
fbfb102
d7e432c
aba1f01
3ccece2
ef2fe71
1b88ca5
d0ef64b
8f24eb8
3ff45cc
7a3bb59
295809c
4c18664
73183b9
cc7e06b
6e49efa
9eba2e9
e83bbe8
07790da
e440411
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; | ||
| import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; | ||
| import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||
| import ScreenWrapper from '@components/ScreenWrapper'; | ||
|
|
@@ -11,6 +12,7 @@ import useDynamicBackPath from '@hooks/useDynamicBackPath'; | |
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; | ||
| import usePolicy from '@hooks/usePolicy'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import {inviteToGroupChat, searchUserInServer} from '@libs/actions/Report'; | ||
|
|
@@ -22,19 +24,21 @@ import {getHeaderMessage} from '@libs/PersonalDetailOptionsListUtils'; | |
| import type {OptionData} from '@libs/PersonalDetailOptionsListUtils'; | ||
| import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; | ||
| import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; | ||
| import {getGroupChatName} from '@libs/ReportNameUtils'; | ||
| import {getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils'; | ||
| import {getReportName} from '@libs/ReportNameUtils'; | ||
| import {getParticipantsAccountIDsForDisplay, isCurrentUserSubmitter, isGroupChat, isGroupChatAdmin, isMoneyRequestReport, isOpenExpenseReport, isPolicyAdmin} from '@libs/ReportUtils'; | ||
| import StringUtils from '@libs/StringUtils'; | ||
|
|
||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import {DYNAMIC_ROUTES} from '@src/ROUTES'; | ||
| import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; | ||
|
|
||
| import React, {useEffect, useState} from 'react'; | ||
| import reportByIDsSelector from '@selectors/ReportAttributes'; | ||
| import React, {useEffect, useMemo, useState} from 'react'; | ||
|
|
||
| import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; | ||
|
|
||
| import withReportOrNotFound from './inbox/report/withReportOrNotFound'; | ||
| import getInvitedEmailsToAccountIDs from './InviteReportParticipantsPageUtils'; | ||
|
|
||
| type DynamicReportParticipantsInvitePageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps; | ||
|
|
||
|
|
@@ -46,7 +50,17 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants | |
| const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); | ||
| const [personalDetailsList] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); | ||
| const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); | ||
| const reportID = report.reportID; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the Invite button now visible for more report types, this page is also reachable via deep link (e.g.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should add a guard here mirroring shouldShowInviteButton cc @M00rish
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you please elaborate more on this @abzokhattab, did not really get the point, thanks.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the issue is that this page only checks if the report exists (via The button on the parent page is gated by Something like: function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProps) {
const shouldShowInviteButton = isGroupChat(report) || isMoneyRequestReport(report);
if (!shouldShowInviteButton) {
return <FullPageNotFoundView shouldShow />;
}
// ...rest of the component
} |
||
| const reportAttributesSelector = useMemo(() => reportByIDsSelector([reportID]), [reportID]); | ||
| const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, { | ||
| selector: reportAttributesSelector, | ||
| }); | ||
| const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); | ||
| const [session] = useOnyx(ONYXKEYS.SESSION); | ||
| const currentUserAccountID = Number(session?.accountID); | ||
| const policy = usePolicy(report.policyID); | ||
| const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat(report) && isGroupChatAdmin(report, currentUserAccountID)); | ||
| const shouldShowInvitePage = isReportSubmitterOrAdmin && (isGroupChat(report) || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); | ||
|
|
||
| // Any existing participants and Expensify emails should not be eligible for invitation | ||
| const excludedUsers: Record<string, boolean> = { | ||
|
|
@@ -114,7 +128,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants | |
| toggleSelection(option); | ||
| }; | ||
|
|
||
| const reportName = getGroupChatName(formatPhoneNumber, undefined, true, report); | ||
| const reportName = StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes)); | ||
|
|
||
| const goBack = () => { | ||
| Navigation.goBack(backPath); | ||
|
|
@@ -124,19 +138,18 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants | |
| if (selectedOptions.length === 0) { | ||
| return; | ||
| } | ||
| const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; | ||
| for (const option of selectedOptions) { | ||
| const login = option.login ?? ''; | ||
| const accountID = option.accountID; | ||
| if (!login.toLowerCase().trim() || !accountID) { | ||
| continue; | ||
| } | ||
| invitedEmailsToAccountIDs[login] = accountID; | ||
| const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); | ||
| if (Object.keys(invitedEmailsToAccountIDs).length === 0) { | ||
| return; | ||
| } | ||
| inviteToGroupChat(report, invitedEmailsToAccountIDs, personalDetailsList, formatPhoneNumber); | ||
| goBack(); | ||
| }; | ||
|
|
||
| if (!shouldShowInvitePage) { | ||
| return <FullPageNotFoundView shouldShow />; | ||
| } | ||
|
|
||
| const getHeaderMessageText = () => { | ||
| if (sections.length > 0) { | ||
| return ''; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ import useLocalize from '@hooks/useLocalize'; | |
| import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; | ||
| import useNetwork from '@hooks/useNetwork'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import usePolicy from '@hooks/usePolicy'; | ||
| import useReportAttributes from '@hooks/useReportAttributes'; | ||
| import useReportIsArchived from '@hooks/useReportIsArchived'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
|
|
@@ -30,16 +31,19 @@ import Navigation from '@libs/Navigation/Navigation'; | |
| import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; | ||
| import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; | ||
| import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; | ||
| import {isPolicyAdmin} from '@libs/PolicyUtils'; | ||
| import {getReportName} from '@libs/ReportNameUtils'; | ||
| import { | ||
| getReportPersonalDetailsParticipants, | ||
| isAnnounceRoom, | ||
| isArchivedNonExpenseReport, | ||
| isChatRoom, | ||
| isChatThread, | ||
| isCurrentUserSubmitter, | ||
| isGroupChatAdmin, | ||
| isGroupChat as isGroupChatUtils, | ||
| isMoneyRequestReport, | ||
| isOpenExpenseReport, | ||
| isPolicyExpenseChat, | ||
| isSelfDM, | ||
| isTaskReport, | ||
|
|
@@ -86,6 +90,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
| const currentUserAccountID = Number(session?.accountID); | ||
| const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); | ||
| const isGroupChat = isGroupChatUtils(report); | ||
| const policy = usePolicy(report.policyID); | ||
| const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat && isCurrentUserAdmin); | ||
| const shouldShowInviteButton = isReportSubmitterOrAdmin && (isGroupChat || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the report is a group chat, this new Useful? React with 👍 / 👎. |
||
| const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; | ||
| const {isOffline} = useNetwork(); | ||
| const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); | ||
|
|
@@ -104,8 +111,11 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
| }; | ||
|
|
||
| const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, filterParticipants); | ||
| const shouldShowBulkActionsDropdown = isGroupChat && (isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0); | ||
| const firstSelectedMember = selectedMembers?.at(0); | ||
| const [firstSelectedMemberDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsSelector(firstSelectedMember)}); | ||
| const [firstSelectedMemberDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { | ||
| selector: personalDetailsSelector(firstSelectedMember), | ||
| }); | ||
|
|
||
| // The Table stores selection as string keys, while this page tracks accountIDs as numbers. | ||
| const onRowSelectionChange = (keys: string[]) => setSelectedMembers(keys.map(Number)); | ||
|
|
@@ -141,7 +151,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
|
|
||
| const showRemoveMembersModal = async () => { | ||
| const {action} = await showConfirmModal({ | ||
| title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), | ||
| title: translate('workspace.people.removeMembersTitle', { | ||
| count: selectedMembers.length, | ||
| }), | ||
| prompt: translate('workspace.people.removeMembersPrompt', { | ||
| count: selectedMembers.length, | ||
| memberName: formatPhoneNumber(firstSelectedMemberDetails?.displayName ?? ''), | ||
|
|
@@ -177,7 +189,12 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
| keyForList: `${accountID}`, | ||
| accountID, | ||
| login: details?.login ?? '', | ||
| name: formatPhoneNumber(temporaryGetDisplayNameOrDefault({passedPersonalDetails: details, translate})), | ||
| name: formatPhoneNumber( | ||
| temporaryGetDisplayNameOrDefault({ | ||
| passedPersonalDetails: details, | ||
| translate, | ||
| }), | ||
| ), | ||
| email: formatPhoneNumber(details?.login ?? ''), | ||
| isAdmin: role === CONST.REPORT.ROLE.ADMIN, | ||
| isGroupChat, | ||
|
|
@@ -195,7 +212,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
|
|
||
| const bulkActionsButtonOptions: Array<DropdownOption<WorkspaceMemberBulkActionType>> = [ | ||
| { | ||
| text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), | ||
| text: translate('workspace.people.removeMembersTitle', { | ||
| count: selectedMembers.length, | ||
| }), | ||
| value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, | ||
| icon: icons.RemoveMembers, | ||
| onSelected: showRemoveMembersModal, | ||
|
|
@@ -204,7 +223,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
|
|
||
| if (isAtLeastOneAdminSelected) { | ||
| bulkActionsButtonOptions.push({ | ||
| text: translate('workspace.people.makeMember', {count: selectedMembers.length}), | ||
| text: translate('workspace.people.makeMember', { | ||
| count: selectedMembers.length, | ||
| }), | ||
| value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, | ||
| icon: icons.User, | ||
| onSelected: () => changeUserRole(CONST.REPORT.ROLE.MEMBER), | ||
|
|
@@ -213,7 +234,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
|
|
||
| if (isAtLeastOneMemberSelected) { | ||
| bulkActionsButtonOptions.push({ | ||
| text: translate('workspace.people.makeGroupAdmin', {count: selectedMembers.length}), | ||
| text: translate('workspace.people.makeGroupAdmin', { | ||
| count: selectedMembers.length, | ||
| }), | ||
| value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, | ||
| icon: icons.MakeAdmin, | ||
| onSelected: () => changeUserRole(CONST.REPORT.ROLE.ADMIN), | ||
|
|
@@ -250,9 +273,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
| subtitle={StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes))} | ||
| /> | ||
| <View style={[styles.pl5, styles.pr5]}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small consistency note (relates to lines 316-318, not changed in this PR):
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @M00rish can you handle it please?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| {isGroupChat && ( | ||
| {shouldShowInviteButton && ( | ||
| <View style={styles.w100}> | ||
| {(isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0) ? ( | ||
| {shouldShowBulkActionsDropdown ? ( | ||
| <ButtonWithDropdownMenu<WorkspaceMemberBulkActionType> | ||
| variant={CONST.BUTTON_VARIANT.SUCCESS} | ||
| shouldAlwaysShowDropdownMenu | ||
|
|
@@ -278,7 +301,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr | |
| </View> | ||
| )} | ||
| </View> | ||
| <View style={[styles.w100, isGroupChat ? styles.mt3 : styles.mt0, styles.flex1]}> | ||
| <View style={[styles.w100, shouldShowInviteButton ? styles.mt3 : styles.mt0, styles.flex1]}> | ||
| <ReportParticipantsTable | ||
| ref={tableRef} | ||
| members={participants} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; | ||
|
|
||
| type InviteOption = { | ||
| accountID?: number; | ||
| login?: string; | ||
| }; | ||
|
|
||
| function getInvitedEmailsToAccountIDs(selectedOptions: InviteOption[]): InvitedEmailsToAccountIDs { | ||
| const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; | ||
|
|
||
| for (const option of selectedOptions) { | ||
| const login = option.login ?? ''; | ||
| const accountID = option.accountID; | ||
| if (!login.trim() || accountID === undefined) { | ||
| continue; | ||
| } | ||
| invitedEmailsToAccountIDs[login] = accountID; | ||
| } | ||
|
|
||
| return invitedEmailsToAccountIDs; | ||
| } | ||
|
|
||
| export default getInvitedEmailsToAccountIDs; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import getInvitedEmailsToAccountIDs from '@pages/InviteReportParticipantsPageUtils'; | ||
|
|
||
| describe('InviteReportParticipantsPage', () => { | ||
| describe('getInvitedEmailsToAccountIDs', () => { | ||
| it('preserves optimistic account IDs for new invitees', () => { | ||
| // Given a typed invitee that does not have a real account yet | ||
| const login = 'new-user@example.com'; | ||
| const selectedOptions = [ | ||
| { | ||
| login, | ||
| accountID: 0, | ||
| }, | ||
| ]; | ||
|
|
||
| // When building the invite map | ||
| const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); | ||
|
|
||
| // Then the optimistic accountID is preserved for the API layer | ||
| expect(Object.entries(invitedEmailsToAccountIDs)).toStrictEqual([[login, 0]]); | ||
| }); | ||
|
|
||
| it('skips malformed selected options', () => { | ||
| // Given selected options that cannot be invited | ||
| const selectedOptions = [ | ||
| { | ||
| accountID: 123, | ||
| }, | ||
| { | ||
| login: 'missing-account-id@example.com', | ||
| }, | ||
| ]; | ||
|
|
||
| // When building the invite map | ||
| const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); | ||
|
|
||
| // Then no invalid invitee is sent to the API layer | ||
| expect(invitedEmailsToAccountIDs).toStrictEqual({}); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file change is not related to the issue ... is it intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes it is, it solves messaging the invited member error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry i am not following. What messaging error? It would be nice if you could share can video
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here's video @abzokhattab
Video.mp4