diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 0e794a1187eca..57682109a7c2b 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -200,7 +200,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio itemOneTransactionThreadReport?.reportID, ); - const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, visibleReportActionsData, lastAction); + const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, itemReportNameValuePairs?.private_isArchived, visibleReportActionsData, lastAction); const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 194fa4ac6f32c..af032efb34ad9 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -175,7 +175,6 @@ import type { ReportActions, ReportAttributesDerivedValue, ReportMetadata, - ReportNameValuePairs, VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; @@ -215,15 +214,6 @@ Onyx.connect({ }, }); -let allReportNameValuePairsOnyxConnect: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, - waitForCollectionCallback: true, - callback: (value) => { - allReportNameValuePairsOnyxConnect = value; - }, -}); - const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; const cachedOneTransactionThreadReportIDs: Record = {}; @@ -533,13 +523,16 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV /** * Get IOU report ID of report last action if the action is report action preview */ -function getIOUReportIDOfLastAction(report: OnyxEntry, visibleReportActionsData?: VisibleReportActionsDerivedValue, lastAction?: OnyxEntry): string | undefined { +function getIOUReportIDOfLastAction( + report: OnyxEntry, + privateIsArchived: string | undefined, + visibleReportActionsData?: VisibleReportActionsDerivedValue, + lastAction?: OnyxEntry, +): string | undefined { if (!report?.reportID) { return; } - // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters - const reportNameValuePairs = allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; - const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const isReportArchived = !!privateIsArchived; const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); const action = lastAction ?? getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData); if (!isReportPreviewAction(action)) { @@ -557,13 +550,12 @@ function getLastActorDisplayNameFromLastVisibleActions( lastActorDetails: Partial | null, currentUserAccountIDParam: number, personalDetails: OnyxEntry, + privateIsArchived: string | undefined, visibleReportActionsData?: VisibleReportActionsDerivedValue, lastAction?: OnyxEntry, ): string { const reportID = report?.reportID; - // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters - const reportNameValuePairs = reportID ? allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`] : undefined; - const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const isReportArchived = !!privateIsArchived; const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); const lastReportAction = lastAction ?? getLastVisibleAction(reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData); @@ -955,9 +947,9 @@ function createOption( personalDetails: OnyxInputOrEntry, report: OnyxInputOrEntry, currentUserAccountID: number, + privateIsArchived: string | undefined, config?: PreviewConfig, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], - privateIsArchived?: string, visibleReportActionsData: VisibleReportActionsDerivedValue = {}, translate?: LocalizedTranslate, ): SearchOptionData { @@ -1018,10 +1010,7 @@ function createOption( result.participantsList = personalDetailList; if (report) { - const reportNameValuePairsForReport = allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; - - // Set properties that are used in SearchOption context - result.private_isArchived = privateIsArchived ?? reportNameValuePairsForReport?.private_isArchived; + result.private_isArchived = privateIsArchived; result.keyForList = String(report.reportID); // Type/category flags already set in initialization above, but update brickRoadIndicator @@ -1124,12 +1113,12 @@ function getReportOption( personalDetails ?? {}, !isEmptyObject(report) ? report : undefined, currentUserAccountID, + privateIsArchived, { showChatPreviewLine: false, forcePolicyNamePreview: false, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ); @@ -1183,12 +1172,12 @@ function getReportDisplayOption( personalDetails ?? {}, !isEmptyObject(report) ? report : undefined, currentUserAccountID, + privateIsArchived, { showChatPreviewLine: false, forcePolicyNamePreview: false, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ); @@ -1236,12 +1225,12 @@ function getPolicyExpenseReportOption( personalDetails ?? {}, !isEmptyObject(expenseReport) ? expenseReport : null, currentUserAccountID, + privateIsArchived, { showChatPreviewLine: false, forcePolicyNamePreview: false, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ); @@ -1378,7 +1367,7 @@ function processReport( reportMapEntry, reportOption: { item: report, - ...createOption(accountIDs, personalDetails, report, currentUserAccountID, undefined, reportAttributesDerived, privateIsArchived, visibleReportActionsData), + ...createOption(accountIDs, personalDetails, report, currentUserAccountID, privateIsArchived, undefined, reportAttributesDerived, visibleReportActionsData), }, }; } @@ -1422,11 +1411,11 @@ function createOptionList( personalDetails, report, currentUserAccountID, + privateIsArchived, { showPersonalDetails: true, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ), }; @@ -1557,9 +1546,9 @@ function createFilteredOptionList( personalDetails, reportMapForAccountIDs[accountID], currentUserAccountID, + privateIsArchived, {showPersonalDetails: true}, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ), }; @@ -1585,7 +1574,7 @@ function createOptionFromReport( return { item: report, - ...createOption(accountIDs, personalDetails, report, currentUserAccountID, config, reportAttributesDerived, privateIsArchived, visibleReportActionsData), + ...createOption(accountIDs, personalDetails, report, currentUserAccountID, privateIsArchived, config, reportAttributesDerived, visibleReportActionsData), }; } @@ -1909,7 +1898,7 @@ function getUserToInviteOption({ login: searchValue, }, }; - const userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, currentUserAccountID, {showChatPreviewLine}, undefined, undefined, visibleReportActionsData); + const userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, currentUserAccountID, undefined, {showChatPreviewLine}, undefined, visibleReportActionsData); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 5351d9094ff96..5c814294e2e6b 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1114,7 +1114,15 @@ function getOptionData({ } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { const displayName = (lastMessageTextFromReport.length > 0 && - getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails, visibleReportActionsData, lastAction)) || + getLastActorDisplayNameFromLastVisibleActions( + report, + lastActorDetails, + currentUserAccountID, + personalDetails, + reportNameValuePairs?.private_isArchived, + visibleReportActionsData, + lastAction, + )) || lastActorDisplayName; result.alternateText = formatReportLastMessageText(`${displayName}: ${lastMessageText}`); } else { @@ -1158,7 +1166,15 @@ function getOptionData({ if (shouldShowLastActorDisplayName(report, lastActorDetails, lastAction, currentUserAccountID) && !isReportArchived) { const displayName = (lastMessageTextFromReport.length > 0 && - getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails, visibleReportActionsData, lastAction)) || + getLastActorDisplayNameFromLastVisibleActions( + report, + lastActorDetails, + currentUserAccountID, + personalDetails, + reportNameValuePairs?.private_isArchived, + visibleReportActionsData, + lastAction, + )) || lastActorDisplayName; result.alternateText = `${displayName}: ${formatReportLastMessageText(lastMessageText)}`; } else { diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 39842320d8b66..c475e57a27784 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -26,6 +26,7 @@ import { formatSectionsFromSearchTerm, getCurrentUserSearchTerms, getFilteredRecentAttendees, + getIOUReportIDOfLastAction, getLastActorDisplayName, getLastActorDisplayNameFromLastVisibleActions, getLastMessageTextForReport, @@ -2384,10 +2385,14 @@ describe('OptionsListUtils', () => { it('should find archived chats', () => { const searchText = 'Archived'; - // Given a set of options + // Given a set of options with report 10 marked as archived + const archivedMap: PrivateIsArchivedMap = { + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}10`]: reportNameValuePairs.private_isArchived, + }; + const OPTIONS_WITH_ARCHIVED = createOptionList(PERSONAL_DETAILS, CURRENT_USER_ACCOUNT_ID, archivedMap, REPORTS, MOCK_REPORT_ATTRIBUTES_DERIVED); // When we call getSearchOptions with all betas const options = getSearchOptions({ - options: OPTIONS, + options: OPTIONS_WITH_ARCHIVED, reportAttributesDerived: MOCK_REPORT_ATTRIBUTES_DERIVED, draftComments: {}, nvpDismissedProductTraining, @@ -3615,8 +3620,11 @@ describe('OptionsListUtils', () => { '1': getFakeAdvancedReportAction(CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT), }, }); - // When we call createOptionList - const reports = createOptionList(PERSONAL_DETAILS, CURRENT_USER_ACCOUNT_ID, EMPTY_PRIVATE_IS_ARCHIVED_MAP, REPORTS).reports; + // When we call createOptionList with report 10 marked as archived + const archivedMap: PrivateIsArchivedMap = { + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}10`]: reportNameValuePairs.private_isArchived, + }; + const reports = createOptionList(PERSONAL_DETAILS, CURRENT_USER_ACCOUNT_ID, archivedMap, REPORTS).reports; const archivedReport = reports.find((report) => report.reportID === '10'); // Then the returned report should contain default archived reason @@ -4186,11 +4194,40 @@ describe('OptionsListUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); await waitForBatchedUpdates(); - const result = createOption([1, 2], PERSONAL_DETAILS, report, CURRENT_USER_ACCOUNT_ID, {showChatPreviewLine: true}); + const result = createOption([1, 2], PERSONAL_DETAILS, report, CURRENT_USER_ACCOUNT_ID, undefined, {showChatPreviewLine: true}); expect(result.alternateText).toBe('Iron Man owes ₫34'); }); + it('should work correctly when reports collection with chatReport is passed', async () => { + const reportID = '123'; + const chatReportID = '456'; + + const report: Report = { + ...createRandomReport(0, undefined), + reportID, + chatReportID, + participants: { + 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + }, + }; + + const chatReport: Report = { + ...createRandomReport(1, undefined), + reportID: chatReportID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport); + await waitForBatchedUpdates(); + + const result = createOption([1, 2], PERSONAL_DETAILS, report, 1, undefined, undefined); + + expect(result.reportID).toBe(reportID); + expect(typeof result.text).toBe('string'); + }); + it('should work correctly when reports is undefined', async () => { const report: Report = { ...createRandomReport(0, undefined), @@ -4927,7 +4964,7 @@ describe('OptionsListUtils', () => { const personalDetails: PersonalDetailsList = PERSONAL_DETAILS; // When we call getLastActorDisplayNameFromLastVisibleActions - const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails); + const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails, undefined); // Then it should return the display name from lastActorDetails expect(result).toBe('Spider-Man'); @@ -4967,7 +5004,7 @@ describe('OptionsListUtils', () => { await waitForBatchedUpdates(); // When we call getLastActorDisplayNameFromLastVisibleActions - const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails); + const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails, undefined); // Then it should return the display name from personalDetails for the actor expect(result).toBe('Spider-Man'); @@ -5008,7 +5045,7 @@ describe('OptionsListUtils', () => { await waitForBatchedUpdates(); // When we call getLastActorDisplayNameFromLastVisibleActions - const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails); + const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails, undefined); // Then it should return the display name from reportAction.person // Note: formatPhoneNumberPhoneUtils replaces spaces with non-breaking spaces @@ -5048,7 +5085,7 @@ describe('OptionsListUtils', () => { await waitForBatchedUpdates(); // When we call getLastActorDisplayNameFromLastVisibleActions - const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails); + const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails, undefined); // Then it should return "You" for the current user expect(result).toBe('You'); @@ -5087,12 +5124,33 @@ describe('OptionsListUtils', () => { await waitForBatchedUpdates(); // When we call getLastActorDisplayNameFromLastVisibleActions - const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, 0, personalDetails); + const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, 0, personalDetails, undefined); // Then it should fall back to lastActorDetails // getLastActorDisplayName returns firstName if available, otherwise formatPhoneNumberPhoneUtils(getDisplayNameOrDefault(...)) expect(result).toBe('Spider'); }); + + it('should use privateIsArchived string to determine archived status', () => { + // Given a report with no last visible action and lastActorDetails + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'test-report-archived', + }; + const lastActorDetails: Partial = { + accountID: 3, + displayName: 'Spider-Man', + login: 'peterparker@expensify.com', + }; + const personalDetails: PersonalDetailsList = PERSONAL_DETAILS; + + // When we pass a non-empty privateIsArchived string (archived report) + const privateIsArchived = '2023-01-01 00:00:00'; + const result = getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, CURRENT_USER_ACCOUNT_ID, personalDetails, privateIsArchived); + + // Then it should still return the display name from lastActorDetails since there's no last visible action + expect(result).toBe('Spider-Man'); + }); }); describe('getReportDisplayOption', () => { @@ -6721,6 +6779,71 @@ describe('OptionsListUtils', () => { expect(results.personalDetails).toBeDefined(); }); + it('createOption should look up chatReport from reports collection when report has chatReportID', async () => { + // This test verifies the core functionality: using reports to look up linked chat reports + const reportID = 'expense-report-123'; + const chatReportID = 'linked-chat-456'; + + const expenseReport: Report = { + ...createRandomReport(0, undefined), + reportID, + chatReportID, + type: CONST.REPORT.TYPE.EXPENSE, + participants: { + 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + }, + }; + + const linkedChatReport: Report = { + ...createRandomReport(1, undefined), + reportID: chatReportID, + type: CONST.REPORT.TYPE.CHAT, + reportName: 'Linked Chat Report', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, expenseReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, linkedChatReport); + await waitForBatchedUpdates(); + + // When we call createOption with the linked chat report + const result = createOption([1, 2], PERSONAL_DETAILS, expenseReport, CURRENT_USER_ACCOUNT_ID, undefined, undefined); + + // Then the option should be created successfully + expect(result).toBeDefined(); + expect(result.reportID).toBe(reportID); + }); + + it('getReportDisplayOption should use reports parameter to look up chat report', async () => { + const reportID = 'test-report-789'; + const chatReportID = 'test-chat-101'; + + const report: Report = { + ...createRandomReport(0, undefined), + reportID, + chatReportID, + participants: { + 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + }, + }; + + const chatReport: Report = { + ...createRandomReport(1, undefined), + reportID: chatReportID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport); + await waitForBatchedUpdates(); + + // When we call getReportDisplayOption with chat report + const option = getReportDisplayOption(report, undefined, CURRENT_USER_ACCOUNT_ID, PERSONAL_DETAILS, undefined, undefined); + + // Then the option should be created successfully using the reports collection + expect(option).toBeDefined(); + expect(option.reportID).toBe(reportID); + }); + it('getPolicyExpenseReportOption should use reports parameter correctly', async () => { const reportID = 'policy-expense-123'; const testPolicyID = 'test-policy-456'; @@ -7016,4 +7139,97 @@ describe('OptionsListUtils', () => { expect(johnSmith).toBeDefined(); }); }); + + describe('getIOUReportIDOfLastAction', () => { + it('should return undefined when report is undefined', () => { + const result = getIOUReportIDOfLastAction(undefined, undefined); + expect(result).toBeUndefined(); + }); + + it('should return undefined when report has no reportID', () => { + const report = {} as Report; + const result = getIOUReportIDOfLastAction(report, undefined); + expect(result).toBeUndefined(); + }); + + it('should return undefined when lastAction is not a REPORT_PREVIEW action', () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'iou-test-1', + }; + const lastAction: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + }; + + const result = getIOUReportIDOfLastAction(report, undefined, undefined, lastAction); + expect(result).toBeUndefined(); + }); + + it('should return IOU report ID when lastAction is a REPORT_PREVIEW action with a valid IOU report', async () => { + const iouReportID = 'iou-report-1'; + const reportID = 'iou-test-2'; + const report: Report = { + ...createRandomReport(0, undefined), + reportID, + }; + + // Create the IOU report in Onyx so getReportOrDraftReport can find it + const iouReport: Report = { + ...createRandomReport(0, undefined), + reportID: iouReportID, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, iouReport); + await waitForBatchedUpdates(); + + const lastAction: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + originalMessage: { + linkedReportID: iouReportID, + }, + } as ReportAction; + + const result = getIOUReportIDOfLastAction(report, undefined, undefined, lastAction); + expect(result).toBe(iouReportID); + }); + + it('should return undefined when report is archived and canUserPerformWrite returns false', async () => { + const reportID = 'iou-test-archived'; + const report: Report = { + ...createRandomReport(0, undefined), + reportID, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + type: CONST.REPORT.TYPE.CHAT, + }; + + // Set up the report in Onyx + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + await waitForBatchedUpdates(); + + // When we pass a non-empty privateIsArchived string, the report is considered archived + const privateIsArchived = '2023-01-01 00:00:00'; + + // With no lastAction provided and no visible actions in Onyx, it falls through to the lastVisibleAction lookup + // which returns undefined, so isReportPreviewAction returns false + const result = getIOUReportIDOfLastAction(report, privateIsArchived); + expect(result).toBeUndefined(); + }); + + it('should handle privateIsArchived as undefined (non-archived report)', () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'iou-test-not-archived', + }; + const lastAction: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + }; + + // privateIsArchived is undefined means the report is not archived + const result = getIOUReportIDOfLastAction(report, undefined, undefined, lastAction); + expect(result).toBeUndefined(); + }); + }); });