Fix react-hooks/exhaustive-deps in CustomReport (#6867)

* Fix react-hooks/exhaustive-deps in CustomReport

* Add release notes for PR #6867

* Fix typecheck errors

* [autofix.ci] apply automated fixes

* Change category to Maintenance and update description

* [autofix.ci] apply automated fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Joel Jeremy Marquez
2026-02-05 09:06:54 -08:00
committed by GitHub
parent 2fb98156f6
commit b271de32b6
7 changed files with 111 additions and 56 deletions

View File

@@ -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;
};

View File

@@ -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: string;
savedStatus: SavedStatus;
setGraphType: (value: string) => void;
viewLegend: boolean;
viewSummary: boolean;

View File

@@ -18,7 +18,7 @@ import {
import { LoadingIndicator } from './LoadingIndicator';
import { SaveReportChoose } from './SaveReportChoose';
import { SaveReportDelete } from './SaveReportDelete';
import { SaveReportMenu } from './SaveReportMenu';
import { SaveReportMenu, type SavedStatus } 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<T extends CustomReportEntity = CustomReportEntity> = {
customReportItems: T;
report: CustomReportEntity;
savedStatus: string;
savedStatus: SavedStatus;
onReportChange: (
params:
| {

View File

@@ -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: string;
savedStatus: SavedStatus;
listReports: number;
}) {
const { t } = useTranslation();

View File

@@ -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';
@@ -52,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';
@@ -168,11 +170,11 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) {
if (['/reports'].includes(prevUrl)) sessionStorage.clear();
const reportFromSessionStorage = sessionStorage.getItem('report');
const session = reportFromSessionStorage
const session: Partial<CustomReportEntity> = reportFromSessionStorage
? JSON.parse(reportFromSessionStorage)
: {};
const combine = initialReport ?? defaultReport;
const loadReport = { ...combine, ...session };
const loadReport: CustomReportEntity = { ...combine, ...session };
const [allIntervals, setAllIntervals] = useState<
Array<{
@@ -274,39 +276,44 @@ 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<TransactionEntity['date']>('');
const [latestTransactionDate, setLatestTransactionDate] =
useState<TransactionEntity['date']>('');
const [report, setReport] = useState(loadReport);
const [savedStatus, setSavedStatus] = useState(
session.savedStatus ?? (initialReport ? 'saved' : 'new'),
const [savedStatus, setSavedStatus] = useState<SavedStatus>(
'savedStatus' in session
? (session.savedStatus as 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),
);
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(),
);
filtersToApply?.forEach(onApplyFilter);
if (conditionsOpToApply) {
onConditionsOpChange(conditionsOpToApply);
}
},
);
const onSetAllIntervals = useEffectEvent(
async (
earliestTransaction: TransactionEntity,
latestTransaction: TransactionEntity,
interval: CustomReportEntity['interval'],
) => {
const fromDate =
interval === 'Weekly'
? 'dayFromDate'
@@ -380,7 +387,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 +411,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 +658,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 +873,8 @@ function CustomReportInner({ report: initialReport }: CustomReportInnerProps) {
disabledItems={disabledItems}
defaultItems={defaultItems}
defaultModeItems={defaultModeItems}
earliestTransaction={earliestTransaction}
latestTransaction={latestTransaction}
earliestTransaction={earliestTransactionDate}
latestTransaction={latestTransactionDate}
firstDayOfWeekIdx={firstDayOfWeekIdx}
isComplexCategoryCondition={isComplexCategoryCondition}
/>

View File

@@ -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<string, { $oneof: string[] }>;

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [joel-jeremy]
---
Fix type safety issues and react-hooks/exhaustive-deps errors in CustomReport