diff --git a/src/components/Search/SearchPageHeader/useSearchPageInput.tsx b/src/components/Search/SearchPageHeader/useSearchPageInput.tsx index 9fd3276d772a..0522d14a1435 100644 --- a/src/components/Search/SearchPageHeader/useSearchPageInput.tsx +++ b/src/components/Search/SearchPageHeader/useSearchPageInput.tsx @@ -23,7 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getAutocompleteQueryWithComma, getTrimmedUserSearchQueryPreservingComma} from '@libs/SearchAutocompleteUtils'; -import {buildUserReadableQueryString, getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils'; +import {buildUserReadableQueryString, getKeywordQueryWithCurrentSearchContext, getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -119,7 +119,8 @@ function useSearchPageInput({queryJSON, onSearch, onSubmit}: UseSearchPageInputP function submitSearch(queryString: SearchQueryString, shouldSkipAmountConversion = false) { const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions, currentUserAccountID); - const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion); + const queryWithContext = getKeywordQueryWithCurrentSearchContext(queryWithSubstitutions, queryJSON); + const updatedQuery = getQueryWithUpdatedValues(queryWithContext, shouldSkipAmountConversion); if (!updatedQuery) { return; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index efb8cbbfbda6..156507e8802d 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -13,6 +13,7 @@ import type { ReportFieldNegatedKey, ReportFieldTextKey, SearchAmountFilterKeys, + SearchAutocompleteResult, SearchDateFilterKeys, SearchDateKey, SearchDatePreset, @@ -48,6 +49,7 @@ import type {SearchFullscreenNavigatorParamList} from './Navigation/types'; import {getDisplayNameOrDefault, getPersonalDetailByEmail} from './PersonalDetailsUtils'; import {getCleanedTagName} from './PolicyUtils'; import {getReportName} from './ReportNameUtils'; +import {parse as parseForAutocomplete} from './SearchParser/autocompleteParser'; import {parse as parseSearchQuery} from './SearchParser/searchParser'; import StringUtils from './StringUtils'; import {hashText} from './UserUtils'; @@ -1860,6 +1862,18 @@ function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON | Readonly): SearchQueryString { + const autocompleteRanges = (parseForAutocomplete(queryString) as SearchAutocompleteResult).ranges; + const hasOnlyKeywordSearch = queryString.trim().length > 0 && autocompleteRanges.length === 0; + if (!hasOnlyKeywordSearch) { + return queryString; + } + + const currentFiltersWithoutKeywords = currentQueryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD); + const currentQueryString = buildSearchQueryString({...currentQueryJSON, flatFilters: currentFiltersWithoutKeywords}); + return `${currentQueryString} ${queryString}`; +} + /** * Returns new string query, after parsing it and traversing to update some filter values. * If there are any personal emails, it will try to substitute them with accountIDs @@ -2161,6 +2175,7 @@ export { buildCannedSearchQuery, sanitizeSearchValue, getQueryWithUpdatedValues, + getKeywordQueryWithCurrentSearchContext, getCurrentSearchQueryJSON, getQueryWithoutFilters, isDefaultExpensesQuery, diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 84127e88da74..6a4f54877bbe 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -17,6 +17,7 @@ import { getDateRangeDisplayValueFromFormValue, getDisplayQueryFiltersForKey, getFilterDisplayValue, + getKeywordQueryWithCurrentSearchContext, getQueryWithUpdatedValues, getRangeBoundariesFromFormValue, serializeQueryJSONForBackend, @@ -2905,4 +2906,52 @@ describe('SearchQueryUtils', () => { expect(serialized.rawFilterList.at(0)?.operator).toBe(CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); }); }); + + describe('getKeywordQueryWithCurrentSearchContext', () => { + it('should prepend current search context to keyword-only input', () => { + const currentQueryJSON = buildSearchQueryJSON('type:trip status:outstanding') as SearchQueryJSON; + + const result = getKeywordQueryWithCurrentSearchContext('hello', currentQueryJSON); + expect(result).toContain('hello'); + expect(result).toContain('type:trip'); + expect(result).toContain('status:outstanding'); + }); + + it('should return query unchanged when it contains explicit filters', () => { + const currentQueryJSON = buildSearchQueryJSON('type:trip status:all') as SearchQueryJSON; + + const result = getKeywordQueryWithCurrentSearchContext('type:expense hello', currentQueryJSON); + expect(result).toBe('type:expense hello'); + }); + + it('should return query unchanged when it contains only explicit filters without keywords', () => { + const currentQueryJSON = buildSearchQueryJSON('type:trip status:all') as SearchQueryJSON; + + const result = getKeywordQueryWithCurrentSearchContext('type:expense status:open', currentQueryJSON); + expect(result).toBe('type:expense status:open'); + }); + + it('should return empty query unchanged', () => { + const currentQueryJSON = buildSearchQueryJSON('type:trip status:all') as SearchQueryJSON; + + const result = getKeywordQueryWithCurrentSearchContext('', currentQueryJSON); + expect(result).toBe(''); + }); + + it('should strip existing keyword filters from current context before prepending', () => { + const currentQueryJSON = buildSearchQueryJSON('type:expense status:all existing') as SearchQueryJSON; + + const result = getKeywordQueryWithCurrentSearchContext('new-keyword', currentQueryJSON); + expect(result).toContain('new-keyword'); + expect(result).not.toContain('existing'); + }); + + it('should handle multi-word keyword input', () => { + const currentQueryJSON = buildSearchQueryJSON('type:trip status:all') as SearchQueryJSON; + + const result = getKeywordQueryWithCurrentSearchContext('hello world', currentQueryJSON); + expect(result).toContain('hello world'); + expect(result).toContain('type:trip'); + }); + }); });