Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ReportFieldNegatedKey,
ReportFieldTextKey,
SearchAmountFilterKeys,
SearchAutocompleteResult,
SearchDateFilterKeys,
SearchDateKey,
SearchDatePreset,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1860,6 +1862,18 @@ function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON | Readonly<SearchQue
return standardQuery;
}

function getKeywordQueryWithCurrentSearchContext(queryString: SearchQueryString, currentQueryJSON: Readonly<SearchQueryJSON>): 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
Expand Down Expand Up @@ -2161,6 +2175,7 @@ export {
buildCannedSearchQuery,
sanitizeSearchValue,
getQueryWithUpdatedValues,
getKeywordQueryWithCurrentSearchContext,
getCurrentSearchQueryJSON,
getQueryWithoutFilters,
isDefaultExpensesQuery,
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/Search/SearchQueryUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getDateRangeDisplayValueFromFormValue,
getDisplayQueryFiltersForKey,
getFilterDisplayValue,
getKeywordQueryWithCurrentSearchContext,
getQueryWithUpdatedValues,
getRangeBoundariesFromFormValue,
serializeQueryJSONForBackend,
Expand Down Expand Up @@ -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');
});
});
});
Loading