diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 62e3f206b184..f5b9599a1b1f 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -147,7 +147,7 @@ const OptionRowLHN = (props) => { tooltipEnabled numberOfLines={1} textStyles={displayNameStyle} - shouldUseFullTitle={optionItem.isChatRoom || optionItem.isPolicyExpenseChat || optionItem.isTaskReport} + shouldUseFullTitle={optionItem.isChatRoom || optionItem.isPolicyExpenseChat || optionItem.isTaskReport || optionItem.isMoneyRequestReport} /> {optionItem.isChatRoom && ( `request ${amount}`, splitAmount: ({amount}) => `split ${amount}`, + payerOwesAmount: ({payer, amount}) => `${payer} owes ${amount}`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", error: { diff --git a/src/languages/es.js b/src/languages/es.js index f32bc8e8da03..01878fafe5e2 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -324,6 +324,7 @@ export default { settlePaypalMe: 'Pagar con PayPal.me', requestAmount: ({amount}) => `solicitar ${amount}`, splitAmount: ({amount}) => `dividir ${amount}`, + payerOwesAmount: ({payer, amount}) => `${payer} debe ${amount}`, noReimbursableExpenses: 'El monto de este informe es inválido', pendingConversionMessage: 'El total se actualizará cuando estés online', error: { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f26a4c71a927..18a479675110 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -727,7 +727,7 @@ function getIcons(report, personalDetails, defaultIcon = null) { result.source = Expensicons.ActiveRoomAvatar; return [result]; } - if (isPolicyExpenseChat(report)) { + if (isPolicyExpenseChat(report) || isExpenseReport(report)) { const workspaceName = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'name']); const policyExpenseChatAvatarSource = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'avatar']) || getDefaultWorkspaceAvatar(workspaceName); @@ -885,6 +885,19 @@ function getPolicyExpenseChatName(report) { return reportOwnerDisplayName; } +/** + * Get the title for a IOU or expense chat which will be showing the payer and the amount + * + * @param {Object} report + * @returns {String} + */ +function getMoneyRequestReportName(report) { + const formattedAmount = CurrencyUtils.convertToDisplayString(report.total || 0, report.currency); + const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerEmail); + + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); +} + /** * Get the title for a report. * @@ -901,6 +914,10 @@ function getReportName(report) { formattedName = getPolicyExpenseChatName(report); } + if (isMoneyRequestReport(report)) { + formattedName = getMoneyRequestReportName(report); + } + if (isArchivedRoom(report)) { formattedName += ` (${Localize.translateLocal('common.archived')})`; } @@ -1596,7 +1613,7 @@ function shouldReportBeInOptionList(report, reportIDFromRoute, isInGSDMode, curr // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. - if (!report || !report.reportID || !report.participants || (_.isEmpty(report.participants) && !isPublicRoom(report) && !isArchivedRoom(report)) || isIOUReport(report)) { + if (!report || !report.reportID || (_.isEmpty(report.participants) && !isPublicRoom(report) && !isArchivedRoom(report) && !isMoneyRequestReport(report))) { return false; } @@ -1877,6 +1894,23 @@ function getWhisperDisplayNames(participants) { return _.map(participants, (login) => getDisplayNameForParticipant(login, !isWhisperOnlyVisibleToCurrentUSer)).join(', '); } +/** + * Show subscript on IOU or expense report + * @param {Object} report + * @returns {Boolean} + */ +function shouldReportShowSubscript(report) { + if (isArchivedRoom(report)) { + return false; + } + + if (isPolicyExpenseChat(report) && !report.isOwnPolicyExpenseChat) { + return true; + } + + return isExpenseReport(report); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -1956,4 +1990,5 @@ export { canRequestMoney, getWhisperDisplayNames, getWorkspaceAvatar, + shouldReportShowSubscript, }; diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 76d34e361b33..ca208d67321b 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -21,15 +21,15 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; // data anyway and cause SidebarLinks to rerender. const chatReports = {}; -const iouReports = {}; +const moneyRequestReports = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { if (!report) { - delete iouReports[key]; + delete moneyRequestReports[key]; delete chatReports[key]; - } else if (ReportUtils.isIOUReport(report)) { - iouReports[key] = report; + } else if (ReportUtils.isMoneyRequestReport(report)) { + moneyRequestReports[key] = report; } else { chatReports[key] = report; } @@ -108,7 +108,9 @@ function getOrderedReportIDs(reportIDFromRoute) { const isInDefaultMode = !isInGSDMode; // Filter out all the reports that shouldn't be displayed - const reportsToDisplay = _.filter(chatReports, (report) => ReportUtils.shouldReportBeInOptionList(report, reportIDFromRoute, isInGSDMode, currentUserLogin, iouReports, betas, policies)); + const reportsToDisplay = _.filter({...chatReports, ...moneyRequestReports}, (report) => + ReportUtils.shouldReportBeInOptionList(report, reportIDFromRoute, isInGSDMode, currentUserLogin, moneyRequestReports, betas, policies), + ); // There are a few properties that need to be calculated for the report which are used when sorting reports. _.each(reportsToDisplay, (report) => { @@ -119,7 +121,7 @@ function getOrderedReportIDs(reportIDFromRoute) { report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getIOUTotal(report, iouReports); + report.iouReportAmount = ReportUtils.getIOUTotal(report, moneyRequestReports); }); // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: @@ -144,7 +146,7 @@ function getOrderedReportIDs(reportIDFromRoute) { return; } - if (report.hasOutstandingIOU && !ReportUtils.isIOUOwnedByCurrentUser(report, iouReports)) { + if (report.hasOutstandingIOU && !ReportUtils.isIOUOwnedByCurrentUser(report, moneyRequestReports)) { outstandingIOUReports.push(report); return; } @@ -192,7 +194,8 @@ function getOrderedReportIDs(reportIDFromRoute) { * @returns {Object} */ function getOptionData(reportID) { - const report = chatReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + const report = chatReports[reportKey] || moneyRequestReports[reportKey]; // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -229,6 +232,7 @@ function getOptionData(reportID) { isArchivedRoom: false, shouldShowSubscript: false, isPolicyExpenseChat: false, + isMoneyRequestReport: false, }; const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForLogins(report.participants, personalDetails)); @@ -238,7 +242,8 @@ function getOptionData(reportID) { result.isTaskReport = ReportUtils.isTaskReport(report); result.isArchivedRoom = ReportUtils.isArchivedRoom(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - result.shouldShowSubscript = result.isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !result.isArchivedRoom; + result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); + result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; @@ -325,8 +330,8 @@ function getOptionData(reportID) { result.alternateText = lastMessageText || formattedLogin; } - result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, iouReports); - result.iouReportAmount = ReportUtils.getIOUTotal(result, iouReports); + result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, moneyRequestReports); + result.iouReportAmount = ReportUtils.getIOUTotal(result, moneyRequestReports); if (!hasMultipleParticipants) { result.login = personalDetail.login; diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index a21c4b55c310..10658ec79137 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -360,6 +360,7 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email7@test.com', 'email8@test.com']), type: CONST.REPORT.TYPE.IOU, ownerEmail: 'email2@test.com', + managerEmail: 'email2@test.com', hasOutstandingIOU: true, total: 10000, currency: 'USD', @@ -391,12 +392,13 @@ describe('Sidebar', () => { .then(() => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); - expect(displayNames).toHaveLength(3); + expect(displayNames).toHaveLength(4); expect(screen.queryAllByTestId('Pin Icon')).toHaveLength(1); expect(screen.queryAllByTestId('Pencil Icon')).toHaveLength(1); expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('One, Two'); expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Five, Six'); - expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Three, Four'); + expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Two owes $100.00'); + expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Three, Four'); }) ); }); @@ -676,6 +678,7 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email7@test.com', 'email8@test.com']), type: CONST.REPORT.TYPE.IOU, ownerEmail: 'email2@test.com', + managerEmail: 'email2@test.com', hasOutstandingIOU: true, total: 10000, currency: 'USD', @@ -685,6 +688,7 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email9@test.com', 'email10@test.com']), type: CONST.REPORT.TYPE.IOU, ownerEmail: 'email2@test.com', + managerEmail: 'email2@test.com', hasOutstandingIOU: true, total: 10000, currency: 'USD', @@ -694,6 +698,7 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email11@test.com', 'email12@test.com']), type: CONST.REPORT.TYPE.IOU, ownerEmail: 'email2@test.com', + managerEmail: 'email2@test.com', hasOutstandingIOU: true, total: 10000, currency: 'USD', @@ -727,10 +732,12 @@ describe('Sidebar', () => { .then(() => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); - expect(displayNames).toHaveLength(3); + expect(displayNames).toHaveLength(5); expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Five, Six'); expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('One, Two'); expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Three, Four'); + expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Email Two owes $100.00'); + expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('Email Two owes $100.00'); }) ); });