From 4af298a03f941f63c396c56c772b3ce8a2073637 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 22 Jan 2026 18:52:04 +0700 Subject: [PATCH 1/9] refactor createOptionList --- src/components/OptionListContextProvider.tsx | 20 ++++++++------ src/hooks/useFilteredOptions.ts | 4 ++- src/libs/OptionsListUtils/index.ts | 18 ++++++++++--- tests/perf-test/OptionsListUtils.perf-test.ts | 2 +- tests/perf-test/SearchRouter.perf-test.tsx | 2 +- tests/unit/OptionsListUtilsTest.tsx | 26 +++++++++---------- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 29e47b3256b04..2d449bdb3bfab 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -2,6 +2,7 @@ import React, {createContext, useCallback, useContext, useEffect, useMemo, useRe import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; +import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap'; import {createOptionFromReport, createOptionList, processReport, shallowOptionsListCompare} from '@libs/OptionsListUtils'; import type {OptionList, SearchOption} from '@libs/OptionsListUtils'; import {isSelfDM} from '@libs/ReportUtils'; @@ -55,14 +56,15 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { const personalDetails = usePersonalDetails(); const prevPersonalDetails = usePrevious(personalDetails); const hasInitialData = useMemo(() => Object.keys(personalDetails ?? {}).length > 0, [personalDetails]); + const privateIsArchivedMap = usePrivateIsArchivedMap(); const loadOptions = useCallback(() => { - const optionLists = createOptionList(personalDetails, reports, reportAttributes?.reports); + const optionLists = createOptionList(personalDetails, privateIsArchivedMap, reports, reportAttributes?.reports); setOptions({ reports: optionLists.reports, personalDetails: optionLists.personalDetails, }); - }, [personalDetails, reports, reportAttributes?.reports]); + }, [personalDetails, privateIsArchivedMap, reports, reportAttributes?.reports]); /** * This effect is responsible for generating the options list when their data is not yet initialized @@ -118,7 +120,8 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { for (const reportKey of changedReportKeys) { const report = changedReportsEntries[reportKey]; const reportID = reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ''); - const {reportOption} = processReport(report, personalDetails, reportAttributes?.reports); + const privateIsArchived = privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; + const {reportOption} = processReport(report, personalDetails, privateIsArchived, reportAttributes?.reports); if (reportOption) { updatedReportsMap.set(reportID, reportOption); @@ -132,7 +135,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { reports: Array.from(updatedReportsMap.values()), }; }); - }, [changedReportsEntries, personalDetails, reportAttributes?.reports]); + }, [changedReportsEntries, personalDetails, privateIsArchivedMap, reportAttributes?.reports]); useEffect(() => { if (!changedReportActions || !areOptionsInitialized.current) { @@ -152,7 +155,8 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { } const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - const {reportOption} = processReport(updatedReportsMap.get(reportID)?.item, personalDetails, reportAttributes?.reports); + const privateIsArchived = privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; + const {reportOption} = processReport(updatedReportsMap.get(reportID)?.item, personalDetails, privateIsArchived, reportAttributes?.reports); if (reportOption) { updatedReportsMap.set(reportID, reportOption); @@ -164,7 +168,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { reports: Array.from(updatedReportsMap.values()), }; }); - }, [changedReportActions, personalDetails, reportAttributes?.reports]); + }, [changedReportActions, personalDetails, privateIsArchivedMap, reportAttributes?.reports]); /** * This effect is used to update the options list when personal details change. @@ -182,7 +186,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { // Handle initial personal details load. This initialization is required here specifically to prevent // UI freezing that occurs when resetting the app from the troubleshooting page. if (!prevPersonalDetails) { - const {personalDetails: newPersonalDetailsOptions, reports: newReports} = createOptionList(personalDetails, reports, reportAttributes?.reports); + const {personalDetails: newPersonalDetailsOptions, reports: newReports} = createOptionList(personalDetails, privateIsArchivedMap, reports, reportAttributes?.reports); setOptions((prevOptions) => ({ ...prevOptions, personalDetails: newPersonalDetailsOptions, @@ -225,7 +229,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { } // since personal details are not a collection, we need to recreate the whole list from scratch - const newPersonalDetailsOptions = createOptionList(personalDetails, reports, reportAttributes?.reports).personalDetails; + const newPersonalDetailsOptions = createOptionList(personalDetails, privateIsArchivedMap, reports, reportAttributes?.reports).personalDetails; setOptions((prevOptions) => { const newOptions = {...prevOptions}; diff --git a/src/hooks/useFilteredOptions.ts b/src/hooks/useFilteredOptions.ts index da77f8ba42cec..1ec798cbe0b37 100644 --- a/src/hooks/useFilteredOptions.ts +++ b/src/hooks/useFilteredOptions.ts @@ -6,6 +6,7 @@ import type {OptionList} from '@libs/OptionsListUtils/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type Beta from '@src/types/onyx/Beta'; import useOnyx from './useOnyx'; +import usePrivateIsArchivedMap from './usePrivateIsArchivedMap'; type UseFilteredOptionsConfig = { /** Maximum number of recent reports to pre-filter and process (default: 500). */ @@ -76,12 +77,13 @@ function useFilteredOptions(config: UseFilteredOptionsConfig = {}): UseFilteredO canBeMissing: true, selector: reportsSelector, }); + const privateIsArchivedMap = usePrivateIsArchivedMap(); const totalReports = allReports ? Object.keys(allReports).length : 0; const options: OptionList | null = enabled && allReports && allPersonalDetails - ? createFilteredOptionList(allPersonalDetails, allReports, reportAttributesDerived, { + ? createFilteredOptionList(allPersonalDetails, privateIsArchivedMap, allReports, reportAttributesDerived, { maxRecentReports: reportsLimit, includeP2P, searchTerm, diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 80f76b73e6165..155486e5c4b34 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ +import type {PrivateIsArchivedMap} from '@selectors/ReportNameValuePairs'; import * as Sentry from '@sentry/react-native'; import {Str} from 'expensify-common'; import deburr from 'lodash/deburr'; @@ -1226,6 +1227,7 @@ function isReportSelected(reportOption: SearchOptionData, selectedOptions: Array function processReport( report: OnyxEntry | null, personalDetails: OnyxEntry, + privateIsArchived: string | undefined, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], ): { reportMapEntry?: [number, Report]; // The entry to add to reportMapForAccountIDs if applicable @@ -1250,12 +1252,17 @@ function processReport( reportMapEntry, reportOption: { item: report, - ...createOption(accountIDs, personalDetails, report, undefined, reportAttributesDerived), + ...createOption(accountIDs, personalDetails, report, undefined, reportAttributesDerived, privateIsArchived), }, }; } -function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection, reportAttributesDerived?: ReportAttributesDerivedValue['reports']) { +function createOptionList( + personalDetails: OnyxEntry, + privateIsArchivedMap: PrivateIsArchivedMap, + reports?: OnyxCollection, + reportAttributesDerived?: ReportAttributesDerivedValue['reports'], +) { const span = Sentry.startInactiveSpan({name: 'createOptionList'}); const reportMapForAccountIDs: Record = {}; @@ -1263,7 +1270,8 @@ function createOptionList(personalDetails: OnyxEntry, repor if (reports) { for (const report of Object.values(reports)) { - const {reportMapEntry, reportOption} = processReport(report, personalDetails, reportAttributesDerived); + const privateIsArchived = privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`]; + const {reportMapEntry, reportOption} = processReport(report, personalDetails, privateIsArchived, reportAttributesDerived); if (reportMapEntry) { const [accountID, reportValue] = reportMapEntry; @@ -1316,6 +1324,7 @@ function createOptionList(personalDetails: OnyxEntry, repor */ function createFilteredOptionList( personalDetails: OnyxEntry, + privateIsArchivedMap: PrivateIsArchivedMap, reports: OnyxCollection, reportAttributesDerived: ReportAttributesDerivedValue['reports'] | undefined, options: { @@ -1373,7 +1382,8 @@ function createFilteredOptionList( // Step 5: Process the limited set of reports (performance optimization) const reportOptions: Array> = []; for (const report of limitedReports) { - const {reportMapEntry, reportOption} = processReport(report, personalDetails, reportAttributesDerived); + const privateIsArchived = privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; + const {reportMapEntry, reportOption} = processReport(report, personalDetails, privateIsArchived, reportAttributesDerived); if (reportMapEntry) { const [accountID, reportValue] = reportMapEntry; diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index e6867e5337a58..bd32c89a81fb4 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -90,7 +90,7 @@ jest.mock('@react-navigation/native', () => { }; }); -const options = createOptionList(personalDetails, reports); +const options = createOptionList(personalDetails, {}, reports); const ValidOptionsConfig = { betas: mockedBetas, diff --git a/tests/perf-test/SearchRouter.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx index 10f4f21c435f9..16cd80c195979 100644 --- a/tests/perf-test/SearchRouter.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -89,7 +89,7 @@ const getMockedPersonalDetails = (length = 100) => const mockedReports = getMockedReports(600); const mockedBetas = Object.values(CONST.BETAS); const mockedPersonalDetails = getMockedPersonalDetails(100); -const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); +const mockedOptions = createOptionList(mockedPersonalDetails, {}, mockedReports); beforeAll(() => Onyx.init({ diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index f0d67978153c4..db176d316b2de 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -638,12 +638,12 @@ describe('OptionsListUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}10`, reportNameValuePairs); await waitForBatchedUpdates(); - OPTIONS = createOptionList(PERSONAL_DETAILS, REPORTS); - OPTIONS_WITH_CONCIERGE = createOptionList(PERSONAL_DETAILS_WITH_CONCIERGE, REPORTS_WITH_CONCIERGE); - OPTIONS_WITH_CHRONOS = createOptionList(PERSONAL_DETAILS_WITH_CHRONOS, REPORTS_WITH_CHRONOS); - OPTIONS_WITH_RECEIPTS = createOptionList(PERSONAL_DETAILS_WITH_RECEIPTS, REPORTS_WITH_RECEIPTS); - OPTIONS_WITH_WORKSPACE_ROOM = createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE_ROOMS); - OPTIONS_WITH_MANAGER_MCTEST = createOptionList(PERSONAL_DETAILS_WITH_MANAGER_MCTEST); + OPTIONS = createOptionList(PERSONAL_DETAILS, {}, REPORTS); + OPTIONS_WITH_CONCIERGE = createOptionList(PERSONAL_DETAILS_WITH_CONCIERGE, {}, REPORTS_WITH_CONCIERGE); + OPTIONS_WITH_CHRONOS = createOptionList(PERSONAL_DETAILS_WITH_CHRONOS, {}, REPORTS_WITH_CHRONOS); + OPTIONS_WITH_RECEIPTS = createOptionList(PERSONAL_DETAILS_WITH_RECEIPTS, {}, REPORTS_WITH_RECEIPTS); + OPTIONS_WITH_WORKSPACE_ROOM = createOptionList(PERSONAL_DETAILS, {}, REPORTS_WITH_WORKSPACE_ROOMS); + OPTIONS_WITH_MANAGER_MCTEST = createOptionList(PERSONAL_DETAILS_WITH_MANAGER_MCTEST, {}); }); describe('getSearchOptions()', () => { @@ -1640,7 +1640,7 @@ describe('OptionsListUtils', () => { // cspell:disable-next-line const searchText = 'barryallen'; // Given a set of options created from PERSONAL_DETAILS_WITH_PERIODS - const OPTIONS_WITH_PERIODS = createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); + const OPTIONS_WITH_PERIODS = createOptionList(PERSONAL_DETAILS_WITH_PERIODS, {}, REPORTS); // When we call getSearchOptions with all betas const options = getSearchOptions({options: OPTIONS_WITH_PERIODS, draftComments: {}, nvpDismissedProductTraining, loginList, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value and sortByReportTypeInSearch param @@ -1682,7 +1682,7 @@ describe('OptionsListUtils', () => { it('should prioritize options with matching display name over chat rooms', () => { const searchText = 'spider'; // Given a set of options with chat rooms - const OPTIONS_WITH_CHAT_ROOMS = createOptionList(PERSONAL_DETAILS, REPORTS_WITH_CHAT_ROOM); + const OPTIONS_WITH_CHAT_ROOMS = createOptionList(PERSONAL_DETAILS, {}, REPORTS_WITH_CHAT_ROOM); // When we call getSearchOptions with all betas const options = getSearchOptions({options: OPTIONS_WITH_CHAT_ROOMS, draftComments: {}, nvpDismissedProductTraining, loginList, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value @@ -2111,7 +2111,7 @@ describe('OptionsListUtils', () => { .then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, PERSONAL_DETAILS_WITH_PERIODS)) .then(() => { // Given a set of options with periods - const OPTIONS_WITH_PERIODS = createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); + const OPTIONS_WITH_PERIODS = createOptionList(PERSONAL_DETAILS_WITH_PERIODS, {}, REPORTS); // When we call getSearchOptions const results = getSearchOptions({options: OPTIONS_WITH_PERIODS, draftComments: {}, nvpDismissedProductTraining, loginList}); // When we pass the returned options to filterAndOrderOptions with a search value @@ -2144,7 +2144,7 @@ describe('OptionsListUtils', () => { it('should order self dm always on top if the search matches with the self dm login', () => { const searchTerm = 'tonystark@expensify.com'; - const OPTIONS_WITH_SELF_DM = createOptionList(PERSONAL_DETAILS, REPORTS_WITH_SELF_DM); + const OPTIONS_WITH_SELF_DM = createOptionList(PERSONAL_DETAILS, {}, REPORTS_WITH_SELF_DM); // Given a set of options with self dm and all betas const options = getSearchOptions({options: OPTIONS_WITH_SELF_DM, draftComments: {}, nvpDismissedProductTraining, loginList, betas: [CONST.BETAS.ALL]}); @@ -2196,7 +2196,7 @@ describe('OptionsListUtils', () => { renderLocaleContextProvider(); // Given a set of reports and personal details // When we call createOptionList and extract the reports - const reports = createOptionList(PERSONAL_DETAILS, REPORTS).reports; + const reports = createOptionList(PERSONAL_DETAILS, {}, REPORTS).reports; // Then the returned reports should match the expected values expect(reports.at(10)?.subtitle).toBe(`Submits to Mister Fantastic`); @@ -2207,7 +2207,7 @@ describe('OptionsListUtils', () => { .then(() => Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES)) .then(() => { // When we call createOptionList again - const newReports = createOptionList(PERSONAL_DETAILS, REPORTS).reports; + const newReports = createOptionList(PERSONAL_DETAILS, {}, REPORTS).reports; // Then the returned reports should change to Spanish // cspell:disable-next-line expect(newReports.at(10)?.subtitle).toBe('Se envĂ­a a Mister Fantastic'); @@ -2287,7 +2287,7 @@ describe('OptionsListUtils', () => { }, }); // When we call createOptionList - const reports = createOptionList(PERSONAL_DETAILS, REPORTS).reports; + const reports = createOptionList(PERSONAL_DETAILS, {}, REPORTS).reports; const archivedReport = reports.find((report) => report.reportID === '10'); // Then the returned report should contain default archived reason From aa1f7c0fb38639051841f6938d760da1567d2121 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 28 Jan 2026 16:15:31 +0700 Subject: [PATCH 2/9] prettier --- tests/unit/OptionsListUtilsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 1c6d9d76ef671..a3d477271a109 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2660,7 +2660,7 @@ describe('OptionsListUtils', () => { // Given a set of reports and personal details // When we call createOptionList and extract the reports const reports = createOptionList(PERSONAL_DETAILS, {}, CURRENT_USER_ACCOUNT_ID, REPORTS).reports; - + // Then the returned reports should match the expected values expect(reports.at(10)?.subtitle).toBe(`Submits to Mister Fantastic`); From 6d415ad70d7d633b683dcd8436918596ea4f54d9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 6 Feb 2026 11:59:38 +0700 Subject: [PATCH 3/9] add tests --- src/libs/OptionsListUtils/index.ts | 2 - tests/unit/OptionsListUtilsTest.tsx | 108 ++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 3184fbc32e941..2110b1e72d425 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -149,7 +149,6 @@ import {generateAccountID} from '@libs/UserUtils'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PrivateIsArchivedMap} from '@src/selectors/ReportNameValuePairs'; import type { Beta, DismissedProductTraining, @@ -1277,7 +1276,6 @@ function processReport( function createOptionList( personalDetails: OnyxEntry, - privateIsArchivedMap: PrivateIsArchivedMap, currentUserAccountID: number, privateIsArchivedMap: PrivateIsArchivedMap, reports?: OnyxCollection, diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 156d27dfba20a..af1cc59da456a 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -14,6 +14,7 @@ import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTop import type {OptionList, Options, SearchOption} from '@libs/OptionsListUtils'; import { canCreateOptimisticPersonalDetailOption, + createFilteredOptionList, createOption, createOptionList, filterAndOrderOptions, @@ -3131,6 +3132,113 @@ describe('OptionsListUtils', () => { expect(misterFantasticOption?.private_isArchived).toBe('2023-06-15 10:00:00'); expect(invisibleWomanOption?.private_isArchived).toBe('2023-07-20 15:30:00'); }); + + it('should set private_isArchived on report options when privateIsArchivedMap is provided', () => { + renderLocaleContextProvider(); + // Given a privateIsArchivedMap with archived reports + const privateIsArchivedMap: PrivateIsArchivedMap = { + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}3`]: '2023-06-15 10:00:00', + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}5`]: '2023-07-20 15:30:00', + }; + + // When we call createOptionList with this privateIsArchivedMap + const result = createOptionList(PERSONAL_DETAILS, CURRENT_USER_ACCOUNT_ID, privateIsArchivedMap, REPORTS); + + // Then the report options should have the correct private_isArchived values + const report3Option = result.reports.find((r) => r.item?.reportID === '3'); + const report5Option = result.reports.find((r) => r.item?.reportID === '5'); + const report1Option = result.reports.find((r) => r.item?.reportID === '1'); + + expect(report3Option?.private_isArchived).toBe('2023-06-15 10:00:00'); + expect(report5Option?.private_isArchived).toBe('2023-07-20 15:30:00'); + // Report 1 should not have private_isArchived since it's not in the map + expect(report1Option?.private_isArchived).toBeUndefined(); + }); + }); + + describe('createFilteredOptionList()', () => { + it('should set private_isArchived on report options when privateIsArchivedMap is provided', () => { + renderLocaleContextProvider(); + // Given a privateIsArchivedMap with archived reports + const privateIsArchivedMap: PrivateIsArchivedMap = { + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}3`]: '2023-06-15 10:00:00', + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}5`]: '2023-07-20 15:30:00', + }; + + // When we call createFilteredOptionList with this privateIsArchivedMap + const result = createFilteredOptionList(PERSONAL_DETAILS, privateIsArchivedMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined); + + // Then the report options should have the correct private_isArchived values + const report3Option = result.reports.find((r) => r.item?.reportID === '3'); + const report5Option = result.reports.find((r) => r.item?.reportID === '5'); + const report1Option = result.reports.find((r) => r.item?.reportID === '1'); + + expect(report3Option?.private_isArchived).toBe('2023-06-15 10:00:00'); + expect(report5Option?.private_isArchived).toBe('2023-07-20 15:30:00'); + // Report 1 should not have private_isArchived since it's not in the map + expect(report1Option?.private_isArchived).toBeUndefined(); + }); + + it('should not set private_isArchived from map when privateIsArchivedMap is empty', () => { + renderLocaleContextProvider(); + // Given an empty privateIsArchivedMap + const emptyMap: PrivateIsArchivedMap = {}; + + // When we call createFilteredOptionList with an empty privateIsArchivedMap + const result = createFilteredOptionList(PERSONAL_DETAILS, emptyMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined); + + // Then reports NOT in Onyx (like report 3, 5) should not have private_isArchived set + // Note: Report 10 gets private_isArchived from Onyx (set in beforeAll) + const report3Option = result.reports.find((r) => r.item?.reportID === '3'); + const report5Option = result.reports.find((r) => r.item?.reportID === '5'); + expect(report3Option?.private_isArchived).toBeUndefined(); + expect(report5Option?.private_isArchived).toBeUndefined(); + }); + + it('should correctly map multiple archived reports in privateIsArchivedMap', () => { + renderLocaleContextProvider(); + // Given a privateIsArchivedMap with multiple archived reports + const privateIsArchivedMap: PrivateIsArchivedMap = { + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}1`]: '2023-01-01 00:00:00', + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}3`]: '2023-06-15 10:00:00', + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}5`]: '2023-07-20 15:30:00', + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}7`]: '2023-12-31 23:59:59', + }; + + // When we call createFilteredOptionList with this privateIsArchivedMap + const result = createFilteredOptionList(PERSONAL_DETAILS, privateIsArchivedMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined); + + // Then the report options should have the correct private_isArchived values + const report1Option = result.reports.find((r) => r.item?.reportID === '1'); + const report3Option = result.reports.find((r) => r.item?.reportID === '3'); + const report5Option = result.reports.find((r) => r.item?.reportID === '5'); + const report7Option = result.reports.find((r) => r.item?.reportID === '7'); + const report2Option = result.reports.find((r) => r.item?.reportID === '2'); + + expect(report1Option?.private_isArchived).toBe('2023-01-01 00:00:00'); + expect(report3Option?.private_isArchived).toBe('2023-06-15 10:00:00'); + expect(report5Option?.private_isArchived).toBe('2023-07-20 15:30:00'); + expect(report7Option?.private_isArchived).toBe('2023-12-31 23:59:59'); + // Report 2 should not have private_isArchived since it's not in the map + expect(report2Option?.private_isArchived).toBeUndefined(); + }); + + it('should respect maxRecentReports option while preserving archived status', () => { + renderLocaleContextProvider(); + // Given a privateIsArchivedMap and a small maxRecentReports limit + const privateIsArchivedMap: PrivateIsArchivedMap = { + [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}7`]: '2023-12-31 23:59:59', // Report 7 has largest lastVisibleActionCreated + }; + + // When we call createFilteredOptionList with maxRecentReports limit + const result = createFilteredOptionList(PERSONAL_DETAILS, privateIsArchivedMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined, { + maxRecentReports: 5, + }); + + // Then the report 7 (most recent) should still have private_isArchived set + const report7Option = result.reports.find((r) => r.item?.reportID === '7'); + expect(report7Option?.private_isArchived).toBe('2023-12-31 23:59:59'); + }); }); describe('filterSelfDMChat()', () => { From b863b4a59a3d2b476c3e764fbc2c44e1e3a77e21 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 10 Feb 2026 16:29:49 +0700 Subject: [PATCH 4/9] add tests --- tests/unit/OptionListContextProviderTest.tsx | 61 +++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/unit/OptionListContextProviderTest.tsx b/tests/unit/OptionListContextProviderTest.tsx index 47db5681c93d2..e1dac1a319bbc 100644 --- a/tests/unit/OptionListContextProviderTest.tsx +++ b/tests/unit/OptionListContextProviderTest.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import OptionListContextProvider, {useOptionsList} from '@components/OptionListContextProvider'; import useOnyx from '@hooks/useOnyx'; -import {createOptionList} from '@libs/OptionsListUtils'; +import {createOptionList, processReport} from '@libs/OptionsListUtils'; import ONYXKEYS from '@src/ONYXKEYS'; jest.mock('@libs/OptionsListUtils', () => { @@ -19,11 +19,17 @@ jest.mock('@libs/OptionsListUtils', () => { }); jest.mock('@hooks/useOnyx', () => jest.fn()); +const EMPTY_ARCHIVED_MAP = {}; +jest.mock('@hooks/usePrivateIsArchivedMap', () => ({ + __esModule: true, + default: jest.fn(() => EMPTY_ARCHIVED_MAP), +})); jest.mock('@components/OnyxListItemProvider', () => ({ usePersonalDetails: jest.fn(), })); const mockCreateOptionList = createOptionList as jest.MockedFunction; +const mockProcessReport = processReport as jest.MockedFunction; const mockUseOnyx = useOnyx as jest.MockedFunction; const mockUsePersonalDetails = usePersonalDetails as jest.MockedFunction; @@ -114,4 +120,57 @@ describe('OptionListContextProvider', () => { expect(mockCreateOptionList).toHaveBeenCalledTimes(1); }); + + it('calls processReport with privateIsArchived when reports change', () => { + const reportID = '1'; + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + const report = {reportID}; + + const {result, rerender} = renderHook(({shouldInitialize}) => useOptionsList({shouldInitialize}), { + initialProps: {shouldInitialize: false}, + wrapper, + }); + + act(() => { + result.current.initializeOptions(); + }); + + mockProcessReport.mockClear(); + + onyxState = { + ...onyxState, + [ONYXKEYS.COLLECTION.REPORT]: {[reportKey]: report}, + }; + onyxSourceValues = { + ...onyxSourceValues, + [ONYXKEYS.COLLECTION.REPORT]: {[reportKey]: report}, + }; + rerender({shouldInitialize: false}); + + expect(mockProcessReport).toHaveBeenCalled(); + }); + + it('calls processReport with privateIsArchived when report actions change', () => { + const reportID = '2'; + const reportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; + + const {result, rerender} = renderHook(({shouldInitialize}) => useOptionsList({shouldInitialize}), { + initialProps: {shouldInitialize: false}, + wrapper, + }); + + act(() => { + result.current.initializeOptions(); + }); + + mockProcessReport.mockClear(); + + onyxSourceValues = { + ...onyxSourceValues, + [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: {[reportActionsKey]: {someAction: {}}}, + }; + rerender({shouldInitialize: false}); + + expect(mockProcessReport).toHaveBeenCalled(); + }); }); From 38cfdcbc00dcfff85097cf6d1dc4cb0797918150 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 11 Feb 2026 11:59:16 +0700 Subject: [PATCH 5/9] update test --- tests/unit/OptionsListUtilsTest.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 099591b5d8b14..d724c90cf9a6c 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -3584,7 +3584,7 @@ describe('OptionsListUtils', () => { }; // When we call createFilteredOptionList with this privateIsArchivedMap - const result = createFilteredOptionList(PERSONAL_DETAILS, privateIsArchivedMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined); + const result = createFilteredOptionList(PERSONAL_DETAILS, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined, privateIsArchivedMap); // Then the report options should have the correct private_isArchived values const report3Option = result.reports.find((r) => r.item?.reportID === '3'); @@ -3603,7 +3603,7 @@ describe('OptionsListUtils', () => { const emptyMap: PrivateIsArchivedMap = {}; // When we call createFilteredOptionList with an empty privateIsArchivedMap - const result = createFilteredOptionList(PERSONAL_DETAILS, emptyMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined); + const result = createFilteredOptionList(PERSONAL_DETAILS, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined, emptyMap); // Then reports NOT in Onyx (like report 3, 5) should not have private_isArchived set // Note: Report 10 gets private_isArchived from Onyx (set in beforeAll) @@ -3624,7 +3624,7 @@ describe('OptionsListUtils', () => { }; // When we call createFilteredOptionList with this privateIsArchivedMap - const result = createFilteredOptionList(PERSONAL_DETAILS, privateIsArchivedMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined); + const result = createFilteredOptionList(PERSONAL_DETAILS, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined, privateIsArchivedMap); // Then the report options should have the correct private_isArchived values const report1Option = result.reports.find((r) => r.item?.reportID === '1'); @@ -3649,7 +3649,7 @@ describe('OptionsListUtils', () => { }; // When we call createFilteredOptionList with maxRecentReports limit - const result = createFilteredOptionList(PERSONAL_DETAILS, privateIsArchivedMap, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined, { + const result = createFilteredOptionList(PERSONAL_DETAILS, REPORTS, CURRENT_USER_ACCOUNT_ID, undefined, privateIsArchivedMap, { maxRecentReports: 5, }); From bc9ea0d65b64dd345394a70f2af77babc8895478 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 11 Feb 2026 12:07:14 +0700 Subject: [PATCH 6/9] update test --- tests/unit/OptionListContextProviderTest.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unit/OptionListContextProviderTest.tsx b/tests/unit/OptionListContextProviderTest.tsx index 8a747ddc62c45..de332e9df3fc3 100644 --- a/tests/unit/OptionListContextProviderTest.tsx +++ b/tests/unit/OptionListContextProviderTest.tsx @@ -23,10 +23,7 @@ jest.mock('@libs/OptionsListUtils', () => { jest.mock('@hooks/useOnyx', () => jest.fn()); const EMPTY_ARCHIVED_MAP = {}; -jest.mock('@hooks/usePrivateIsArchivedMap', () => ({ - __esModule: true, - default: jest.fn(() => EMPTY_ARCHIVED_MAP), -})); +jest.mock('@hooks/usePrivateIsArchivedMap', () => jest.fn()); jest.mock('@components/OnyxListItemProvider', () => ({ usePersonalDetails: jest.fn(), })); From 00baefb493f140f6625f668a00b5beae6d7d2881 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 11 Feb 2026 12:18:07 +0700 Subject: [PATCH 7/9] update test --- tests/unit/OptionListContextProviderTest.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/OptionListContextProviderTest.tsx b/tests/unit/OptionListContextProviderTest.tsx index de332e9df3fc3..1239397dcc637 100644 --- a/tests/unit/OptionListContextProviderTest.tsx +++ b/tests/unit/OptionListContextProviderTest.tsx @@ -22,7 +22,6 @@ jest.mock('@libs/OptionsListUtils', () => { }); jest.mock('@hooks/useOnyx', () => jest.fn()); -const EMPTY_ARCHIVED_MAP = {}; jest.mock('@hooks/usePrivateIsArchivedMap', () => jest.fn()); jest.mock('@components/OnyxListItemProvider', () => ({ usePersonalDetails: jest.fn(), From 7de117ac5985a058a88b0af62ef71f8326e45169 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Sun, 22 Feb 2026 11:29:34 +0700 Subject: [PATCH 8/9] remove unnessary import --- src/libs/OptionsListUtils/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 55e3872991713..57a9e68bb7bef 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/prefer-for-of */ -import type {PrivateIsArchivedMap} from '@selectors/ReportNameValuePairs'; import * as Sentry from '@sentry/react-native'; import {Str} from 'expensify-common'; import deburr from 'lodash/deburr'; From bbad7cfd47cc5bce127f8672a2ee3c5e862ae368 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Sun, 22 Feb 2026 11:37:27 +0700 Subject: [PATCH 9/9] lint fix --- src/libs/OptionsListUtils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 57a9e68bb7bef..264b3ff0d6e4b 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/prefer-for-of */ import * as Sentry from '@sentry/react-native'; import {Str} from 'expensify-common'; import deburr from 'lodash/deburr';