From 661119627c12823c64015a89537db4e5a83b2860 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 10 Mar 2026 15:21:54 +0700 Subject: [PATCH 1/3] clean up --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- src/libs/OptionsListUtils/index.ts | 49 +++---- src/libs/SidebarUtils.ts | 20 ++- tests/unit/OptionsListUtilsTest.tsx | 131 ++++++++++++++++-- 4 files changed, 161 insertions(+), 41 deletions(-) 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 b83731c459cdd..7320158803695 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -174,7 +174,6 @@ import type { ReportActions, ReportAttributesDerivedValue, ReportMetadata, - ReportNameValuePairs, VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; @@ -214,15 +213,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 = {}; let allReportActions: OnyxCollection; @@ -531,13 +521,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)) { @@ -555,13 +548,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); @@ -954,9 +946,9 @@ function createOption( report: OnyxInputOrEntry, currentUserAccountID: number, chatReport: OnyxEntry, + privateIsArchived: string | undefined, config?: PreviewConfig, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], - privateIsArchived?: string, visibleReportActionsData: VisibleReportActionsDerivedValue = {}, translate?: LocalizedTranslate, ): SearchOptionData { @@ -1017,10 +1009,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 @@ -1127,12 +1116,12 @@ function getReportOption( !isEmptyObject(report) ? report : undefined, currentUserAccountID, chatReport, + privateIsArchived, { showChatPreviewLine: false, forcePolicyNamePreview: false, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ); @@ -1188,12 +1177,12 @@ function getReportDisplayOption( !isEmptyObject(report) ? report : undefined, currentUserAccountID, chatReport, + privateIsArchived, { showChatPreviewLine: false, forcePolicyNamePreview: false, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ); @@ -1243,12 +1232,12 @@ function getPolicyExpenseReportOption( !isEmptyObject(expenseReport) ? expenseReport : null, currentUserAccountID, chatReport, + privateIsArchived, { showChatPreviewLine: false, forcePolicyNamePreview: false, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ); @@ -1386,7 +1375,7 @@ function processReport( reportMapEntry, reportOption: { item: report, - ...createOption(accountIDs, personalDetails, report, currentUserAccountID, chatReport, undefined, reportAttributesDerived, privateIsArchived, visibleReportActionsData), + ...createOption(accountIDs, personalDetails, report, currentUserAccountID, chatReport, privateIsArchived, undefined, reportAttributesDerived, visibleReportActionsData), }, }; } @@ -1442,11 +1431,11 @@ function createOptionList( report, currentUserAccountID, chatReport, + privateIsArchived, { showPersonalDetails: true, }, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ), }; @@ -1580,9 +1569,9 @@ function createFilteredOptionList( reportMapForAccountIDs[accountID], currentUserAccountID, chatReport, + privateIsArchived, {showPersonalDetails: true}, reportAttributesDerived, - privateIsArchived, visibleReportActionsData, ), }; @@ -1609,7 +1598,7 @@ function createOptionFromReport( return { item: report, - ...createOption(accountIDs, personalDetails, report, currentUserAccountID, chatReport, config, reportAttributesDerived, privateIsArchived, visibleReportActionsData), + ...createOption(accountIDs, personalDetails, report, currentUserAccountID, chatReport, privateIsArchived, config, reportAttributesDerived, visibleReportActionsData), }; } @@ -1939,8 +1928,8 @@ function getUserToInviteOption({ null, currentUserAccountID, undefined, - {showChatPreviewLine}, undefined, + {showChatPreviewLine}, undefined, visibleReportActionsData, ); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 04b67d8c90d14..baff3bc711b03 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1115,7 +1115,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 { @@ -1143,7 +1151,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 95b38894382d6..55bb333582fe8 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -26,6 +26,7 @@ import { formatSectionsFromSearchTerm, getCurrentUserSearchTerms, getFilteredRecentAttendees, + getIOUReportIDOfLastAction, getLastActorDisplayName, getLastActorDisplayNameFromLastVisibleActions, getLastMessageTextForReport, @@ -4186,7 +4187,7 @@ 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, undefined, {showChatPreviewLine: true}); + const result = createOption([1, 2], PERSONAL_DETAILS, report, CURRENT_USER_ACCOUNT_ID, undefined, undefined, {showChatPreviewLine: true}); expect(result.alternateText).toBe('Iron Man owes ₫34'); }); @@ -4214,7 +4215,7 @@ describe('OptionsListUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport); await waitForBatchedUpdates(); - const result = createOption([1, 2], PERSONAL_DETAILS, report, 1, chatReport); + const result = createOption([1, 2], PERSONAL_DETAILS, report, 1, chatReport, undefined); expect(result.reportID).toBe(reportID); expect(typeof result.text).toBe('string'); @@ -4956,7 +4957,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'); @@ -4996,7 +4997,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'); @@ -5037,7 +5038,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 @@ -5077,7 +5078,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'); @@ -5116,12 +5117,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', () => { @@ -6828,7 +6850,7 @@ describe('OptionsListUtils', () => { await waitForBatchedUpdates(); // When we call createOption with the linked chat report - const result = createOption([1, 2], PERSONAL_DETAILS, expenseReport, CURRENT_USER_ACCOUNT_ID, linkedChatReport); + const result = createOption([1, 2], PERSONAL_DETAILS, expenseReport, CURRENT_USER_ACCOUNT_ID, linkedChatReport, undefined); // Then the option should be created successfully expect(result).toBeDefined(); @@ -7160,4 +7182,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(); + }); + }); }); From d2192521f16e42a7ab06fdfaa9fa6cd5be8ee48d Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 10 Mar 2026 16:07:33 +0700 Subject: [PATCH 2/3] update test --- tests/unit/OptionsListUtilsTest.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 55bb333582fe8..258d0aa384594 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2385,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, @@ -3616,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 From f82a3714ef4b3854c1e7679673725d3c3cc989dd Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 11 Mar 2026 16:41:12 +0700 Subject: [PATCH 3/3] update max warnings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9650a8225b035..ac8702a1fb7a7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "typecheck-tsgo": "tsgo --project tsconfig.tsgo.json", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=335 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=334 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed",