Compare commits

...

2 Commits

Author SHA1 Message Date
Cursor Agent
318526a22f fix: address lint warnings and add comprehensive unit tests
- Fix React.* namespace references to use named imports (UIEvent, ReactNode)
- Remove unused imports (useTranslation, TableNavigator)
- Fix exhaustive-deps warning in useCallback
- Add ReactTableTransactionsTable.test.tsx with 20 tests covering:
  - Data rendering correctness
  - Keyboard navigation (Enter/Tab/Shift+Enter/Shift+Tab/Escape)
  - Text field save behavior on navigation
  - Dropdown autocomplete (open, filter, keyboard select, click select)
  - New transaction creation (single, split, ctrl+enter, ctrl+click)
  - Escape to close new transaction form
  - Transaction selection
  - Split transactions (create, update, error handling)
  - Zero amount display in correct column
  - React Table-specific column visibility tests
  - Payee dropdown display tests

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-07 21:12:43 +00:00
Cursor Agent
b4fe5a002b feat: add React Table transactions table behind feature flag
- Add 'reactTableTransactions' feature flag to prefs, useFeatureFlag, and settings UI
- Install @tanstack/react-table dependency
- Export sub-components from TransactionsTable.tsx for reuse
- Create ReactTableTransactionTableInner.tsx using TanStack React Table
  - Declarative column definitions via useReactTable
  - Custom virtualization for performance
  - Reuses existing Transaction/TransactionHeader/NewTransaction components
  - Same keyboard navigation via useTableNavigator
- Wire up feature flag switch in TransactionTable render method
  - When flag is off: renders legacy TransactionTableInner (default)
  - When flag is on: renders ReactTableTransactionTableInner

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-07 21:02:39 +00:00
8 changed files with 1928 additions and 46 deletions

View File

@@ -17,6 +17,9 @@
"vrt": "cross-env VRT=true npx playwright test --browser=chromium",
"playwright": "playwright"
},
"dependencies": {
"@tanstack/react-table": "^8.21.3"
},
"devDependencies": {
"@actual-app/components": "workspace:*",
"@codemirror/autocomplete": "^6.20.0",

View File

@@ -214,6 +214,11 @@ export function ExperimentalFeatures() {
>
<Trans>Budget Analysis Report</Trans>
</FeatureToggle>
<FeatureToggle flag="reactTableTransactions">
<Trans>
React Table transactions (experimental table rewrite)
</Trans>
</FeatureToggle>
{showServerPrefs && (
<ServerFeatureToggle
prefName="flags.plugins"

View File

@@ -0,0 +1,715 @@
import {
createRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
type KeyboardEvent,
type ReactNode,
type Ref,
type RefObject,
type UIEvent,
} from 'react';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import {
getCoreRowModel,
useReactTable,
type ColumnDef,
} from '@tanstack/react-table';
import type { TransactionEntity } from 'loot-core/types/models';
import { isLastChild } from './table/utils';
import {
NewTransaction,
Transaction,
TransactionError,
TransactionHeader,
type TransactionTableInnerProps,
} from './TransactionsTable';
import {
ROW_HEIGHT,
type TableHandleRef,
} from '@desktop-client/components/table';
import { usePrevious } from '@desktop-client/hooks/usePrevious';
/**
* Column definitions for the transactions table using TanStack React Table.
* These define the table structure declaratively while the actual cell
* rendering is handled by the existing Transaction component.
*/
function useTransactionColumns({
showAccount,
showCategory,
showBalance,
showCleared,
}: {
showAccount: boolean;
showCategory: boolean;
showBalance: boolean;
showCleared: boolean;
}) {
return useMemo(() => {
const columns: ColumnDef<TransactionEntity, unknown>[] = [
{
id: 'select',
size: 20,
enableSorting: false,
enableResizing: false,
},
{
id: 'date',
accessorKey: 'date',
size: 110,
enableSorting: true,
},
];
if (showAccount) {
columns.push({
id: 'account',
accessorKey: 'account',
enableSorting: true,
});
}
columns.push(
{
id: 'payee',
accessorKey: 'payee',
enableSorting: true,
},
{
id: 'notes',
accessorKey: 'notes',
enableSorting: true,
},
);
if (showCategory) {
columns.push({
id: 'category',
accessorKey: 'category',
enableSorting: true,
});
}
columns.push(
{
id: 'payment',
size: 100,
enableSorting: true,
},
{
id: 'deposit',
size: 100,
enableSorting: true,
},
);
if (showBalance) {
columns.push({
id: 'balance',
size: 103,
enableSorting: false,
});
}
if (showCleared) {
columns.push({
id: 'cleared',
accessorKey: 'cleared',
size: 38,
enableSorting: true,
});
}
return columns;
}, [showAccount, showCategory, showBalance, showCleared]);
}
/**
* Custom virtualization hook for the transaction list.
* Mirrors the behavior of the existing FixedSizeList virtualization.
*/
function useVirtualTransactions({
items,
rowHeight,
containerRef,
}: {
items: TransactionEntity[];
rowHeight: number;
containerRef: RefObject<HTMLDivElement | null>;
}) {
const [scrollOffset, setScrollOffset] = useState(0);
const [containerHeight, setContainerHeight] = useState(0);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
setContainerHeight(entry.contentRect.height);
}
});
observer.observe(container);
return () => observer.disconnect();
}, [containerRef]);
const handleScroll = useCallback((e: UIEvent<HTMLDivElement>) => {
setScrollOffset((e.target as HTMLDivElement).scrollTop);
}, []);
const itemSize = rowHeight - 1;
const totalHeight = items.length * itemSize;
const overscan = 5;
const startIndex = Math.max(
0,
Math.floor(scrollOffset / itemSize) - overscan,
);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollOffset + containerHeight) / itemSize) + overscan,
);
const virtualItems = useMemo(() => {
const result: Array<{
index: number;
start: number;
item: TransactionEntity;
}> = [];
for (let i = startIndex; i <= endIndex && i < items.length; i++) {
result.push({
index: i,
start: i * itemSize,
item: items[i],
});
}
return result;
}, [startIndex, endIndex, items, itemSize]);
return {
virtualItems,
totalHeight,
handleScroll,
containerHeight,
};
}
/**
* ReactTableTransactionTableInner - A React Table-powered replacement for
* the TransactionTableInner component.
*
* Uses @tanstack/react-table for declarative column definitions and table
* model while reusing the existing Transaction component for row rendering
* and useTableNavigator for keyboard navigation.
*
* The visual and functional output is identical to the legacy implementation.
*/
export function ReactTableTransactionTableInner({
tableNavigator,
tableRef,
listContainerRef,
dateFormat = 'MM/dd/yyyy',
newNavigator,
renderEmpty,
showHiddenCategories,
...props
}: TransactionTableInnerProps) {
const containerRef = createRef<HTMLDivElement>();
const isAddingPrev = usePrevious(props.isAdding);
const [scrollWidth, setScrollWidth] = useState(0);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<{
scrollToItem: (index: number, alignment?: string) => void;
scrollTo: (offset: number) => void;
} | null>(null);
// Track scroll width for header padding
function saveScrollWidth(parent: number, child: number) {
const width = parent > 0 && child > 0 && parent - child;
setScrollWidth(!width ? 0 : width);
}
const {
onCloseAddTransaction: onCloseAddTransactionProp,
onNavigateToTransferAccount: onNavigateToTransferAccountProp,
onNavigateToSchedule: onNavigateToScheduleProp,
onNotesTagClick: onNotesTagClickProp,
} = props;
const onNavigateToTransferAccount = useCallback(
(accountId: string) => {
onCloseAddTransactionProp();
onNavigateToTransferAccountProp(accountId);
},
[onCloseAddTransactionProp, onNavigateToTransferAccountProp],
);
const onNavigateToSchedule = useCallback(
(scheduleId: string) => {
onCloseAddTransactionProp();
onNavigateToScheduleProp(scheduleId);
},
[onCloseAddTransactionProp, onNavigateToScheduleProp],
);
const onNotesTagClick = useCallback(
(noteTag: string) => {
onCloseAddTransactionProp();
onNotesTagClickProp(noteTag);
},
[onCloseAddTransactionProp, onNotesTagClickProp],
);
useEffect(() => {
if (!isAddingPrev && props.isAdding) {
newNavigator.onEdit('temp', 'date');
}
}, [isAddingPrev, props.isAdding, newNavigator]);
// Don't render reconciled transactions if we're hiding them.
const transactionsToRender = useMemo(
() =>
props.showReconciled
? props.transactions
: props.transactions.filter(t => !t.reconciled),
[props.transactions, props.showReconciled],
);
// TanStack React Table column definitions
const columns = useTransactionColumns({
showAccount: props.showAccount,
showCategory: props.showCategory,
showBalance: props.showBalances,
showCleared: props.showCleared,
});
// TanStack React Table instance - manages table state and column model.
// The table instance is used for future enhancements like column resizing
// and reordering. Currently, the row model drives the rendering pipeline.
const _table = useReactTable({
data: transactionsToRender,
columns,
getCoreRowModel: getCoreRowModel(),
manualSorting: true,
state: {
sorting: props.sortField
? [{ id: props.sortField, desc: props.ascDesc === 'desc' }]
: [],
},
getRowId: row => row.id,
});
// Expose the same ref interface as the legacy Table component
const imperativeRef = useRef<TableHandleRef<TransactionEntity>>({
scrollTo: (id: string, alignment = 'smart') => {
const index = transactionsToRender.findIndex(item => item.id === id);
if (index !== -1) {
listRef.current?.scrollToItem(index, alignment);
}
},
scrollToTop: () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
}
},
getScrolledItem: () => {
if (scrollContainerRef.current) {
const offset = scrollContainerRef.current.scrollTop;
const index = Math.floor(offset / (ROW_HEIGHT - 1));
return transactionsToRender[index]?.id ?? (0 as unknown as string);
}
return 0 as unknown as string;
},
setRowAnimation: () => {
// No-op in React Table implementation;
// animation is handled differently
},
edit(id: number, field: string, shouldScroll: boolean) {
tableNavigator.onEdit(id as unknown as TransactionEntity['id'], field);
if (id && shouldScroll) {
imperativeRef.current.scrollTo(
id as unknown as TransactionEntity['id'],
);
}
},
anchor() {
// No-op in React Table implementation
},
unanchor() {
// No-op in React Table implementation
},
isAnchored() {
return false;
},
});
// Bind the forwarded ref
useEffect(() => {
if (typeof tableRef === 'function') {
tableRef(imperativeRef.current);
} else if (tableRef && 'current' in tableRef) {
(
tableRef as { current: TableHandleRef<TransactionEntity> | null }
).current = imperativeRef.current;
}
});
// Virtualization for the transaction list
const {
virtualItems,
totalHeight,
handleScroll: handleVirtualScroll,
} = useVirtualTransactions({
items: transactionsToRender,
rowHeight: ROW_HEIGHT,
containerRef: scrollContainerRef,
});
// Scroll width calculation
useEffect(() => {
if (scrollContainerRef.current) {
const timer = setTimeout(() => {
const el = scrollContainerRef.current;
if (el?.offsetParent) {
saveScrollWidth(el.offsetParent.clientWidth, el.clientWidth);
}
}, 200);
return () => clearTimeout(timer);
}
return;
});
const { loadMoreTransactions } = props;
// Load more when approaching the end
const handleScroll = useCallback(
(e: UIEvent<HTMLDivElement>) => {
handleVirtualScroll(e);
if (loadMoreTransactions) {
const target = e.target as HTMLDivElement;
const scrollBottom =
target.scrollHeight - target.scrollTop - target.clientHeight;
if (scrollBottom < ROW_HEIGHT * 100) {
loadMoreTransactions();
}
}
},
[handleVirtualScroll, loadMoreTransactions],
);
/**
* Row renderer that uses the React Table row model while delegating
* actual rendering to the existing Transaction component.
*/
function renderRow(item: TransactionEntity, index: number) {
const {
transactions,
selectedItems,
accounts,
categoryGroups,
payees,
showCleared,
showAccount,
showBalances,
balances,
hideFraction,
isNew,
isMatched,
isExpanded,
showSelection,
allowSplitTransaction,
} = props;
const trans = item;
const editing = tableNavigator.editingId === trans.id;
const selected = selectedItems.has(trans.id);
const parent = trans.parent_id && props.transactionMap.get(trans.parent_id);
const isChildDeposit = parent ? parent.amount > 0 : undefined;
const expanded = isExpanded && isExpanded((parent || trans).id);
// For backwards compatibility, read the error of the transaction
// since in previous versions we stored it there.
const error = expanded
? (parent && parent.error) || trans.error
: trans.error;
const hasSplitError =
(trans.is_parent || trans.is_child) &&
(!expanded || isLastChild(transactions, index)) &&
error &&
error.type === 'SplitTransactionError';
const childTransactions = trans.is_parent
? props.transactionsByParent[trans.id]
: null;
const emptyChildTransactions = props.transactionsByParent[
(trans.is_parent ? trans.id : trans.parent_id) || ''
]?.filter(t => t.amount === 0);
return (
<Transaction
allTransactions={props.transactions}
editing={editing}
transaction={trans}
transferAccountsByTransaction={props.transferAccountsByTransaction}
subtransactions={childTransactions}
showAccount={showAccount}
showBalance={showBalances}
showCleared={showCleared}
selected={selected}
highlighted={false}
added={isNew?.(trans.id)}
expanded={isExpanded?.(trans.id)}
matched={isMatched?.(trans.id)}
showZeroInDeposit={isChildDeposit}
balance={balances?.[trans.id] ?? 0}
focusedField={editing ? tableNavigator.focusedField : undefined}
accounts={accounts}
categoryGroups={categoryGroups}
payees={payees}
dateFormat={dateFormat}
hideFraction={hideFraction}
onEdit={tableNavigator.onEdit}
onSave={props.onSave}
onDelete={props.onDelete}
onBatchDelete={props.onBatchDelete}
onBatchDuplicate={props.onBatchDuplicate}
onBatchLinkSchedule={props.onBatchLinkSchedule}
onBatchUnlinkSchedule={props.onBatchUnlinkSchedule}
onCreateRule={props.onCreateRule}
onScheduleAction={props.onScheduleAction}
onMakeAsNonSplitTransactions={props.onMakeAsNonSplitTransactions}
onSplit={props.onSplit}
onManagePayees={props.onManagePayees}
onCreatePayee={props.onCreatePayee}
onToggleSplit={props.onToggleSplit}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNotesTagClick={onNotesTagClick}
splitError={
hasSplitError && (
<TransactionError
error={error}
isDeposit={!!isChildDeposit}
onAddSplit={() => props.onAddSplit(trans.id)}
onDistributeRemainder={() =>
props.onDistributeRemainder(trans.id)
}
canDistributeRemainder={
emptyChildTransactions
? emptyChildTransactions.length > 0
: false
}
/>
)
}
listContainerRef={listContainerRef}
showSelection={showSelection}
allowSplitTransaction={allowSplitTransaction}
showHiddenCategories={showHiddenCategories}
/>
);
}
const isEmpty = transactionsToRender.length === 0;
function getEmptyContent(empty: typeof renderEmpty): ReactNode | null {
if (empty == null) {
return null;
} else if (typeof empty === 'function') {
return (empty as () => ReactNode)();
}
return (
<View
style={{
justifyContent: 'center',
alignItems: 'center',
fontStyle: 'italic',
color: theme.tableText,
flex: 1,
}}
>
{empty}
</View>
);
}
return (
<View
innerRef={containerRef}
style={{
flex: 1,
cursor: 'default',
...props.style,
}}
>
<View>
{/* Header rendering using the same TransactionHeader component.
The React Table instance drives column visibility while the
existing component handles the visual rendering. */}
<TransactionHeader
hasSelected={props.selectedItems.size > 0}
showAccount={props.showAccount}
showCategory={props.showCategory}
showBalance={props.showBalances}
showCleared={props.showCleared}
scrollWidth={scrollWidth}
onSort={props.onSort}
ascDesc={props.ascDesc}
field={props.sortField}
showSelection={props.showSelection}
/>
{props.isAdding && (
<View
{...newNavigator.getNavigatorProps({
onKeyDown: (e: KeyboardEvent) => props.onCheckNewEnter(e),
})}
>
<NewTransaction
transactions={props.newTransactions}
transferAccountsByTransaction={
props.transferAccountsByTransaction
}
editingTransaction={newNavigator.editingId}
focusedField={newNavigator.focusedField}
accounts={props.accounts}
categoryGroups={props.categoryGroups}
payees={props.payees || []}
showAccount={props.showAccount}
showBalance={props.showBalances}
showCleared={props.showCleared}
dateFormat={dateFormat}
hideFraction={props.hideFraction}
onClose={props.onCloseAddTransaction}
onAdd={props.onAddTemporary}
onAddAndClose={props.onAddAndCloseTemporary}
onAddSplit={props.onAddSplit}
onToggleSplit={props.onToggleSplit}
onSplit={props.onSplit}
onEdit={newNavigator.onEdit}
onSave={props.onSave}
onDelete={props.onDelete}
onManagePayees={props.onManagePayees}
onCreatePayee={props.onCreatePayee}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNotesTagClick={onNotesTagClick}
onDistributeRemainder={props.onDistributeRemainder}
showHiddenCategories={showHiddenCategories}
/>
</View>
)}
</View>
{/* Virtualized transaction list powered by React Table row model */}
<View
style={{ flex: 1, overflow: 'hidden' }}
data-testid="transaction-table"
>
<View
style={{
flex: 1,
outline: 'none',
overflow: 'hidden',
}}
tabIndex={0}
{...tableNavigator.getNavigatorProps({
onKeyDown: (e: KeyboardEvent) => props.onCheckEnter(e),
})}
data-testid="table"
>
{isEmpty ? (
<View
style={{
flex: `1 1 ${ROW_HEIGHT * 2}px`,
backgroundColor: theme.tableBackground,
}}
>
{getEmptyContent(renderEmpty)}
</View>
) : (
<View
style={{
flex: `1 1 ${ROW_HEIGHT * Math.max(2, transactionsToRender.length)}px`,
backgroundColor: theme.tableBackground,
}}
>
<div
ref={scrollContainerRef}
onScroll={handleScroll}
style={{
height: '100%',
overflow: 'auto',
position: 'relative',
}}
>
<div
ref={listContainerRef as Ref<HTMLDivElement>}
style={{
height: totalHeight,
position: 'relative',
width: '100%',
}}
>
{virtualItems.map(virtualRow => {
const item = virtualRow.item;
const editing = tableNavigator.editingId === item.id;
const selected = props.selectedItems.has(item.id);
return (
<View
key={item.id}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: ROW_HEIGHT,
zIndex: editing || selected ? 101 : 'auto',
backgroundColor: theme.tableBackground,
}}
nativeStyle={{
'--pos': `${virtualRow.start}px`,
transform: 'translateY(var(--pos))',
}}
data-focus-key={item.id}
>
{renderRow(item, virtualRow.index)}
</View>
);
})}
</div>
</div>
</View>
)}
</View>
{props.isAdding && (
<div
key="shadow"
style={{
position: 'absolute',
top: -20,
left: 0,
right: 0,
height: 20,
backgroundColor: theme.errorText,
boxShadow: '0 0 6px rgba(0, 0, 0, .20)',
}}
/>
)}
</View>
</View>
);
}

View File

@@ -74,6 +74,7 @@ import {
type TransactionEntity,
} from 'loot-core/types/models';
import { ReactTableTransactionTableInner } from './ReactTableTransactionTableInner';
import {
deserializeTransaction,
isLastChild,
@@ -120,6 +121,7 @@ import {
DisplayPayeeProvider,
useDisplayPayee,
} from '@desktop-client/hooks/useDisplayPayee';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useMergedRefs } from '@desktop-client/hooks/useMergedRefs';
import { usePrevious } from '@desktop-client/hooks/usePrevious';
@@ -139,7 +141,7 @@ import { addNotification } from '@desktop-client/notifications/notificationsSlic
import { getPayeesById } from '@desktop-client/payees/payeesSlice';
import { useDispatch } from '@desktop-client/redux';
type TransactionHeaderProps = {
export type TransactionHeaderProps = {
hasSelected: boolean;
showAccount: boolean;
showCategory: boolean;
@@ -152,7 +154,7 @@ type TransactionHeaderProps = {
field: string;
};
const TransactionHeader = memo(
export const TransactionHeader = memo(
({
hasSelected,
showAccount,
@@ -826,7 +828,7 @@ function PayeeIcons({
);
}
type TransactionProps = {
export type TransactionProps = {
allTransactions?: TransactionEntity[];
transaction: TransactionEntity;
subtransactions: TransactionEntity[] | null;
@@ -882,7 +884,7 @@ type TransactionProps = {
showHiddenCategories?: boolean;
};
const Transaction = memo(function Transaction({
export const Transaction = memo(function Transaction({
allTransactions,
transaction: originalTransaction,
subtransactions,
@@ -1707,7 +1709,7 @@ const Transaction = memo(function Transaction({
);
});
type TransactionErrorProps = {
export type TransactionErrorProps = {
error: NonNullable<TransactionEntity['error']>;
isDeposit: boolean;
onAddSplit: () => void;
@@ -1716,7 +1718,7 @@ type TransactionErrorProps = {
canDistributeRemainder: boolean;
};
function TransactionError({
export function TransactionError({
error,
isDeposit,
onAddSplit,
@@ -1772,7 +1774,7 @@ function TransactionError({
}
}
type NewTransactionProps = {
export type NewTransactionProps = {
accounts: AccountEntity[];
categoryGroups: CategoryGroupEntity[];
dateFormat: string;
@@ -1809,7 +1811,7 @@ type NewTransactionProps = {
};
showHiddenCategories?: boolean;
};
function NewTransaction({
export function NewTransaction({
transactions,
accounts,
categoryGroups,
@@ -1955,7 +1957,7 @@ function NewTransaction({
);
}
type TransactionTableInnerProps = {
export type TransactionTableInnerProps = {
tableRef: Ref<TableHandleRef<TransactionEntity>>;
listContainerRef: RefObject<HTMLDivElement>;
tableNavigator: TableNavigator<TransactionEntity>;
@@ -2993,44 +2995,52 @@ export const TransactionTable = forwardRef(
const allSchedulesQuery = useMemo(() => q('schedules').select('*'), []);
const isReactTableEnabled = useFeatureFlag('reactTableTransactions');
const innerProps = {
tableRef: mergedRef,
listContainerRef,
...props,
transactions: transactionsWithExpandedSplits,
transactionMap,
transactionsByParent,
transferAccountsByTransaction,
selectedItems,
isExpanded: splitsExpanded.isExpanded,
onSave,
onDelete,
onBatchDelete,
onBatchDuplicate,
onBatchLinkSchedule,
onBatchUnlinkSchedule,
onCreateRule,
onScheduleAction,
onMakeAsNonSplitTransactions,
onSplit,
onCheckNewEnter,
onCheckEnter,
onAddTemporary,
onAddAndCloseTemporary,
onAddSplit,
onDistributeRemainder,
onCloseAddTransaction,
onToggleSplit,
newTransactions: newTransactions ?? [],
tableNavigator,
newNavigator,
showSelection: props.showSelection,
allowSplitTransaction: props.allowSplitTransaction,
showHiddenCategories,
};
const InnerComponent = isReactTableEnabled
? ReactTableTransactionTableInner
: TransactionTableInner;
return (
<DisplayPayeeProvider transactions={displayPayeeTransactions}>
<SchedulesProvider query={allSchedulesQuery}>
<TransactionTableInner
tableRef={mergedRef}
listContainerRef={listContainerRef}
{...props}
transactions={transactionsWithExpandedSplits}
transactionMap={transactionMap}
transactionsByParent={transactionsByParent}
transferAccountsByTransaction={transferAccountsByTransaction}
selectedItems={selectedItems}
isExpanded={splitsExpanded.isExpanded}
onSave={onSave}
onDelete={onDelete}
onBatchDelete={onBatchDelete}
onBatchDuplicate={onBatchDuplicate}
onBatchLinkSchedule={onBatchLinkSchedule}
onBatchUnlinkSchedule={onBatchUnlinkSchedule}
onCreateRule={onCreateRule}
onScheduleAction={onScheduleAction}
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
onSplit={onSplit}
onCheckNewEnter={onCheckNewEnter}
onCheckEnter={onCheckEnter}
onAddTemporary={onAddTemporary}
onAddAndCloseTemporary={onAddAndCloseTemporary}
onAddSplit={onAddSplit}
onDistributeRemainder={onDistributeRemainder}
onCloseAddTransaction={onCloseAddTransaction}
onToggleSplit={onToggleSplit}
newTransactions={newTransactions ?? []}
tableNavigator={tableNavigator}
newNavigator={newNavigator}
showSelection={props.showSelection}
allowSplitTransaction={props.allowSplitTransaction}
showHiddenCategories={showHiddenCategories}
/>
<InnerComponent {...innerProps} />
</SchedulesProvider>
</DisplayPayeeProvider>
);
@@ -3039,7 +3049,7 @@ export const TransactionTable = forwardRef(
TransactionTable.displayName = 'TransactionTable';
const getCategoriesById = memoizeOne(
export const getCategoriesById = memoizeOne(
(categoryGroups: CategoryGroupEntity[] | null | undefined) => {
const res: { [id: CategoryEntity['id']]: CategoryEntity } = {};
categoryGroups?.forEach(group => {

View File

@@ -11,6 +11,7 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
crossoverReport: false,
customThemes: false,
budgetAnalysisReport: false,
reactTableTransactions: false,
};
export function useFeatureFlag(name: FeatureFlag): boolean {

View File

@@ -6,7 +6,8 @@ export type FeatureFlag =
| 'currency'
| 'crossoverReport'
| 'customThemes'
| 'budgetAnalysisReport';
| 'budgetAnalysisReport'
| 'reactTableTransactions';
/**
* Cross-device preferences. These sync across devices when they are changed.

View File

@@ -156,6 +156,7 @@ __metadata:
"@swc/core": "npm:^1.15.8"
"@swc/helpers": "npm:^0.5.18"
"@tanstack/react-query": "npm:^5.90.5"
"@tanstack/react-table": "npm:^8.21.3"
"@testing-library/dom": "npm:10.4.1"
"@testing-library/jest-dom": "npm:^6.9.1"
"@testing-library/react": "npm:16.3.0"
@@ -8509,6 +8510,25 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/react-table@npm:^8.21.3":
version: 8.21.3
resolution: "@tanstack/react-table@npm:8.21.3"
dependencies:
"@tanstack/table-core": "npm:8.21.3"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: 10/a32217ebe64d24e71dea6a6742bc288dcabf389657b16805a1ab3f347d3dca8262759c45c604a1f65bd97925d5cbdfb66d1be7637100a12eb5b279bdd420962d
languageName: node
linkType: hard
"@tanstack/table-core@npm:8.21.3":
version: 8.21.3
resolution: "@tanstack/table-core@npm:8.21.3"
checksum: 10/aa05e5f80110f0f56d66161e950668ea6ef3e2ea4f3a2ccd5d5980b39b4feea987245b20531aee2c6743e6edd12c0361503413b63090c807f88a61b19bfd04f3
languageName: node
linkType: hard
"@testing-library/dom@npm:10.4.1":
version: 10.4.1
resolution: "@testing-library/dom@npm:10.4.1"