From 01f7b7cbb92879f4c6c28d124894919ebf3286c1 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 10 Nov 2025 18:21:32 +0700 Subject: [PATCH 01/11] refactor isActionLoading --- .../MoneyRequestReportNavigation.tsx | 18 +-- src/components/Search/index.tsx | 15 ++- .../Search/ReportListItemHeader.tsx | 3 +- .../Search/TransactionGroupListItem.tsx | 14 ++- .../Search/TransactionListItem.tsx | 4 +- .../Search/UserInfoAndActionButtonRow.tsx | 13 +-- src/libs/SearchUIUtils.ts | 42 ++++--- src/libs/actions/Search.ts | 109 ++++++++++++------ src/types/onyx/ReportMetadata.ts | 3 + src/types/onyx/SearchResults.ts | 3 - tests/unit/Search/SearchUIUtilsTest.ts | 97 +++++++++++++--- 11 files changed, 227 insertions(+), 94 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index c9c44726d3d5..3979a2f50ae3 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -25,6 +25,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, {canBeMissing: true}); const currentUserDetails = useCurrentUserPersonalDetails(); const {localeCompare, formatPhoneNumber} = useLocalize(); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { canEvict: false, @@ -37,17 +38,18 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const {type, status, sortBy, sortOrder, groupBy} = lastSearchQuery?.queryJSON ?? {}; let results: Array = []; if (!!type && !!currentSearchResults?.data && !!currentSearchResults?.search) { - const searchData = getSections( + const searchData = getSections({ type, - currentSearchResults.data, - currentUserDetails.accountID, - currentUserDetails.email ?? '', + data: currentSearchResults.data, + currentAccountID: currentUserDetails.accountID, + currentUserEmail: currentUserDetails.email ?? '', formatPhoneNumber, groupBy, - exportReportActions, - lastSearchQuery?.searchKey, - archivedReportsIdSet, - ); + reportActions: exportReportActions, + currentSearch: lastSearchQuery?.searchKey, + archivedReportsIDList: archivedReportsIdSet, + reportMetadata, + }); results = getSortedSections(type, status ?? '', searchData, localeCompare, sortBy, sortOrder, groupBy).map((value) => value.reportID); } const allReports = results; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9ae0110fd7bd..dc38e81b60fa 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -257,6 +257,7 @@ function Search({ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const {accountID, email} = useCurrentUserPersonalDetails(); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); // Filter violations based on user visibility const filteredViolations = useMemo(() => { @@ -405,7 +406,19 @@ function Search({ return [[], 0]; } - const data1 = getSections(type, searchResults.data, accountID, email ?? '', formatPhoneNumber, validGroupBy, exportReportActions, searchKey, archivedReportsIdSet, queryJSON); + const data1 = getSections({ + type, + data: searchResults.data, + currentAccountID: accountID, + currentUserEmail: email ?? '', + formatPhoneNumber, + groupBy: validGroupBy, + reportActions: exportReportActions, + currentSearch: searchKey, + archivedReportsIDList: archivedReportsIdSet, + queryJSON, + reportMetadata, + }); return [data1, data1.length]; }, [searchKey, exportReportActions, validGroupBy, isDataLoaded, searchResults, type, archivedReportsIdSet, formatPhoneNumber, accountID, queryJSON, email]); diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx index b59158413b9b..ab5e8699c2a6 100644 --- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx @@ -108,6 +108,7 @@ function HeaderFirstRow({ const StyleUtils = useStyleUtils(); const {isLargeScreenWidth} = useResponsiveLayout(); const theme = useTheme(); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {canBeMissing: true}); const {total, currency} = useMemo(() => { let reportTotal = reportItem.total ?? 0; @@ -180,7 +181,7 @@ function HeaderFirstRow({ action={reportItem.action} goToItem={handleOnButtonPress} isSelected={reportItem.isSelected} - isLoading={reportItem.isActionLoading} + isLoading={reportMetadata?.isActionLoading} policyID={reportItem.policyID} reportID={reportItem.reportID} hash={reportItem.hash} diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index a74a1f396fc2..996af80f9979 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -94,6 +94,7 @@ function TransactionGroupListItem({ const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; const [transactionsVisibleLimit, setTransactionsVisibleLimit] = useState(CONST.TRANSACTION.RESULTS_PAGE_SIZE as number); const [isExpanded, setIsExpanded] = useState(false); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); const transactions = useMemo(() => { if (isExpenseReportType) { @@ -102,13 +103,14 @@ function TransactionGroupListItem({ if (!transactionsSnapshot?.data) { return []; } - const sectionData = getSections( - CONST.SEARCH.DATA_TYPES.EXPENSE, - transactionsSnapshot?.data, - accountID, - currentUserDetails.email ?? '', + const sectionData = getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: transactionsSnapshot?.data, + currentAccountID: accountID, + currentUserEmail: currentUserDetails.email ?? '', formatPhoneNumber, - ) as TransactionListItemType[]; + reportMetadata, + }) as TransactionListItemType[]; return sectionData.map((transactionItem) => ({ ...transactionItem, isSelected: selectedTransactionIDsSet.has(transactionItem.transactionID), diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 701c7295fa54..168b56495c95 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -58,6 +58,8 @@ function TransactionListItem({ return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as SearchReport; }, [snapshot, transactionItem.reportID]); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true}); + const snapshotPolicy = useMemo(() => { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; }, [snapshot, transactionItem.policyID]); @@ -189,7 +191,7 @@ function TransactionListItem({ onCheckboxPress={handleCheckboxPress} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} - isActionLoading={isLoading ?? transactionItem.isActionLoading ?? snapshotReport.isActionLoading} + isActionLoading={isLoading ?? transactionItem.isActionLoading ?? reportMetadata.isActionLoading} isSelected={!!transactionItem.isSelected} dateColumnSize={dateColumnSize} amountColumnSize={amountColumnSize} diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx index 2478ea68e336..4a2b29fb8f58 100644 --- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx @@ -1,7 +1,6 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; -import {useSearchContext} from '@components/Search/SearchContext'; import type {TransactionListItemType, TransactionReportGroupListItemType} from '@components/SelectionListWithSections/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -11,7 +10,6 @@ import {isCorrectSearchUserName} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {SearchReport} from '@src/types/onyx/SearchResults'; import ActionCell from './ActionCell'; import UserInfoCellsWithArrow from './UserInfoCellsWithArrow'; @@ -30,18 +28,13 @@ function UserInfoAndActionButtonRow({ const {isLargeScreenWidth} = useResponsiveLayout(); const {translate} = useLocalize(); const transactionItem = item as unknown as TransactionListItemType; - const {currentSearchHash} = useSearchContext(); - const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true}); const hasFromSender = !!item?.from && !!item?.from?.accountID && !!item?.from?.displayName; const hasToRecipient = !!item?.to && !!item?.to?.accountID && !!item?.to?.displayName; const participantFromDisplayName = item?.from?.displayName ?? item?.from?.login ?? translate('common.hidden'); const participantToDisplayName = item?.to?.displayName ?? item?.to?.login ?? translate('common.hidden'); const shouldShowToRecipient = hasFromSender && hasToRecipient && !!item?.to?.accountID && !!isCorrectSearchUserName(participantToDisplayName); - const snapshotReport = useMemo(() => { - return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as SearchReport; - }, [snapshot, transactionItem.reportID]); - return ( ; type ArchivedReportsIDSet = ReadonlySet; +type GetSectionsParams = { + type: SearchDataTypes; + data: OnyxTypes.SearchResults['data']; + currentAccountID: number | undefined; + currentUserEmail: string; + formatPhoneNumber: LocaleContextProps['formatPhoneNumber']; + groupBy?: SearchGroupBy; + reportActions?: Record; + currentSearch?: SearchKey; + archivedReportsIDList?: ArchivedReportsIDSet; + queryJSON?: SearchQueryJSON; + reportMetadata?: Record | undefined; +}; + /** * Returns a list of all possible searches in the LHN, along with their query & hash. * *NOTE* When rendering the LHN, you should use the "createTypeMenuSections" method, which @@ -1392,6 +1406,7 @@ function getReportSections( currentUserEmail: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], reportActions: Record = {}, + reportMetadata: OnyxTypes.ReportMetadata | undefined = undefined, ): TransactionGroupListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); @@ -1428,7 +1443,7 @@ function getReportSections( const actions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`]; let shouldShow = true; - if (queryJSON && !reportItem.isActionLoading) { + if (queryJSON && reportMetadata && !reportMetadata[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`]?.isActionLoading) { if (queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { const status = queryJSON.status; @@ -1634,18 +1649,19 @@ function getListItem(type: SearchDataTypes, status: SearchStatus, groupBy?: Sear /** * Organizes data into appropriate list sections for display based on the type of search results. */ -function getSections( - type: SearchDataTypes, - data: OnyxTypes.SearchResults['data'], - currentAccountID: number | undefined, - currentUserEmail: string, - formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], - groupBy?: SearchGroupBy, - reportActions: Record = {}, - currentSearch: SearchKey = CONST.SEARCH.SEARCH_KEYS.EXPENSES, - archivedReportsIDList?: ArchivedReportsIDSet, - queryJSON?: SearchQueryJSON, -) { +function getSections({ + type, + data, + currentAccountID, + currentUserEmail, + formatPhoneNumber, + groupBy, + reportActions = {}, + currentSearch = CONST.SEARCH.SEARCH_KEYS.EXPENSES, + archivedReportsIDList, + queryJSON, + reportMetadata, +}: GetSectionsParams) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return getReportActionsSections(data); } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 5b2eb1471591..02fe08e830c2 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -454,18 +454,30 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com // eslint-disable-next-line @typescript-eslint/no-deprecated function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: Policy[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: transactionIDList - ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : // eslint-disable-next-line @typescript-eslint/no-deprecated - (Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, update])) as Partial), + const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: transactionIDList + ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) + : undefined, + }, }, - }, - ]; + ]; + reportList.forEach((report) => { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`, + value: { + ...update, + }, + }); + }); + + return optimisticData; + }; const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}); const failureData: OnyxUpdate[] = createOnyxData({isActionLoading: false}); @@ -487,18 +499,30 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: transactionIDList - ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : // eslint-disable-next-line @typescript-eslint/no-deprecated - (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, update])) as Partial), + const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: transactionIDList + ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) + : undefined, + }, }, - }, - ]; + ]; + reportIDList.forEach((reportID) => { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + ...update, + }, + }); + }); + + return optimisticData; + }; const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}); const failureData: OnyxUpdate[] = createOnyxData({isActionLoading: false, errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}); @@ -522,11 +546,9 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { - data: { - [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: update, - }, + ...update, }, }, { @@ -557,18 +579,31 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: transactionIDList - ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : // eslint-disable-next-line @typescript-eslint/no-deprecated - (Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, update])) as Partial), + const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: transactionIDList + ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) + : undefined, + }, }, - }, - ]; + ]; + + paymentData.forEach((item) => { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${item.reportID}`, + value: { + ...update, + }, + }); + }); + + return optimisticData; + }; const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}); const failureData: OnyxUpdate[] = createOnyxData({isActionLoading: false, errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}); diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts index 2c9e0e9f0d2b..6d55dd0ec5e0 100644 --- a/src/types/onyx/ReportMetadata.ts +++ b/src/types/onyx/ReportMetadata.ts @@ -43,6 +43,9 @@ type ReportMetadata = { /** Pending members of the report */ pendingChatMembers?: PendingChatMember[]; + + /** Whether the action is loading */ + isActionLoading?: boolean; }; export default ReportMetadata; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index de1adab7c3ce..5f4e9b7f5458 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -139,9 +139,6 @@ type SearchReport = { /** For expense reports, this is the total amount requested */ unheldTotal?: number; - /** Whether the action is loading */ - isActionLoading?: boolean; - /** Whether the report has violations or errors */ errors?: OnyxCommon.Errors; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index a21f27c56f8a..f53a70abdae0 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1803,11 +1803,27 @@ describe('SearchUIUtils', () => { describe('Test getSections', () => { it('should return getReportActionsSections result when type is CHAT', () => { - expect(SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.CHAT, searchResults.data, 2074551, '', formatPhoneNumber)).toStrictEqual(reportActionListItems); + expect( + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.CHAT, + data: searchResults.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + }), + ).toStrictEqual(reportActionListItems); }); it('should return getTransactionsSections result when groupBy is undefined', () => { - expect(SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE, searchResults.data, 20745, '', formatPhoneNumber)).toEqual(transactionsListItems); + expect( + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: searchResults.data, + currentAccountID: 20745, + currentUserEmail: '', + formatPhoneNumber, + }), + ).toEqual(transactionsListItems); }); it('should include iouRequestType property for distance transactions', () => { @@ -1825,7 +1841,13 @@ describe('SearchUIUtils', () => { }, }; - const result = SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE, testSearchResults.data, 2074551, '', formatPhoneNumber) as TransactionListItemType[]; + const result = SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: testSearchResults.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + }) as TransactionListItemType[]; const distanceTransaction = result.find((item) => item.transactionID === distanceTransactionID); @@ -1851,7 +1873,13 @@ describe('SearchUIUtils', () => { }, }; - const result = SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, testSearchResults.data, 2074551, '', formatPhoneNumber) as TransactionGroupListItemType[]; + const result = SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + data: testSearchResults.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + }) as TransactionGroupListItemType[]; const reportGroup = result.find((group) => group.transactions?.some((transaction) => transaction.transactionID === distanceTransactionID)); @@ -1865,7 +1893,15 @@ describe('SearchUIUtils', () => { }); it('should return getReportSections result when type is EXPENSE REPORT', () => { - expect(SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, searchResults.data, 2074551, '', formatPhoneNumber)).toStrictEqual(transactionReportGroupListItems); + expect( + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + data: searchResults.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + }), + ).toStrictEqual(transactionReportGroupListItems); }); it('should handle data where transaction keys appear before report keys in getReportSections', () => { @@ -1893,8 +1929,20 @@ describe('SearchUIUtils', () => { [`policy_${policyID}`]: searchResults.data[`policy_${policyID}`], }; - const resultTransactionFirst = SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, testDataTransactionFirst, 2074551, '', formatPhoneNumber); - const resultReportFirst = SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, testDataReportFirst, 2074551, '', formatPhoneNumber); + const resultTransactionFirst = SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + data: testDataTransactionFirst, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + }); + const resultReportFirst = SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + data: testDataReportFirst, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + }); expect(resultTransactionFirst).toBeDefined(); expect(Array.isArray(resultTransactionFirst)).toBe(true); @@ -1907,20 +1955,41 @@ describe('SearchUIUtils', () => { }); it('should return getMemberSections result when type is EXPENSE and groupBy is from', () => { - expect(SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE, searchResultsGroupByFrom.data, 2074551, '', formatPhoneNumber, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual( - transactionMemberGroupListItems, - ); + expect( + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: searchResultsGroupByFrom.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + groupBy: CONST.SEARCH.GROUP_BY.FROM, + }), + ).toStrictEqual(transactionMemberGroupListItems); }); it('should return getCardSections result when type is EXPENSE and groupBy is card', () => { - expect(SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE, searchResultsGroupByCard.data, 2074551, '', formatPhoneNumber, CONST.SEARCH.GROUP_BY.CARD)).toStrictEqual( - transactionCardGroupListItems, - ); + expect( + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: searchResultsGroupByCard.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + groupBy: CONST.SEARCH.GROUP_BY.CARD, + }), + ).toStrictEqual(transactionCardGroupListItems); }); it('should return getWithdrawalIDSections result when type is EXPENSE and groupBy is withdrawal-id', () => { expect( - SearchUIUtils.getSections(CONST.SEARCH.DATA_TYPES.EXPENSE, searchResultsGroupByWithdrawalID.data, 2074551, '', formatPhoneNumber, CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID), + SearchUIUtils.getSections({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + data: searchResultsGroupByWithdrawalID.data, + currentAccountID: 2074551, + currentUserEmail: '', + formatPhoneNumber, + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + }), ).toStrictEqual(transactionWithdrawalIDGroupListItems); }); }); From 558c5586d833e1fdfb8f7efccbaeacac9a161680 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 10 Nov 2025 18:44:25 +0700 Subject: [PATCH 02/11] fix typecheck --- src/components/Search/index.tsx | 2 +- .../Search/TransactionGroupListItem.tsx | 2 +- .../Search/TransactionListItem.tsx | 2 +- .../Search/UserInfoAndActionButtonRow.tsx | 2 +- src/libs/SearchUIUtils.ts | 4 ++-- src/libs/actions/Search.ts | 11 +++++------ src/types/onyx/ReportMetadata.ts | 3 +++ 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index dc38e81b60fa..57da50b900eb 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -420,7 +420,7 @@ function Search({ reportMetadata, }); return [data1, data1.length]; - }, [searchKey, exportReportActions, validGroupBy, isDataLoaded, searchResults, type, archivedReportsIdSet, formatPhoneNumber, accountID, queryJSON, email]); + }, [searchKey, exportReportActions, validGroupBy, isDataLoaded, searchResults, type, archivedReportsIdSet, formatPhoneNumber, accountID, queryJSON, email, reportMetadata]); useEffect(() => { /** We only want to display the skeleton for the status filters the first time we load them for a specific data type */ diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 996af80f9979..656c22282922 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -115,7 +115,7 @@ function TransactionGroupListItem({ ...transactionItem, isSelected: selectedTransactionIDsSet.has(transactionItem.transactionID), })); - }, [isExpenseReportType, transactionsSnapshot?.data, accountID, formatPhoneNumber, groupItem.transactions, selectedTransactionIDsSet, currentUserDetails.email]); + }, [isExpenseReportType, transactionsSnapshot?.data, accountID, formatPhoneNumber, groupItem.transactions, selectedTransactionIDsSet, currentUserDetails.email, reportMetadata]); const selectedItemsLength = useMemo(() => { return transactions.reduce((acc, transaction) => { diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 168b56495c95..baf9c5997a00 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -191,7 +191,7 @@ function TransactionListItem({ onCheckboxPress={handleCheckboxPress} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} - isActionLoading={isLoading ?? transactionItem.isActionLoading ?? reportMetadata.isActionLoading} + isActionLoading={isLoading ?? transactionItem.isActionLoading ?? reportMetadata?.isActionLoading} isSelected={!!transactionItem.isSelected} dateColumnSize={dateColumnSize} amountColumnSize={amountColumnSize} diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx index 4a2b29fb8f58..8798a53ab73b 100644 --- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx @@ -67,7 +67,7 @@ function UserInfoAndActionButtonRow({ action={item.action} goToItem={handleActionButtonPress} isSelected={item.isSelected} - isLoading={item.isActionLoading ?? reportMetadata.isActionLoading} + isLoading={item.isActionLoading ?? reportMetadata?.isActionLoading} policyID={item.policyID} reportID={item.reportID} hash={item.hash} diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 6d6e5c404bb5..03323892ec3b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1406,7 +1406,7 @@ function getReportSections( currentUserEmail: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], reportActions: Record = {}, - reportMetadata: OnyxTypes.ReportMetadata | undefined = undefined, + reportMetadata: Record | undefined; ): TransactionGroupListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); @@ -1670,7 +1670,7 @@ function getSections({ } if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { - return getReportSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, reportActions); + return getReportSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, reportActions, reportMetadata); } if (groupBy) { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 02fe08e830c2..3eabec49d65d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -41,7 +41,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; -import type {ExportTemplate, LastPaymentMethod, LastPaymentMethodType, Policy, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import type {ExportTemplate, LastPaymentMethod, LastPaymentMethodType, Policy, ReportAction, ReportActions, ReportMetadata, Transaction} from '@src/types/onyx'; import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; @@ -454,7 +454,7 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com // eslint-disable-next-line @typescript-eslint/no-deprecated function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: Policy[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -499,7 +499,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -541,8 +541,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; const optimisticReportActionID = optimisticAction.reportActionID; - // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null, reportAction?: OptimisticExportIntegrationAction | null): OnyxUpdate[] => [ + const createOnyxData = (update: Partial | null, reportAction?: OptimisticExportIntegrationAction | null): OnyxUpdate[] => [ // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 { onyxMethod: Onyx.METHOD.MERGE, @@ -579,7 +578,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts index 6d55dd0ec5e0..a006bd90c5b2 100644 --- a/src/types/onyx/ReportMetadata.ts +++ b/src/types/onyx/ReportMetadata.ts @@ -46,6 +46,9 @@ type ReportMetadata = { /** Whether the action is loading */ isActionLoading?: boolean; + + /** Whether the report has violations or errors */ + errors?: OnyxCommon.Errors; }; export default ReportMetadata; From ab08a1db00beecb15220074366e0541825c6edec Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 10 Nov 2025 18:49:20 +0700 Subject: [PATCH 03/11] minor fix --- .../Search/UserInfoAndActionButtonRow.tsx | 2 +- src/libs/SearchUIUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx index 8798a53ab73b..00bef24af8c9 100644 --- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx @@ -67,7 +67,7 @@ function UserInfoAndActionButtonRow({ action={item.action} goToItem={handleActionButtonPress} isSelected={item.isSelected} - isLoading={item.isActionLoading ?? reportMetadata?.isActionLoading} + isLoading={item?.isActionLoading ?? reportMetadata?.isActionLoading} policyID={item.policyID} reportID={item.reportID} hash={item.hash} diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 03323892ec3b..64963b78973f 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1406,7 +1406,7 @@ function getReportSections( currentUserEmail: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], reportActions: Record = {}, - reportMetadata: Record | undefined; + reportMetadata: Record | undefined, ): TransactionGroupListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); From 58d5077df5e98968e008bb7820e5322aab6dcf3b Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 10 Nov 2025 19:02:37 +0700 Subject: [PATCH 04/11] chore: eslint --- .../Search/UserInfoAndActionButtonRow.tsx | 4 ++-- src/libs/SearchUIUtils.ts | 4 ++-- src/libs/actions/Search.ts | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx index 00bef24af8c9..0e17f779dab5 100644 --- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx @@ -34,7 +34,7 @@ function UserInfoAndActionButtonRow({ const participantFromDisplayName = item?.from?.displayName ?? item?.from?.login ?? translate('common.hidden'); const participantToDisplayName = item?.to?.displayName ?? item?.to?.login ?? translate('common.hidden'); const shouldShowToRecipient = hasFromSender && hasToRecipient && !!item?.to?.accountID && !!isCorrectSearchUserName(participantToDisplayName); - + const isLoading = 'isActionLoading' in item ? item?.isActionLoading : reportMetadata?.isActionLoading; return ( = {}, reportMetadata: Record | undefined, + reportActions: Record = {}, ): TransactionGroupListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); @@ -1670,7 +1670,7 @@ function getSections({ } if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { - return getReportSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, reportActions, reportMetadata); + return getReportSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, reportMetadata, reportActions); } if (groupBy) { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 3eabec49d65d..f2a2f54a4ba3 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -542,7 +542,6 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN const optimisticReportActionID = optimisticAction.reportActionID; const createOnyxData = (update: Partial | null, reportAction?: OptimisticExportIntegrationAction | null): OnyxUpdate[] => [ - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, From 366db854f836d52e9ffd264d035990444f9026d6 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 10 Nov 2025 23:45:44 +0700 Subject: [PATCH 05/11] add selector --- .../MoneyRequestReportView/MoneyRequestReportNavigation.tsx | 3 ++- src/components/Search/index.tsx | 3 ++- .../Search/TransactionGroupListItem.tsx | 3 ++- src/selectors/ReportMetaData.ts | 6 ++++++ 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 src/selectors/ReportMetaData.ts diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index 3979a2f50ae3..e8cf2b784305 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -14,6 +14,7 @@ import {saveLastSearchParams} from '@userActions/ReportNavigation'; import {search} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; type MoneyRequestReportNavigationProps = { reportID?: string; @@ -25,7 +26,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, {canBeMissing: true}); const currentUserDetails = useCurrentUserPersonalDetails(); const {localeCompare, formatPhoneNumber} = useLocalize(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSelector}); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { canEvict: false, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 57da50b900eb..80d0847f4787 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -64,6 +64,7 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import type {OutstandingReportsByPolicyIDDerivedValue} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; @@ -257,7 +258,7 @@ function Search({ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const {accountID, email} = useCurrentUserPersonalDetails(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSelector}); // Filter violations based on user visibility const filteredViolations = useMemo(() => { diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 656c22282922..20880b9e693d 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -35,6 +35,7 @@ import {getSections} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import CardListItemHeader from './CardListItemHeader'; import MemberListItemHeader from './MemberListItemHeader'; @@ -94,7 +95,7 @@ function TransactionGroupListItem({ const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; const [transactionsVisibleLimit, setTransactionsVisibleLimit] = useState(CONST.TRANSACTION.RESULTS_PAGE_SIZE as number); const [isExpanded, setIsExpanded] = useState(false); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSelector}); const transactions = useMemo(() => { if (isExpenseReportType) { diff --git a/src/selectors/ReportMetaData.ts b/src/selectors/ReportMetaData.ts new file mode 100644 index 000000000000..fea3a344f9eb --- /dev/null +++ b/src/selectors/ReportMetaData.ts @@ -0,0 +1,6 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {ReportMetadata} from '@src/types/onyx'; + +const isActionLoadingSelector = (reportMetadata: OnyxEntry | undefined) => reportMetadata?.isActionLoading; + +export {isActionLoadingSelector}; From c0da99585e75c727827d5e21574def55b9bc1ebe Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 10 Nov 2025 23:59:34 +0700 Subject: [PATCH 06/11] fix eslint --- .../MoneyRequestReportView/MoneyRequestReportNavigation.tsx | 2 +- src/components/Search/index.tsx | 2 +- .../Search/TransactionGroupListItem.tsx | 2 +- src/selectors/ReportMetaData.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index e8cf2b784305..b98f5347d6db 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -14,7 +14,7 @@ import {saveLastSearchParams} from '@userActions/ReportNavigation'; import {search} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; +import isActionLoadingSelector from '@src/selectors/ReportMetaData'; type MoneyRequestReportNavigationProps = { reportID?: string; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 80d0847f4787..19a4dab9fa20 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -64,7 +64,7 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; +import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import type {OutstandingReportsByPolicyIDDerivedValue} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 20880b9e693d..11296e0c845f 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -35,7 +35,7 @@ import {getSections} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; +import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import CardListItemHeader from './CardListItemHeader'; import MemberListItemHeader from './MemberListItemHeader'; diff --git a/src/selectors/ReportMetaData.ts b/src/selectors/ReportMetaData.ts index fea3a344f9eb..aad3cd372cfa 100644 --- a/src/selectors/ReportMetaData.ts +++ b/src/selectors/ReportMetaData.ts @@ -3,4 +3,4 @@ import type {ReportMetadata} from '@src/types/onyx'; const isActionLoadingSelector = (reportMetadata: OnyxEntry | undefined) => reportMetadata?.isActionLoading; -export {isActionLoadingSelector}; +export default isActionLoadingSelector; From 254a44242db3449754f4744f6c0bdebe3e34c546 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 11 Nov 2025 00:32:27 +0700 Subject: [PATCH 07/11] update selector --- .../MoneyRequestReportView/MoneyRequestReportNavigation.tsx | 3 +-- src/components/Search/index.tsx | 3 +-- .../Search/ReportListItemHeader.tsx | 5 +++-- .../Search/TransactionGroupListItem.tsx | 3 +-- .../SelectionListWithSections/Search/TransactionListItem.tsx | 5 +++-- .../Search/UserInfoAndActionButtonRow.tsx | 5 +++-- src/selectors/ReportMetaData.ts | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index b98f5347d6db..3979a2f50ae3 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -14,7 +14,6 @@ import {saveLastSearchParams} from '@userActions/ReportNavigation'; import {search} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import isActionLoadingSelector from '@src/selectors/ReportMetaData'; type MoneyRequestReportNavigationProps = { reportID?: string; @@ -26,7 +25,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, {canBeMissing: true}); const currentUserDetails = useCurrentUserPersonalDetails(); const {localeCompare, formatPhoneNumber} = useLocalize(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSelector}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { canEvict: false, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 19a4dab9fa20..57da50b900eb 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -64,7 +64,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import type {OutstandingReportsByPolicyIDDerivedValue} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; @@ -258,7 +257,7 @@ function Search({ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const {accountID, email} = useCurrentUserPersonalDetails(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSelector}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); // Filter violations based on user visibility const filteredViolations = useMemo(() => { diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx index 01ce19ede3c4..54fe3aaad446 100644 --- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx @@ -16,6 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {handleActionButtonPress} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import type {Policy} from '@src/types/onyx'; import type {SearchReport} from '@src/types/onyx/SearchResults'; import ActionCell from './ActionCell'; @@ -108,7 +109,7 @@ function HeaderFirstRow({ const StyleUtils = useStyleUtils(); const {isLargeScreenWidth} = useResponsiveLayout(); const theme = useTheme(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {canBeMissing: true}); + const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); const {total, currency} = useMemo(() => { let reportTotal = reportItem.total ?? 0; @@ -181,7 +182,7 @@ function HeaderFirstRow({ action={reportItem.action} goToItem={handleOnButtonPress} isSelected={reportItem.isSelected} - isLoading={reportMetadata?.isActionLoading} + isLoading={isActionLoading} policyID={reportItem.policyID} reportID={reportItem.reportID} hash={reportItem.hash} diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 11296e0c845f..656c22282922 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -35,7 +35,6 @@ import {getSections} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import CardListItemHeader from './CardListItemHeader'; import MemberListItemHeader from './MemberListItemHeader'; @@ -95,7 +94,7 @@ function TransactionGroupListItem({ const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; const [transactionsVisibleLimit, setTransactionsVisibleLimit] = useState(CONST.TRANSACTION.RESULTS_PAGE_SIZE as number); const [isExpanded, setIsExpanded] = useState(false); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSelector}); + const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); const transactions = useMemo(() => { if (isExpenseReportType) { diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index aba315ac03d7..7a36c7988529 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -24,6 +24,7 @@ import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils' import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import type {Policy, ReportAction, ReportActions} from '@src/types/onyx'; import type {SearchReport} from '@src/types/onyx/SearchResults'; import type {TransactionViolation} from '@src/types/onyx/TransactionViolation'; @@ -58,7 +59,7 @@ function TransactionListItem({ return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as SearchReport; }, [snapshot, transactionItem.reportID]); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true}); + const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); const snapshotPolicy = useMemo(() => { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; @@ -178,7 +179,7 @@ function TransactionListItem({ onCheckboxPress={handleCheckboxPress} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} - isActionLoading={isLoading ?? transactionItem.isActionLoading ?? reportMetadata?.isActionLoading} + isActionLoading={isLoading ?? transactionItem.isActionLoading ?? isActionLoading} isSelected={!!transactionItem.isSelected} dateColumnSize={dateColumnSize} amountColumnSize={amountColumnSize} diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx index 61838a8fdcb3..0a995fa9e0a9 100644 --- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx @@ -10,6 +10,7 @@ import {isCorrectSearchUserName} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import isActionLoadingSelector from '@src/selectors/ReportMetaData'; import ActionCell from './ActionCell'; import UserInfoCellsWithArrow from './UserInfoCellsWithArrow'; @@ -30,13 +31,13 @@ function UserInfoAndActionButtonRow({ const {isLargeScreenWidth} = useResponsiveLayout(); const {translate} = useLocalize(); const transactionItem = item as unknown as TransactionListItemType; - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true}); + const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); const hasFromSender = !!item?.from && !!item?.from?.accountID && !!item?.from?.displayName; const hasToRecipient = !!item?.to && !!item?.to?.accountID && !!item?.to?.displayName; const participantFromDisplayName = item?.from?.displayName ?? item?.from?.login ?? translate('common.hidden'); const participantToDisplayName = item?.to?.displayName ?? item?.to?.login ?? translate('common.hidden'); const shouldShowToRecipient = hasFromSender && hasToRecipient && !!item?.to?.accountID && !!isCorrectSearchUserName(participantToDisplayName); - const isLoading = 'isActionLoading' in item ? item?.isActionLoading : reportMetadata?.isActionLoading; + const isLoading = 'isActionLoading' in item ? item?.isActionLoading : isActionLoading; return ( | undefined) => reportMetadata?.isActionLoading; +const isActionLoadingSelector = (reportMetadata: OnyxEntry | undefined) => reportMetadata?.isActionLoading ?? false; export default isActionLoadingSelector; From 75cae2bac1837795f5b072b0abf017adcf3d023d Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 11 Nov 2025 16:21:13 +0700 Subject: [PATCH 08/11] add selector --- .../MoneyRequestReportNavigation.tsx | 5 +++-- src/components/Search/index.tsx | 7 ++++--- .../Search/TransactionGroupListItem.tsx | 7 ++++--- src/libs/SearchUIUtils.ts | 10 +++++----- src/selectors/ReportMetaData.ts | 18 ++++++++++++++++-- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index 3979a2f50ae3..b7d6290ec7ac 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -14,6 +14,7 @@ import {saveLastSearchParams} from '@userActions/ReportNavigation'; import {search} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isActionLoadingSetSelector} from '@src/selectors/ReportMetaData'; type MoneyRequestReportNavigationProps = { reportID?: string; @@ -25,7 +26,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, {canBeMissing: true}); const currentUserDetails = useCurrentUserPersonalDetails(); const {localeCompare, formatPhoneNumber} = useLocalize(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); + const [isActionLoadingSet = new Set()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSetSelector}); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { canEvict: false, @@ -48,7 +49,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo reportActions: exportReportActions, currentSearch: lastSearchQuery?.searchKey, archivedReportsIDList: archivedReportsIdSet, - reportMetadata, + isActionLoadingSet, }); results = getSortedSections(type, status ?? '', searchData, localeCompare, sortBy, sortOrder, groupBy).map((value) => value.reportID); } diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 57da50b900eb..2c4de12e33ce 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -64,6 +64,7 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import {isActionLoadingSetSelector} from '@src/selectors/ReportMetaData'; import type {OutstandingReportsByPolicyIDDerivedValue} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; @@ -257,7 +258,7 @@ function Search({ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const {accountID, email} = useCurrentUserPersonalDetails(); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); + const [isActionLoadingSet = new Set()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSetSelector}); // Filter violations based on user visibility const filteredViolations = useMemo(() => { @@ -417,10 +418,10 @@ function Search({ currentSearch: searchKey, archivedReportsIDList: archivedReportsIdSet, queryJSON, - reportMetadata, + isActionLoadingSet, }); return [data1, data1.length]; - }, [searchKey, exportReportActions, validGroupBy, isDataLoaded, searchResults, type, archivedReportsIdSet, formatPhoneNumber, accountID, queryJSON, email, reportMetadata]); + }, [searchKey, exportReportActions, validGroupBy, isDataLoaded, searchResults, type, archivedReportsIdSet, formatPhoneNumber, accountID, queryJSON, email, isActionLoadingSet]); useEffect(() => { /** We only want to display the skeleton for the status filters the first time we load them for a specific data type */ diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 656c22282922..318f8764bf19 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -35,6 +35,7 @@ import {getSections} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isActionLoadingSetSelector} from '@src/selectors/ReportMetaData'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import CardListItemHeader from './CardListItemHeader'; import MemberListItemHeader from './MemberListItemHeader'; @@ -94,7 +95,7 @@ function TransactionGroupListItem({ const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; const [transactionsVisibleLimit, setTransactionsVisibleLimit] = useState(CONST.TRANSACTION.RESULTS_PAGE_SIZE as number); const [isExpanded, setIsExpanded] = useState(false); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true}); + const [isActionLoadingSet = new Set()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSetSelector}); const transactions = useMemo(() => { if (isExpenseReportType) { @@ -109,13 +110,13 @@ function TransactionGroupListItem({ currentAccountID: accountID, currentUserEmail: currentUserDetails.email ?? '', formatPhoneNumber, - reportMetadata, + isActionLoadingSet, }) as TransactionListItemType[]; return sectionData.map((transactionItem) => ({ ...transactionItem, isSelected: selectedTransactionIDsSet.has(transactionItem.transactionID), })); - }, [isExpenseReportType, transactionsSnapshot?.data, accountID, formatPhoneNumber, groupItem.transactions, selectedTransactionIDsSet, currentUserDetails.email, reportMetadata]); + }, [isExpenseReportType, transactionsSnapshot?.data, accountID, formatPhoneNumber, groupItem.transactions, selectedTransactionIDsSet, currentUserDetails.email, isActionLoadingSet]); const selectedItemsLength = useMemo(() => { return transactions.reduce((acc, transaction) => { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e3f1baa9a6b8..b0d2c2a08c8b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -288,7 +288,7 @@ type GetSectionsParams = { currentSearch?: SearchKey; archivedReportsIDList?: ArchivedReportsIDSet; queryJSON?: SearchQueryJSON; - reportMetadata?: Record | undefined; + isActionLoadingSet?: ReadonlySet; }; /** @@ -1388,7 +1388,7 @@ function getReportSections( currentAccountID: number | undefined, currentUserEmail: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], - reportMetadata: Record | undefined, + isActionLoadingSet: ReadonlySet | undefined, reportActions: Record = {}, ): TransactionGroupListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); @@ -1426,7 +1426,7 @@ function getReportSections( const actions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`]; let shouldShow = true; - if (queryJSON && reportMetadata && !reportMetadata[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`]?.isActionLoading) { + if (queryJSON && isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`)) { if (queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { const status = queryJSON.status; @@ -1643,7 +1643,7 @@ function getSections({ currentSearch = CONST.SEARCH.SEARCH_KEYS.EXPENSES, archivedReportsIDList, queryJSON, - reportMetadata, + isActionLoadingSet, }: GetSectionsParams) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return getReportActionsSections(data); @@ -1653,7 +1653,7 @@ function getSections({ } if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { - return getReportSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, reportMetadata, reportActions); + return getReportSections(data, currentSearch, currentAccountID, currentUserEmail, formatPhoneNumber, isActionLoadingSet, reportActions); } if (groupBy) { diff --git a/src/selectors/ReportMetaData.ts b/src/selectors/ReportMetaData.ts index 5e4f45a05b1a..5d05a2bbb43b 100644 --- a/src/selectors/ReportMetaData.ts +++ b/src/selectors/ReportMetaData.ts @@ -1,6 +1,20 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ReportMetadata} from '@src/types/onyx'; const isActionLoadingSelector = (reportMetadata: OnyxEntry | undefined) => reportMetadata?.isActionLoading ?? false; -export default isActionLoadingSelector; +const isActionLoadingSetSelector = (all: OnyxCollection): ReadonlySet => { + const ids = new Set(); + if (!all) { + return ids; + } + + for (const [key, value] of Object.entries(all)) { + if (value?.isActionLoading) { + ids.add(key); + } + } + return ids; +}; + +export {isActionLoadingSelector, isActionLoadingSetSelector}; From aa8486bfa14cd9be33a9c977577ed758256bd08e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 11 Nov 2025 16:41:44 +0700 Subject: [PATCH 09/11] update import --- .../SelectionListWithSections/Search/ReportListItemHeader.tsx | 2 +- .../SelectionListWithSections/Search/TransactionListItem.tsx | 2 +- .../Search/UserInfoAndActionButtonRow.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx index 54fe3aaad446..2307830ec3a9 100644 --- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx @@ -16,7 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {handleActionButtonPress} from '@userActions/Search'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import isActionLoadingSelector from '@src/selectors/ReportMetaData'; +import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import type {Policy} from '@src/types/onyx'; import type {SearchReport} from '@src/types/onyx/SearchResults'; import ActionCell from './ActionCell'; diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 7a36c7988529..18fb5d8c8f89 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -24,7 +24,7 @@ import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils' import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import isActionLoadingSelector from '@src/selectors/ReportMetaData'; +import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import type {Policy, ReportAction, ReportActions} from '@src/types/onyx'; import type {SearchReport} from '@src/types/onyx/SearchResults'; import type {TransactionViolation} from '@src/types/onyx/TransactionViolation'; diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx index 0a995fa9e0a9..eba0588ee621 100644 --- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx @@ -10,7 +10,7 @@ import {isCorrectSearchUserName} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import isActionLoadingSelector from '@src/selectors/ReportMetaData'; +import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import ActionCell from './ActionCell'; import UserInfoCellsWithArrow from './UserInfoCellsWithArrow'; From a3b19cb23ba265f02db729c95b9280c80004e474 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 13 Nov 2025 17:01:16 +0700 Subject: [PATCH 10/11] add removing logic --- src/libs/actions/Search.ts | 72 +++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 7a4d3446640c..8f25766bbf4b 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -453,7 +453,7 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com // eslint-disable-next-line @typescript-eslint/no-deprecated function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: Policy[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -461,7 +461,10 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po value: { data: transactionIDList ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : undefined, + : // eslint-disable-next-line @typescript-eslint/no-deprecated + shouldRemoveReportFromView + ? (Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null])) as Partial) + : undefined, }, }, ]; @@ -481,7 +484,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}); const failureData: OnyxUpdate[] = createOnyxData({isActionLoading: false}); // If we are on the 'Submit' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search - const successData: OnyxUpdate[] = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT ? createOnyxData(null) : createOnyxData({isActionLoading: false}); + const successData: OnyxUpdate[] = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT ? createOnyxData({isActionLoading: false}, true) : createOnyxData({isActionLoading: false}); // eslint-disable-next-line @typescript-eslint/no-deprecated const report = (reportList.at(0) ?? {}) as SearchReport; @@ -498,7 +501,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -506,7 +509,10 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], trans value: { data: transactionIDList ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : undefined, + : // eslint-disable-next-line @typescript-eslint/no-deprecated + shouldRemoveReportFromView + ? (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])) as Partial) + : undefined, }, }, ]; @@ -529,7 +535,7 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], trans // If we are on the 'Approve', `Unapproved cash` or the `Unapproved company cards` suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search const approveActionSuggestedSearches: Partial = [CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]; - const successData: OnyxUpdate[] = approveActionSuggestedSearches.includes(currentSearchKey) ? createOnyxData(null) : createOnyxData({isActionLoading: false}); + const successData: OnyxUpdate[] = approveActionSuggestedSearches.includes(currentSearchKey) ? createOnyxData({isActionLoading: false}, true) : createOnyxData({isActionLoading: false}); playSound(SOUNDS.SUCCESS); API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData}); @@ -540,27 +546,42 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; const optimisticReportActionID = optimisticAction.reportActionID; - const createOnyxData = (update: Partial | null, reportAction?: OptimisticExportIntegrationAction | null): OnyxUpdate[] => [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - ...update, + const createOnyxData = (update: Partial | null, reportAction?: OptimisticExportIntegrationAction | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + ...update, + }, }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: reportAction, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActionID]: reportAction, + }, }, - }, - ]; + ]; + if (shouldRemoveReportFromView) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, + }, + }, + }); + } + return optimisticData; + }; const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}, optimisticAction); const failureData: OnyxUpdate[] = createOnyxData({errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), isActionLoading: false}, null); // If we are on the 'Export' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search - const successData: OnyxUpdate[] = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT ? createOnyxData(null, successAction) : createOnyxData({isActionLoading: false}, successAction); + const successData: OnyxUpdate[] = + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT ? createOnyxData({isActionLoading: false}, successAction, true) : createOnyxData({isActionLoading: false}, successAction); const params = { reportIDList: reportID, @@ -576,7 +597,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -584,7 +605,10 @@ function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], trans value: { data: transactionIDList ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : undefined, + : // eslint-disable-next-line @typescript-eslint/no-deprecated + shouldRemoveReportFromView + ? (Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])) as Partial) + : undefined, }, }, ]; @@ -605,7 +629,7 @@ function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], trans const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}); const failureData: OnyxUpdate[] = createOnyxData({isActionLoading: false, errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}); // If we are on the 'Pay' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search - const successData: OnyxUpdate[] = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY ? createOnyxData(null) : createOnyxData({isActionLoading: false}); + const successData: OnyxUpdate[] = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY ? createOnyxData({isActionLoading: false}, true) : createOnyxData({isActionLoading: false}); // eslint-disable-next-line rulesdir/no-api-side-effects-method API.makeRequestWithSideEffects( From 0bbac19cddbdcd29b4f7fdf252542bb915587a6e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 13 Nov 2025 17:18:39 +0700 Subject: [PATCH 11/11] type fix --- src/libs/actions/Search.ts | 59 ++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 8f25766bbf4b..bf3bca4629f8 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -453,18 +453,23 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com // eslint-disable-next-line @typescript-eslint/no-deprecated function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: Policy[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView = false): OnyxUpdate[] => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + let data: Partial | Partial | undefined; + + if (transactionIDList) { + data = Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial; + } else if (shouldRemoveReportFromView) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + data = Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null])) as Partial; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: transactionIDList - ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : // eslint-disable-next-line @typescript-eslint/no-deprecated - shouldRemoveReportFromView - ? (Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null])) as Partial) - : undefined, + data, }, }, ]; @@ -501,18 +506,23 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView = false): OnyxUpdate[] => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + let data: Partial | Partial | undefined; + + if (transactionIDList) { + data = Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial; + } else if (shouldRemoveReportFromView) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + data = Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])) as Partial; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: transactionIDList - ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : // eslint-disable-next-line @typescript-eslint/no-deprecated - shouldRemoveReportFromView - ? (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])) as Partial) - : undefined, + data, }, }, ]; @@ -546,7 +556,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; const optimisticReportActionID = optimisticAction.reportActionID; - const createOnyxData = (update: Partial | null, reportAction?: OptimisticExportIntegrationAction | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { + const createOnyxData = (update: Partial | null, reportAction?: OptimisticExportIntegrationAction | null, shouldRemoveReportFromView = false): OnyxUpdate[] => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -597,18 +607,23 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[], currentSearchKey?: SearchKey) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView: boolean = false): OnyxUpdate[] => { + const createOnyxData = (update: Partial | Partial | null, shouldRemoveReportFromView = false): OnyxUpdate[] => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + let data: Partial | Partial | undefined; + + if (transactionIDList) { + data = Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial; + } else if (shouldRemoveReportFromView) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + data = Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])) as Partial; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: transactionIDList - ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial) - : // eslint-disable-next-line @typescript-eslint/no-deprecated - shouldRemoveReportFromView - ? (Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])) as Partial) - : undefined, + data, }, }, ];