From b0830d6752a1339c4dbcc8d9f61cda4a000c0a90 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 09:55:23 -0800 Subject: [PATCH 01/11] feat: add LINE and PIE constants to SEARCH.VIEW - Add LINE: 'line' and PIE: 'pie' to CONST.SEARCH.VIEW - Add VIEW: 'view' to CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS Part of implementing view:line and view:pie support in search query language --- src/CONST/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 411ce7eb0c68c..1b4c78a04dfa0 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7149,6 +7149,8 @@ const CONST = { VIEW: { TABLE: 'table', BAR: 'bar', + LINE: 'line', + PIE: 'pie', }, SYNTAX_FILTER_KEYS: { TYPE: 'type', @@ -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', From 0df21b115ef83cf5130cc332a10b725488c94a1c Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 09:57:37 -0800 Subject: [PATCH 02/11] test: add tests for view:line and view:pie parsing - Add test cases for view:line parameter parsing - Add test cases for view:pie parameter parsing - Tests verify that view values are correctly parsed in search queries --- tests/unit/SearchParserTest.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts index 31c8f96bf551d..8aa73a26fe5e0 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -919,6 +919,36 @@ const keywordTests = [ }, }, }, + { + query: 'type:expense view:line category:travel', + expected: { + type: 'expense', + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + sortBy: 'date', + sortOrder: 'desc', + view: 'line', + filters: { + operator: 'eq', + left: 'category', + right: 'travel', + }, + }, + }, + { + query: 'type:expense view:pie category:travel', + expected: { + type: 'expense', + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + sortBy: 'date', + sortOrder: 'desc', + view: 'pie', + filters: { + operator: 'eq', + left: 'category', + right: 'travel', + }, + }, + }, ]; const limitTests = [ From 16edb6fb7ef3370e89c185a73a1ef9c4c4e3d060 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 09:58:53 -0800 Subject: [PATCH 03/11] feat: add autocomplete support for view parameter - Add viewAutocompleteList useMemo to generate view suggestions - Add VIEW case to autocompleteSuggestions switch in SearchAutocompleteList - Add userFriendlyViewList constant for validation - Add VIEW validation case in filterOutRangesWithCorrectValue function This enables autocomplete suggestions for view values (table, bar, line, pie) when users type 'view:' in the search query. --- src/components/Search/SearchAutocompleteList.tsx | 10 ++++++++++ src/libs/SearchAutocompleteUtils.ts | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index d6c4f88b1871d..36f06026e9693 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -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) { @@ -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)) diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 547b56418d032..e6e0aa7387ed6 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -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, @@ -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[]; @@ -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); From f54bb3ecaf869ace1e55a03946a3537ef9084623 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 10:01:51 -0800 Subject: [PATCH 04/11] test: add query utils tests for view parameter handling - Add tests for view:bar, view:line, and view:pie in getQueryWithUpdatedValues - Fix buildSearchQueryString to preserve view when it differs from default - View parameter is now correctly preserved when query is updated Tests verify that view values are correctly parsed and preserved in search queries. --- src/libs/SearchQueryUtils.ts | 6 ++++-- tests/unit/Search/SearchQueryUtilsTest.ts | 24 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 0392115d6aadf..44a5718ea1e1e 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -473,8 +473,10 @@ 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) || + (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 diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 67aff2e48b3be..e8bfb1283d893 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -94,6 +94,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'; From d1e3269a1306156e1e9282eea02302a046971428 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 10:24:56 -0800 Subject: [PATCH 05/11] test: add UI utils tests for view autocomplete values - Add test to verify all view values (table, bar, line, pie) are included - Add test to verify view values are correctly mapped to user-friendly values - Tests ensure view autocomplete suggestions work correctly These tests verify that the view autocomplete list generation logic correctly includes all four view types and maps them properly. --- tests/unit/Search/SearchUIUtilsTest.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 10403c1b1c8b0..f427d9d870415 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5231,4 +5231,25 @@ 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 {getUserFriendlyValue} = require('@src/libs/SearchQueryUtils'); + 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); + }); + }); }); From e24dad9bd6dbf894d9ac755b70d2b4c0ea4cb89e Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 10:29:39 -0800 Subject: [PATCH 06/11] fix: add view to autocompleteKey rule in autocomplete parser - Add 'view' to autocompleteKey rule in autocompleteParser.peggy - Regenerate autocompleteParser.js - This enables autocomplete suggestions to appear when typing 'view:' Previously, view was missing from the autocompleteKey rule, so the parser didn't recognize 'view:' as a key that should trigger autocomplete suggestions. Now autocomplete works for view: just like it does for group-by: --- src/libs/SearchParser/autocompleteParser.js | 51 ++++++++++--------- .../SearchParser/autocompleteParser.peggy | 1 + 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 6155382cf255d..cae2ed04fa592 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -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(); + } } } } diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index a50efdbcdbdd1..2299252b119d2 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -96,6 +96,7 @@ autocompleteKey "key" / cardID / feed / groupBy + / view / reimbursable / billable / policyID From 339ae40bb2bdeadbe65e3f0c0ba542a2342cfc2e Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 10:41:26 -0800 Subject: [PATCH 07/11] fix: resolve lint errors in view parameter implementation - Fix nullish coalescing operator usage in buildSearchQueryString - Replace require() with proper import for getUserFriendlyValue in tests - All lint errors resolved, tests passing --- src/libs/SearchQueryUtils.ts | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 44a5718ea1e1e..08e9250202897 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -475,7 +475,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { // 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) || + (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)) { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index f427d9d870415..8ad5c3c4cf236 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -35,6 +35,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Connections} from '@src/types/onyx/Policy'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; +import {getUserFriendlyValue} from '@src/libs/SearchQueryUtils'; import getOnyxValue from '../../utils/getOnyxValue'; import {formatPhoneNumber, localeCompare, translateLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; @@ -5243,7 +5244,6 @@ describe('SearchUIUtils', () => { }); test('should correctly map view values to user-friendly values', () => { - const {getUserFriendlyValue} = require('@src/libs/SearchQueryUtils'); const viewValues = Object.values(CONST.SEARCH.VIEW); const userFriendlyValues = viewValues.map((value) => getUserFriendlyValue(value)); From 296e0708e9e2d5b3d760079a286536962ca52441 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 10:44:25 -0800 Subject: [PATCH 08/11] prettier --- src/libs/SearchQueryUtils.ts | 3 +-- tests/unit/Search/SearchUIUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 08e9250202897..2c1e00911ddd7 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -475,8 +475,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { // 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); + (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 diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 8ad5c3c4cf236..966060607bd09 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -29,13 +29,13 @@ 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'; import type * as OnyxTypes from '@src/types/onyx'; import type {Connections} from '@src/types/onyx/Policy'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; -import {getUserFriendlyValue} from '@src/libs/SearchQueryUtils'; import getOnyxValue from '../../utils/getOnyxValue'; import {formatPhoneNumber, localeCompare, translateLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; From 35d5c1e20dacc4a03a87e24bd0fd132344e9800b Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 10:57:08 -0800 Subject: [PATCH 09/11] test: add coverage for view parameter in buildSearchQueryString and autocomplete validation - Add 7 tests for buildSearchQueryString covering view parameter edge cases - Add 8 tests for view filter validation in parseForLiveMarkdown - Cover all view types (table, bar, line, pie) individually - Tests verify view is included/excluded correctly based on rawFilterList and default values --- tests/unit/Search/SearchQueryUtilsTest.ts | 61 +++++++++++++++++++ tests/unit/SearchAutocompleteUtilsTest.ts | 74 +++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index e8bfb1283d893..c3ccf1c4d2c80 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -9,6 +9,7 @@ import { buildFilterFormValuesFromQuery, buildQueryStringFromFilterFormValues, buildSearchQueryJSON, + buildSearchQueryString, buildUserReadableQueryString, getFilterDisplayValue, getQueryWithUpdatedValues, @@ -949,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'); + }); + }); }); diff --git a/tests/unit/SearchAutocompleteUtilsTest.ts b/tests/unit/SearchAutocompleteUtilsTest.ts index 7b92ad879ee24..73ed309c63f71 100644 --- a/tests/unit/SearchAutocompleteUtilsTest.ts +++ b/tests/unit/SearchAutocompleteUtilsTest.ts @@ -280,5 +280,79 @@ describe('SearchAutocompleteUtils', () => { // Total amounts with more than 8 digits fail validation expect(result).toEqual([]); }); + + describe('view filter highlighting', () => { + it('highlights valid view values', () => { + const validViews = ['table', 'bar', 'line', 'pie']; + + for (const view of validViews) { + const input = `view:${view}`; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([{start: 5, type: 'mention-user', length: view.length}]); + } + }); + + it('does not highlight invalid view values', () => { + const input = 'view:invalid'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([]); + }); + + it('highlights view in complex query with other filters', () => { + const input = 'type:expense view:line category:Travel'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([ + {start: 5, type: 'mention-user', length: 7}, // type:expense + {start: 18, type: 'mention-user', length: 4}, // view:line + {start: 32, type: 'mention-user', length: 6}, // category:Travel + ]); + }); + + it('does not highlight empty view value', () => { + const input = 'view:'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([]); + }); + + it('highlights view:table in query', () => { + const input = 'view:table'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([{start: 5, type: 'mention-user', length: 5}]); + }); + + it('highlights view:bar in query', () => { + const input = 'view:bar'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([{start: 5, type: 'mention-user', length: 3}]); + }); + + it('highlights view:line in query', () => { + const input = 'view:line'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([{start: 5, type: 'mention-user', length: 4}]); + }); + + it('highlights view:pie in query', () => { + const input = 'view:pie'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList); + + expect(result).toEqual([{start: 5, type: 'mention-user', length: 3}]); + }); + }); }); }); From c75af786c16c7abfcd1d2c93245db81ea20ac9c7 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 11:09:40 -0800 Subject: [PATCH 10/11] fix: add VIEW to SearchFilterKey and SearchAdvancedFiltersForm types - Add VIEW to SearchFilterKey type to fix TypeScript errors in switch statements - Add VIEW filter key to FILTER_KEYS constant - Add VIEW property to SearchAdvancedFiltersForm type definition - Resolves typecheck errors in SearchAutocompleteUtils and SearchAutocompleteList --- src/components/Search/types.ts | 3 ++- src/types/form/SearchAdvancedFiltersForm.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 0019a764dc943..0c938c94454c6 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -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; type UserFriendlyValue = ValueOf; diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 09489573ea3dc..09ccb3f64b511 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -168,6 +168,7 @@ const FILTER_KEYS = { COLUMNS: 'columns', LIMIT: 'limit', + VIEW: 'view', } as const; const ALLOWED_TYPE_FILTERS = { @@ -535,6 +536,7 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.GROUP_BY]: SearchGroupBy; [FILTER_KEYS.TYPE]: SearchDataTypes; [FILTER_KEYS.COLUMNS]: SearchCustomColumnIds[]; + [FILTER_KEYS.VIEW]: ValueOf; [FILTER_KEYS.STATUS]: string[] | string; From cc300466e8b821fada6bc08408eaa9babba33a7c Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Wed, 28 Jan 2026 13:41:07 -0800 Subject: [PATCH 11/11] fix: add viewAutocompleteList to useMemo dependency array - Add viewAutocompleteList to dependency array to satisfy ESLint exhaustive-deps rule - Ensures hook correctly tracks all dependencies for maintainability - Consistent with other autocomplete lists (groupByAutocompleteList, statusAutocompleteList) --- src/components/Search/SearchAutocompleteList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 36f06026e9693..42a7aee121cef 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -649,6 +649,7 @@ function SearchAutocompleteList({ currentUserAccountID, currentUserEmail, groupByAutocompleteList, + viewAutocompleteList, statusAutocompleteList, feedAutoCompleteList, cardAutocompleteList,