From f1ff96b09104c932a05e6349e45f4c22707a5af8 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Wed, 4 Feb 2026 13:59:48 -0800 Subject: [PATCH 1/6] Fix react-hooks/exhaustive-deps in CustomReport --- .../src/components/reports/ReportSidebar.tsx | 39 ++++--- .../src/components/reports/ReportTopbar.tsx | 2 +- .../src/components/reports/SaveReport.tsx | 2 +- .../src/components/reports/SaveReportMenu.tsx | 2 +- .../reports/reports/CustomReport.tsx | 104 ++++++++++++------ packages/loot-core/src/types/models/rule.ts | 2 +- 6 files changed, 97 insertions(+), 54 deletions(-) diff --git a/packages/desktop-client/src/components/reports/ReportSidebar.tsx b/packages/desktop-client/src/components/reports/ReportSidebar.tsx index 25e610575d5..aee5f528eed 100644 --- a/packages/desktop-client/src/components/reports/ReportSidebar.tsx +++ b/packages/desktop-client/src/components/reports/ReportSidebar.tsx @@ -19,6 +19,7 @@ import { type CustomReportEntity, type sortByOpType, type TimeFrame, + type TransactionEntity, } from 'loot-core/types/models'; import { type SyncedPrefs } from 'loot-core/types/prefs'; @@ -39,20 +40,26 @@ type ReportSidebarProps = { categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] }; dateRangeLine: number; allIntervals: { name: string; pretty: string }[]; - setDateRange: (value: string) => void; - setGraphType: (value: string) => void; - setGroupBy: (value: string) => void; - setInterval: (value: string) => void; - setBalanceType: (value: string) => void; - setSortBy: (value: string) => void; - setMode: (value: string) => void; - setIsDateStatic: (value: boolean) => void; - setShowEmpty: (value: boolean) => void; - setShowOffBudget: (value: boolean) => void; - setShowHiddenCategories: (value: boolean) => void; - setShowUncategorized: (value: boolean) => void; - setTrimIntervals: (value: boolean) => void; - setIncludeCurrentInterval: (value: boolean) => void; + setDateRange: (value: CustomReportEntity['dateRange']) => void; + setGraphType: (value: CustomReportEntity['graphType']) => void; + setGroupBy: (value: CustomReportEntity['groupBy']) => void; + setInterval: (value: CustomReportEntity['interval']) => void; + setBalanceType: (value: CustomReportEntity['balanceType']) => void; + setSortBy: (value: CustomReportEntity['sortBy']) => void; + setMode: (value: CustomReportEntity['mode']) => void; + setIsDateStatic: (value: CustomReportEntity['isDateStatic']) => void; + setShowEmpty: (value: CustomReportEntity['showEmpty']) => void; + setShowOffBudget: (value: CustomReportEntity['showOffBudget']) => void; + setShowHiddenCategories: ( + value: CustomReportEntity['showHiddenCategories'], + ) => void; + setShowUncategorized: ( + value: CustomReportEntity['showUncategorized'], + ) => void; + setTrimIntervals: (value: CustomReportEntity['trimIntervals']) => void; + setIncludeCurrentInterval: ( + value: CustomReportEntity['includeCurrentInterval'], + ) => void; setSelectedCategories: (value: CategoryEntity[]) => void; onChangeDates: ( dateStart: string, @@ -63,8 +70,8 @@ type ReportSidebarProps = { disabledItems: (type: string) => string[]; defaultItems: (item: string) => void; defaultModeItems: (graph: string, item: string) => void; - earliestTransaction: string; - latestTransaction: string; + earliestTransaction: TransactionEntity['date']; + latestTransaction: TransactionEntity['date']; firstDayOfWeekIdx: SyncedPrefs['firstDayOfWeekIdx']; isComplexCategoryCondition?: boolean; }; diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.tsx b/packages/desktop-client/src/components/reports/ReportTopbar.tsx index deaf296e090..ac5064edb53 100644 --- a/packages/desktop-client/src/components/reports/ReportTopbar.tsx +++ b/packages/desktop-client/src/components/reports/ReportTopbar.tsx @@ -33,7 +33,7 @@ import { FilterButton } from '@desktop-client/components/filters/FiltersMenu'; type ReportTopbarProps = { customReportItems: CustomReportEntity; report: CustomReportEntity; - savedStatus: string; + savedStatus: 'saved' | 'new' | 'modified'; setGraphType: (value: string) => void; viewLegend: boolean; viewSummary: boolean; diff --git a/packages/desktop-client/src/components/reports/SaveReport.tsx b/packages/desktop-client/src/components/reports/SaveReport.tsx index 9da720a4e4a..44d11cf4166 100644 --- a/packages/desktop-client/src/components/reports/SaveReport.tsx +++ b/packages/desktop-client/src/components/reports/SaveReport.tsx @@ -28,7 +28,7 @@ import { useReports } from '@desktop-client/hooks/useReports'; type SaveReportProps = { customReportItems: T; report: CustomReportEntity; - savedStatus: string; + savedStatus: 'saved' | 'new' | 'modified'; onReportChange: ( params: | { diff --git a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx index 38d607579e8..37b68a8867a 100644 --- a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx +++ b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx @@ -9,7 +9,7 @@ export function SaveReportMenu({ listReports, }: { onMenuSelect: (item: string) => void; - savedStatus: string; + savedStatus: 'saved' | 'new' | 'modified'; listReports: number; }) { const { t } = useTranslation(); diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx index 553b9fe2764..f54ee72851c 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useEffectEvent, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useLocation, useParams } from 'react-router'; @@ -20,6 +20,7 @@ import { type DataEntity, type RuleConditionEntity, type sortByOpType, + type TransactionEntity, } from 'loot-core/types/models'; import { type TransObjectLiteral } from 'loot-core/types/util'; @@ -168,11 +169,11 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { if (['/reports'].includes(prevUrl)) sessionStorage.clear(); const reportFromSessionStorage = sessionStorage.getItem('report'); - const session = reportFromSessionStorage + const session: Partial = reportFromSessionStorage ? JSON.parse(reportFromSessionStorage) : {}; const combine = initialReport ?? defaultReport; - const loadReport = { ...combine, ...session }; + const loadReport: CustomReportEntity = { ...combine, ...session }; const [allIntervals, setAllIntervals] = useState< Array<{ @@ -274,39 +275,42 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { const [intervals, setIntervals] = useState( monthUtils.rangeInclusive(startDate, endDate), ); - const [earliestTransaction, setEarliestTransaction] = useState(''); - const [latestTransaction, setLatestTransaction] = useState(''); + const [earliestTransactionDate, setEarliestTransactionDate] = + useState(''); + const [latestTransactionDate, setLatestTransactionDate] = + useState(''); const [report, setReport] = useState(loadReport); - const [savedStatus, setSavedStatus] = useState( - session.savedStatus ?? (initialReport ? 'saved' : 'new'), + const [savedStatus, setSavedStatus] = useState<'saved' | 'new' | 'modified'>( + 'savedStatus' in session + ? (session.savedStatus as typeof savedStatus) + : initialReport + ? 'saved' + : 'new', ); - useEffect(() => { - async function run() { + const onApplyFilterConditions = useEffectEvent( + ( + currentConditions: RuleConditionEntity[], + currentConditionsOp: RuleConditionEntity['conditionsOp'], + ) => { onApplyFilter(null); const filtersToApply = - savedStatus !== 'saved' ? conditions : report.conditions; + savedStatus !== 'saved' ? conditions : currentConditions; const conditionsOpToApply = - savedStatus !== 'saved' ? conditionsOp : report.conditionsOp; + savedStatus !== 'saved' ? conditionsOp : currentConditionsOp; - filtersToApply?.forEach((condition: RuleConditionEntity) => - onApplyFilter(condition), - ); + filtersToApply?.forEach(onApplyFilter); onConditionsOpChange(conditionsOpToApply); + }, + ); - const earliestTransaction = await send('get-earliest-transaction'); - setEarliestTransaction( - earliestTransaction - ? earliestTransaction.date - : monthUtils.currentDay(), - ); - - const latestTransaction = await send('get-latest-transaction'); - setLatestTransaction( - latestTransaction ? latestTransaction.date : monthUtils.currentDay(), - ); - + const onSetAllIntervals = useEffectEvent( + async ( + earliestTransaction: TransactionEntity, + latestTransaction: TransactionEntity, + interval: CustomReportEntity['interval'], + ) => { const fromDate = interval === 'Weekly' ? 'dayFromDate' @@ -380,7 +384,17 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { .reverse(); setAllIntervals(allIntervalsMap); + }, + ); + const onSetStartAndEndDates = useEffectEvent( + ( + earliestTransaction: TransactionEntity, + latestTransaction: TransactionEntity, + dateRange: CustomReportEntity['dateRange'], + isDateStatic: CustomReportEntity['isDateStatic'], + includeCurrentInterval: CustomReportEntity['includeCurrentInterval'], + ) => { if (!isDateStatic) { const [dateStart, dateEnd] = getLiveRange( dateRange, @@ -394,22 +408,44 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { setStartDate(dateStart); setEndDate(dateEnd); } + }, + ); + + useEffect(() => { + async function run() { + onApplyFilterConditions(report.conditions, report.conditionsOp); + + const earliestTransaction = await send('get-earliest-transaction'); + setEarliestTransactionDate( + earliestTransaction + ? earliestTransaction.date + : monthUtils.currentDay(), + ); + + const latestTransaction = await send('get-latest-transaction'); + setLatestTransactionDate( + latestTransaction ? latestTransaction.date : monthUtils.currentDay(), + ); + + onSetAllIntervals(earliestTransaction, latestTransaction, interval); + onSetStartAndEndDates( + earliestTransaction, + latestTransaction, + dateRange, + isDateStatic, + includeCurrentInterval, + ); } run(); - // omitted `conditions` and `conditionsOp` from dependencies to avoid infinite loops - // oxlint-disable-next-line react/exhaustive-deps }, [ interval, dateRange, firstDayOfWeekIdx, isDateStatic, - onApplyFilter, - onConditionsOpChange, report.conditions, report.conditionsOp, includeCurrentInterval, - locale, savedStatus, ]); @@ -619,7 +655,7 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { const defaultSort = defaultsGraphList(mode, chooseGraph, 'defaultSort'); if (defaultSort) { setSessionReport('sortBy', defaultSort); - setSortBy(defaultSort); + setSortBy(defaultSort as CustomReportEntity['sortBy']); } }; @@ -834,8 +870,8 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { disabledItems={disabledItems} defaultItems={defaultItems} defaultModeItems={defaultModeItems} - earliestTransaction={earliestTransaction} - latestTransaction={latestTransaction} + earliestTransaction={earliestTransactionDate} + latestTransaction={latestTransactionDate} firstDayOfWeekIdx={firstDayOfWeekIdx} isComplexCategoryCondition={isComplexCategoryCondition} /> diff --git a/packages/loot-core/src/types/models/rule.ts b/packages/loot-core/src/types/models/rule.ts index 3819b69384f..b63fcd3108b 100644 --- a/packages/loot-core/src/types/models/rule.ts +++ b/packages/loot-core/src/types/models/rule.ts @@ -47,7 +47,7 @@ type BaseConditionEntity< month?: boolean; year?: boolean; }; - conditionsOp?: string; + conditionsOp?: 'and' | 'or'; type?: 'id' | 'boolean' | 'date' | 'number' | 'string'; customName?: string; queryFilter?: Record; From b8a3ea99457f442aace43aec16fb6cf9b8d73187 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 22:22:24 +0000 Subject: [PATCH 2/6] Add release notes for PR #6867 --- upcoming-release-notes/6867.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/6867.md diff --git a/upcoming-release-notes/6867.md b/upcoming-release-notes/6867.md new file mode 100644 index 00000000000..6fe274794ff --- /dev/null +++ b/upcoming-release-notes/6867.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Fix type safety issues and refactor CustomReport component to improve dependency handling. From ee093ea5d059aeb8dce72230b4ab2163fac41b1f Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Wed, 4 Feb 2026 15:48:29 -0800 Subject: [PATCH 3/6] Fix typecheck errors --- .../src/components/reports/ReportTopbar.tsx | 3 ++- .../src/components/reports/SaveReport.tsx | 4 ++-- .../src/components/reports/SaveReportMenu.tsx | 4 +++- .../components/reports/reports/CustomReport.tsx | 14 +++++++++----- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.tsx b/packages/desktop-client/src/components/reports/ReportTopbar.tsx index ac5064edb53..e460b29cce5 100644 --- a/packages/desktop-client/src/components/reports/ReportTopbar.tsx +++ b/packages/desktop-client/src/components/reports/ReportTopbar.tsx @@ -25,6 +25,7 @@ import { import { GraphButton } from './GraphButton'; import { SaveReportWrapper } from './SaveReport'; +import { type SavedStatus } from './SaveReportMenu'; import { setSessionReport } from './setSessionReport'; import { SnapshotButton } from './SnapshotButton'; @@ -33,7 +34,7 @@ import { FilterButton } from '@desktop-client/components/filters/FiltersMenu'; type ReportTopbarProps = { customReportItems: CustomReportEntity; report: CustomReportEntity; - savedStatus: 'saved' | 'new' | 'modified'; + savedStatus: SavedStatus; setGraphType: (value: string) => void; viewLegend: boolean; viewSummary: boolean; diff --git a/packages/desktop-client/src/components/reports/SaveReport.tsx b/packages/desktop-client/src/components/reports/SaveReport.tsx index 44d11cf4166..ae0246f52f5 100644 --- a/packages/desktop-client/src/components/reports/SaveReport.tsx +++ b/packages/desktop-client/src/components/reports/SaveReport.tsx @@ -18,7 +18,7 @@ import { import { LoadingIndicator } from './LoadingIndicator'; import { SaveReportChoose } from './SaveReportChoose'; import { SaveReportDelete } from './SaveReportDelete'; -import { SaveReportMenu } from './SaveReportMenu'; +import { type SavedStatus, SaveReportMenu } from './SaveReportMenu'; import { SaveReportName } from './SaveReportName'; import { FormField, FormLabel } from '@desktop-client/components/forms'; @@ -28,7 +28,7 @@ import { useReports } from '@desktop-client/hooks/useReports'; type SaveReportProps = { customReportItems: T; report: CustomReportEntity; - savedStatus: 'saved' | 'new' | 'modified'; + savedStatus: SavedStatus; onReportChange: ( params: | { diff --git a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx index 37b68a8867a..07605d31a3a 100644 --- a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx +++ b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx @@ -3,13 +3,15 @@ import { useTranslation } from 'react-i18next'; import { Menu, type MenuItem } from '@actual-app/components/menu'; +export type SavedStatus = 'saved' | 'new' | 'modified'; + export function SaveReportMenu({ onMenuSelect, savedStatus, listReports, }: { onMenuSelect: (item: string) => void; - savedStatus: 'saved' | 'new' | 'modified'; + savedStatus: SavedStatus; listReports: number; }) { const { t } = useTranslation(); diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx index f54ee72851c..511c0cf1616 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx @@ -24,6 +24,8 @@ import { } from 'loot-core/types/models'; import { type TransObjectLiteral } from 'loot-core/types/util'; +import { type SavedStatus } from '@desktop-client/components/reports/SaveReportMenu'; + import { Warning } from '@desktop-client/components/alerts'; import { AppliedFilters } from '@desktop-client/components/filters/AppliedFilters'; import { FinancialText } from '@desktop-client/components/FinancialText'; @@ -280,9 +282,9 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { const [latestTransactionDate, setLatestTransactionDate] = useState(''); const [report, setReport] = useState(loadReport); - const [savedStatus, setSavedStatus] = useState<'saved' | 'new' | 'modified'>( + const [savedStatus, setSavedStatus] = useState( 'savedStatus' in session - ? (session.savedStatus as typeof savedStatus) + ? (session.savedStatus as SavedStatus) : initialReport ? 'saved' : 'new', @@ -290,8 +292,8 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { const onApplyFilterConditions = useEffectEvent( ( - currentConditions: RuleConditionEntity[], - currentConditionsOp: RuleConditionEntity['conditionsOp'], + currentConditions?: RuleConditionEntity[], + currentConditionsOp?: RuleConditionEntity['conditionsOp'], ) => { onApplyFilter(null); @@ -301,7 +303,9 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) { savedStatus !== 'saved' ? conditionsOp : currentConditionsOp; filtersToApply?.forEach(onApplyFilter); - onConditionsOpChange(conditionsOpToApply); + if (conditionsOpToApply) { + onConditionsOpChange(conditionsOpToApply); + } }, ); From 874456c9e510ca6d5452954ec9551dff4370ec03 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:49:18 +0000 Subject: [PATCH 4/6] [autofix.ci] apply automated fixes --- packages/desktop-client/src/components/reports/SaveReport.tsx | 2 +- .../src/components/reports/reports/CustomReport.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/desktop-client/src/components/reports/SaveReport.tsx b/packages/desktop-client/src/components/reports/SaveReport.tsx index ae0246f52f5..e6c0e185306 100644 --- a/packages/desktop-client/src/components/reports/SaveReport.tsx +++ b/packages/desktop-client/src/components/reports/SaveReport.tsx @@ -18,7 +18,7 @@ import { import { LoadingIndicator } from './LoadingIndicator'; import { SaveReportChoose } from './SaveReportChoose'; import { SaveReportDelete } from './SaveReportDelete'; -import { type SavedStatus, SaveReportMenu } from './SaveReportMenu'; +import { SaveReportMenu, type SavedStatus } from './SaveReportMenu'; import { SaveReportName } from './SaveReportName'; import { FormField, FormLabel } from '@desktop-client/components/forms'; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx index 511c0cf1616..aabc1ffc62d 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx @@ -24,8 +24,6 @@ import { } from 'loot-core/types/models'; import { type TransObjectLiteral } from 'loot-core/types/util'; -import { type SavedStatus } from '@desktop-client/components/reports/SaveReportMenu'; - import { Warning } from '@desktop-client/components/alerts'; import { AppliedFilters } from '@desktop-client/components/filters/AppliedFilters'; import { FinancialText } from '@desktop-client/components/FinancialText'; @@ -55,6 +53,7 @@ import { import { ReportSidebar } from '@desktop-client/components/reports/ReportSidebar'; import { ReportSummary } from '@desktop-client/components/reports/ReportSummary'; import { ReportTopbar } from '@desktop-client/components/reports/ReportTopbar'; +import { type SavedStatus } from '@desktop-client/components/reports/SaveReportMenu'; import { setSessionReport } from '@desktop-client/components/reports/setSessionReport'; import { createCustomSpreadsheet } from '@desktop-client/components/reports/spreadsheets/custom-spreadsheet'; import { createGroupedSpreadsheet } from '@desktop-client/components/reports/spreadsheets/grouped-spreadsheet'; From e101f496053d1dee812643e5dfdeae41e74b238c Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Wed, 4 Feb 2026 16:40:24 -0800 Subject: [PATCH 5/6] Change category to Maintenance and update description --- upcoming-release-notes/6867.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/upcoming-release-notes/6867.md b/upcoming-release-notes/6867.md index 6fe274794ff..d04307102b5 100644 --- a/upcoming-release-notes/6867.md +++ b/upcoming-release-notes/6867.md @@ -1,6 +1,6 @@ --- -category: Enhancements +category: Maintenance authors: [joel-jeremy] --- -Fix type safety issues and refactor CustomReport component to improve dependency handling. +Fix type safety issues and react-hooks/exhaustive-deps errors in CustomReport From 3201ad51e38e7fb283bd8f0151b254361c032be8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:49:15 +0000 Subject: [PATCH 6/6] [autofix.ci] apply automated fixes --- upcoming-release-notes/6867.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upcoming-release-notes/6867.md b/upcoming-release-notes/6867.md index d04307102b5..b58bd99fd2d 100644 --- a/upcoming-release-notes/6867.md +++ b/upcoming-release-notes/6867.md @@ -3,4 +3,4 @@ category: Maintenance authors: [joel-jeremy] --- -Fix type safety issues and react-hooks/exhaustive-deps errors in CustomReport +Fix type safety issues and react-hooks/exhaustive-deps errors in CustomReport