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
7 changes: 4 additions & 3 deletions src/components/Search/FilterDropdowns/DisplayPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles';

import {close} from '@libs/actions/Modal';
import Navigation from '@libs/Navigation/Navigation';
import {getGroupBySections, getSearchColumnTranslationKey, getViewOptions} from '@libs/SearchUIUtils';
import {getGroupBySections, getSearchColumnTranslationKey, getValidGroupBy, getViewOptions} from '@libs/SearchUIUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -75,6 +75,7 @@ function DisplayPopup({queryJSON, searchResults, closeOverlay, onSort}: DisplayP
const sortByValue = queryJSON.sortBy;
const sortOrderValue = queryJSON.sortOrder;
const groupByValue = searchAdvancedFilters[CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY];
const validGroupByValue = getValidGroupBy(groupByValue);
const groupCurrencyValue = searchAdvancedFilters[CONST.SEARCH.SYNTAX_FILTER_KEYS.GROUP_CURRENCY];
const viewValue = searchAdvancedFilters[CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW];

Expand All @@ -91,7 +92,7 @@ function DisplayPopup({queryJSON, searchResults, closeOverlay, onSort}: DisplayP
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('search.display.groupBy')}
title={groupByValue ? translate(`search.filters.groupBy.${groupByValue}`) : undefined}
title={validGroupByValue ? translate(`search.filters.groupBy.${validGroupByValue}`) : undefined}
onPress={() => setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY)}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FILTER_GROUP_BY}
/>
Expand All @@ -105,7 +106,7 @@ function DisplayPopup({queryJSON, searchResults, closeOverlay, onSort}: DisplayP
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FILTER_GROUP_CURRENCY}
/>
)}
{isExpenseType && !!groupByValue && (
{isExpenseType && !!validGroupByValue && (
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('search.view.label')}
Expand Down
21 changes: 15 additions & 6 deletions src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ const VALID_IS_TYPES = new Set(Object.values(CONST.SEARCH.IS_VALUES));
const VALID_WITHDRAWAL_TYPES = new Set(Object.values(CONST.SEARCH.WITHDRAWAL_TYPE));
const VALID_WITHDRAWAL_STATUSES = new Set<string>(Object.values(CONST.SEARCH.SETTLEMENT_STATUS));
const VALID_PAID_STATUSES = new Set<string>(Object.values(CONST.SEARCH.PAID_STATUS));

// Create reverse lookup maps for O(1) performance
const createKeyToUserFriendlyMap = () => {
const map = new Map<string, string>();
Expand Down Expand Up @@ -177,7 +176,16 @@ function sanitizeSearchValue(str: string) {
return escaped;
}

const syntaxRegex = new RegExp(`^-?(${Object.values(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS).join('|')}|report-?field(-.+)+)[:><=].+$`);
const syntaxKeyPattern = `-?(?:${Object.values(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS).join('|')}|report-?field(?:-[^\\s:><=]+)+)`;
const syntaxOperatorPattern = '\\*?:|[<>]=?|=';
const syntaxSpanRegex = new RegExp(`(^|\\s)(${syntaxKeyPattern})\\s*(${syntaxOperatorPattern})\\s*([^\\s]+)`, 'gi');

function quoteSyntaxSpans(segment: string) {
return segment.replace(syntaxSpanRegex, (match: string, prefix: string) => {
const syntaxValue = match.slice(prefix.length).trim();
return `${prefix}"${syntaxValue}"`;
});
}
/**
* Escapes each keyword that would otherwise be re-interpreted as query syntax by wrapping it in quotes.
* A keyword that looks like a filter (e.g. `type:expense`) becomes `"type:expense"` so it is matched as a
Expand All @@ -186,12 +194,12 @@ const syntaxRegex = new RegExp(`^-?(${Object.values(CONST.SEARCH.SEARCH_USER_FRI
function escapeKeyword(keywords: string) {
return (
keywords
.match(/"([^"]*)"|(\S+)/g)
.match(/"(?:\\.|[^"\\])*"|(?:\\.|[^"\\])+/g)
?.map((q) => {
if (q.toLowerCase().match(syntaxRegex)) {
return `"${q}"`;
if (q.startsWith('"')) {
return q;
}
return q;
return quoteSyntaxSpans(q).trim();
})
.join(' ') ?? ''
);
Expand Down Expand Up @@ -729,6 +737,7 @@ function getCachedSearchQueryJSON(query: SearchQueryString, rawQuery?: SearchQue
// Add the full input and hash to the results
result.inputQuery = query;
result.flatFilters = flatFilters;

result.isViewExplicitlySet = rawFilterList?.some((filter) => filter.key === CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW) ?? false;

// Normalize limit before computing hashes to ensure invalid values don't affect hash
Expand Down
4 changes: 2 additions & 2 deletions src/pages/Search/SearchSavePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import useThemeStyles from '@hooks/useThemeStyles';

import {saveSearch} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import {getCustomColumnDefault, getSearchColumnTranslationKey, mapFiltersFormToLabelValueList} from '@libs/SearchUIUtils';
import {getCustomColumnDefault, getSearchColumnTranslationKey, getValidGroupBy, mapFiltersFormToLabelValueList} from '@libs/SearchUIUtils';
import type {SearchFilter} from '@libs/SearchUIUtils';
import {getFieldRequiredErrors} from '@libs/ValidationUtils';

Expand Down Expand Up @@ -112,7 +112,7 @@ function FilterValue({filterKey, value}: FilterValueWithKeyProps) {

function getAppliedDisplays(searchAdvancedFiltersForm: Partial<SearchAdvancedFiltersForm>, queryJSON: SearchQueryJSON | undefined, translate: LocalizedTranslate) {
const appliedDisplays = [];
const groupBy = searchAdvancedFiltersForm.groupBy;
const groupBy = getValidGroupBy(searchAdvancedFiltersForm.groupBy);
if (groupBy) {
appliedDisplays.push({label: translate('search.display.groupBy'), value: translate(`search.filters.groupBy.${groupBy}`)});
}
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/Search/SearchQueryUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3617,6 +3617,23 @@ describe('SearchQueryUtils', () => {
expect(result).toContain('type:trip');
});

it('should stop escaping after the unquoted filter value', () => {
const currentQueryJSON = buildSearchQueryJSON('type:trip status:all');

const result = currentQueryJSON ? getKeywordQueryWithCurrentSearchContext('type:expense foo bar', currentQueryJSON) : '';

expect(result).toContain('"type:expense" foo bar');
expect(result).not.toContain('"type:expense foo bar"');
});

it('should escape syntax with whitespace between the operator and value', () => {
const currentQueryJSON = buildSearchQueryJSON('type:expense from:me');

const result = currentQueryJSON ? getKeywordQueryWithCurrentSearchContext('group-by: reports', currentQueryJSON) : '';

expect(result).toContain('"group-by: reports"');
});

it('should escape input that uses a comparison operator with a filter key', () => {
const currentQueryJSON = buildSearchQueryJSON('type:trip status:all');

Expand Down
Loading