Addition of scoped ErrorBoundarys per #7391 (#7497)

* Addition of scoped ErrorBoundarys per 7391

* Adjusted to use FeatureErrorfallback from #7437
This commit is contained in:
tempiz
2026-04-17 15:52:57 -05:00
committed by GitHub
parent a4e401bc8b
commit 13abe0cb00
7 changed files with 253 additions and 218 deletions

View File

@@ -1,14 +1,19 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';
import { ManageRules } from './ManageRules';
import { Page } from './Page';
export function ManageRulesPage() {
const { t } = useTranslation();
return (
<Page header={t('Rules')}>
<ManageRules isModal={false} payeeId={null} />
</Page>
<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
<Page header={t('Rules')}>
<ManageRules isModal={false} payeeId={null} />
</Page>
</ErrorBoundary>
);
}

View File

@@ -1,5 +1,6 @@
import React, { createRef, PureComponent, useEffect, useMemo } from 'react';
import type { ReactElement, RefObject } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Trans } from 'react-i18next';
import { Navigate, useLocation, useParams } from 'react-router';
@@ -43,6 +44,7 @@ import {
useUpdateAccountMutation,
} from '#accounts';
import { markAccountRead } from '#accounts/accountsSlice';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';
import type { SavedFilter } from '#components/filters/SavedFilterMenuButton';
import { TransactionList } from '#components/transactions/TransactionList';
import { validateAccountName } from '#components/util/accountValidation';
@@ -2031,48 +2033,50 @@ export function Account() {
createPayee.mutateAsync({ name });
return (
<SchedulesProvider query={schedulesQuery}>
<SplitsExpandedProvider
initialMode={expandSplits ? 'collapse' : 'expand'}
>
<AccountHack
newTransactions={newTransactions}
matchedTransactions={matchedTransactions}
accounts={accounts}
failedAccounts={failedAccounts}
dateFormat={dateFormat}
hideFraction={String(hideFraction) === 'true'}
expandSplits={expandSplits}
showBalances={String(showBalances) === 'true'}
setShowBalances={showBalances =>
setShowBalances(String(showBalances))
}
showNetWorthChart={String(showNetWorthChart) === 'true'}
setShowNetWorthChart={val => setShowNetWorthChart(String(val))}
showCleared={String(hideCleared) !== 'true'}
setShowCleared={val => setHideCleared(String(!val))}
showReconciled={String(hideReconciled) !== 'true'}
setShowReconciled={val => setHideReconciled(String(!val))}
showExtraBalances={String(showExtraBalances) === 'true'}
setShowExtraBalances={extraBalances =>
setShowExtraBalances(String(extraBalances))
}
payees={payees}
modalShowing={modalShowing}
accountsSyncing={accountsSyncing}
filterConditions={filterConditions}
categoryGroups={categoryGroups}
accountId={params.id}
categoryId={location?.state?.categoryId}
location={location}
savedFilters={savedFiters}
onReopenAccount={onReopenAccount}
onUpdateAccount={onUpdateAccount}
onUnlinkAccount={onUnlinkAccount}
onSyncAndDownload={onSyncAndDownload}
onCreatePayee={onCreatePayee}
/>
</SplitsExpandedProvider>
</SchedulesProvider>
<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
<SchedulesProvider query={schedulesQuery}>
<SplitsExpandedProvider
initialMode={expandSplits ? 'collapse' : 'expand'}
>
<AccountHack
newTransactions={newTransactions}
matchedTransactions={matchedTransactions}
accounts={accounts}
failedAccounts={failedAccounts}
dateFormat={dateFormat}
hideFraction={String(hideFraction) === 'true'}
expandSplits={expandSplits}
showBalances={String(showBalances) === 'true'}
setShowBalances={showBalances =>
setShowBalances(String(showBalances))
}
showNetWorthChart={String(showNetWorthChart) === 'true'}
setShowNetWorthChart={val => setShowNetWorthChart(String(val))}
showCleared={String(hideCleared) !== 'true'}
setShowCleared={val => setHideCleared(String(!val))}
showReconciled={String(hideReconciled) !== 'true'}
setShowReconciled={val => setHideReconciled(String(!val))}
showExtraBalances={String(showExtraBalances) === 'true'}
setShowExtraBalances={extraBalances =>
setShowExtraBalances(String(extraBalances))
}
payees={payees}
modalShowing={modalShowing}
accountsSyncing={accountsSyncing}
filterConditions={filterConditions}
categoryGroups={categoryGroups}
accountId={params.id}
categoryId={location?.state?.categoryId}
location={location}
savedFilters={savedFiters}
onReopenAccount={onReopenAccount}
onUpdateAccount={onUpdateAccount}
onUnlinkAccount={onUnlinkAccount}
onSyncAndDownload={onSyncAndDownload}
onCreatePayee={onCreatePayee}
/>
</SplitsExpandedProvider>
</SchedulesProvider>
</ErrorBoundary>
);
}

View File

@@ -1,12 +1,14 @@
// @ts-strict-ignore
import React, { useEffect } from 'react';
import type { ComponentProps } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useHotkeys } from 'react-hotkeys-hook';
import { AutoSizer } from 'react-virtualized-auto-sizer';
import { View } from '@actual-app/components/view';
import * as monthUtils from '@actual-app/core/shared/months';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';
import { useGlobalPref } from '#hooks/useGlobalPref';
import { useBudgetMonthCount } from './BudgetMonthCountContext';
@@ -131,20 +133,22 @@ const DynamicBudgetTable = ({
}}
>
<View style={{ width: '100%', maxWidth }}>
<BudgetPageHeader
startMonth={prewarmStartMonth}
numMonths={numMonths}
monthBounds={monthBounds}
onMonthSelect={_onMonthSelect}
/>
<BudgetTable
type={type}
prewarmStartMonth={prewarmStartMonth}
startMonth={startMonth}
numMonths={numMonths}
monthBounds={monthBounds}
{...props}
/>
<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
<BudgetPageHeader
startMonth={prewarmStartMonth}
numMonths={numMonths}
monthBounds={monthBounds}
onMonthSelect={_onMonthSelect}
/>
<BudgetTable
type={type}
prewarmStartMonth={prewarmStartMonth}
startMonth={startMonth}
numMonths={numMonths}
monthBounds={monthBounds}
{...props}
/>
</ErrorBoundary>
</View>
</View>
);

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useMemo, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Trans, useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
@@ -9,6 +10,7 @@ import { q } from '@actual-app/core/shared/query';
import type { ScheduleEntity } from '@actual-app/core/types/models';
import { Search } from '#components/common/Search';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';
import { Page } from '#components/Page';
import { useSchedules } from '#hooks/useSchedules';
import { pushModal } from '#modals/modalsSlice';
@@ -85,66 +87,68 @@ export function Schedules() {
} = useSchedules({ query: schedulesQuery });
return (
<Page header={t('Schedules')}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
padding: '0 0 15px',
}}
>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
}}
>
<Search
placeholder={t('Filter schedules…')}
value={filter}
onChange={setFilter}
/>
</View>
</View>
<SchedulesTable
isLoading={isSchedulesLoading}
schedules={schedules}
filter={filter}
statuses={statuses}
allowCompleted
onSelect={onEdit}
onAction={onAction}
style={{ backgroundColor: theme.tableBackground }}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
margin: '20px 0',
flexShrink: 0,
}}
>
<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
<Page header={t('Schedules')}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: '1em',
padding: '0 0 15px',
}}
>
<Button onPress={onDiscover}>
<Trans>Find schedules</Trans>
</Button>
<Button onPress={onChangeUpcomingLength}>
<Trans>Change upcoming length</Trans>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
}}
>
<Search
placeholder={t('Filter schedules…')}
value={filter}
onChange={setFilter}
/>
</View>
</View>
<SchedulesTable
isLoading={isSchedulesLoading}
schedules={schedules}
filter={filter}
statuses={statuses}
allowCompleted
onSelect={onEdit}
onAction={onAction}
style={{ backgroundColor: theme.tableBackground }}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
margin: '20px 0',
flexShrink: 0,
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: '1em',
}}
>
<Button onPress={onDiscover}>
<Trans>Find schedules</Trans>
</Button>
<Button onPress={onChangeUpcomingLength}>
<Trans>Change upcoming length</Trans>
</Button>
</View>
<Button variant="primary" onPress={onAdd}>
<Trans>Add new schedule</Trans>
</Button>
</View>
<Button variant="primary" onPress={onAdd}>
<Trans>Add new schedule</Trans>
</Button>
</View>
</Page>
</Page>
</ErrorBoundary>
);
}

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import type { CSSProperties } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
@@ -11,6 +12,7 @@ import * as Platform from '@actual-app/core/shared/platform';
import { css } from '@emotion/css';
import { Resizable } from 're-resizable';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';
import { useGlobalPref } from '#hooks/useGlobalPref';
import { useLocalPref } from '#hooks/useLocalPref';
import { useResizeObserver } from '#hooks/useResizeObserver';
@@ -67,69 +69,75 @@ export function Sidebar() {
});
return (
<Resizable
defaultSize={{
width: sidebarWidth,
height: '100%',
}}
onResizeStop={onResizeStop}
maxWidth={MAX_SIDEBAR_WIDTH}
minWidth={MIN_SIDEBAR_WIDTH}
enable={{
top: false,
right: true,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
>
<View
innerRef={containerRef}
className={css({
color: theme.sidebarItemText,
<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
<Resizable
defaultSize={{
width: sidebarWidth,
height: '100%',
backgroundColor: theme.sidebarBackground,
'& .float': {
opacity: isFloating ? 1 : 0,
transition: 'opacity .25s, width .25s',
width: hasWindowButtons || isFloating ? null : 0,
} as CSSProperties,
'&:hover .float': {
opacity: 1,
width: hasWindowButtons ? null : 'auto',
} as CSSProperties,
flex: 1,
...styles.darkScrollbar,
})}
}}
onResizeStop={onResizeStop}
maxWidth={MAX_SIDEBAR_WIDTH}
minWidth={MIN_SIDEBAR_WIDTH}
enable={{
top: false,
right: true,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
>
<BudgetName>
{!sidebar.alwaysFloats && (
<ToggleButton isFloating={isFloating} onFloat={onFloat} />
)}
</BudgetName>
<View
style={{
flexGrow: 1,
'@media screen and (max-height: 480px)': {
overflowY: 'auto',
},
}}
innerRef={containerRef}
className={css({
color: theme.sidebarItemText,
height: '100%',
backgroundColor: theme.sidebarBackground,
'& .float': {
opacity: isFloating ? 1 : 0,
transition: 'opacity .25s, width .25s',
width: hasWindowButtons || isFloating ? null : 0,
} as CSSProperties,
'&:hover .float': {
opacity: 1,
width: hasWindowButtons ? null : 'auto',
} as CSSProperties,
flex: 1,
...styles.darkScrollbar,
})}
>
<PrimaryButtons />
<BudgetName>
{!sidebar.alwaysFloats && (
<ToggleButton isFloating={isFloating} onFloat={onFloat} />
)}
</BudgetName>
<Accounts />
<View
style={{
flexGrow: 1,
'@media screen and (max-height: 480px)': {
overflowY: 'auto',
},
}}
>
<PrimaryButtons />
<SecondaryButtons
buttons={[
{ title: t('Add account'), Icon: SvgAdd, onClick: onAddAccount },
]}
/>
<Accounts />
<SecondaryButtons
buttons={[
{
title: t('Add account'),
Icon: SvgAdd,
onClick: onAddAccount,
},
]}
/>
</View>
</View>
</View>
</Resizable>
</Resizable>
</ErrorBoundary>
);
}

View File

@@ -2,6 +2,7 @@
// TODO: remove strict
import { useCallback, useLayoutEffect, useRef } from 'react';
import type { RefObject } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { theme } from '@actual-app/components/theme';
@@ -29,6 +30,7 @@ import type {
TransactionFilterEntity,
} from '@actual-app/core/types/models';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';
import type { TableHandleRef } from '#components/table';
import { isValidBoundaryDrop } from '#hooks/useDragDrop';
import type { DropPosition } from '#hooks/useDragDrop';
@@ -722,53 +724,55 @@ export function TransactionList({
);
return (
<TransactionTable
ref={tableRef}
transactions={allTransactions}
loadMoreTransactions={loadMoreTransactions}
accounts={accounts}
categoryGroups={categoryGroups}
payees={payees}
balances={balances}
showBalances={showBalances}
showReconciled={showReconciled}
showCleared={showCleared}
showAccount={showAccount}
showCategory
currentAccountId={account && account.id}
currentCategoryId={category && category.id}
isAdding={isAdding}
isNew={isNew}
isMatched={isMatched}
dateFormat={dateFormat}
hideFraction={hideFraction}
renderEmpty={renderEmpty}
onSave={onSave}
onApplyRules={onApplyRules}
onSplit={onSplit}
onCloseAddTransaction={onCloseAddTransaction}
onAdd={onAdd}
onAddSplit={onAddSplit}
onManagePayees={onManagePayees}
onCreatePayee={onCreatePayee}
style={{ backgroundColor: theme.tableBackground }}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNotesTagClick={onNotesTagClick}
onSort={onSort}
sortField={sortField}
ascDesc={ascDesc}
isFiltered={isFiltered}
onReorder={allowReorder ? onReorder : undefined}
onBatchDelete={onBatchDelete}
onBatchDuplicate={onBatchDuplicate}
onBatchLinkSchedule={onBatchLinkSchedule}
onBatchUnlinkSchedule={onBatchUnlinkSchedule}
onCreateRule={onCreateRule}
onScheduleAction={onScheduleAction}
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
showSelection={showSelection}
allowSplitTransaction={allowSplitTransaction}
/>
<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
<TransactionTable
ref={tableRef}
transactions={allTransactions}
loadMoreTransactions={loadMoreTransactions}
accounts={accounts}
categoryGroups={categoryGroups}
payees={payees}
balances={balances}
showBalances={showBalances}
showReconciled={showReconciled}
showCleared={showCleared}
showAccount={showAccount}
showCategory
currentAccountId={account && account.id}
currentCategoryId={category && category.id}
isAdding={isAdding}
isNew={isNew}
isMatched={isMatched}
dateFormat={dateFormat}
hideFraction={hideFraction}
renderEmpty={renderEmpty}
onSave={onSave}
onApplyRules={onApplyRules}
onSplit={onSplit}
onCloseAddTransaction={onCloseAddTransaction}
onAdd={onAdd}
onAddSplit={onAddSplit}
onManagePayees={onManagePayees}
onCreatePayee={onCreatePayee}
style={{ backgroundColor: theme.tableBackground }}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNotesTagClick={onNotesTagClick}
onSort={onSort}
sortField={sortField}
ascDesc={ascDesc}
isFiltered={isFiltered}
onReorder={allowReorder ? onReorder : undefined}
onBatchDelete={onBatchDelete}
onBatchDuplicate={onBatchDuplicate}
onBatchLinkSchedule={onBatchLinkSchedule}
onBatchUnlinkSchedule={onBatchUnlinkSchedule}
onCreateRule={onCreateRule}
onScheduleAction={onScheduleAction}
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
showSelection={showSelection}
allowSplitTransaction={allowSplitTransaction}
/>
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [tempiz]
---
Add scoped error boundaries to prevent feature-level crashes from taking down the entire app