Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7149,6 +7149,8 @@ const CONST = {
VIEW: {
TABLE: 'table',
BAR: 'bar',
LINE: 'line',
PIE: 'pie',
},
SYNTAX_FILTER_KEYS: {
TYPE: 'type',
Expand Down Expand Up @@ -7219,6 +7221,7 @@ const CONST = {
SORT_ORDER: 'sort-order',
POLICY_ID: 'workspace',
GROUP_BY: 'group-by',
VIEW: 'view',
DATE: 'date',
AMOUNT: 'amount',
TOTAL: 'total',
Expand Down
11 changes: 11 additions & 0 deletions src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ function SearchAutocompleteList({
}
}, [currentType]);

const viewAutocompleteList = useMemo(() => {
return Object.values(CONST.SEARCH.VIEW).map((value) => getUserFriendlyValue(value));
}, []);

const statusAutocompleteList = useMemo(() => {
let suggestedStatuses;
switch (currentType) {
Expand Down Expand Up @@ -495,6 +499,12 @@ function SearchAutocompleteList({
);
return filteredGroupBy.map((groupByValue) => ({filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.GROUP_BY, text: groupByValue}));
}
case CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW: {
const filteredViews = viewAutocompleteList.filter(
(viewValue) => viewValue.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(viewValue.toLowerCase()),
);
return filteredViews.map((viewValue) => ({filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.VIEW, text: viewValue}));
}
case CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS: {
const filteredStatuses = statusAutocompleteList
.filter((status) => status.includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(status))
Expand Down Expand Up @@ -639,6 +649,7 @@ function SearchAutocompleteList({
currentUserAccountID,
currentUserEmail,
groupByAutocompleteList,
viewAutocompleteList,
statusAutocompleteList,
feedAutoCompleteList,
cardAutocompleteList,
Expand Down
3 changes: 2 additions & 1 deletion src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ type SearchFilterKey =
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.COLUMNS
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT;
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW;

type UserFriendlyKey = ValueOf<typeof CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS>;
type UserFriendlyValue = ValueOf<typeof CONST.SEARCH.SEARCH_USER_FRIENDLY_VALUES_MAP>;
Expand Down
4 changes: 4 additions & 0 deletions src/libs/SearchAutocompleteUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) {

const userFriendlyExpenseTypeList = Object.values(CONST.SEARCH.TRANSACTION_TYPE).map((value) => getUserFriendlyValue(value));
const userFriendlyGroupByList = Object.values(CONST.SEARCH.GROUP_BY).map((value) => getUserFriendlyValue(value));
const userFriendlyViewList = Object.values(CONST.SEARCH.VIEW).map((value) => getUserFriendlyValue(value));
const userFriendlyStatusList = Object.values({
...CONST.SEARCH.STATUS.EXPENSE,
...CONST.SEARCH.STATUS.INVOICE,
Expand Down Expand Up @@ -160,6 +161,7 @@ function filterOutRangesWithCorrectValue(
const withdrawalTypeList = Object.values(CONST.SEARCH.WITHDRAWAL_TYPE) as string[];
const statusList = userFriendlyStatusList;
const groupByList = userFriendlyGroupByList;
const viewList = userFriendlyViewList;
const booleanList = Object.values(CONST.SEARCH.BOOLEAN) as string[];
const actionList = Object.values(CONST.SEARCH.ACTION_FILTERS) as string[];
const datePresetList = Object.values(CONST.SEARCH.DATE_PRESETS) as string[];
Expand Down Expand Up @@ -208,6 +210,8 @@ function filterOutRangesWithCorrectValue(
return false;
}
return groupByList.includes(range.value);
case CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW:
return viewList.includes(range.value);
case CONST.SEARCH.SYNTAX_FILTER_KEYS.BILLABLE:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE:
return booleanList.includes(range.value);
Expand Down
51 changes: 27 additions & 24 deletions src/libs/SearchParser/autocompleteParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1031,53 +1031,56 @@ function peg$parse(input, options) {
if (s1 === peg$FAILED) {
s1 = peg$parsegroupBy();
if (s1 === peg$FAILED) {
s1 = peg$parsereimbursable();
s1 = peg$parseview();
if (s1 === peg$FAILED) {
s1 = peg$parsebillable();
s1 = peg$parsereimbursable();
if (s1 === peg$FAILED) {
s1 = peg$parsepolicyID();
s1 = peg$parsebillable();
if (s1 === peg$FAILED) {
s1 = peg$parseaction();
s1 = peg$parsepolicyID();
if (s1 === peg$FAILED) {
s1 = peg$parsedate();
s1 = peg$parseaction();
if (s1 === peg$FAILED) {
s1 = peg$parsesubmitted();
s1 = peg$parsedate();
if (s1 === peg$FAILED) {
s1 = peg$parseapproved();
s1 = peg$parsesubmitted();
if (s1 === peg$FAILED) {
s1 = peg$parsepaid();
s1 = peg$parseapproved();
if (s1 === peg$FAILED) {
s1 = peg$parseexported();
s1 = peg$parsepaid();
if (s1 === peg$FAILED) {
s1 = peg$parsewithdrawn();
s1 = peg$parseexported();
if (s1 === peg$FAILED) {
s1 = peg$parseposted();
s1 = peg$parsewithdrawn();
if (s1 === peg$FAILED) {
s1 = peg$parsehas();
s1 = peg$parseposted();
if (s1 === peg$FAILED) {
s1 = peg$parseis();
s1 = peg$parsehas();
if (s1 === peg$FAILED) {
s1 = peg$parsepurchaseCurrency();
s1 = peg$parseis();
if (s1 === peg$FAILED) {
s1 = peg$parsepurchaseAmount();
s1 = peg$parsepurchaseCurrency();
if (s1 === peg$FAILED) {
s1 = peg$parseamount();
s1 = peg$parsepurchaseAmount();
if (s1 === peg$FAILED) {
s1 = peg$parsemerchant();
s1 = peg$parseamount();
if (s1 === peg$FAILED) {
s1 = peg$parsedescription();
s1 = peg$parsemerchant();
if (s1 === peg$FAILED) {
s1 = peg$parsereportID();
s1 = peg$parsedescription();
if (s1 === peg$FAILED) {
s1 = peg$parsewithdrawalID();
s1 = peg$parsereportID();
if (s1 === peg$FAILED) {
s1 = peg$parsetitle();
s1 = peg$parsewithdrawalID();
if (s1 === peg$FAILED) {
s1 = peg$parsereportFieldDynamic();
s1 = peg$parsetitle();
if (s1 === peg$FAILED) {
s1 = peg$parsecolumns();
s1 = peg$parsereportFieldDynamic();
if (s1 === peg$FAILED) {
s1 = peg$parselimit();
s1 = peg$parsecolumns();
if (s1 === peg$FAILED) {
s1 = peg$parselimit();
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/libs/SearchParser/autocompleteParser.peggy
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ autocompleteKey "key"
/ cardID
/ feed
/ groupBy
/ view
/ reimbursable
/ billable
/ policyID
Expand Down
5 changes: 3 additions & 2 deletions src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,9 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
const queryParts: string[] = [];
const defaultQueryJSON = buildSearchQueryJSON('');

// Check if view was explicitly set by the user (exists in rawFilterList)
const wasViewExplicitlySet = queryJSON?.rawFilterList?.some((filter) => filter.key === CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW);
// Check if view was explicitly set by the user (exists in rawFilterList or differs from default)
const wasViewExplicitlySet =
(queryJSON?.rawFilterList?.some((filter) => filter.key === CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW) ?? false) || (queryJSON?.view && queryJSON.view !== defaultQueryJSON?.view);

for (const [, key] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) {
// Skip view if it wasn't explicitly set by the user
Expand Down
2 changes: 2 additions & 0 deletions src/types/form/SearchAdvancedFiltersForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const FILTER_KEYS = {

COLUMNS: 'columns',
LIMIT: 'limit',
VIEW: 'view',
} as const;

const ALLOWED_TYPE_FILTERS = {
Expand Down Expand Up @@ -535,6 +536,7 @@ type SearchAdvancedFiltersForm = Form<
[FILTER_KEYS.GROUP_BY]: SearchGroupBy;
[FILTER_KEYS.TYPE]: SearchDataTypes;
[FILTER_KEYS.COLUMNS]: SearchCustomColumnIds[];
[FILTER_KEYS.VIEW]: ValueOf<typeof CONST.SEARCH.VIEW>;

[FILTER_KEYS.STATUS]: string[] | string;

Expand Down
85 changes: 85 additions & 0 deletions tests/unit/Search/SearchQueryUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
buildFilterFormValuesFromQuery,
buildQueryStringFromFilterFormValues,
buildSearchQueryJSON,
buildSearchQueryString,
buildUserReadableQueryString,
getFilterDisplayValue,
getQueryWithUpdatedValues,
Expand Down Expand Up @@ -94,6 +95,30 @@ describe('SearchQueryUtils', () => {
expect(result).toEqual(`${defaultQuery} groupBy:reports from:12345`);
});

test('returns query with updated view', () => {
const userQuery = 'from:johndoe@example.com view:bar';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} view:bar from:12345`);
});

test('returns query with view:line', () => {
const userQuery = 'type:expense view:line category:travel';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} view:line category:travel`);
});

test('returns query with view:pie', () => {
const userQuery = 'type:expense view:pie merchant:Amazon';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} view:pie merchant:Amazon`);
});

test('deduplicates conflicting type filters keeping the last occurrence', () => {
const userQuery = 'type:expense-report action:submit from:me type:expense';

Expand Down Expand Up @@ -925,4 +950,64 @@ describe('SearchQueryUtils', () => {
}
});
});

describe('buildSearchQueryString', () => {
test('includes view when explicitly set in rawFilterList', () => {
const queryJSON = buildSearchQueryJSON('type:expense view:line', 'type:expense view:line');

const result = buildSearchQueryString(queryJSON);

expect(result).toContain('view:line');
});

test('includes view when differs from default even without rawFilterList', () => {
const queryJSON = buildSearchQueryJSON('type:expense view:pie');

const result = buildSearchQueryString(queryJSON);

expect(result).toContain('view:pie');
});

test('includes view when set to bar', () => {
const queryJSON = buildSearchQueryJSON('type:expense view:bar');

const result = buildSearchQueryString(queryJSON);

expect(result).toContain('view:bar');
});

test('skips view when not explicitly set and matches default', () => {
const queryJSON = buildSearchQueryJSON('type:expense');

const result = buildSearchQueryString(queryJSON);

expect(result).not.toContain('view:table');
});

test('includes view when explicitly set to table in rawFilterList', () => {
const queryJSON = buildSearchQueryJSON('type:expense view:table', 'type:expense view:table');

const result = buildSearchQueryString(queryJSON);

expect(result).toContain('view:table');
});

test('preserves view along with other filters', () => {
const queryJSON = buildSearchQueryJSON('type:expense view:line category:travel');

const result = buildSearchQueryString(queryJSON);

expect(result).toContain('view:line');
expect(result).toContain('category:travel');
});

test('handles view with rawFilterList containing other filters', () => {
const queryJSON = buildSearchQueryJSON('type:expense view:pie merchant:Amazon', 'type:expense view:pie merchant:Amazon');

const result = buildSearchQueryString(queryJSON);

expect(result).toContain('view:pie');
expect(result).toContain('merchant:Amazon');
});
});
});
21 changes: 21 additions & 0 deletions tests/unit/Search/SearchUIUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {setOptimisticDataForTransactionThreadPreview} from '@userActions/Search'
import CONST from '@src/CONST';
import IntlStore from '@src/languages/IntlStore';
import type {CardFeedForDisplay} from '@src/libs/CardFeedUtils';
import {getUserFriendlyValue} from '@src/libs/SearchQueryUtils';
import * as SearchUIUtils from '@src/libs/SearchUIUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -5231,4 +5232,24 @@ describe('SearchUIUtils', () => {
expect(result).toBeDefined();
});
});

describe('view autocomplete values', () => {
test('should include all view values (table, bar, line, pie)', () => {
const viewValues = Object.values(CONST.SEARCH.VIEW);
expect(viewValues).toContain('table');
expect(viewValues).toContain('bar');
expect(viewValues).toContain('line');
expect(viewValues).toContain('pie');
expect(viewValues).toHaveLength(4);
});

test('should correctly map view values to user-friendly values', () => {
const viewValues = Object.values(CONST.SEARCH.VIEW);
const userFriendlyValues = viewValues.map((value) => getUserFriendlyValue(value));

// All view values should be mapped (they may be the same or different)
expect(userFriendlyValues).toHaveLength(4);
expect(userFriendlyValues.every((value) => typeof value === 'string')).toBe(true);
});
});
});
Loading
Loading