From cf60401118d5e9f17f78fbb6a27cf83c36241405 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:13:41 -0800 Subject: [PATCH 01/14] Seed Spend over time suggested search --- src/CONST/index.ts | 2 ++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/SearchUIUtils.ts | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 55eaf64d919f1..5af548dbc4a94 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7182,6 +7182,7 @@ const CONST = { VIEW: { TABLE: 'table', BAR: 'bar', + LINE: 'line', }, SYNTAX_FILTER_KEYS: { TYPE: 'type', @@ -7398,6 +7399,7 @@ const CONST = { TOP_SPENDERS: 'topSpenders', TOP_CATEGORIES: 'topCategories', TOP_MERCHANTS: 'topMerchants', + SPEND_OVER_TIME: 'spendOverTime', }, GROUP_PREFIX: 'group_', ANIMATION: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 78fe93d1e5d07..bbc96bffedbd5 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6913,6 +6913,7 @@ const translations = { savedSearchesMenuItemTitle: 'Saved', topCategories: 'Top categories', topMerchants: 'Top merchants', + spendOverTime: 'Spend over time', groupedExpenses: 'grouped expenses', bulkActions: { approve: 'Approve', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3a3e63299da42..8d033e603be6b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6664,6 +6664,7 @@ ${amount} para ${merchant} - ${date}`, savedSearchesMenuItemTitle: 'Guardadas', topCategories: 'Categorías principales', topMerchants: 'Principales comerciantes', + spendOverTime: 'Gastos a lo largo del tiempo', searchName: 'Nombre de la búsqueda', deleteSavedSearch: 'Eliminar búsqueda guardada', deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 287e33075044b..38394eee8a79a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -752,6 +752,33 @@ function getSuggestedSearches( CONST.SEARCH.GROUP_BY.MERCHANT, CONST.SEARCH.TOP_SEARCH_LIMIT, ), + [CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: { + key: CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, + translationPath: 'search.spendOverTime', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'Receipt', + searchQuery: buildQueryStringFromFilterFormValues( + { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + groupBy: CONST.SEARCH.GROUP_BY.MONTH, + dateOn: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE, + view: CONST.SEARCH.VIEW.BAR, + }, + { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + }, + ), + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }, }; } @@ -776,6 +803,7 @@ function getSuggestedSearchesVisibility( let shouldShowTopSpendersSuggestion = false; let shouldShowTopCategoriesSuggestion = false; let shouldShowTopMerchantsSuggestion = false; + let shouldShowSpendOverTimeSuggestion = false; const hasCardFeed = Object.values(cardFeedsByPolicy ?? {}).some((feeds) => feeds.length > 0); @@ -814,6 +842,7 @@ function getSuggestedSearchesVisibility( const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); const isEligibleForTopCategoriesSuggestion = isPaidPolicy && policy.areCategoriesEnabled === true; const isEligibleForTopMerchantsSuggestion = isPaidPolicy; + const isEligibleForSpendOverTimeSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -826,6 +855,7 @@ function getSuggestedSearchesVisibility( shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion; shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion; shouldShowTopMerchantsSuggestion ||= isEligibleForTopMerchantsSuggestion; + shouldShowSpendOverTimeSuggestion ||= isEligibleForSpendOverTimeSuggestion; // We don't need to check the rest of the policies if we already determined that all suggestions should be displayed return ( @@ -858,6 +888,7 @@ function getSuggestedSearchesVisibility( [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion, + [CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: shouldShowSpendOverTimeSuggestion, }; } @@ -3626,7 +3657,12 @@ function createTypeMenuSections( menuItems: [], }; - const insightsSearchKeys = [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]; + const insightsSearchKeys = [ + CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, + CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, + CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS, + CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, + ]; for (const key of insightsSearchKeys) { if (!suggestedSearchesVisibility[key]) { From 3903c5505935d63687ae7b61b97528765a88fc1c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:37:18 -0800 Subject: [PATCH 02/14] Add translations --- src/languages/de.ts | 3 ++- src/languages/en.ts | 1 + src/languages/fr.ts | 3 ++- src/languages/it.ts | 3 ++- src/languages/ja.ts | 3 ++- src/languages/nl.ts | 3 ++- src/languages/pl.ts | 3 ++- src/languages/pt-BR.ts | 3 ++- src/languages/zh-hans.ts | 3 ++- 9 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index a5904b5ce028e..a2c9fb43c23c5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7090,7 +7090,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt', }, topSpenders: 'Top-Ausgaben', - view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar'}, + view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar', line: 'Zeile'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Von', [CONST.SEARCH.GROUP_BY.CARD]: 'Karten', @@ -7103,6 +7103,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard [CONST.SEARCH.GROUP_BY.YEAR]: 'Jahre', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Quartale', }, + spendOverTime: 'Ausgaben im Zeitverlauf', }, genericErrorPage: { title: 'Oh je, etwas ist schiefgelaufen!', diff --git a/src/languages/en.ts b/src/languages/en.ts index bbc96bffedbd5..8ed142349ac46 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7003,6 +7003,7 @@ const translations = { label: 'View', table: 'Table', bar: 'Bar', + line: 'Line', }, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'From', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 55297041b7030..06da9a5e53faa 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7102,7 +7102,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: 'Barre'}, + view: {label: 'Afficher', table: 'Tableau', bar: 'Barre', line: 'Ligne'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', [CONST.SEARCH.GROUP_BY.CARD]: 'Cartes', @@ -7115,6 +7115,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin [CONST.SEARCH.GROUP_BY.YEAR]: 'Années', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Trimestres', }, + spendOverTime: 'Dépenses au fil du temps', }, genericErrorPage: { title: 'Oh oh, quelque chose s’est mal passé !', diff --git a/src/languages/it.ts b/src/languages/it.ts index 169e7064b93c1..d932590600a89 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7079,7 +7079,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: 'Bar'}, + view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar', line: 'Riga'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Da', [CONST.SEARCH.GROUP_BY.CARD]: 'Carte', @@ -7092,6 +7092,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori [CONST.SEARCH.GROUP_BY.YEAR]: 'Anni', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Trimestri', }, + spendOverTime: 'Spesa nel tempo', }, genericErrorPage: { title: 'Uh-oh, qualcosa è andato storto!', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c4669e8b00932..5d1be06c4e3e9 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7017,7 +7017,7 @@ ${reportName} allMatchingItemsSelected: '一致する項目をすべて選択済み', }, topSpenders: 'トップ支出者', - view: {label: '表示', table: 'テーブル', bar: 'バー'}, + view: {label: '表示', table: 'テーブル', bar: 'バー', line: '行'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: '差出人', [CONST.SEARCH.GROUP_BY.CARD]: 'カード', @@ -7030,6 +7030,7 @@ ${reportName} [CONST.SEARCH.GROUP_BY.YEAR]: '年', [CONST.SEARCH.GROUP_BY.QUARTER]: '四半期', }, + spendOverTime: '時間別支出', }, genericErrorPage: { title: 'おっと、問題が発生しました!', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e6bd121bd966e..e5f91fad3c6f9 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7062,7 +7062,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd', }, topSpenders: 'Grootste uitgaven', - view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar'}, + view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar', line: 'Regel'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Van', [CONST.SEARCH.GROUP_BY.CARD]: 'Kaarten', @@ -7075,6 +7075,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten [CONST.SEARCH.GROUP_BY.YEAR]: 'Jaren', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Kwartalen', }, + spendOverTime: 'Uitgaven in de tijd', }, genericErrorPage: { title: 'O jee, er is iets misgegaan!', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8953295506bae..4c5420e9737a7 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7049,7 +7049,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: 'Zobacz', table: 'Tabela', bar: 'Pasek'}, + view: {label: 'Zobacz', table: 'Tabela', bar: 'Pasek', line: 'Linia'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Od', [CONST.SEARCH.GROUP_BY.CARD]: 'Karty', @@ -7062,6 +7062,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i [CONST.SEARCH.GROUP_BY.YEAR]: 'Lata', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Kwartały', }, + spendOverTime: 'Wydatki w czasie', }, genericErrorPage: { title: 'Ups, coś poszło nie tak!', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 74bc594d78d7e..ba717f28d7057 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7051,7 +7051,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: 'Ver', table: 'Tabela', bar: 'Bar'}, + view: {label: 'Ver', table: 'Tabela', bar: 'Bar', line: 'Linha'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', [CONST.SEARCH.GROUP_BY.CARD]: 'Cartões', @@ -7064,6 +7064,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe [CONST.SEARCH.GROUP_BY.YEAR]: 'Anos', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Trimestres', }, + spendOverTime: 'Gastos ao longo do tempo', }, genericErrorPage: { title: 'Opa, algo deu errado!', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 41c860b495bc3..8608289985c7a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6896,7 +6896,7 @@ ${reportName} allMatchingItemsSelected: '已选择所有匹配的项目', }, topSpenders: '最高支出者', - view: {label: '查看', table: '表格', bar: '栏'}, + view: {label: '查看', table: '表格', bar: '栏', line: '行'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: '来自', [CONST.SEARCH.GROUP_BY.CARD]: '卡片', @@ -6909,6 +6909,7 @@ ${reportName} [CONST.SEARCH.GROUP_BY.YEAR]: '年', [CONST.SEARCH.GROUP_BY.QUARTER]: '季度', }, + spendOverTime: '一段时间内的支出', }, genericErrorPage: { title: '哎呀,出错了!', From 1a464e174218a2ca42f901797a80fdef0013a0a5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:43:38 -0800 Subject: [PATCH 03/14] Add Spanish translation --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d033e603be6b..75694ac0bef2d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6659,12 +6659,12 @@ ${amount} para ${merchant} - ${date}`, unapprovedCard: 'Tarjeta no aprobada', reconciliation: 'Conciliación', topSpenders: 'Mayores gastadores', - view: {label: 'Ver', table: 'Tabla', bar: 'Barra'}, + view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'}, saveSearch: 'Guardar búsqueda', savedSearchesMenuItemTitle: 'Guardadas', topCategories: 'Categorías principales', topMerchants: 'Principales comerciantes', - spendOverTime: 'Gastos a lo largo del tiempo', + spendOverTime: 'Evolución de gastos', searchName: 'Nombre de la búsqueda', deleteSavedSearch: 'Eliminar búsqueda guardada', deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', From c9e846651a4fd1fa50fabf10c871e45bef5dec7d Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:57:33 -0800 Subject: [PATCH 04/14] Add tests --- tests/unit/Search/SpendOverTimeTest.ts | 180 +++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/unit/Search/SpendOverTimeTest.ts diff --git a/tests/unit/Search/SpendOverTimeTest.ts b/tests/unit/Search/SpendOverTimeTest.ts new file mode 100644 index 0000000000000..254c90448d372 --- /dev/null +++ b/tests/unit/Search/SpendOverTimeTest.ts @@ -0,0 +1,180 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import * as SearchUIUtils from '@src/libs/SearchUIUtils'; +import type * as OnyxTypes from '@src/types/onyx'; + +const adminAccountID = 18439984; +const adminEmail = 'admin@policy.com'; +const auditorEmail = 'auditor@policy.com'; +const approverEmail = 'approver@policy.com'; +const userEmail = 'user@policy.com'; +const policyID = 'A1B2C3'; + +describe('Test Spend Over Time Search', () => { + + describe('Test getSuggestedSearchesVisibility for Spend Over Time', () => { + test('Should show Spend Over Time for Admin role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Auditor role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.AUDITOR, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(auditorEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Approver role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approver: approverEmail, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time for User role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should hide Spend Over Time for free policies even with Admin role', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.PERSONAL, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time if all policies have User role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + }); + + describe('Test getSuggestedSearches for Spend Over Time', () => { + test('Should return Spend Over Time search with correct properties', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch).toBeDefined(); + expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); + expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); + expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(spendOverTimeSearch.icon).toBe('Receipt'); + }); + + test('Should return Spend Over Time search query with correct parameters', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQueryJSON = spendOverTimeSearch.searchQueryJSON; + + expect(searchQueryJSON).toBeDefined(); + expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH); + + // Check that date filter with year-to-date preset exists in flatFilters + const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + expect(dateFilter).toBeDefined(); + expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); + + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); + expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + }); + + test('Should return Spend Over Time search with valid hash', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch.hash).toBeGreaterThan(0); + expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0); + }); + + test('Should return Spend Over Time search query string with correct format', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQuery = spendOverTimeSearch.searchQuery; + + expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); + expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); + expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); + expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + }); + }); +}); From 8adc4a740da75319e8efa1aff1f0a93fe97adff8 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 15:42:06 -0800 Subject: [PATCH 05/14] Fix tests --- tests/unit/Search/SearchUIUtilsTest.ts | 169 ++++++++++++++++++++++- tests/unit/Search/SpendOverTimeTest.ts | 180 ------------------------- 2 files changed, 166 insertions(+), 183 deletions(-) delete mode 100644 tests/unit/Search/SpendOverTimeTest.ts diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 0dc7c41788c2a..76196b9305c94 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5145,6 +5145,168 @@ describe('SearchUIUtils', () => { const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); expect(response.topCategories).toBe(false); }); + + test('Should show Spend Over Time for Admin role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Auditor role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.AUDITOR, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('auditor@policy.com', {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Approver role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approver: approverEmail, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time for User role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should hide Spend Over Time for free policies even with Admin role', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.PERSONAL, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time if all policies have User role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should return Spend Over Time search with correct properties', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch).toBeDefined(); + expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); + expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); + expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(spendOverTimeSearch.icon).toBe('Receipt'); + }); + + test('Should return Spend Over Time search query with correct parameters', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQueryJSON = spendOverTimeSearch.searchQueryJSON; + + expect(searchQueryJSON).toBeDefined(); + expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH); + + // Check that date filter with year-to-date preset exists in flatFilters + const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + expect(dateFilter).toBeDefined(); + expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); + + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); + expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + }); + + test('Should return Spend Over Time search with valid hash', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch.hash).toBeGreaterThan(0); + expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0); + }); + + test('Should return Spend Over Time search query string with correct format', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQuery = spendOverTimeSearch.searchQuery; + + expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); + expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); + expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); + expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + }); }); describe('Test getColumnsToShow', () => { @@ -6242,11 +6404,12 @@ describe('SearchUIUtils', () => { }); describe('view autocomplete values', () => { - test('should include all view values (table, bar)', () => { + test('should include all view values (table, bar, line)', () => { const viewValues = Object.values(CONST.SEARCH.VIEW); expect(viewValues).toContain('table'); expect(viewValues).toContain('bar'); - expect(viewValues).toHaveLength(2); + expect(viewValues).toContain('line'); + expect(viewValues).toHaveLength(3); }); test('should correctly map view values to user-friendly values', () => { @@ -6254,7 +6417,7 @@ describe('SearchUIUtils', () => { const userFriendlyValues = viewValues.map((value) => getUserFriendlyValue(value)); // All view values should be mapped (they may be the same or different) - expect(userFriendlyValues).toHaveLength(2); + expect(userFriendlyValues).toHaveLength(3); expect(userFriendlyValues.every((value) => typeof value === 'string')).toBe(true); }); }); diff --git a/tests/unit/Search/SpendOverTimeTest.ts b/tests/unit/Search/SpendOverTimeTest.ts deleted file mode 100644 index 254c90448d372..0000000000000 --- a/tests/unit/Search/SpendOverTimeTest.ts +++ /dev/null @@ -1,180 +0,0 @@ -import type {OnyxCollection} from 'react-native-onyx'; -import CONST from '@src/CONST'; -import * as SearchUIUtils from '@src/libs/SearchUIUtils'; -import type * as OnyxTypes from '@src/types/onyx'; - -const adminAccountID = 18439984; -const adminEmail = 'admin@policy.com'; -const auditorEmail = 'auditor@policy.com'; -const approverEmail = 'approver@policy.com'; -const userEmail = 'user@policy.com'; -const policyID = 'A1B2C3'; - -describe('Test Spend Over Time Search', () => { - - describe('Test getSuggestedSearchesVisibility for Spend Over Time', () => { - test('Should show Spend Over Time for Admin role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.ADMIN, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should show Spend Over Time for Auditor role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.AUDITOR, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(auditorEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should show Spend Over Time for Approver role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - approver: approverEmail, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should hide Spend Over Time for User role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(false); - }); - - test('Should hide Spend Over Time for free policies even with Admin role', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.PERSONAL, - role: CONST.POLICY.ROLE.ADMIN, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(false); - }); - - test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => { - const policies: OnyxCollection = { - policyOne: { - id: 'policyOne', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - policyTwo: { - id: 'policyTwo', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.ADMIN, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should hide Spend Over Time if all policies have User role', () => { - const policies: OnyxCollection = { - policyOne: { - id: 'policyOne', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - policyTwo: { - id: 'policyTwo', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(false); - }); - }); - - describe('Test getSuggestedSearches for Spend Over Time', () => { - test('Should return Spend Over Time search with correct properties', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - - expect(spendOverTimeSearch).toBeDefined(); - expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); - expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); - expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(spendOverTimeSearch.icon).toBe('Receipt'); - }); - - test('Should return Spend Over Time search query with correct parameters', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - const searchQueryJSON = spendOverTimeSearch.searchQueryJSON; - - expect(searchQueryJSON).toBeDefined(); - expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH); - - // Check that date filter with year-to-date preset exists in flatFilters - const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); - expect(dateFilter).toBeDefined(); - expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); - - expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); - expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); - expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); - }); - - test('Should return Spend Over Time search with valid hash', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - - expect(spendOverTimeSearch.hash).toBeGreaterThan(0); - expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0); - }); - - test('Should return Spend Over Time search query string with correct format', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - const searchQuery = spendOverTimeSearch.searchQuery; - - expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); - expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); - expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); - expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); - expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); - expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); - }); - }); -}); From ce974830fa60f58c8e4f89625c471e3983b66280 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 3 Feb 2026 21:28:26 +0200 Subject: [PATCH 06/14] Add translations back after resolving conflicts --- src/languages/de.ts | 6 +----- src/languages/fr.ts | 8 ++------ src/languages/it.ts | 7 ++----- src/languages/ja.ts | 7 ++----- src/languages/nl.ts | 7 ++----- src/languages/pl.ts | 7 ++----- src/languages/pt-BR.ts | 7 ++----- src/languages/zh-hans.ts | 7 ++----- 8 files changed, 15 insertions(+), 41 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 0096039b04c50..69ed67f4528c4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7078,11 +7078,7 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und }, has: 'Hat', groupBy: 'Gruppieren nach', - view: { - label: 'Anzeigen', - table: 'Tabelle', - bar: 'Leiste', - }, + view: {label: 'Anzeigen', table: 'Tabelle', bar: 'Leiste', line: 'Zeile'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Von', [CONST.SEARCH.GROUP_BY.CARD]: 'Karten', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e6653f6abab67..114035896cfb6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7094,11 +7094,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip }, has: 'A A', groupBy: 'Regrouper par', - view: { - label: 'Afficher', - table: 'Table', - bar: 'Bar', - }, + view: {label: 'Afficher', table: 'Table', bar: 'Bar', line: 'Ligne'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', [CONST.SEARCH.GROUP_BY.CARD]: 'Cartes', @@ -7134,7 +7130,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip selectAllMatchingItems: 'Sélectionnez tous les éléments correspondants', allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés', }, - spendOverTime: 'Dépenses au fil du temps', + spendOverTime: 'Dépenses dans le temps', }, genericErrorPage: { title: 'Oups, quelque chose s’est mal passé !', diff --git a/src/languages/it.ts b/src/languages/it.ts index a2500c3225d24..3cec5d5f9d90a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7061,11 +7061,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo }, has: 'Ha', groupBy: 'Raggruppa per', - view: { - label: 'Visualizza', - table: 'Tabella', - bar: 'Bar', - }, + view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar', line: 'Riga'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Da', [CONST.SEARCH.GROUP_BY.CARD]: 'Carte', @@ -7101,6 +7097,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo selectAllMatchingItems: 'Seleziona tutti gli elementi corrispondenti', allMatchingItemsSelected: 'Tutti gli elementi corrispondenti sono stati selezionati', }, + spendOverTime: 'Spesa nel tempo', }, genericErrorPage: { title: 'Oops, qualcosa è andato storto!', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 5aae4900c1be3..3c1c338baeae7 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7002,11 +7002,7 @@ ${reportName} }, has: '持っている', groupBy: 'グループ化基準', - view: { - label: '表示', - table: 'テーブル', - bar: 'バー', - }, + view: {label: '表示', table: 'テーブル', bar: 'バー', line: '行'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: '差出人', [CONST.SEARCH.GROUP_BY.CARD]: 'カード', @@ -7042,6 +7038,7 @@ ${reportName} selectAllMatchingItems: '一致する項目をすべて選択', allMatchingItemsSelected: '一致するすべての項目を選択済み', }, + spendOverTime: '時間経過による支出', }, genericErrorPage: { title: 'おっと、問題が発生しました!', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 428e4147c106c..422068e25548f 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7049,11 +7049,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar }, has: 'Heeft', groupBy: 'Groeperen op', - view: { - label: 'Bekijken', - table: 'Tabel', - bar: 'Balk', - }, + view: {label: 'Bekijken', table: 'Tabel', bar: 'Balk', line: 'Regel'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Van', [CONST.SEARCH.GROUP_BY.CARD]: 'Kaarten', @@ -7089,6 +7085,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar selectAllMatchingItems: 'Selecteer alle overeenkomende items', allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd', }, + spendOverTime: 'Uitgaven in de tijd', }, genericErrorPage: { title: 'Oeps, er is iets misgegaan!', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c32c6bfb44411..1741d67911581 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7032,11 +7032,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i }, has: 'Ma', groupBy: 'Grupuj według', - view: { - label: 'Pokaż', - table: 'Tabela', - bar: 'Bar', - }, + view: {label: 'Pokaż', table: 'Tabela', bar: 'Bar', line: 'Linia'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Od', [CONST.SEARCH.GROUP_BY.CARD]: 'Karty', @@ -7072,6 +7068,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i selectAllMatchingItems: 'Zaznacz wszystkie pasujące elementy', allMatchingItemsSelected: 'Zaznaczono wszystkie pasujące elementy', }, + spendOverTime: 'Wydatki w czasie', }, genericErrorPage: { title: 'Ups, coś poszło nie tak!', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 8247dec9e9de2..00c361a59eed5 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7031,11 +7031,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e }, has: 'Tem', groupBy: 'Agrupar por', - view: { - label: 'Visualizar', - table: 'Tabela', - bar: 'Bar', - }, + view: {label: 'Visualizar', table: 'Tabela', bar: 'Bar', line: 'Linha'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', [CONST.SEARCH.GROUP_BY.CARD]: 'Cartões', @@ -7071,6 +7067,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e selectAllMatchingItems: 'Selecione todos os itens correspondentes', allMatchingItemsSelected: 'Todos os itens correspondentes selecionados', }, + spendOverTime: 'Gastos ao longo do tempo', }, genericErrorPage: { title: 'Opa, algo deu errado!', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c01f95fdb8afd..5d45115aee0ec 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6898,11 +6898,7 @@ ${reportName} }, has: '有', groupBy: '分组依据', - view: { - label: '查看', - table: '表', - bar: '栏', - }, + view: {label: '查看', table: '表', bar: '栏', line: '行'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: '来自', [CONST.SEARCH.GROUP_BY.CARD]: '卡片', @@ -6938,6 +6934,7 @@ ${reportName} selectAllMatchingItems: '选择所有匹配的项目', allMatchingItemsSelected: '已选中所有匹配项', }, + spendOverTime: '随时间支出', }, genericErrorPage: { title: '哎呀,出错了!', From 774a5fe0c5d31f50736ecef33341b7056c9471dd Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Tue, 3 Feb 2026 11:49:50 -0800 Subject: [PATCH 07/14] Revert "fix: device back button doesn't close FAB menu" --- src/components/PopoverMenu.tsx | 4 ---- src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx | 1 - 2 files changed, 5 deletions(-) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 58b2844ba2d25..701e604c9dfdf 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -175,8 +175,6 @@ type PopoverMenuProps = Partial & { /** Used to locate the component in the tests */ testID?: string; - /** Whether to handle navigation back */ - shouldHandleNavigationBack?: boolean; /** Badge style to be shown near the right end. */ badgeStyle?: StyleProp; }; @@ -297,7 +295,6 @@ function BasePopoverMenu({ shouldAvoidSafariException = false, shouldMaintainFocusAfterSubItemSelect: shouldPreserveFocusOnSubItems = true, testID, - shouldHandleNavigationBack = false, }: PopoverMenuProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -599,7 +596,6 @@ function BasePopoverMenu({ innerContainerStyle={{...styles.pv0, ...innerContainerStyle}} shouldUseModalPaddingStyle={shouldUseModalPaddingStyle} testID={testID} - shouldHandleNavigationBack={shouldHandleNavigationBack} > Date: Tue, 3 Feb 2026 23:20:31 +0200 Subject: [PATCH 08/14] Reapply "fix: device back button doesn't close FAB menu" This reverts commit 774a5fe0c5d31f50736ecef33341b7056c9471dd. --- src/components/PopoverMenu.tsx | 4 ++++ src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx | 1 + 2 files changed, 5 insertions(+) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 701e604c9dfdf..58b2844ba2d25 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -175,6 +175,8 @@ type PopoverMenuProps = Partial & { /** Used to locate the component in the tests */ testID?: string; + /** Whether to handle navigation back */ + shouldHandleNavigationBack?: boolean; /** Badge style to be shown near the right end. */ badgeStyle?: StyleProp; }; @@ -295,6 +297,7 @@ function BasePopoverMenu({ shouldAvoidSafariException = false, shouldMaintainFocusAfterSubItemSelect: shouldPreserveFocusOnSubItems = true, testID, + shouldHandleNavigationBack = false, }: PopoverMenuProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -596,6 +599,7 @@ function BasePopoverMenu({ innerContainerStyle={{...styles.pv0, ...innerContainerStyle}} shouldUseModalPaddingStyle={shouldUseModalPaddingStyle} testID={testID} + shouldHandleNavigationBack={shouldHandleNavigationBack} > Date: Tue, 3 Feb 2026 23:45:00 +0200 Subject: [PATCH 09/14] use Calendar icon for Spend over time --- src/hooks/useSearchTypeMenu.tsx | 1 + src/libs/SearchUIUtils.ts | 9 +++++++-- src/pages/Search/SearchTypeMenu.tsx | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index 9c3d582367e59..f30141f5c7533 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -51,6 +51,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { const expensifyIcons = useMemoizedLazyExpensifyIcons([ 'Basket', 'Bookmark', + 'CalendarSolid', 'Checkmark', 'Pencil', 'Receipt', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index cdd27d7a17d71..820318e2c09ef 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -411,7 +411,12 @@ type SearchTypeMenuItem = { key: SearchKey; translationPath: TranslationPaths; type: SearchDataTypes; - icon?: IconAsset | Extract; + icon?: + | IconAsset + | Extract< + ExpensifyIconName, + 'Receipt' | 'ChatBubbles' | 'MoneyBag' | 'CreditCard' | 'MoneyHourglass' | 'CreditCardHourglass' | 'Bank' | 'User' | 'Folder' | 'Basket' | 'CalendarSolid' + >; searchQuery: string; searchQueryJSON: SearchQueryJSON | undefined; hash: number; @@ -755,7 +760,7 @@ function getSuggestedSearches( key: CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, translationPath: 'search.spendOverTime', type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'Receipt', + icon: 'CalendarSolid', searchQuery: buildQueryStringFromFilterFormValues( { type: CONST.SEARCH.DATA_TYPES.EXPENSE, diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 102a347013216..2eda0487176ac 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -61,6 +61,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const expensifyIcons = useMemoizedLazyExpensifyIcons([ 'Basket', 'Bookmark', + 'CalendarSolid', 'Pencil', 'Receipt', 'ChatBubbles', From 35adc8b5a937337798748c1ed4e7d18aa28a4142 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 00:12:24 +0200 Subject: [PATCH 10/14] Show Sppend over time first --- src/libs/SearchUIUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 820318e2c09ef..6e8b82a99c9ec 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3658,10 +3658,10 @@ function createTypeMenuSections( }; const insightsSearchKeys = [ + CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS, - CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, ]; for (const key of insightsSearchKeys) { From 0a9ff8da569dd3a58fd4bffd6b312d727383d7ea Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 10 Feb 2026 15:19:59 +0200 Subject: [PATCH 11/14] Default to view:line --- src/libs/SearchUIUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index df18dcea886a3..9ef8c820bac93 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -821,7 +821,7 @@ function getSuggestedSearches( type: CONST.SEARCH.DATA_TYPES.EXPENSE, groupBy: CONST.SEARCH.GROUP_BY.MONTH, dateOn: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE, - view: CONST.SEARCH.VIEW.BAR, + view: CONST.SEARCH.VIEW.LINE, }, { sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, From dba258553d07c6ad6888b9655d2f7b8a8994edc5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 10 Feb 2026 15:44:57 +0200 Subject: [PATCH 12/14] Update tests/unit/Search/SearchUIUtilsTest.ts Co-authored-by: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> --- tests/unit/Search/SearchUIUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index fd2985bbe0b90..db41c7d466e24 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5357,7 +5357,7 @@ describe('SearchUIUtils', () => { expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(spendOverTimeSearch.icon).toBe('Receipt'); + expect(spendOverTimeSearch.icon).toBe('CalendarSolid'); }); test('Should return Spend Over Time search query with correct parameters', () => { From 38a5191965a01e8883227e83b79da049cc4622ed Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 10 Feb 2026 15:47:46 +0200 Subject: [PATCH 13/14] Make tests pass --- tests/unit/Search/SearchUIUtilsTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index db41c7d466e24..ca7a904573d2c 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5374,7 +5374,7 @@ describe('SearchUIUtils', () => { expect(dateFilter).toBeDefined(); expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); - expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.LINE); expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); }); @@ -5395,7 +5395,7 @@ describe('SearchUIUtils', () => { expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); - expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.LINE}`); expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); }); From 00207875ca3b8aca5e3c9bd3da3fe19af0ab7c2c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 10 Feb 2026 16:45:16 +0200 Subject: [PATCH 14/14] Default to descending order for spend over time suggested search --- src/libs/SearchUIUtils.ts | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 9ef8c820bac93..409a858a356f4 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -825,7 +825,7 @@ function getSuggestedSearches( }, { sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, - sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + sortOrder: CONST.SEARCH.SORT_ORDER.ASC, }, ), get searchQueryJSON() { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index ca7a904573d2c..6ec4d41895d6f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5376,7 +5376,7 @@ describe('SearchUIUtils', () => { expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.LINE); expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); - expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.ASC); }); test('Should return Spend Over Time search with valid hash', () => { @@ -5397,7 +5397,7 @@ describe('SearchUIUtils', () => { expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.LINE}`); expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); - expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.ASC}`); }); });