From 612dcfdb50f42f656d038bfe9f5dc89c5086d4da Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 15 Oct 2026 16:06:35 +0530 Subject: [PATCH 01/14] Deprecate Year to date in favor of This year Removes Year to date from the date picker, switches the Insights default to this-year, and resolves and labels saved year-to-date values as This year. --- src/languages/de.ts | 2 +- src/languages/el.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.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 +- src/libs/SearchQueryUtils.ts | 6 ++---- src/libs/SearchUIUtils.ts | 1 - src/pages/Insights/insightsFilters.ts | 2 +- tests/unit/Search/SearchQueryUtilsTest.ts | 6 +++--- tests/unit/Search/SearchUIUtilsTest.ts | 4 +++- tests/unit/hooks/useInsightsFilters.test.ts | 6 +++--- tests/unit/insightsGroupByUtilsTest.ts | 12 ++++++------ 18 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 883c883f9d72..ab31bc6f08e4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -10169,7 +10169,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Nie', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Letzter Monat', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Diesen Monat', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Laufendes Jahr', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Dieses Jahr', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Letzte 12 Monate', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Diese Woche', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'Letzte Woche', diff --git a/src/languages/el.ts b/src/languages/el.ts index 30eab9df0968..d98089c2045e 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -10369,7 +10369,7 @@ ${reportName}`, [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Ποτέ', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Τον προηγούμενο μήνα', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Αυτόν τον μήνα', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Έως σήμερα μέσα στο έτος', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Φέτος', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Τελευταίοι 12 μήνες', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Αυτή την εβδομάδα', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'Την προηγούμενη εβδομάδα', diff --git a/src/languages/en.ts b/src/languages/en.ts index 131ef37b8bc5..44c8b0a03ece 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -10403,7 +10403,7 @@ const translations = { [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Never', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Last month', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'This month', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Year to date', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'This year', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Last 12 months', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'This week', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'Last week', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9bf69ccc5475..2f01c7e8e436 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -10016,7 +10016,7 @@ ${reportName}`, [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Nunca', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'El mes pasado', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Este mes', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Año hasta la fecha', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Este año', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Últimos 12 meses', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Esta semana', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'La semana pasada', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2abb9ca934b1..b167d7a337d7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -10195,7 +10195,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Jamais', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Mois dernier', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Ce mois-ci', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Depuis le début de l’année', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Cette année', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: '12 derniers mois', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Cette semaine', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'Semaine dernière', diff --git a/src/languages/it.ts b/src/languages/it.ts index e33be39bf51c..7f4394f4ad6e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -10121,7 +10121,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Mai', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Il mese scorso', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Questo mese', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Da inizio anno', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Quest’anno', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Ultimi 12 mesi', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Questa settimana', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'La settimana scorsa', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 8bc69875d6e8..3b8f742b1bce 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9978,7 +9978,7 @@ ${reportName}`, [CONST.SEARCH.DATE_PRESETS.NEVER]: '決してない', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: '先月', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: '今月', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: '年初来(累計)', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: '今年', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: '過去12か月', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: '今週', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: '先週', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 443b659e3095..12a6bea48c33 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -10087,7 +10087,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Nooit', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Vorige maand', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Deze maand', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Jaar tot nu toe', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Dit jaar', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Laatste 12 maanden', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Deze week', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'Vorige week', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 748d68be88b0..6a86dbb0cbfb 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -10104,7 +10104,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Nigdy', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'W zeszłym miesiącu', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Ten miesiąc', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Od początku roku', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Ten rok', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Ostatnie 12 miesięcy', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Ten tydzień', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'W zeszłym tygodniu', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index cfc19b0d307d..3fe3f668e22e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -10085,7 +10085,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, [CONST.SEARCH.DATE_PRESETS.NEVER]: 'Nunca', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: 'Mês passado', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: 'Este mês', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Ano até agora', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: 'Este ano', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: 'Últimos 12 meses', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: 'Esta semana', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: 'Semana passada', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8cc10df14b01..d6b1f1ce53cd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -9709,7 +9709,7 @@ ${reportName}`, [CONST.SEARCH.DATE_PRESETS.NEVER]: '从不', [CONST.SEARCH.DATE_PRESETS.LAST_MONTH]: '上个月', [CONST.SEARCH.DATE_PRESETS.THIS_MONTH]: '本月', - [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: '本年度至今', + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE]: '今年', [CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS]: '过去 12 个月', [CONST.SEARCH.DATE_PRESETS.THIS_WEEK]: '本周', [CONST.SEARCH.DATE_PRESETS.LAST_WEEK]: '上周', diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index c95fff932d46..674ee22cc594 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1822,7 +1822,9 @@ function getDateRangeForPreset(preset: SearchDatePreset): {start: string; end: s start = startOfQuarter(lastQuarter); end = endOfQuarter(lastQuarter); break; + // Year to date is deprecated and kept as an alias of this year, so saved searches and links still resolve. case CONST.SEARCH.DATE_PRESETS.THIS_YEAR: + case CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE: start = startOfYear(now); end = endOfYear(now); break; @@ -1830,10 +1832,6 @@ function getDateRangeForPreset(preset: SearchDatePreset): {start: string; end: s start = startOfYear(lastYear); end = endOfYear(lastYear); break; - case CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE: - start = startOfYear(now); - end = now; - break; case CONST.SEARCH.DATE_PRESETS.LAST_30_DAYS: start = subDays(now, 29); end = now; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 545b755107cc..8f70c4c499b9 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -5455,7 +5455,6 @@ function getDatePresets(filterKey: SearchDateFilterKeys, hasFeed: boolean): Sear CONST.SEARCH.DATE_PRESETS.THIS_MONTH, CONST.SEARCH.DATE_PRESETS.THIS_QUARTER, CONST.SEARCH.DATE_PRESETS.THIS_YEAR, - CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE, CONST.SEARCH.DATE_PRESETS.LAST_WEEK, CONST.SEARCH.DATE_PRESETS.LAST_MONTH, CONST.SEARCH.DATE_PRESETS.LAST_QUARTER, diff --git a/src/pages/Insights/insightsFilters.ts b/src/pages/Insights/insightsFilters.ts index 2131fea686bf..325f052f253e 100644 --- a/src/pages/Insights/insightsFilters.ts +++ b/src/pages/Insights/insightsFilters.ts @@ -25,7 +25,7 @@ type InsightsFilters = { }; const DEFAULT_INSIGHTS_FILTERS: Omit = { - date: {preset: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}, + date: {preset: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}, policyIDs: [], groupBy: CONST.SEARCH.GROUP_BY.MONTH, }; diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index f94025165124..9fdca8f3e51b 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -1875,7 +1875,7 @@ describe('SearchQueryUtils', () => { }); test('merges date preset with inclusive range into a single range', () => { - const queryString = 'type:expense date:year-to-date date>=2026-04-01 date<=2026-04-30'; + const queryString = 'type:expense date:last-30-days date>=2026-04-01 date<=2026-04-30'; const queryJSON = buildSearchQueryJSON(queryString); if (!queryJSON) { @@ -1905,7 +1905,7 @@ describe('SearchQueryUtils', () => { }); test('merges all date-based filters, not only "date"', () => { - const queryString = 'type:expense approved:year-to-date approved>=2026-04-01 approved<=2026-04-30'; + const queryString = 'type:expense approved:last-30-days approved>=2026-04-01 approved<=2026-04-30'; const queryJSON = buildSearchQueryJSON(queryString); if (!queryJSON) { @@ -1920,7 +1920,7 @@ describe('SearchQueryUtils', () => { }); test('merges date preset and strict inequalities into a single range', () => { - const queryString = 'type:expense date:year-to-date date>2026-04-01 date<2026-04-30'; + const queryString = 'type:expense date:last-30-days date>2026-04-01 date<2026-04-30'; const queryJSON = buildSearchQueryJSON(queryString); if (!queryJSON) { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index b6ac8e06903e..21ffd033ba41 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -4704,6 +4704,7 @@ describe('SearchUIUtils', () => { [CONST.SEARCH.DATE_PRESETS.THIS_QUARTER, {start: '2026-01-01', end: '2026-03-31'}], [CONST.SEARCH.DATE_PRESETS.LAST_QUARTER, {start: '2025-10-01', end: '2025-12-31'}], [CONST.SEARCH.DATE_PRESETS.THIS_YEAR, {start: '2026-01-01', end: '2026-12-31'}], + [CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE, {start: '2026-01-01', end: '2026-12-31'}], [CONST.SEARCH.DATE_PRESETS.LAST_YEAR, {start: '2025-01-01', end: '2025-12-31'}], [CONST.SEARCH.DATE_PRESETS.LAST_30_DAYS, {start: '2026-01-17', end: '2026-02-15'}], [CONST.SEARCH.DATE_PRESETS.LAST_90_DAYS, {start: '2025-11-18', end: '2026-02-15'}], @@ -4714,7 +4715,8 @@ describe('SearchUIUtils', () => { // When each preset is resolved const range = getDateRangeForPreset(preset); - // Then current periods run to the end of the period, previous periods cover the whole period, and rolling day ranges end today + // Then current periods run to the end of the period, previous periods cover the whole period, rolling day ranges end today, + // and the deprecated year to date matches this year expect(range).toEqual(expectedRange); } }); diff --git a/tests/unit/hooks/useInsightsFilters.test.ts b/tests/unit/hooks/useInsightsFilters.test.ts index 31b9863d7f5f..9b4c5fa02829 100644 --- a/tests/unit/hooks/useInsightsFilters.test.ts +++ b/tests/unit/hooks/useInsightsFilters.test.ts @@ -72,9 +72,9 @@ describe('useInsightsFilters', () => { const {result} = renderHook(() => useInsightsFilters(CONST.INSIGHTS.DASHBOARD.SPEND)); await waitFor(() => expect(result.current.isResolved).toBe(true)); - // Then it reports on the year to date, across every workspace, in that currency + // Then it reports on this year, across every workspace, in that currency expect(result.current.filters).toEqual({ - date: {preset: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}, + date: {preset: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}, policyIDs: [], groupBy: CONST.SEARCH.GROUP_BY.MONTH, groupCurrency: 'PLN', @@ -117,7 +117,7 @@ describe('useInsightsFilters', () => { // Then the defaults ignore the stored selections, keeping the default workspace currency for Group currency's Reset expect(result.current.defaultFilters).toEqual({ - date: {preset: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}, + date: {preset: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}, policyIDs: [], groupBy: CONST.SEARCH.GROUP_BY.MONTH, groupCurrency: 'PLN', diff --git a/tests/unit/insightsGroupByUtilsTest.ts b/tests/unit/insightsGroupByUtilsTest.ts index f539c11a09ac..74ce0517911a 100644 --- a/tests/unit/insightsGroupByUtilsTest.ts +++ b/tests/unit/insightsGroupByUtilsTest.ts @@ -4,7 +4,7 @@ import {getFittingGroupBy, getInsightsGroupByOptions, getQueryGroupBy} from '@pa import CONST from '@src/CONST'; const {DAY, WEEK, MONTH, QUARTER, YEAR} = CONST.SEARCH.GROUP_BY; -const {THIS_MONTH, LAST_MONTH, YEAR_TO_DATE, LAST_12_MONTHS} = CONST.SEARCH.DATE_PRESETS; +const {THIS_MONTH, LAST_MONTH, THIS_YEAR, YEAR_TO_DATE, LAST_12_MONTHS} = CONST.SEARCH.DATE_PRESETS; describe('insightsGroupByUtils', () => { beforeEach(() => { @@ -20,7 +20,7 @@ describe('insightsGroupByUtils', () => { it.each<[string, InsightsFilters['date'], Array]>([ ['this month', {preset: THIS_MONTH}, [DAY, WEEK]], ['last month', {preset: LAST_MONTH}, [DAY, WEEK]], - ['year to date in October', {preset: YEAR_TO_DATE}, [WEEK, MONTH, QUARTER]], + ['this year', {preset: THIS_YEAR}, [WEEK, MONTH, QUARTER]], ['last 12 months', {preset: LAST_12_MONTHS}, [WEEK, MONTH, QUARTER]], ['a range under two weeks', {from: '2026-03-01', to: '2026-03-10'}, [DAY]], ['a range of 13 days', {from: '2026-03-01', to: '2026-03-13'}, [DAY]], @@ -40,15 +40,15 @@ describe('insightsGroupByUtils', () => { expect(options).toEqual(expected); }); - it('offers Day and Week for year to date in January', () => { - // Given it's late January, so the year so far is a few weeks long + it('offers the whole year options for year to date in January', () => { + // Given it's late January, and year to date is a deprecated alias of this year jest.setSystemTime(new Date(2026, 0, 20)); // When the Group by options are listed for year to date const options = getInsightsGroupByOptions({preset: YEAR_TO_DATE}); - // Then the range is too short for monthly points and short enough for daily ones - expect(options).toEqual([DAY, WEEK]); + // Then they fit the whole year, not just the few weeks so far + expect(options).toEqual([WEEK, MONTH, QUARTER]); }); }); From 83ed09dfc47d611dc0c1753d2dcdf594f9bcd33f Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sun, 25 Oct 2026 16:52:14 +0530 Subject: [PATCH 02/14] Treat Year to date as This year in code merged from main Insights comparisons compare Year to date month for month like This year, so its separate same-days-last-year comparison is removed. The anyApproval getDatePresets test no longer expects Year to date. --- src/pages/Insights/insightsCompare.ts | 10 ++-------- tests/unit/Search/SearchUIUtilsTest.ts | 10 +++++++++- tests/unit/insightsCompareTest.ts | 10 ++++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/pages/Insights/insightsCompare.ts b/src/pages/Insights/insightsCompare.ts index bd0ccbf79cbd..13dc99c5408b 100644 --- a/src/pages/Insights/insightsCompare.ts +++ b/src/pages/Insights/insightsCompare.ts @@ -4,7 +4,7 @@ import {getDateRangeForPreset} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; -import {differenceInCalendarDays, differenceInCalendarMonths, format, parseISO, startOfYear, subDays, subMonths, subYears} from 'date-fns'; +import {differenceInCalendarDays, differenceInCalendarMonths, format, parseISO, subDays, subMonths} from 'date-fns'; import type {InsightsFilters} from './insightsFilters'; @@ -30,13 +30,7 @@ function resolveComparisonPeriods(dateFilter: InsightsFilters['date']): ChartCom const start = parseISO(primary.start); const end = parseISO(primary.end); - // The year so far compares against the same days of the year before. - if ('preset' in dateFilter && dateFilter.preset === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE) { - const sameDayLastYear = subYears(end, 1); - return {primary, comparison: formatPeriod(startOfYear(sameDayLastYear), sameDayLastYear), monthsApart: 12}; - } - - // The other presets cover whole months and compare month for month. + // Presets compare month for month, so year to date compares whole years like this year. if ('preset' in dateFilter) { const monthsApart = differenceInCalendarMonths(end, start) + 1; return {primary, comparison: formatPeriod(subMonths(start, monthsApart), subDays(start, 1)), monthsApart}; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 22ab4e92b3f2..a6902d932354 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -15829,9 +15829,17 @@ describe('getDatePresets', () => { // Then only the common date presets are returned, without never expect(presets).toEqual([ + CONST.SEARCH.DATE_PRESETS.THIS_WEEK, CONST.SEARCH.DATE_PRESETS.THIS_MONTH, + CONST.SEARCH.DATE_PRESETS.THIS_QUARTER, + CONST.SEARCH.DATE_PRESETS.THIS_YEAR, + CONST.SEARCH.DATE_PRESETS.LAST_WEEK, CONST.SEARCH.DATE_PRESETS.LAST_MONTH, - CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE, + CONST.SEARCH.DATE_PRESETS.LAST_QUARTER, + CONST.SEARCH.DATE_PRESETS.LAST_YEAR, + CONST.SEARCH.DATE_PRESETS.LAST_30_DAYS, + CONST.SEARCH.DATE_PRESETS.LAST_90_DAYS, + CONST.SEARCH.DATE_PRESETS.LAST_6_MONTHS, CONST.SEARCH.DATE_PRESETS.LAST_12_MONTHS, ]); expect(presets).not.toContain(CONST.SEARCH.DATE_PRESETS.NEVER); diff --git a/tests/unit/insightsCompareTest.ts b/tests/unit/insightsCompareTest.ts index 5122668bc1c3..222dc776f22f 100644 --- a/tests/unit/insightsCompareTest.ts +++ b/tests/unit/insightsCompareTest.ts @@ -12,12 +12,14 @@ describe('resolveComparisonPeriods', () => { jest.useRealTimers(); }); - it('compares the year so far against the same stretch of the year before', () => { - // When the page reports on the year to date + it('compares year to date against the whole year before, like this year', () => { + // When the page reports on year to date, which is kept as an alias of this year const periods = resolveComparisonPeriods({preset: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}); - // Then each period runs from January 1st to the same day of its own year, and each day pairs with its date a year before - expect(periods).toEqual({primary: {start: '2026-01-01', end: '2026-09-22'}, comparison: {start: '2025-01-01', end: '2025-09-22'}, monthsApart: 12}); + // Then both periods are whole calendar years, a year apart + const expectedPeriods = {primary: {start: '2026-01-01', end: '2026-12-31'}, comparison: {start: '2025-01-01', end: '2025-12-31'}, monthsApart: 12}; + expect(periods).toEqual(expectedPeriods); + expect(resolveComparisonPeriods({preset: CONST.SEARCH.DATE_PRESETS.THIS_YEAR})).toEqual(expectedPeriods); }); it('compares a month against the month before it', () => { From ee0f6f5296f98a646ad9faa323dc2a0fb5d4b2fc Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Wed, 28 Oct 2026 09:36:57 +0530 Subject: [PATCH 03/14] Read saved year-to-date filters as this-year --- src/libs/SearchQueryUtils.ts | 14 +++++++++++--- src/pages/Insights/insightsFilterParsing.ts | 15 ++++++++++++--- tests/unit/Search/SearchQueryUtilsTest.ts | 15 +++++++++++++++ tests/unit/insightsFilterParsingTest.ts | 8 ++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index f168721d4ec2..ef0689003e53 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1081,6 +1081,13 @@ function isSearchDatePreset(date: string | undefined): date is SearchDatePreset return Object.values(CONST.SEARCH.DATE_PRESETS).some((datePreset) => datePreset === date); } +/** + * Maps the deprecated Year to date preset to This year, which the date picker lists in its place + */ +function normalizeDatePreset(date: string): string { + return date === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE ? CONST.SEARCH.DATE_PRESETS.THIS_YEAR : date; +} + /** * Returns whether a given search filter is supported in a given search data type */ @@ -2061,8 +2068,8 @@ function buildFilterFormValuesFromQuery( filtersForm[beforeKey] = getEarlierDate(beforeFilter?.value.toString(), filtersForm[beforeKey]); filtersForm[afterKey] = getLaterDate(afterFilter?.value.toString(), filtersForm[afterKey]); - filtersForm[onKey] = onFilter?.value.toString() ?? filtersForm[onKey]; - filtersForm[negatedKey] = negatedFilter?.value.toString() ?? filtersForm[negatedKey]; + filtersForm[onKey] = onFilter ? normalizeDatePreset(onFilter.value.toString()) : filtersForm[onKey]; + filtersForm[negatedKey] = negatedFilter ? normalizeDatePreset(negatedFilter.value.toString()) : filtersForm[negatedKey]; if (rangeValue) { filtersForm[rangeKey] = rangeValue; @@ -2150,7 +2157,7 @@ function buildFilterFormValuesFromQuery( filtersForm[textKey] = textFilter?.value.toString() ?? filtersForm[textKey]; filtersForm[negatedKey] = negatedFilter?.value.toString() ?? filtersForm[negatedKey]; - filtersForm[dateOnKey] = dateOnFilter?.value.toString() ?? filtersForm[dateOnKey]; + filtersForm[dateOnKey] = dateOnFilter ? normalizeDatePreset(dateOnFilter.value.toString()) : filtersForm[dateOnKey]; filtersForm[dateBeforeKey] = getEarlierDate(dateBeforeFilter?.value.toString(), filtersForm[dateBeforeKey]); filtersForm[dateAfterKey] = getLaterDate(dateAfterFilter?.value.toString(), filtersForm[dateAfterKey]); @@ -3235,6 +3242,7 @@ export { hasFiltersChangedFromDefault, isSearchQuerySavable, isSearchDatePreset, + normalizeDatePreset, getDateRangeForPreset, getDateFilterRange, isSearchBeforeViolationsSnapshotStarted, diff --git a/src/pages/Insights/insightsFilterParsing.ts b/src/pages/Insights/insightsFilterParsing.ts index 0bdd95830e8c..361ec2aee868 100644 --- a/src/pages/Insights/insightsFilterParsing.ts +++ b/src/pages/Insights/insightsFilterParsing.ts @@ -1,7 +1,15 @@ import type {SearchQueryJSON} from '@components/Search/types'; import type {SearchDateValues} from '@libs/SearchQueryUtils'; -import {buildSearchQueryJSON, getEmptyDateValues, getFilterFromQuery, getRangeBoundariesFromFormValue, getRangeQueryValue, isSearchDatePreset} from '@libs/SearchQueryUtils'; +import { + buildSearchQueryJSON, + getEmptyDateValues, + getFilterFromQuery, + getRangeBoundariesFromFormValue, + getRangeQueryValue, + isSearchDatePreset, + normalizeDatePreset, +} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; @@ -15,8 +23,9 @@ const RUNNING_TOTAL_GROUP_BY_REGEX = new RegExp(`\\bgroup-?by:["']?${INSIGHTS_RU function parseDate(queryJSON: SearchQueryJSON): Pick | undefined { const dateFilters = queryJSON.flatFilters.filter(({key}) => key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE).flatMap(({filters}) => filters); - const on = dateFilters.find(({operator}) => operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO)?.value.toString(); - if (on) { + const onValue = dateFilters.find(({operator}) => operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO)?.value.toString(); + if (onValue) { + const on = normalizeDatePreset(onValue); return {date: isSearchDatePreset(on) ? {preset: on} : {on}}; } diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index beaa73591b57..99445177a60a 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -2084,6 +2084,21 @@ describe('SearchQueryUtils', () => { expect(result.dateRange).toBeUndefined(); }); + test('reads the deprecated year-to-date preset as this-year', () => { + // Given a saved search from before Year to date was replaced by This year + const queryJSON = buildSearchQueryJSON('type:expense date:year-to-date approved!=year-to-date'); + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + // When the form values are built from the query + const result = buildFilterFormValuesFromQuery(queryJSON, {}, {}, {}, {}, {}, {}); + + // Then both read as this-year, so the date picker checks the This year row it lists + expect(result.dateOn).toBe(CONST.SEARCH.DATE_PRESETS.THIS_YEAR); + expect(result.approvedNot).toBe(CONST.SEARCH.DATE_PRESETS.THIS_YEAR); + }); + test('merges multiple ranges into one', () => { const queryString = 'type:expense date>=2026-04-01 date<=2026-04-30 date>=2026-03-01 date<=2026-04-15'; const queryJSON = buildSearchQueryJSON(queryString); diff --git a/tests/unit/insightsFilterParsingTest.ts b/tests/unit/insightsFilterParsingTest.ts index 486dfac3510d..6fb6fa38336e 100644 --- a/tests/unit/insightsFilterParsingTest.ts +++ b/tests/unit/insightsFilterParsingTest.ts @@ -54,6 +54,14 @@ describe('insightsFilterParsing', () => { expect(day.date).toEqual({on: '2026-03-04'}); }); + it('reads the deprecated year-to-date preset as this year', () => { + // Given a dashboard saved before Year to date was replaced by This year + const parsed = parseInsightsFilters('groupBy:month groupCurrency:USD date:year-to-date'); + + // Then it reads back as This year, so the date picker checks the row it lists + expect(parsed.date).toEqual({preset: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}); + }); + it('reads both ends of a range, which the query carries as two separate filters', () => { // Given a query bounded at both ends const query = 'groupBy:month groupCurrency:USD date>=2026-01-01 date<=2026-03-31'; From fd4fc492eb848a7487072f6bf604fe2b77378f9d Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Wed, 28 Oct 2026 09:55:52 +0530 Subject: [PATCH 04/14] Add When section to year-to-date parsing test --- tests/unit/insightsFilterParsingTest.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/insightsFilterParsingTest.ts b/tests/unit/insightsFilterParsingTest.ts index 6fb6fa38336e..57fc5a85c37d 100644 --- a/tests/unit/insightsFilterParsingTest.ts +++ b/tests/unit/insightsFilterParsingTest.ts @@ -56,7 +56,10 @@ describe('insightsFilterParsing', () => { it('reads the deprecated year-to-date preset as this year', () => { // Given a dashboard saved before Year to date was replaced by This year - const parsed = parseInsightsFilters('groupBy:month groupCurrency:USD date:year-to-date'); + const query = 'groupBy:month groupCurrency:USD date:year-to-date'; + + // When its stored query is read back + const parsed = parseInsightsFilters(query); // Then it reads back as This year, so the date picker checks the row it lists expect(parsed.date).toEqual({preset: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}); From 0efcc15fa7186ec1fe1228584ec0b14292fde51b Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Fri, 30 Oct 2026 19:14:25 +0530 Subject: [PATCH 05/14] Treat year to date and this year as the same date filter when checking for edits --- src/libs/SearchQueryUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index ef0689003e53..99079ccb4479 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -965,7 +965,8 @@ function getQueryHashWithoutFilters(query: SearchQueryJSON, exclude: ReadonlySet const flatFilters = query.flatFilters .map((filter) => { const filterKey = filter.key; - const filters = cloneDeep(filter.filters); + const isDateFilter = (DATE_FILTER_KEYS as readonly string[]).includes(filterKey); + const filters = cloneDeep(filter.filters).map((queryFilter) => (isDateFilter ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter)); filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString())); return {filterString: buildFilterValuesString(filterKey, filters), filterKey}; }) From 11316188a189e7aa9399a4a99b59f352681747df Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Fri, 30 Oct 2026 19:14:31 +0530 Subject: [PATCH 06/14] Test that reapplying a saved year to date search isn't an edit --- tests/unit/Search/SearchQueryUtilsTest.ts | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 99445177a60a..256374050b4f 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -3292,6 +3292,36 @@ describe('SearchQueryUtils', () => { // Then the group currency doesn't count as a change expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON, onlyGroupCurrencyIgnored)).toBe(false); }); + + it('returns false when a saved year to date search is applied again as this year', () => { + // Given a saved search on the deprecated year to date preset, which the date picker shows as this year + const defaultQueryJSON = buildSearchQueryJSON('type:expense date:year-to-date'); + + // When the unchanged date filter is applied, which writes this year back into the query + const currentQueryJSON = buildSearchQueryJSON('type:expense date:this-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the saved search isn't treated as edited, so Reset and Save stay hidden + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(false); + }); + + it('returns true when a saved year to date search is changed to another date preset', () => { + // Given a saved search on the deprecated year to date preset + const defaultQueryJSON = buildSearchQueryJSON('type:expense date:year-to-date'); + + // When a different preset is applied + const currentQueryJSON = buildSearchQueryJSON('type:expense date:last-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the change still counts, so Reset and Save appear + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); + }); }); describe('isSearchQuerySavable', () => { From 066ef3d1230c7fa59d99dcff5e44feeef227abc5 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Fri, 30 Oct 2026 20:01:15 +0530 Subject: [PATCH 07/14] Treat year to date report field dates as this year when checking for edits --- src/libs/SearchQueryUtils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 8f815acb92ef..8d179abe7c33 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -965,7 +965,12 @@ function getQueryHashWithoutFilters(query: SearchQueryJSON, exclude: ReadonlySet .map((filter) => { const filterKey = filter.key; const isDateFilter = (DATE_FILTER_KEYS as readonly string[]).includes(filterKey); - const filters = cloneDeep(filter.filters).map((queryFilter) => (isDateFilter ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter)); + const isReportField = filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX); + const filters = cloneDeep(filter.filters).map((queryFilter) => { + // The filter form reads equal-to report field values as dates and negated ones as text + const canBeDatePreset = isDateFilter || (isReportField && queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); + return canBeDatePreset ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter; + }); filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString())); return {filterString: buildFilterValuesString(filterKey, filters), filterKey}; }) From ed51918238aa4bd1298c03862a1faea12cdb3f99 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Fri, 30 Oct 2026 20:01:16 +0530 Subject: [PATCH 08/14] Test that reapplying a saved year to date report field search isn't an edit --- tests/unit/Search/SearchQueryUtilsTest.ts | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 8b7862d9057b..f06acdef5477 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -3535,6 +3535,36 @@ describe('SearchQueryUtils', () => { // Then the change still counts, so Reset and Save appear expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); }); + + it('returns false when a saved year to date report field date search is applied again as this year', () => { + // Given a saved search on a report field date using the deprecated year to date preset, which the date picker shows as this year + const defaultQueryJSON = buildSearchQueryJSON('type:expense report-field-trip-date:year-to-date'); + + // When the unchanged report field filter is applied, which writes this year back into the query + const currentQueryJSON = buildSearchQueryJSON('type:expense report-field-trip-date:this-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the saved search isn't treated as edited, so Reset and Save stay hidden + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(false); + }); + + it('returns true when a saved negated report field search on year to date is changed to this year', () => { + // Given a saved search excluding the year to date text from a report field, which the filter form keeps as text + const defaultQueryJSON = buildSearchQueryJSON('type:expense -report-field-trip-date:year-to-date'); + + // When the excluded text is changed to this year + const currentQueryJSON = buildSearchQueryJSON('type:expense -report-field-trip-date:this-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the change still counts, so Reset and Save appear + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); + }); }); describe('isSearchQuerySavable', () => { From 1d0ac1c924361c71a066a56e98dc93aa264bd53d Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sat, 31 Oct 2026 08:21:33 +0530 Subject: [PATCH 09/14] Add a helper that maps year to date to this year in a query --- src/libs/SearchQueryUtils.ts | 31 ++++++++++++++-- tests/unit/Search/SearchQueryUtilsTest.ts | 44 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 8d179abe7c33..5aacc3d3a010 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1093,6 +1093,29 @@ function normalizeDatePreset(date: string): string { return date === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE ? CONST.SEARCH.DATE_PRESETS.THIS_YEAR : date; } +/** + * Maps the deprecated Year to date preset to This year in a query, so a search saved with it opens and saves with This year + */ +function normalizeDatePresetsInQuery(query: SearchQueryString): SearchQueryString { + const queryJSON = query ? buildSearchQueryJSON(query) : undefined; + const hasYearToDate = queryJSON?.flatFilters.some((filter) => filter.filters.some((queryFilter) => queryFilter.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)); + if (!queryJSON || !hasYearToDate) { + return query; + } + + const normalizedQueryJSON = traverseAndUpdatedQuery(queryJSON, (filterKey, filterValue, operator) => { + // The filter form reads equal-to report field values as dates and negated ones as text + const isDateFilter = + (DATE_FILTER_KEYS as readonly string[]).includes(filterKey) || + (filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX) && operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); + if (!isDateFilter) { + return filterValue; + } + return Array.isArray(filterValue) ? filterValue.map(normalizeDatePreset) : normalizeDatePreset(filterValue); + }); + return buildSearchQueryString(normalizedQueryJSON); +} + /** * Returns whether a given search filter is supported in a given search data type */ @@ -2748,7 +2771,10 @@ const sortOptionsWithEmptyValue = (a: string, b: string, localeCompare: LocaleCo /** * Given a search query, this function will standardize the query by replacing display values with their corresponding IDs. */ -function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON | Readonly, computeNodeValue: (left: SyntaxFilterKey, right: string | string[]) => string | string[]) { +function traverseAndUpdatedQuery( + queryJSON: SearchQueryJSON | Readonly, + computeNodeValue: (left: SyntaxFilterKey, right: string | string[], operator: ASTNode['operator']) => string | string[], +) { const standardQuery = cloneDeep(queryJSON) as SearchQueryJSON; const filters = standardQuery.filters; const traverse = (node: ASTNode) => { @@ -2764,7 +2790,7 @@ function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON | Readonly { }); }); + describe('normalizeDatePresetsInQuery', () => { + const getFilterValues = (query: string, filterKey: string) => + buildSearchQueryJSON(query) + ?.flatFilters.find((filter) => filter.key === filterKey) + ?.filters.map((queryFilter) => ({operator: queryFilter.operator, value: queryFilter.value})); + + it('maps year to date to this year on a date filter', () => { + // Given a search saved with the deprecated year to date preset + const query = 'type:expense date:year-to-date'; + + // When the query is normalized before it's opened or saved + const normalizedQuery = normalizeDatePresetsInQuery(query); + + // Then it uses this year, the preset the date picker lists in its place + expect(getFilterValues(normalizedQuery, CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE)).toEqual([ + {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}, + ]); + }); + + it('maps year to date on a report field date but keeps a negated report field value', () => { + // Given a search with year to date on a report field date and as excluded text on another report field + const query = 'type:expense report-field-trip-date:year-to-date -report-field-notes:year-to-date'; + + // When the query is normalized + const normalizedQuery = normalizeDatePresetsInQuery(query); + + // Then only the date value changes, because the filter form keeps negated report field values as text + expect(getFilterValues(normalizedQuery, 'reportField-trip-date')).toEqual([{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: CONST.SEARCH.DATE_PRESETS.THIS_YEAR}]); + expect(getFilterValues(normalizedQuery, 'reportField-notes')).toEqual([{operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, value: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}]); + }); + + it('returns the query unchanged when it has no year to date preset', () => { + // Given a search on a current date preset + const query = 'type:expense date:last-year'; + + // When the query is normalized + const normalizedQuery = normalizeDatePresetsInQuery(query); + + // Then the same query string comes back, so searches without the deprecated preset open exactly as saved + expect(normalizedQuery).toBe(query); + }); + }); + describe('isSearchQuerySavable', () => { it('returns true when there is no default query, even without filters or a keyword', () => { // Given a query that isn't bound to any suggested or saved search From f74a476a8c4e12ab96dec0ca90590869d97f22f2 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sat, 31 Oct 2026 08:21:35 +0530 Subject: [PATCH 10/14] Open saved year to date searches with this year --- src/components/Search/hooks/useSearchKeyParam.ts | 6 +++--- src/pages/Search/SavedSearchList.tsx | 4 ++-- src/pages/Search/SearchTypeMenuNarrow.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Search/hooks/useSearchKeyParam.ts b/src/components/Search/hooks/useSearchKeyParam.ts index c23bcedfb4a5..e7f2ac0f18ec 100644 --- a/src/components/Search/hooks/useSearchKeyParam.ts +++ b/src/components/Search/hooks/useSearchKeyParam.ts @@ -7,7 +7,7 @@ import useRootNavigationState from '@hooks/useRootNavigationState'; import Navigation, {getDeepestFocusedScreen} from '@libs/Navigation/Navigation'; import type {SearchKey} from '@libs/SearchKeyUtils'; import {getSearchKeyForDataType, isExistingSearchKey, savedSearchIDToSearchKey, searchKeyToSavedSearchID} from '@libs/SearchKeyUtils'; -import {buildSearchQueryJSON, doesQueryMatchDefaultFilterKeysAndType} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON, doesQueryMatchDefaultFilterKeysAndType, normalizeDatePresetsInQuery} from '@libs/SearchQueryUtils'; import type {SearchTypeMenuItem} from '@libs/SearchUIUtils'; import {getLastSearchQuery} from '@libs/SearchUIUtils'; @@ -72,7 +72,7 @@ function useSearchKeyParam(currentSearchQueryJSON: SearchQueryJSON | undefined, const lastSavedSearchQuery = getLastSearchQuery(searchFilters, savedSearchIDToSearchKey(id)); return ( - (savedSearchQuery ? buildSearchQueryJSON(savedSearchQuery)?.hash === queryJSON?.hash : false) || + (savedSearchQuery ? buildSearchQueryJSON(normalizeDatePresetsInQuery(savedSearchQuery))?.hash === queryJSON?.hash : false) || (lastSavedSearchQuery ? buildSearchQueryJSON(lastSavedSearchQuery)?.hash === queryJSON?.hash : false) ); }); @@ -91,7 +91,7 @@ function useSearchKeyParam(currentSearchQueryJSON: SearchQueryJSON | undefined, const savedSearchID = searchKeyToSavedSearchID(searchKey); const defaultSearchQueryString = savedSearchID ? savedSearches?.[savedSearchID]?.query : suggestedSearches[searchKey]?.searchQuery; - return defaultSearchQueryString ? buildSearchQueryJSON(defaultSearchQueryString) : undefined; + return defaultSearchQueryString ? buildSearchQueryJSON(normalizeDatePresetsInQuery(defaultSearchQueryString)) : undefined; }; // The `searchKey` param is sticky: it survives a `Navigation.setParams({q})`, so tweaking a filter keeps the diff --git a/src/pages/Search/SavedSearchList.tsx b/src/pages/Search/SavedSearchList.tsx index 2ed606ddd2aa..603f2dad0d29 100644 --- a/src/pages/Search/SavedSearchList.tsx +++ b/src/pages/Search/SavedSearchList.tsx @@ -19,7 +19,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; import type {SearchKey} from '@libs/SearchKeyUtils'; import {savedSearchIDToSearchKey} from '@libs/SearchKeyUtils'; -import {getValidLastQuery} from '@libs/SearchQueryUtils'; +import {getValidLastQuery, normalizeDatePresetsInQuery} from '@libs/SearchQueryUtils'; import type {SavedSearchMenuItem} from '@libs/SearchUIUtils'; import {createBaseSavedSearchMenuItem, getLastSearchQuery, getOverflowMenu as getOverflowMenuUtil, SAVED_SEARCH_FALLBACK_ICON_NAME, SAVED_SEARCH_ICON_NAMES} from '@libs/SearchUIUtils'; @@ -146,7 +146,7 @@ function SavedSearchList() { const savedSearchesMenuItems = savedSearches ? Object.entries(savedSearches) .map(([key, item], index) => { - const itemQuery = getValidLastQuery(getLastSearchQuery(searchFilters, savedSearchIDToSearchKey(key)), item.query); + const itemQuery = normalizeDatePresetsInQuery(getValidLastQuery(getLastSearchQuery(searchFilters, savedSearchIDToSearchKey(key)), item.query)); return buildSavedSearchMenuItem({ item, itemQuery, diff --git a/src/pages/Search/SearchTypeMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx index 9056e6ce8e04..23f5884fe26c 100644 --- a/src/pages/Search/SearchTypeMenuNarrow.tsx +++ b/src/pages/Search/SearchTypeMenuNarrow.tsx @@ -26,7 +26,7 @@ import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {savedSearchIDToSearchKey} from '@libs/SearchKeyUtils'; import type {SearchKey} from '@libs/SearchKeyUtils'; -import {getValidLastQuery} from '@libs/SearchQueryUtils'; +import {getValidLastQuery, normalizeDatePresetsInQuery} from '@libs/SearchQueryUtils'; import {getItemBadgeText, getLastSearchQuery, getOverflowMenu, SAVED_SEARCH_FALLBACK_ICON_NAME, SAVED_SEARCH_ICON_NAMES, SEARCH_TYPE_MENU_ICON_NAMES} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -214,7 +214,7 @@ function SearchTypeMenuNarrow({queryJSON, onTabPress}: SearchTypeMenuNarrowProps return; } onTabPress?.(); - const query = getValidLastQuery(getLastSearchQuery(searchFilters, tabKey), searchData.query); + const query = normalizeDatePresetsInQuery(getValidLastQuery(getLastSearchQuery(searchFilters, tabKey), searchData.query)); setSearchContext(false); navigation.dispatch({ type: CONST.NAVIGATION.ACTION_TYPE.PUSH_PARAMS, From ade439a434e706c0be5c6ed29a8e4f7837ed4e4e Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sat, 31 Oct 2026 08:21:35 +0530 Subject: [PATCH 11/14] Save year to date searches with this year --- src/libs/actions/Search.ts | 10 ++++++---- tests/unit/SearchActionsTest.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index e6e832d7ac2b..48e852793238 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -69,7 +69,7 @@ import { } from '@libs/ReportUtils'; import type {SearchKey} from '@libs/SearchKeyUtils'; import {savedSearchIDToSearchKey} from '@libs/SearchKeyUtils'; -import {buildSearchQueryJSON, buildSearchQueryString, getFooterSelectionFromQuery, serializeQueryJSONForBackend} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON, buildSearchQueryString, getFooterSelectionFromQuery, normalizeDatePresetsInQuery, serializeQueryJSONForBackend} from '@libs/SearchQueryUtils'; import {getExpenseReportIDs, TODO_SEARCH_KEYS} from '@libs/SearchUIUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {cancelSpan, endSpan, startSpan} from '@libs/telemetry/activeSpans'; @@ -900,8 +900,10 @@ function getOnyxLoadingData( } function saveSearch({id, queryJSON, newName}: {id: string; queryJSON: Readonly; newName?: string}) { - const saveSearchName = newName ?? queryJSON?.inputQuery ?? ''; - const jsonQuery = JSON.stringify(queryJSON); + const normalizedQuery = normalizeDatePresetsInQuery(queryJSON.inputQuery); + const savedQueryJSON = normalizedQuery === queryJSON.inputQuery ? queryJSON : (buildSearchQueryJSON(normalizedQuery) ?? queryJSON); + const saveSearchName = newName ?? savedQueryJSON?.inputQuery ?? ''; + const jsonQuery = JSON.stringify(savedQueryJSON); const optimisticData: Array> = [ { @@ -911,7 +913,7 @@ function saveSearch({id, queryJSON, newName}: {id: string; queryJSON: Readonly { }), ); }); + + it('saves a year to date query with this year', () => { + // Given a query on the deprecated year to date preset + const yearToDateQueryJSON = getQueryJSON('type:expense date:year-to-date'); + + // When it's saved + saveSearch({id: savedSearchID, queryJSON: yearToDateQueryJSON, newName: 'My search'}); + + // Then the saved query uses this year, so the saved search opens with the preset the date picker lists + const savedQueryJSON = getQueryJSON(normalizeDatePresetsInQuery(yearToDateQueryJSON.inputQuery)); + expect(savedQueryJSON.inputQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.THIS_YEAR}`); + expect(mockWrite).toHaveBeenCalledWith( + WRITE_COMMANDS.SAVE_SEARCH, + {jsonQuery: JSON.stringify(savedQueryJSON), savedSearchID, newName: 'My search'}, + expect.objectContaining({ + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SAVED_SEARCHES, + value: {[savedSearchID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, name: 'My search', query: savedQueryJSON.inputQuery}}, + }, + ], + }), + ); + }); }); describe('deleteSavedSearch', () => { From 01973f75cd6ee70b336746c6bce0ff47f90d21f6 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sat, 31 Oct 2026 08:21:36 +0530 Subject: [PATCH 12/14] Remove the year to date normalization from the edit hash --- src/libs/SearchQueryUtils.ts | 8 +-- tests/unit/Search/SearchQueryUtilsTest.ts | 60 ----------------------- 2 files changed, 1 insertion(+), 67 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 5aacc3d3a010..e2ecdc3833a0 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -964,13 +964,7 @@ function getQueryHashWithoutFilters(query: SearchQueryJSON, exclude: ReadonlySet const flatFilters = query.flatFilters .map((filter) => { const filterKey = filter.key; - const isDateFilter = (DATE_FILTER_KEYS as readonly string[]).includes(filterKey); - const isReportField = filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX); - const filters = cloneDeep(filter.filters).map((queryFilter) => { - // The filter form reads equal-to report field values as dates and negated ones as text - const canBeDatePreset = isDateFilter || (isReportField && queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); - return canBeDatePreset ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter; - }); + const filters = cloneDeep(filter.filters); filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString())); return {filterString: buildFilterValuesString(filterKey, filters), filterKey}; }) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 30d34191c2c0..b456b8c67904 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -3506,66 +3506,6 @@ describe('SearchQueryUtils', () => { // Then the group currency doesn't count as a change expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON, onlyGroupCurrencyIgnored)).toBe(false); }); - - it('returns false when a saved year to date search is applied again as this year', () => { - // Given a saved search on the deprecated year to date preset, which the date picker shows as this year - const defaultQueryJSON = buildSearchQueryJSON('type:expense date:year-to-date'); - - // When the unchanged date filter is applied, which writes this year back into the query - const currentQueryJSON = buildSearchQueryJSON('type:expense date:this-year'); - - if (!defaultQueryJSON || !currentQueryJSON) { - throw new Error('Failed to parse query string'); - } - - // Then the saved search isn't treated as edited, so Reset and Save stay hidden - expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(false); - }); - - it('returns true when a saved year to date search is changed to another date preset', () => { - // Given a saved search on the deprecated year to date preset - const defaultQueryJSON = buildSearchQueryJSON('type:expense date:year-to-date'); - - // When a different preset is applied - const currentQueryJSON = buildSearchQueryJSON('type:expense date:last-year'); - - if (!defaultQueryJSON || !currentQueryJSON) { - throw new Error('Failed to parse query string'); - } - - // Then the change still counts, so Reset and Save appear - expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); - }); - - it('returns false when a saved year to date report field date search is applied again as this year', () => { - // Given a saved search on a report field date using the deprecated year to date preset, which the date picker shows as this year - const defaultQueryJSON = buildSearchQueryJSON('type:expense report-field-trip-date:year-to-date'); - - // When the unchanged report field filter is applied, which writes this year back into the query - const currentQueryJSON = buildSearchQueryJSON('type:expense report-field-trip-date:this-year'); - - if (!defaultQueryJSON || !currentQueryJSON) { - throw new Error('Failed to parse query string'); - } - - // Then the saved search isn't treated as edited, so Reset and Save stay hidden - expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(false); - }); - - it('returns true when a saved negated report field search on year to date is changed to this year', () => { - // Given a saved search excluding the year to date text from a report field, which the filter form keeps as text - const defaultQueryJSON = buildSearchQueryJSON('type:expense -report-field-trip-date:year-to-date'); - - // When the excluded text is changed to this year - const currentQueryJSON = buildSearchQueryJSON('type:expense -report-field-trip-date:this-year'); - - if (!defaultQueryJSON || !currentQueryJSON) { - throw new Error('Failed to parse query string'); - } - - // Then the change still counts, so Reset and Save appear - expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); - }); }); describe('normalizeDatePresetsInQuery', () => { From a21bd58400416544841aa202fb3c2a3e823eaa6d Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sat, 31 Oct 2026 08:38:13 +0530 Subject: [PATCH 13/14] Restore the year to date normalization in the edit hash --- src/libs/SearchQueryUtils.ts | 8 ++- tests/unit/Search/SearchQueryUtilsTest.ts | 60 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index e2ecdc3833a0..5aacc3d3a010 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -964,7 +964,13 @@ function getQueryHashWithoutFilters(query: SearchQueryJSON, exclude: ReadonlySet const flatFilters = query.flatFilters .map((filter) => { const filterKey = filter.key; - const filters = cloneDeep(filter.filters); + const isDateFilter = (DATE_FILTER_KEYS as readonly string[]).includes(filterKey); + const isReportField = filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX); + const filters = cloneDeep(filter.filters).map((queryFilter) => { + // The filter form reads equal-to report field values as dates and negated ones as text + const canBeDatePreset = isDateFilter || (isReportField && queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); + return canBeDatePreset ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter; + }); filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString())); return {filterString: buildFilterValuesString(filterKey, filters), filterKey}; }) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index b456b8c67904..30d34191c2c0 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -3506,6 +3506,66 @@ describe('SearchQueryUtils', () => { // Then the group currency doesn't count as a change expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON, onlyGroupCurrencyIgnored)).toBe(false); }); + + it('returns false when a saved year to date search is applied again as this year', () => { + // Given a saved search on the deprecated year to date preset, which the date picker shows as this year + const defaultQueryJSON = buildSearchQueryJSON('type:expense date:year-to-date'); + + // When the unchanged date filter is applied, which writes this year back into the query + const currentQueryJSON = buildSearchQueryJSON('type:expense date:this-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the saved search isn't treated as edited, so Reset and Save stay hidden + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(false); + }); + + it('returns true when a saved year to date search is changed to another date preset', () => { + // Given a saved search on the deprecated year to date preset + const defaultQueryJSON = buildSearchQueryJSON('type:expense date:year-to-date'); + + // When a different preset is applied + const currentQueryJSON = buildSearchQueryJSON('type:expense date:last-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the change still counts, so Reset and Save appear + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); + }); + + it('returns false when a saved year to date report field date search is applied again as this year', () => { + // Given a saved search on a report field date using the deprecated year to date preset, which the date picker shows as this year + const defaultQueryJSON = buildSearchQueryJSON('type:expense report-field-trip-date:year-to-date'); + + // When the unchanged report field filter is applied, which writes this year back into the query + const currentQueryJSON = buildSearchQueryJSON('type:expense report-field-trip-date:this-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the saved search isn't treated as edited, so Reset and Save stay hidden + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(false); + }); + + it('returns true when a saved negated report field search on year to date is changed to this year', () => { + // Given a saved search excluding the year to date text from a report field, which the filter form keeps as text + const defaultQueryJSON = buildSearchQueryJSON('type:expense -report-field-trip-date:year-to-date'); + + // When the excluded text is changed to this year + const currentQueryJSON = buildSearchQueryJSON('type:expense -report-field-trip-date:this-year'); + + if (!defaultQueryJSON || !currentQueryJSON) { + throw new Error('Failed to parse query string'); + } + + // Then the change still counts, so Reset and Save appear + expect(hasFiltersChangedFromDefault(currentQueryJSON, defaultQueryJSON)).toBe(true); + }); }); describe('normalizeDatePresetsInQuery', () => { From f83ff47a2e4c0384763499c597e1828dcce7d9ac Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sat, 31 Oct 2026 08:38:14 +0530 Subject: [PATCH 14/14] Map year to date to this year in typed search queries --- src/libs/SearchQueryUtils.ts | 45 +++++++++++++---------- tests/unit/Search/SearchQueryUtilsTest.ts | 12 ++++++ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 5aacc3d3a010..8a8b67641005 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -964,13 +964,9 @@ function getQueryHashWithoutFilters(query: SearchQueryJSON, exclude: ReadonlySet const flatFilters = query.flatFilters .map((filter) => { const filterKey = filter.key; - const isDateFilter = (DATE_FILTER_KEYS as readonly string[]).includes(filterKey); - const isReportField = filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX); - const filters = cloneDeep(filter.filters).map((queryFilter) => { - // The filter form reads equal-to report field values as dates and negated ones as text - const canBeDatePreset = isDateFilter || (isReportField && queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); - return canBeDatePreset ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter; - }); + const filters = cloneDeep(filter.filters).map((queryFilter) => + canFilterValueBeDatePreset(filterKey, queryFilter.operator) ? {...queryFilter, value: normalizeDatePreset(queryFilter.value.toString())} : queryFilter, + ); filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString())); return {filterString: buildFilterValuesString(filterKey, filters), filterKey}; }) @@ -1093,6 +1089,25 @@ function normalizeDatePreset(date: string): string { return date === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE ? CONST.SEARCH.DATE_PRESETS.THIS_YEAR : date; } +/** + * Whether a filter value can hold a date preset. The filter form reads equal-to report field values as dates and negated ones as text. + */ +function canFilterValueBeDatePreset(filterKey: string, operator: ASTNode['operator']) { + return ( + (DATE_FILTER_KEYS as readonly string[]).includes(filterKey) || (filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX) && operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO) + ); +} + +/** + * Maps the deprecated Year to date preset to This year in a filter value that can hold a date preset + */ +function normalizeDatePresetFilterValue(filterKey: SyntaxFilterKey, filterValue: string | string[], operator: ASTNode['operator']): string | string[] { + if (!canFilterValueBeDatePreset(filterKey, operator)) { + return filterValue; + } + return Array.isArray(filterValue) ? filterValue.map(normalizeDatePreset) : normalizeDatePreset(filterValue); +} + /** * Maps the deprecated Year to date preset to This year in a query, so a search saved with it opens and saves with This year */ @@ -1103,17 +1118,7 @@ function normalizeDatePresetsInQuery(query: SearchQueryString): SearchQueryStrin return query; } - const normalizedQueryJSON = traverseAndUpdatedQuery(queryJSON, (filterKey, filterValue, operator) => { - // The filter form reads equal-to report field values as dates and negated ones as text - const isDateFilter = - (DATE_FILTER_KEYS as readonly string[]).includes(filterKey) || - (filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX) && operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); - if (!isDateFilter) { - return filterValue; - } - return Array.isArray(filterValue) ? filterValue.map(normalizeDatePreset) : normalizeDatePreset(filterValue); - }); - return buildSearchQueryString(normalizedQueryJSON); + return buildSearchQueryString(traverseAndUpdatedQuery(queryJSON, normalizeDatePresetFilterValue)); } /** @@ -2811,6 +2816,7 @@ function getKeywordQueryWithCurrentSearchContext(queryString: SearchQueryString, /** * Returns new string query, after parsing it and traversing to update some filter values. * If there are any personal emails, it will try to substitute them with accountIDs + * The deprecated Year to date preset is mapped to This year */ function getQueryWithUpdatedValues(query: string, shouldSkipAmountConversion = false, policies?: OnyxCollection) { const queryJSON = buildSearchQueryJSON(query); @@ -2820,7 +2826,8 @@ function getQueryWithUpdatedValues(query: string, shouldSkipAmountConversion = f return; } - const computeNodeValue = (left: SyntaxFilterKey, right: string | string[]) => getUpdatedFilterValue(left, right, shouldSkipAmountConversion, policies); + const computeNodeValue = (left: SyntaxFilterKey, right: string | string[], operator: ASTNode['operator']) => + normalizeDatePresetFilterValue(left, getUpdatedFilterValue(left, right, shouldSkipAmountConversion, policies), operator); const standardizedQuery = traverseAndUpdatedQuery(queryJSON, computeNodeValue); const rawFilterList = getRawFilterListFromQuery(query); const hasInFilter = rawFilterList?.some((filter) => !filter.isDefault && filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) ?? false; diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 30d34191c2c0..3e293001843a 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -718,6 +718,18 @@ describe('SearchQueryUtils', () => { expect(result).toContain('footerCurrency:eur'); }); + test('maps a typed year to date preset to this year', () => { + // Given a typed query on the deprecated year to date preset + const userQuery = 'type:expense date:year-to-date'; + + // When the typed query is rebuilt before it's submitted + const result = getQueryWithUpdatedValues(userQuery); + + // Then it uses this year, so the URL matches what the date picker shows and a saved search stores + expect(result).toContain(`date:${CONST.SEARCH.DATE_PRESETS.THIS_YEAR}`); + expect(result).not.toContain(CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE); + }); + test('rebuilds a single value containing a comma as one value', () => { expect(getQueryWithUpdatedValues(String.raw`merchant:Globex\,Ltd`)).toEqual(`${defaultQuery} merchant*:"Globex,Ltd"`); expect(getQueryWithUpdatedValues('merchant:"Globex,Ltd"')).toEqual(`${defaultQuery} merchant*:"Globex,Ltd"`);