From b58c08d8c82be1e098b25e280efa821ae5ea14ce Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Tue, 18 Nov 2025 16:20:03 +0700 Subject: [PATCH 1/3] Fix - After deleting IOU, nothing to show is shown briefly before updating to default page --- src/components/Search/index.tsx | 1 + src/libs/SearchQueryUtils.ts | 5 +++++ src/pages/Search/EmptySearchView.tsx | 11 ++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 1ff98467f81b..ca5e1bc00bd5 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -899,6 +899,7 @@ function Search({ similarSearchHash={similarSearchHash} type={type} hasResults={searchResults?.search?.hasResults} + queryJSON={queryJSON} /> ); diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index fc52585c16f7..d655d671e585 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1095,6 +1095,10 @@ function isDefaultExpensesQuery(queryJSON: SearchQueryJSON) { return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && !queryJSON.status && !queryJSON.filters && !queryJSON.groupBy && !queryJSON.policyID; } +function isDefaultExpenseReportsQuery(queryJSON: SearchQueryJSON) { + return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && !queryJSON.status && !queryJSON.filters && !queryJSON.groupBy && !queryJSON.policyID; +} + /** * Always show `No category` and `No tag` as the first option */ @@ -1232,6 +1236,7 @@ export { getCurrentSearchQueryJSON, getQueryWithoutFilters, isDefaultExpensesQuery, + isDefaultExpenseReportsQuery, sortOptionsWithEmptyValue, shouldHighlight, getAllPolicyValues, diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 36f2311e2f98..7072c5962d53 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -18,6 +18,7 @@ import MenuItem from '@components/MenuItem'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ScrollView from '@components/ScrollView'; import {SearchScopeProvider} from '@components/Search/SearchScopeProvider'; +import type {SearchQueryJSON} from '@components/Search/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; @@ -39,6 +40,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {areAllGroupPoliciesExpenseChatDisabled, getDefaultChatEnabledPolicy, getGroupPaidPoliciesWithExpenseChatEnabled} from '@libs/PolicyUtils'; import {generateReportID, hasEmptyReportsForPolicy, hasViolations as hasViolationsReportUtils, reportSummariesOnyxSelector} from '@libs/ReportUtils'; +import {isDefaultExpenseReportsQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; import type {SearchTypeMenuSection} from '@libs/SearchUIUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {showContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; @@ -54,6 +56,7 @@ type EmptySearchViewProps = { similarSearchHash: number; type: SearchDataTypes; hasResults: boolean; + queryJSON: SearchQueryJSON; }; type EmptySearchViewContentProps = EmptySearchViewProps & { @@ -94,7 +97,7 @@ const tripsFeatures: FeatureListItem[] = [ type ReportSummary = ReturnType[number]; -function EmptySearchView({similarSearchHash, type, hasResults}: EmptySearchViewProps) { +function EmptySearchView({similarSearchHash, type, hasResults, queryJSON}: EmptySearchViewProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {typeMenuSections, CreateReportConfirmationModal: SearchMenuCreateReportConfirmationModal} = useSearchTypeMenuSections(); @@ -128,6 +131,7 @@ function EmptySearchView({similarSearchHash, type, hasResults}: EmptySearchViewP introSelected={introSelected} hasSeenTour={hasSeenTour} searchMenuCreateReportConfirmationModal={SearchMenuCreateReportConfirmationModal} + queryJSON={queryJSON} /> ); @@ -153,6 +157,7 @@ function EmptySearchViewContent({ introSelected, hasSeenTour, searchMenuCreateReportConfirmationModal, + queryJSON, }: EmptySearchViewContentProps) { const theme = useTheme(); const StyleUtils = useStyleUtils(); @@ -335,7 +340,7 @@ function EmptySearchViewContent({ lottieWebViewStyles: {backgroundColor: theme.travelBG, ...styles.emptyStateFolderWebStyles, ...styles.tripEmptyStateLottieWebView}, }; case CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT: - if (hasResults) { + if (hasResults && (!isDefaultExpenseReportsQuery(queryJSON) || hasExpenseReports)) { return { ...defaultViewItemHeader, title: translate('search.searchResults.emptyResults.title'), @@ -389,7 +394,7 @@ function EmptySearchViewContent({ } // eslint-disable-next-line no-fallthrough case CONST.SEARCH.DATA_TYPES.EXPENSE: - if (hasResults) { + if (hasResults && (!isDefaultExpensesQuery(queryJSON) || hasTransactions)) { return { ...defaultViewItemHeader, title: translate('search.searchResults.emptyResults.title'), From 0d77037f2ab46ef4f08b38fd789747cf457f7b51 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Thu, 4 Dec 2025 19:11:19 +0700 Subject: [PATCH 2/3] Fix - After deleting IOU, nothing to show is shown briefly before updating to default page --- src/pages/Search/EmptySearchView.tsx | 10 ++-- tests/ui/components/EmptySearchViewTest.tsx | 62 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 6705e7a81cab..0e703445b074 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -56,7 +56,7 @@ type EmptySearchViewProps = { similarSearchHash: number; type: SearchDataTypes; hasResults: boolean; - queryJSON: SearchQueryJSON; + queryJSON?: SearchQueryJSON; }; type EmptySearchViewContentProps = EmptySearchViewProps & { @@ -130,8 +130,7 @@ const hasTransactionsSelector = (transactions: OnyxCollection) => Object.values(transactions ?? {}).filter((transaction) => transaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length > 0; const hasExpenseReportsSelector = (reports: OnyxCollection) => - Object.values(reports ?? {}).filter((report) => report?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && report?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length > - 0; + Object.values(reports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.EXPENSE && report?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length > 0; function EmptySearchViewContent({ similarSearchHash, @@ -344,7 +343,7 @@ function EmptySearchViewContent({ lottieWebViewStyles: {backgroundColor: theme.travelBG, ...styles.emptyStateFolderWebStyles, ...styles.tripEmptyStateLottieWebView}, }; case CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT: - if (hasResults && (!isDefaultExpenseReportsQuery(queryJSON) || hasExpenseReports)) { + if (hasResults && (!queryJSON || !isDefaultExpenseReportsQuery(queryJSON) || hasExpenseReports)) { return { ...defaultViewItemHeader, title: translate('search.searchResults.emptyResults.title'), @@ -398,7 +397,7 @@ function EmptySearchViewContent({ } // eslint-disable-next-line no-fallthrough case CONST.SEARCH.DATA_TYPES.EXPENSE: - if (hasResults && (!isDefaultExpensesQuery(queryJSON) || hasTransactions)) { + if (hasResults && (!queryJSON || !isDefaultExpensesQuery(queryJSON) || hasTransactions)) { return { ...defaultViewItemHeader, title: translate('search.searchResults.emptyResults.title'), @@ -500,6 +499,7 @@ function EmptySearchViewContent({ hasExpenseReports, defaultChatEnabledPolicyID, handleCreateReportClick, + queryJSON, ]); return ( diff --git a/tests/ui/components/EmptySearchViewTest.tsx b/tests/ui/components/EmptySearchViewTest.tsx index 32eaa59db97a..ada5b486f8b2 100644 --- a/tests/ui/components/EmptySearchViewTest.tsx +++ b/tests/ui/components/EmptySearchViewTest.tsx @@ -180,6 +180,68 @@ describe('EmptySearchView', () => { expect(screen.queryByText(translateLocal('report.newReport.createReport'))).not.toBeOnTheScreen(); }); }); + + it('should show "emptyExpenseResults" when the user has deleted all expenses, even though hasResults remains true', async () => { + const policy = createPaidGroupPolicy(); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + }); + + // Given: A query string for expense search + const queryString = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + }); + const queryJSON = buildSearchQueryJSON(queryString); + + // When rendering the EmptySearchView component + render( + + + , + ); + + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText(translateLocal('search.searchResults.emptyExpenseResults.title'))).toBeVisible(); + }); + }); + + describe('type is expense Report', () => { + const dataType = CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; + + it('should show "emptyReportResults" when the user has deleted all expenses, even though hasResults remains true', async () => { + const policy = createPaidGroupPolicy(); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + }); + + // Given: A query string for expense report search + const queryString = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + }); + const queryJSON = buildSearchQueryJSON(queryString); + + // When rendering the EmptySearchView component + render( + + + , + ); + + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText(translateLocal('search.searchResults.emptyReportResults.title'))).toBeVisible(); + }); }); describe('type is Invoice', () => { From 5191fd5575d01fef47e3cd9948403a6579e6bf6d Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Thu, 4 Dec 2025 20:21:55 +0700 Subject: [PATCH 3/3] fix lint --- src/libs/SearchQueryUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 7de364376abe..bdc8c3142113 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1000,6 +1000,7 @@ function getFilterDisplayValue( return getCardDescription(cardList?.[cardID]) || filterValue; } if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) { + // eslint-disable-next-line @typescript-eslint/no-deprecated return getReportName(reports?.[`${ONYXKEYS.COLLECTION.REPORT}${filterValue}`]) || filterValue; } if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TOTAL || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.PURCHASE_AMOUNT) {