From 38711742719b5e288e6757759cbf259052e7808b Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 11:42:03 -0800 Subject: [PATCH 01/19] FD add view selector to search with group-by --- .../SearchPageHeader/SearchFiltersBar.tsx | 36 ++++++++++++++- src/languages/en.ts | 5 ++ src/libs/SearchQueryUtils.ts | 11 ++++- src/types/form/SearchAdvancedFiltersForm.ts | 3 ++ tests/unit/Search/SearchQueryUtilsTest.ts | 46 +++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 8d1bc9313f1db..ed556c9736c3b 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -88,8 +88,8 @@ function SearchFiltersBar({ const currentPolicy = usePolicy(currentSelectedPolicyID); const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true}); const [searchAdvancedFiltersForm = getEmptyObject>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); - // type, groupBy and status values are not guaranteed to respect the ts type as they come from user input - const {type: unsafeType, groupBy: unsafeGroupBy, status: unsafeStatus, flatFilters} = queryJSON; + // type, groupBy, status, and view values are not guaranteed to respect the ts type as they come from user input + const {type: unsafeType, groupBy: unsafeGroupBy, status: unsafeStatus, view: unsafeView, flatFilters} = queryJSON; const [selectedIOUReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentSelectedReportID}`, {canBeMissing: true}); const isCurrentSelectedExpenseReport = isExpenseReport(currentSelectedReportID); const theme = useTheme(); @@ -188,6 +188,15 @@ function SearchFiltersBar({ return [options, value]; }, [translate, unsafeGroupBy]); + const [viewOptions, viewValue] = useMemo(() => { + const options = [ + {text: translate('search.view.table'), value: CONST.SEARCH.VIEW.TABLE}, + {text: translate('search.view.bar'), value: CONST.SEARCH.VIEW.BAR}, + ]; + const value = options.find((option) => option.value === unsafeView) ?? options[0]; + return [options, value]; + }, [translate, unsafeView]); + const [groupCurrencyOptions, groupCurrency] = useMemo(() => { const options = getGroupCurrencyOptions(currencyList, getCurrencySymbol); const value = options.find((option) => option.value === searchAdvancedFiltersForm.groupCurrency) ?? null; @@ -373,6 +382,21 @@ function SearchFiltersBar({ [translate, groupByOptions, groupBy, updateFilterForm], ); + const viewComponent = useCallback( + ({closeOverlay}: PopoverComponentProps) => { + return ( + updateFilterForm({view: item?.value ?? CONST.SEARCH.VIEW.TABLE})} + /> + ); + }, + [translate, viewOptions, viewValue, updateFilterForm], + ); + const groupCurrencyComponent = useCallback( ({closeOverlay}: PopoverComponentProps) => { return ( @@ -566,6 +590,12 @@ function SearchFiltersBar({ value: groupBy?.text ?? null, filterKey: FILTER_KEYS.GROUP_BY, }, + { + label: translate('search.view.label'), + PopoverComponent: viewComponent, + value: viewValue?.text ?? null, + filterKey: FILTER_KEYS.VIEW, + }, ] : []), ...(shouldDisplayGroupCurrencyFilter @@ -674,6 +704,7 @@ function SearchFiltersBar({ type?.text, groupBy?.value, groupBy?.text, + viewValue?.text, groupCurrency?.value, withdrawalType?.text, displayDate, @@ -693,6 +724,7 @@ function SearchFiltersBar({ isComponent, typeComponent, groupByComponent, + viewComponent, groupCurrencyComponent, statusComponent, datePickerComponent, diff --git a/src/languages/en.ts b/src/languages/en.ts index 794a9f356e8ce..bd39ad0aa9051 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6908,6 +6908,11 @@ const translations = { }, has: 'Has', groupBy: 'Group by', + view: { + label: 'View', + table: 'Table', + bar: 'Chart', + }, moneyRequestReport: { emptyStateTitle: 'This report has no expenses.', accessPlaceHolder: 'Open for details', diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 0392115d6aadf..9d5f031dc7c26 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -592,7 +592,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial { expect(result).toEqual(`${defaultQuery} groupBy:reports from:12345`); }); + test('returns query with view parameter preserved', () => { + const userQuery = 'type:expense groupBy:category view:bar'; + + const result = getQueryWithUpdatedValues(userQuery); + + expect(result).toContain('view:bar'); + expect(result).toContain('groupBy:category'); + }); + test('deduplicates conflicting type filters keeping the last occurrence', () => { const userQuery = 'type:expense-report action:submit from:me type:expense'; @@ -302,6 +311,43 @@ describe('SearchQueryUtils', () => { expect(result).not.toContain('limit:'); }); }); + + describe('view parameter', () => { + test('with view parameter set to bar', () => { + const filterValues: Partial = { + type: 'expense', + groupBy: CONST.SEARCH.GROUP_BY.CATEGORY, + view: CONST.SEARCH.VIEW.BAR, + }; + + const result = buildQueryStringFromFilterFormValues(filterValues); + + expect(result).toEqual('sortBy:date sortOrder:desc type:expense groupBy:category view:bar'); + }); + + test('with view parameter set to table', () => { + const filterValues: Partial = { + type: 'expense', + groupBy: CONST.SEARCH.GROUP_BY.CATEGORY, + view: CONST.SEARCH.VIEW.TABLE, + }; + + const result = buildQueryStringFromFilterFormValues(filterValues); + + expect(result).toEqual('sortBy:date sortOrder:desc type:expense groupBy:category view:table'); + }); + + test('without view parameter omits view from query', () => { + const filterValues: Partial = { + type: 'expense', + groupBy: CONST.SEARCH.GROUP_BY.CATEGORY, + }; + + const result = buildQueryStringFromFilterFormValues(filterValues); + + expect(result).not.toContain('view:'); + }); + }); }); describe('buildUserReadableQueryString', () => { From 608b0f103c2d60a2a85c5b1364032cc54109d95e Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 11:47:46 -0800 Subject: [PATCH 02/19] Add translations from Polygot parrot --- src/languages/de.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 8 files changed, 8 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 18c6efc2b7d57..27c0937ee8840 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7041,6 +7041,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt', }, topSpenders: 'Top-Ausgaben', + view: {label: 'Ansicht', table: 'Tabelle', bar: 'Diagramm'}, }, genericErrorPage: { title: 'Oh je, etwas ist schiefgelaufen!', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2ed567545af9e..ff02d9ae7c0a0 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7053,6 +7053,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés', }, topSpenders: 'Plus gros dépensiers', + view: {label: 'Afficher', table: 'Tableau', bar: 'Graphique'}, }, genericErrorPage: { title: 'Oh oh, quelque chose s’est mal passé !', diff --git a/src/languages/it.ts b/src/languages/it.ts index d4b2b09b2c71b..fe06f30cd8227 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7030,6 +7030,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori allMatchingItemsSelected: 'Tutti gli elementi corrispondenti selezionati', }, topSpenders: 'Maggiori spenditori', + view: {label: 'Visualizza', table: 'Tabella', bar: 'Grafico'}, }, genericErrorPage: { title: 'Uh-oh, qualcosa è andato storto!', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c34e8eb565fe5..1cd30bb3333e8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6970,6 +6970,7 @@ ${reportName} allMatchingItemsSelected: '一致する項目をすべて選択済み', }, topSpenders: 'トップ支出者', + view: {label: '表示', table: 'テーブル', bar: 'チャート'}, }, genericErrorPage: { title: 'おっと、問題が発生しました!', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index bed7e8069be2c..e7c0a34db9aa0 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7013,6 +7013,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd', }, topSpenders: 'Grootste uitgaven', + view: {label: 'Bekijken', table: 'Tabel', bar: 'Grafiek'}, }, genericErrorPage: { title: 'O jee, er is iets misgegaan!', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 11285d3eeae71..fef40adea62a0 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7002,6 +7002,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i allMatchingItemsSelected: 'Wybrano wszystkie pasujące elementy', }, topSpenders: 'Najwięksi wydający', + view: {label: 'Wyświetl', table: 'Tabela', bar: 'Wykres'}, }, genericErrorPage: { title: 'Ups, coś poszło nie tak!', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index b4b4a727f6462..800ae7659f461 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7003,6 +7003,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe allMatchingItemsSelected: 'Todos os itens correspondentes selecionados', }, topSpenders: 'Maiores gastadores', + view: {label: 'Visualizar', table: 'Tabela', bar: 'Gráfico'}, }, genericErrorPage: { title: 'Opa, algo deu errado!', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d1c30a46ac026..89f17b5e3b06c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6850,6 +6850,7 @@ ${reportName} allMatchingItemsSelected: '已选择所有匹配的项目', }, topSpenders: '最高支出者', + view: {label: '查看', table: '表', bar: '图表'}, }, genericErrorPage: { title: '哎呀,出错了!', From 24cab90a6aeb06c1099dbaa85b6cf15987c62feb Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 11:56:20 -0800 Subject: [PATCH 03/19] Allow view filter for expenses search --- src/types/form/SearchAdvancedFiltersForm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 6f5f3b893e8df..b659b9c7c2b07 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -250,6 +250,7 @@ const ALLOWED_TYPE_FILTERS = { FILTER_KEYS.EXPORTER, FILTER_KEYS.EXPORTER_NOT, FILTER_KEYS.GROUP_BY, + FILTER_KEYS.VIEW, FILTER_KEYS.ACTION, FILTER_KEYS.ACTION_NOT, FILTER_KEYS.HAS, From 026ae7f9840c6860979c6421c9126fac477e5df3 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 11:59:27 -0800 Subject: [PATCH 04/19] Fix view:bar translation --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index bd39ad0aa9051..c9e47f6b90e2a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6911,7 +6911,7 @@ const translations = { view: { label: 'View', table: 'Table', - bar: 'Chart', + bar: 'Bar', }, moneyRequestReport: { emptyStateTitle: 'This report has no expenses.', From af0ff4c28bb425beecbb0afac4c0ff7c13990bb1 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 12:04:51 -0800 Subject: [PATCH 05/19] Update translations --- src/languages/de.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 27c0937ee8840..ccee9c1c6dc94 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7041,7 +7041,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt', }, topSpenders: 'Top-Ausgaben', - view: {label: 'Ansicht', table: 'Tabelle', bar: 'Diagramm'}, + view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar'}, }, genericErrorPage: { title: 'Oh je, etwas ist schiefgelaufen!', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index ff02d9ae7c0a0..50e939a767195 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7053,7 +7053,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés', }, topSpenders: 'Plus gros dépensiers', - view: {label: 'Afficher', table: 'Tableau', bar: 'Graphique'}, + view: {label: 'Afficher', table: 'Tableau', bar: 'Barre'}, }, genericErrorPage: { title: 'Oh oh, quelque chose s’est mal passé !', diff --git a/src/languages/it.ts b/src/languages/it.ts index fe06f30cd8227..d10f53626a841 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7030,7 +7030,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori allMatchingItemsSelected: 'Tutti gli elementi corrispondenti selezionati', }, topSpenders: 'Maggiori spenditori', - view: {label: 'Visualizza', table: 'Tabella', bar: 'Grafico'}, + view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar'}, }, genericErrorPage: { title: 'Uh-oh, qualcosa è andato storto!', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1cd30bb3333e8..72bcb63de1aba 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6970,7 +6970,7 @@ ${reportName} allMatchingItemsSelected: '一致する項目をすべて選択済み', }, topSpenders: 'トップ支出者', - view: {label: '表示', table: 'テーブル', bar: 'チャート'}, + view: {label: '表示', table: 'テーブル', bar: 'バー'}, }, genericErrorPage: { title: 'おっと、問題が発生しました!', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e7c0a34db9aa0..897b8053a47ea 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7013,7 +7013,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd', }, topSpenders: 'Grootste uitgaven', - view: {label: 'Bekijken', table: 'Tabel', bar: 'Grafiek'}, + view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar'}, }, genericErrorPage: { title: 'O jee, er is iets misgegaan!', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fef40adea62a0..ae42898b78976 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7002,7 +7002,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i allMatchingItemsSelected: 'Wybrano wszystkie pasujące elementy', }, topSpenders: 'Najwięksi wydający', - view: {label: 'Wyświetl', table: 'Tabela', bar: 'Wykres'}, + view: {label: 'Zobacz', table: 'Tabela', bar: 'Pasek'}, }, genericErrorPage: { title: 'Ups, coś poszło nie tak!', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 800ae7659f461..b6418a26d126b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7003,7 +7003,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe allMatchingItemsSelected: 'Todos os itens correspondentes selecionados', }, topSpenders: 'Maiores gastadores', - view: {label: 'Visualizar', table: 'Tabela', bar: 'Gráfico'}, + view: {label: 'Ver', table: 'Tabela', bar: 'Bar'}, }, genericErrorPage: { title: 'Opa, algo deu errado!', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 89f17b5e3b06c..3c611d702fd76 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6850,7 +6850,7 @@ ${reportName} allMatchingItemsSelected: '已选择所有匹配的项目', }, topSpenders: '最高支出者', - view: {label: '查看', table: '表', bar: '图表'}, + view: {label: '查看', table: '表格', bar: '栏'}, }, genericErrorPage: { title: '哎呀,出错了!', From c3065951008493d47de6c8c5d14fb9c82f7dfbfb Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 13:59:24 -0800 Subject: [PATCH 06/19] Remove misleading unit test --- tests/unit/Search/SearchQueryUtilsTest.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 97cbbfd6871dd..a62a70ce29b6a 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -94,15 +94,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual(`${defaultQuery} groupBy:reports from:12345`); }); - test('returns query with view parameter preserved', () => { - const userQuery = 'type:expense groupBy:category view:bar'; - - const result = getQueryWithUpdatedValues(userQuery); - - expect(result).toContain('view:bar'); - expect(result).toContain('groupBy:category'); - }); - test('deduplicates conflicting type filters keeping the last occurrence', () => { const userQuery = 'type:expense-report action:submit from:me type:expense'; From da26d6be29b76c362b72709d9e7b57c6def5e2a5 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 14:08:25 -0800 Subject: [PATCH 07/19] Fix tests, view is only allowed with groupBy --- src/libs/SearchQueryUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 9d5f031dc7c26..3f472fa07496a 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1010,10 +1010,11 @@ function buildFilterFormValuesFromQuery( if (queryJSON.groupBy) { filtersForm[FILTER_KEYS.GROUP_BY] = queryJSON.groupBy; - } - if (queryJSON.view) { - filtersForm[FILTER_KEYS.VIEW] = queryJSON.view; + // View is only allowed when groupBy is set + if (queryJSON.view) { + filtersForm[FILTER_KEYS.VIEW] = queryJSON.view; + } } if (queryJSON.columns) { From 926d83c078716bf02e57b272cd2aca218c91cc74 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 28 Jan 2026 14:25:51 -0800 Subject: [PATCH 08/19] FD add view filter to filters page when group by is set --- src/CONST/index.ts | 1 + src/SCREENS.ts | 1 + src/components/Search/types.ts | 1 + src/hooks/useAdvancedSearchFilters.ts | 5 + .../ModalStackNavigators/index.tsx | 1 + .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/SearchUIUtils.ts | 6 ++ src/pages/Search/AdvancedSearchFilters.tsx | 15 +++ .../SearchFiltersViewPage.tsx | 91 +++++++++++++++++++ 10 files changed, 123 insertions(+) create mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersViewPage.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ecbadc2118b7a..64fab97e5a15d 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7220,6 +7220,7 @@ const CONST = { SORT_ORDER: 'sort-order', POLICY_ID: 'workspace', GROUP_BY: 'group-by', + VIEW: 'view', DATE: 'date', AMOUNT: 'amount', TOTAL: 'total', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2475a3b0238c4..3886cc019f33e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -53,6 +53,7 @@ const SCREENS = { ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP', ADVANCED_FILTERS_GROUP_BY_RHP: 'Search_Advanced_Filters_GroupBy_RHP', + ADVANCED_FILTERS_VIEW_RHP: 'Search_Advanced_Filters_View_RHP', ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP', ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP', ADVANCED_FILTERS_SUBMITTED_RHP: 'Search_Advanced_Filters_Submitted_RHP', diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 0019a764dc943..572e255c253f8 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -223,6 +223,7 @@ type SearchFilterKey = | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY + | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.COLUMNS | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT; diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 228db859d845a..107a7b90c6c68 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -27,6 +27,7 @@ const typeFiltersKeys = { CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY, + CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW, CONST.SEARCH.SYNTAX_FILTER_KEYS.GROUP_CURRENCY, CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS, CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT, @@ -253,6 +254,7 @@ function useAdvancedSearchFilters() { const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled); const shouldDisplayWorkspaceFilter = workspaces.some((section) => section.data.length > 1); const shouldDisplayGroupCurrencyFilter = !!searchAdvancedFilters.groupBy; + const shouldDisplayViewFilter = !!searchAdvancedFilters.groupBy; const shouldDisplayReportFieldFilter = Object.values(policies).some((policy): policy is NonNullable => { return Object.values(policy?.fieldList ?? {}).some((val) => val.type !== CONST.POLICY.DEFAULT_FIELD_LIST_TYPE); }); @@ -287,6 +289,9 @@ function useAdvancedSearchFilters() { if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.GROUP_CURRENCY && !shouldDisplayGroupCurrencyFilter) { return; } + if (key === CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW && !shouldDisplayViewFilter) { + return; + } if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE && !shouldDisplayAttendeeFilter) { return; } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 46d2c5ea9c67a..24e4541be09de 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -932,6 +932,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchAdvancedFiltersPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTypePage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_GROUP_BY_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_VIEW_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersViewPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersStatusPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersDatePage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_SUBMITTED_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersSubmittedPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index aecf8e5770f68..fb3964849ea89 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -7,6 +7,7 @@ const SEARCH_TO_RHP: Partial['config'] = { [SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(), [SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SYNTAX_FILTER_KEYS.TYPE), [SCREENS.SEARCH.ADVANCED_FILTERS_GROUP_BY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.GROUP_BY), + [SCREENS.SEARCH.ADVANCED_FILTERS_VIEW_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.VIEW), [SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS), [SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), [SCREENS.SEARCH.ADVANCED_FILTERS_SUBMITTED_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SYNTAX_FILTER_KEYS.SUBMITTED), diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 96e438b09b7c6..64fef003428d6 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -19,6 +19,7 @@ import type { SearchGroupBy, SearchQueryJSON, SearchStatus, + SearchView, SearchWithdrawalType, SingularSearchStatus, SortOrder, @@ -3478,6 +3479,10 @@ function getGroupByOptions(translate: LocalizedTranslate) { return Object.values(CONST.SEARCH.GROUP_BY).map>((value) => ({text: translate(`search.filters.groupBy.${value}`), value})); } +function getViewOptions(translate: LocalizedTranslate) { + return Object.values(CONST.SEARCH.VIEW).map>((value) => ({text: translate(`search.view.${value}`), value})); +} + function getGroupCurrencyOptions(currencyList: OnyxTypes.CurrencyList, getCurrencySymbol: CurrencyListContextProps['getCurrencySymbol']) { return Object.keys(currencyList).reduce( (options, currencyCode) => { @@ -4034,6 +4039,7 @@ export { getStatusOptions, getTypeOptions, getGroupByOptions, + getViewOptions, getGroupCurrencyOptions, getFeedOptions, getWideAmountIndicators, diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index ba254dd159d38..8c77d9f28cb02 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -72,6 +72,11 @@ const baseFilterConfig = { description: 'search.groupBy' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.GROUP_BY), }, + view: { + getTitle: getFilterViewDisplayTitle, + description: 'search.view.label' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS.getRoute(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.VIEW), + }, status: { getTitle: getStatusFilterDisplayTitle, description: 'common.status' as const, @@ -490,6 +495,14 @@ function getFilterDisplayTitle( return Array.isArray(filterValue) ? filterValue.join(', ') : filterValue; } +function getFilterViewDisplayTitle(filters: Partial, translate: LocaleContextProps['translate']) { + const filterValue = filters[CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW]; + if (!filterValue) { + return translate('search.view.table'); + } + return translate(`search.view.${filterValue as ValueOf}`); +} + function getStatusFilterDisplayTitle(filters: Partial, type: SearchDataTypes, translate: LocaleContextProps['translate']) { const statusOptions = getStatusOptions(translate, type).concat({text: translate('common.all'), value: CONST.SEARCH.STATUS.EXPENSE.ALL}); let filterValue = filters?.status; @@ -633,6 +646,8 @@ function AdvancedSearchFilters() { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, workspacesData); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, currentType, translate); + } else if (key === CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW) { + filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, translate); } else { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, key, translate, localeCompare); } diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersViewPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersViewPage.tsx new file mode 100644 index 0000000000000..95f64f15ea2ab --- /dev/null +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersViewPage.tsx @@ -0,0 +1,91 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {SearchView} from '@components/Search/types'; +import SelectionList from '@components/SelectionList'; +import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {updateAdvancedFilters} from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; +import {getViewOptions} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +function SearchFiltersViewPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); + + // Default to 'table' if no view is set + const [selectedItem, setSelectedItem] = useState(searchAdvancedFiltersForm?.view ?? CONST.SEARCH.VIEW.TABLE); + + const listData: Array> = useMemo(() => { + return getViewOptions(translate).map((viewOption) => ({ + text: viewOption.text, + keyForList: viewOption.value, + isSelected: selectedItem === viewOption.value, + })); + }, [translate, selectedItem]); + + const updateSelectedItem = useCallback((item: ListItem) => { + setSelectedItem(item?.keyForList ?? CONST.SEARCH.VIEW.TABLE); + }, []); + + const resetChanges = useCallback(() => { + setSelectedItem(CONST.SEARCH.VIEW.TABLE); + }, []); + + const applyChanges = useCallback(() => { + updateAdvancedFilters({view: selectedItem}); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS.getRoute()); + }, [selectedItem]); + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS.getRoute()); + }} + /> + + + + +