mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
Convert BudgetTable to a functional component (#2459)
* Convert BudgetTable to a functional component * Release notes * Fix lint errors * Remove undo-event listener
This commit is contained in:
committed by
GitHub
parent
c82a6dc5ef
commit
6f600a4fee
@@ -1,9 +1,9 @@
|
||||
import React, { createRef, Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { savePrefs } from 'loot-core/src/client/actions';
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
|
||||
import { useCategories } from '../../hooks/useCategories';
|
||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||
import { theme, styles } from '../../style';
|
||||
import { View } from '../common/View';
|
||||
import { IntersectionBoundary } from '../tooltips';
|
||||
@@ -14,28 +14,41 @@ import { BudgetTotals } from './BudgetTotals';
|
||||
import { MonthsProvider } from './MonthsContext';
|
||||
import { findSortDown, findSortUp, getScrollbarWidth } from './util';
|
||||
|
||||
class BudgetTableInner extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.budgetCategoriesRef = createRef();
|
||||
export function BudgetTable(props) {
|
||||
const {
|
||||
type,
|
||||
prewarmStartMonth,
|
||||
startMonth,
|
||||
numMonths,
|
||||
monthBounds,
|
||||
dataComponents,
|
||||
onSaveCategory,
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
onDeleteGroup,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
onShowActivity,
|
||||
onBudgetAction,
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
editing: null,
|
||||
draggingState: null,
|
||||
};
|
||||
}
|
||||
const budgetCategoriesRef = useRef();
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
const [collapsed = [], setCollapsedPref] = useLocalPref('budget.collapsed');
|
||||
const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref(
|
||||
'budget.showHiddenCategories',
|
||||
);
|
||||
const [editing, setEditing] = useState(null);
|
||||
|
||||
onEditMonth = (id, monthIndex) => {
|
||||
this.setState({ editing: id ? { id, cell: monthIndex } : null });
|
||||
const onEditMonth = (id, monthIndex) => {
|
||||
setEditing(id ? { id, cell: monthIndex } : null);
|
||||
};
|
||||
|
||||
onEditName = id => {
|
||||
this.setState({ editing: id ? { id, cell: 'name' } : null });
|
||||
const onEditName = id => {
|
||||
setEditing(id ? { id, cell: 'name' } : null);
|
||||
};
|
||||
|
||||
onReorderCategory = (id, dropPos, targetId) => {
|
||||
const { categoryGroups } = this.props;
|
||||
|
||||
const _onReorderCategory = (id, dropPos, targetId) => {
|
||||
const isGroup = !!categoryGroups.find(g => g.id === targetId);
|
||||
|
||||
if (isGroup) {
|
||||
@@ -48,7 +61,7 @@ class BudgetTableInner extends Component {
|
||||
|
||||
if (group) {
|
||||
const { categories } = group;
|
||||
this.props.onReorderCategory({
|
||||
onReorderCategory({
|
||||
id,
|
||||
groupId: group.id,
|
||||
targetId:
|
||||
@@ -67,7 +80,7 @@ class BudgetTableInner extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onReorderCategory({
|
||||
onReorderCategory({
|
||||
id,
|
||||
groupId: targetGroup.id,
|
||||
...findSortDown(targetGroup.categories, dropPos, targetId),
|
||||
@@ -75,19 +88,14 @@ class BudgetTableInner extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onReorderGroup = (id, dropPos, targetId) => {
|
||||
const { categoryGroups } = this.props;
|
||||
|
||||
this.props.onReorderGroup({
|
||||
const _onReorderGroup = (id, dropPos, targetId) => {
|
||||
onReorderGroup({
|
||||
id,
|
||||
...findSortDown(categoryGroups, dropPos, targetId),
|
||||
});
|
||||
};
|
||||
|
||||
moveVertically = dir => {
|
||||
const { editing } = this.state;
|
||||
const { type, categoryGroups, collapsed } = this.props;
|
||||
|
||||
const moveVertically = dir => {
|
||||
const flattened = categoryGroups.reduce((all, group) => {
|
||||
if (collapsed.includes(group.id)) {
|
||||
return all.concat({ id: group.id, isGroup: true });
|
||||
@@ -106,7 +114,7 @@ class BudgetTableInner extends Component {
|
||||
nextIdx += dir;
|
||||
continue;
|
||||
} else if (type === 'report' || !next.is_income) {
|
||||
this.onEditMonth(next.id, editing.cell);
|
||||
onEditMonth(next.id, editing.cell);
|
||||
return;
|
||||
} else {
|
||||
break;
|
||||
@@ -115,187 +123,136 @@ class BudgetTableInner extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown = e => {
|
||||
if (!this.state.editing) {
|
||||
const onKeyDown = e => {
|
||||
if (!editing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
this.moveVertically(e.shiftKey ? -1 : 1);
|
||||
moveVertically(e.shiftKey ? -1 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
onShowActivity = (catId, monthIndex) => {
|
||||
this.props.onShowActivity(catId, this.resolveMonth(monthIndex));
|
||||
const resolveMonth = monthIndex => {
|
||||
return monthUtils.addMonths(startMonth, monthIndex);
|
||||
};
|
||||
|
||||
onBudgetAction = (monthIndex, type, args) => {
|
||||
this.props.onBudgetAction(this.resolveMonth(monthIndex), type, args);
|
||||
const _onShowActivity = (catId, monthIndex) => {
|
||||
onShowActivity(catId, resolveMonth(monthIndex));
|
||||
};
|
||||
|
||||
resolveMonth = monthIndex => {
|
||||
return monthUtils.addMonths(this.props.startMonth, monthIndex);
|
||||
const _onBudgetAction = (monthIndex, type, args) => {
|
||||
onBudgetAction(resolveMonth(monthIndex), type, args);
|
||||
};
|
||||
|
||||
// This is called via ref.
|
||||
clearEditing() {
|
||||
this.setState({ editing: null });
|
||||
}
|
||||
|
||||
toggleHiddenCategories = () => {
|
||||
this.props.onToggleHiddenCategories();
|
||||
const onCollapse = collapsedIds => {
|
||||
setCollapsedPref(collapsedIds);
|
||||
};
|
||||
|
||||
expandAllCategories = () => {
|
||||
this.props.onCollapse([]);
|
||||
const onToggleHiddenCategories = () => {
|
||||
setShowHiddenCategoriesPef(!showHiddenCategories);
|
||||
};
|
||||
|
||||
collapseAllCategories = () => {
|
||||
const { onCollapse, categoryGroups } = this.props;
|
||||
const toggleHiddenCategories = () => {
|
||||
onToggleHiddenCategories();
|
||||
};
|
||||
|
||||
const expandAllCategories = () => {
|
||||
onCollapse([]);
|
||||
};
|
||||
|
||||
const collapseAllCategories = () => {
|
||||
onCollapse(categoryGroups.map(g => g.id));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
type,
|
||||
categoryGroups,
|
||||
prewarmStartMonth,
|
||||
startMonth,
|
||||
numMonths,
|
||||
monthBounds,
|
||||
dataComponents,
|
||||
onSaveCategory,
|
||||
onSaveGroup,
|
||||
onDeleteCategory,
|
||||
onDeleteGroup,
|
||||
} = this.props;
|
||||
const { editing, draggingState } = this.state;
|
||||
|
||||
return (
|
||||
return (
|
||||
<View
|
||||
data-testid="budget-table"
|
||||
style={{
|
||||
flex: 1,
|
||||
...(styles.lightScrollbar && {
|
||||
'& ::-webkit-scrollbar': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
'& ::-webkit-scrollbar-thumb:vertical': {
|
||||
backgroundColor: theme.tableHeaderBackground,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<View
|
||||
data-testid="budget-table"
|
||||
style={{
|
||||
flex: 1,
|
||||
...(styles.lightScrollbar && {
|
||||
'& ::-webkit-scrollbar': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
'& ::-webkit-scrollbar-thumb:vertical': {
|
||||
backgroundColor: theme.tableHeaderBackground,
|
||||
},
|
||||
}),
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
// This is necessary to align with the table because the
|
||||
// table has this padding to allow the shadow to show
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5 + getScrollbarWidth(),
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
// This is necessary to align with the table because the
|
||||
// table has this padding to allow the shadow to show
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5 + getScrollbarWidth(),
|
||||
}}
|
||||
>
|
||||
<View style={{ width: 200 }} />
|
||||
<MonthsProvider
|
||||
startMonth={prewarmStartMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
type={type}
|
||||
>
|
||||
<BudgetSummaries
|
||||
SummaryComponent={dataComponents.SummaryComponent}
|
||||
/>
|
||||
</MonthsProvider>
|
||||
</View>
|
||||
|
||||
<View style={{ width: 200 }} />
|
||||
<MonthsProvider
|
||||
startMonth={startMonth}
|
||||
startMonth={prewarmStartMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
type={type}
|
||||
>
|
||||
<BudgetTotals
|
||||
MonthComponent={dataComponents.BudgetTotalsComponent}
|
||||
toggleHiddenCategories={this.toggleHiddenCategories}
|
||||
expandAllCategories={this.expandAllCategories}
|
||||
collapseAllCategories={this.collapseAllCategories}
|
||||
/>
|
||||
<IntersectionBoundary.Provider value={this.budgetCategoriesRef}>
|
||||
<View
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
overflowAnchor: 'none',
|
||||
flex: 1,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
innerRef={this.budgetCategoriesRef}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
opacity: draggingState ? 0.5 : 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onKeyDown={this.onKeyDown}
|
||||
innerRef={el => (this.budgetDataNode = el)}
|
||||
>
|
||||
<BudgetCategories
|
||||
categoryGroups={categoryGroups}
|
||||
editingCell={editing}
|
||||
dataComponents={dataComponents}
|
||||
onEditMonth={this.onEditMonth}
|
||||
onEditName={this.onEditName}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onSaveGroup={onSaveGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onReorderCategory={this.onReorderCategory}
|
||||
onReorderGroup={this.onReorderGroup}
|
||||
onBudgetAction={this.onBudgetAction}
|
||||
onShowActivity={this.onShowActivity}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</IntersectionBoundary.Provider>
|
||||
<BudgetSummaries SummaryComponent={dataComponents.SummaryComponent} />
|
||||
</MonthsProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
<MonthsProvider
|
||||
startMonth={startMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
type={type}
|
||||
>
|
||||
<BudgetTotals
|
||||
MonthComponent={dataComponents.BudgetTotalsComponent}
|
||||
toggleHiddenCategories={toggleHiddenCategories}
|
||||
expandAllCategories={expandAllCategories}
|
||||
collapseAllCategories={collapseAllCategories}
|
||||
/>
|
||||
<IntersectionBoundary.Provider value={budgetCategoriesRef}>
|
||||
<View
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
overflowAnchor: 'none',
|
||||
flex: 1,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
innerRef={budgetCategoriesRef}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<BudgetCategories
|
||||
categoryGroups={categoryGroups}
|
||||
editingCell={editing}
|
||||
dataComponents={dataComponents}
|
||||
onEditMonth={onEditMonth}
|
||||
onEditName={onEditName}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onSaveGroup={onSaveGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onReorderCategory={_onReorderCategory}
|
||||
onReorderGroup={_onReorderGroup}
|
||||
onBudgetAction={_onBudgetAction}
|
||||
onShowActivity={_onShowActivity}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</IntersectionBoundary.Provider>
|
||||
</MonthsProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { grouped: categoryGroups } = state.queries.categories;
|
||||
const collapsed = state.prefs.local?.['budget.collapsed'] || [];
|
||||
return {
|
||||
categoryGroups,
|
||||
collapsed,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
const onCollapse = collapsedIds => {
|
||||
dispatch(savePrefs({ 'budget.collapsed': collapsedIds }));
|
||||
};
|
||||
|
||||
const onToggleHiddenCategories = () =>
|
||||
dispatch((innerDispatch, getState) => {
|
||||
const { prefs } = getState();
|
||||
const showHiddenCategories = prefs.local['budget.showHiddenCategories'];
|
||||
innerDispatch(
|
||||
savePrefs({
|
||||
'budget.showHiddenCategories': !showHiddenCategories,
|
||||
}),
|
||||
);
|
||||
});
|
||||
return {
|
||||
onCollapse,
|
||||
onToggleHiddenCategories,
|
||||
};
|
||||
};
|
||||
|
||||
export const BudgetTable = connect(mapStateToProps, mapDispatchToProps, null, {
|
||||
forwardRef: true,
|
||||
})(BudgetTableInner);
|
||||
BudgetTable.displayName = 'BudgetTable';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { forwardRef, useEffect, type ComponentProps } from 'react';
|
||||
import React, { useEffect, type ComponentProps } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { useActions } from '../../hooks/useActions';
|
||||
import { View } from '../common/View';
|
||||
|
||||
import { useBudgetMonthCount } from './BudgetMonthCountContext';
|
||||
@@ -30,90 +29,72 @@ function getNumPossibleMonths(width: number) {
|
||||
type DynamicBudgetTableInnerProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
} & ComponentProps<typeof BudgetTable>;
|
||||
} & DynamicBudgetTableProps;
|
||||
|
||||
const DynamicBudgetTableInner = forwardRef<
|
||||
typeof BudgetTable,
|
||||
DynamicBudgetTableInnerProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
width,
|
||||
height,
|
||||
prewarmStartMonth,
|
||||
startMonth,
|
||||
maxMonths = 3,
|
||||
monthBounds,
|
||||
onMonthSelect: onMonthSelect_,
|
||||
onPreload,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { setDisplayMax } = useBudgetMonthCount();
|
||||
const actions = useActions();
|
||||
const DynamicBudgetTableInner = ({
|
||||
width,
|
||||
height,
|
||||
prewarmStartMonth,
|
||||
startMonth,
|
||||
maxMonths = 3,
|
||||
monthBounds,
|
||||
onMonthSelect,
|
||||
...props
|
||||
}: DynamicBudgetTableInnerProps) => {
|
||||
const { setDisplayMax } = useBudgetMonthCount();
|
||||
|
||||
const numPossible = getNumPossibleMonths(width);
|
||||
const numMonths = Math.min(numPossible, maxMonths);
|
||||
const maxWidth = 200 + 500 * numMonths;
|
||||
const numPossible = getNumPossibleMonths(width);
|
||||
const numMonths = Math.min(numPossible, maxMonths);
|
||||
const maxWidth = 200 + 500 * numMonths;
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayMax(numPossible);
|
||||
}, [numPossible]);
|
||||
useEffect(() => {
|
||||
setDisplayMax(numPossible);
|
||||
}, [numPossible]);
|
||||
|
||||
function onMonthSelect(month) {
|
||||
onMonthSelect_(month, numMonths);
|
||||
}
|
||||
function _onMonthSelect(month) {
|
||||
onMonthSelect(month, numMonths);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
alignItems: 'center',
|
||||
opacity: width <= 0 || height <= 0 ? 0 : 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ width: '100%', maxWidth }}>
|
||||
<BudgetPageHeader
|
||||
startMonth={prewarmStartMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
onMonthSelect={onMonthSelect}
|
||||
/>
|
||||
<BudgetTable
|
||||
ref={ref}
|
||||
prewarmStartMonth={prewarmStartMonth}
|
||||
startMonth={startMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
{...actions}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
alignItems: 'center',
|
||||
opacity: width <= 0 || height <= 0 ? 0 : 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ width: '100%', maxWidth }}>
|
||||
<BudgetPageHeader
|
||||
startMonth={prewarmStartMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
onMonthSelect={_onMonthSelect}
|
||||
/>
|
||||
<BudgetTable
|
||||
prewarmStartMonth={prewarmStartMonth}
|
||||
startMonth={startMonth}
|
||||
numMonths={numMonths}
|
||||
monthBounds={monthBounds}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DynamicBudgetTableInner.displayName = 'DynamicBudgetTableInner';
|
||||
|
||||
export const DynamicBudgetTable = forwardRef<
|
||||
typeof BudgetTable,
|
||||
DynamicBudgetTableInnerProps
|
||||
>((props, ref) => {
|
||||
type DynamicBudgetTableProps = ComponentProps<typeof BudgetTable>;
|
||||
|
||||
export const DynamicBudgetTable = (props: DynamicBudgetTableProps) => {
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<DynamicBudgetTableInner
|
||||
ref={ref}
|
||||
width={width}
|
||||
height={height}
|
||||
{...props}
|
||||
/>
|
||||
<DynamicBudgetTableInner width={width} height={height} {...props} />
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
DynamicBudgetTable.displayName = 'DynamicBudgetTable';
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, {
|
||||
memo,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import React, { memo, useContext, useMemo, useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import {
|
||||
@@ -69,16 +62,15 @@ type RolloverComponents = {
|
||||
IncomeHeaderComponent: typeof rollover.IncomeHeaderMonth;
|
||||
};
|
||||
|
||||
type BudgetProps = {
|
||||
type BudgetInnerProps = {
|
||||
accountId?: string;
|
||||
reportComponents: ReportComponents;
|
||||
rolloverComponents: RolloverComponents;
|
||||
titlebar: TitlebarContextValue;
|
||||
};
|
||||
|
||||
function BudgetInner(props: BudgetProps) {
|
||||
function BudgetInner(props: BudgetInnerProps) {
|
||||
const currentMonth = monthUtils.currentMonth();
|
||||
const tableRef = useRef(null);
|
||||
const spreadsheet = useSpreadsheet();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
@@ -138,17 +130,6 @@ function BudgetInner(props: BudgetProps) {
|
||||
}),
|
||||
|
||||
listen('undo-event', ({ tables }) => {
|
||||
if (tableRef.current) {
|
||||
// g dammit
|
||||
// We need to clear the editing cell, otherwise when
|
||||
// the user navigates away from the page they will
|
||||
// accidentally clear the undo stack if they have pressed
|
||||
// undo, since the cell will save itself on blur (worst case:
|
||||
// undo takes you to another screen and then you can't redo
|
||||
// any of the budget changes)
|
||||
tableRef.current.clearEditing();
|
||||
}
|
||||
|
||||
if (tables.includes('categories')) {
|
||||
loadCategories();
|
||||
}
|
||||
@@ -377,7 +358,6 @@ function BudgetInner(props: BudgetProps) {
|
||||
onToggleSummaryCollapse={onToggleCollapse}
|
||||
>
|
||||
<DynamicBudgetTable
|
||||
ref={tableRef}
|
||||
type={budgetType}
|
||||
prewarmStartMonth={startMonth}
|
||||
startMonth={startMonth}
|
||||
@@ -404,7 +384,6 @@ function BudgetInner(props: BudgetProps) {
|
||||
onToggleSummaryCollapse={onToggleCollapse}
|
||||
>
|
||||
<DynamicBudgetTable
|
||||
ref={tableRef}
|
||||
type={budgetType}
|
||||
prewarmStartMonth={startMonth}
|
||||
startMonth={startMonth}
|
||||
|
||||
6
upcoming-release-notes/2459.md
Normal file
6
upcoming-release-notes/2459.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Convert BudgetTable component to a functional component.
|
||||
Reference in New Issue
Block a user