mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 09:50:18 -05:00
[Maintenance] Update types for budget table components (#6022)
* [Maintenance] Update types for budget table components + new hooks to reduce prop drilling * Require BudgetCategories props
This commit is contained in:
committed by
GitHub
parent
f3419a4ee2
commit
97dec0d3c8
@@ -1,9 +1,4 @@
|
||||
import React, {
|
||||
memo,
|
||||
useState,
|
||||
useMemo,
|
||||
type ComponentPropsWithoutRef,
|
||||
} from 'react';
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
@@ -52,32 +47,15 @@ type LocalDragState =
|
||||
type BudgetCategoriesProps = {
|
||||
categoryGroups: CategoryGroupEntity[];
|
||||
editingCell: { id: string; cell: string } | null;
|
||||
dataComponents: {
|
||||
ExpenseGroupComponent: ComponentPropsWithoutRef<
|
||||
typeof ExpenseGroup
|
||||
>['MonthComponent'];
|
||||
ExpenseCategoryComponent: ComponentPropsWithoutRef<
|
||||
typeof ExpenseCategory
|
||||
>['MonthComponent'];
|
||||
IncomeHeaderComponent: ComponentPropsWithoutRef<
|
||||
typeof IncomeHeader
|
||||
>['MonthComponent'];
|
||||
IncomeGroupComponent: ComponentPropsWithoutRef<
|
||||
typeof IncomeGroup
|
||||
>['MonthComponent'];
|
||||
IncomeCategoryComponent: ComponentPropsWithoutRef<
|
||||
typeof IncomeCategory
|
||||
>['MonthComponent'];
|
||||
};
|
||||
onBudgetAction: (month: string, action: string, arg: unknown) => void;
|
||||
onShowActivity: (id: CategoryEntity['id'], month?: string) => void;
|
||||
onEditName?: (id: CategoryEntity['id']) => void;
|
||||
onEditMonth?: (id: CategoryEntity['id'], month: string) => void;
|
||||
onSaveCategory?: (category: CategoryEntity) => void;
|
||||
onSaveGroup?: (group: CategoryGroupEntity) => void;
|
||||
onEditName: (id: CategoryEntity['id']) => void;
|
||||
onEditMonth: (id: CategoryEntity['id'], month: string) => void;
|
||||
onSaveCategory: (category: CategoryEntity) => void;
|
||||
onSaveGroup: (group: CategoryGroupEntity) => void;
|
||||
onDeleteCategory: (id: CategoryEntity['id']) => void;
|
||||
onDeleteGroup?: (id: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup?: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onDeleteGroup: (id: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onReorderCategory: OnDropCallback;
|
||||
onReorderGroup: OnDropCallback;
|
||||
};
|
||||
@@ -86,7 +64,6 @@ export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
({
|
||||
categoryGroups,
|
||||
editingCell,
|
||||
dataComponents,
|
||||
onBudgetAction,
|
||||
onShowActivity,
|
||||
onEditName,
|
||||
@@ -319,7 +296,6 @@ export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
group={item.value}
|
||||
editingCell={editingCell}
|
||||
collapsed={collapsedGroupIds.includes(item.value.id)}
|
||||
MonthComponent={dataComponents.ExpenseGroupComponent}
|
||||
dragState={dragState}
|
||||
onEditName={onEditName}
|
||||
onSave={_onSaveGroup}
|
||||
@@ -339,7 +315,6 @@ export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
cat={item.value}
|
||||
categoryGroup={item.group}
|
||||
editingCell={editingCell}
|
||||
MonthComponent={dataComponents.ExpenseCategoryComponent}
|
||||
dragState={dragState}
|
||||
onEditName={onEditName}
|
||||
onEditMonth={onEditMonth}
|
||||
@@ -360,10 +335,7 @@ export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
backgroundColor: theme.tableBackground,
|
||||
}}
|
||||
>
|
||||
<IncomeHeader
|
||||
MonthComponent={dataComponents.IncomeHeaderComponent}
|
||||
onShowNewGroup={onShowNewGroup}
|
||||
/>
|
||||
<IncomeHeader onShowNewGroup={onShowNewGroup} />
|
||||
</View>
|
||||
);
|
||||
break;
|
||||
@@ -372,7 +344,6 @@ export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
<IncomeGroup
|
||||
group={item.value}
|
||||
editingCell={editingCell}
|
||||
MonthComponent={dataComponents.IncomeGroupComponent}
|
||||
collapsed={collapsedGroupIds.includes(item.value.id)}
|
||||
onEditName={onEditName!}
|
||||
onSave={_onSaveGroup}
|
||||
@@ -387,8 +358,7 @@ export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
cat={item.value}
|
||||
editingCell={editingCell}
|
||||
isLast={idx === items.length - 1}
|
||||
MonthComponent={dataComponents.IncomeCategoryComponent}
|
||||
onEditName={onEditName!}
|
||||
onEditName={onEditName}
|
||||
onEditMonth={onEditMonth}
|
||||
onSave={_onSaveCategory}
|
||||
onDelete={onDeleteCategory}
|
||||
|
||||
@@ -12,17 +12,13 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { addMonths, subMonths } from 'loot-core/shared/months';
|
||||
|
||||
import { type BudgetSummary as EnvelopeBudgetSummary } from './envelope/budgetsummary/BudgetSummary';
|
||||
import { MonthsContext } from './MonthsContext';
|
||||
import { type BudgetSummary as TrackingBudgetSummary } from './tracking/budgetsummary/BudgetSummary';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import { useResizeObserver } from '@desktop-client/hooks/useResizeObserver';
|
||||
|
||||
type BudgetSummariesProps = {
|
||||
SummaryComponent: typeof TrackingBudgetSummary | typeof EnvelopeBudgetSummary;
|
||||
};
|
||||
|
||||
export function BudgetSummaries({ SummaryComponent }: BudgetSummariesProps) {
|
||||
export function BudgetSummaries() {
|
||||
const { months } = useContext(MonthsContext);
|
||||
|
||||
const [widthState, setWidthState] = useState(0);
|
||||
@@ -65,6 +61,8 @@ export function BudgetSummaries({ SummaryComponent }: BudgetSummariesProps) {
|
||||
spring.start({ from: { x: -monthWidth }, to: { x: -monthWidth } });
|
||||
}, [monthWidth]);
|
||||
|
||||
const { SummaryComponent } = useBudgetComponents();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css([
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
type ComponentPropsWithoutRef,
|
||||
type ComponentProps,
|
||||
type KeyboardEvent,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { type KeyboardEvent, useMemo, useState } from 'react';
|
||||
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
@@ -39,19 +33,13 @@ type BudgetTableProps = {
|
||||
startMonth: string;
|
||||
numMonths: number;
|
||||
monthBounds: MonthBounds;
|
||||
dataComponents: {
|
||||
SummaryComponent: ComponentPropsWithoutRef<
|
||||
typeof BudgetSummaries
|
||||
>['SummaryComponent'];
|
||||
BudgetTotalsComponent: ComponentPropsWithoutRef<
|
||||
typeof BudgetTotals
|
||||
>['MonthComponent'];
|
||||
} & ComponentProps<typeof BudgetCategories>['dataComponents'];
|
||||
onSaveCategory: (category: CategoryEntity) => void;
|
||||
onDeleteCategory: (id: CategoryEntity['id']) => void;
|
||||
onSaveGroup: (group: CategoryGroupEntity) => void;
|
||||
onDeleteGroup: (id: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onApplyBudgetTemplatesInGroup: (
|
||||
categoryIds: Array<CategoryEntity['id']>,
|
||||
) => void;
|
||||
onReorderCategory: (params: {
|
||||
id: CategoryEntity['id'];
|
||||
groupId?: CategoryGroupEntity['id'];
|
||||
@@ -72,7 +60,6 @@ export function BudgetTable(props: BudgetTableProps) {
|
||||
startMonth,
|
||||
numMonths,
|
||||
monthBounds,
|
||||
dataComponents,
|
||||
onSaveCategory,
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
@@ -265,7 +252,7 @@ export function BudgetTable(props: BudgetTableProps) {
|
||||
monthBounds={monthBounds}
|
||||
type={type}
|
||||
>
|
||||
<BudgetSummaries SummaryComponent={dataComponents.SummaryComponent} />
|
||||
<BudgetSummaries />
|
||||
</MonthsProvider>
|
||||
</View>
|
||||
|
||||
@@ -276,7 +263,6 @@ export function BudgetTable(props: BudgetTableProps) {
|
||||
type={type}
|
||||
>
|
||||
<BudgetTotals
|
||||
MonthComponent={dataComponents.BudgetTotalsComponent}
|
||||
toggleHiddenCategories={toggleHiddenCategories}
|
||||
expandAllCategories={expandAllCategories}
|
||||
collapseAllCategories={collapseAllCategories}
|
||||
@@ -300,7 +286,6 @@ export function BudgetTable(props: BudgetTableProps) {
|
||||
<BudgetCategories
|
||||
categoryGroups={categoryGroups}
|
||||
editingCell={editing}
|
||||
dataComponents={dataComponents}
|
||||
onEditMonth={onEditMonth}
|
||||
onEditName={onEditName}
|
||||
onSaveCategory={onSaveCategory}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type ComponentProps, memo, useRef, useState } from 'react';
|
||||
import React, { memo, useRef, useState } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
@@ -17,17 +17,17 @@ import { View } from '@actual-app/components/view';
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
import { getScrollbarWidth } from './util';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||
|
||||
type BudgetTotalsProps = {
|
||||
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
|
||||
toggleHiddenCategories: () => void;
|
||||
expandAllCategories: () => void;
|
||||
collapseAllCategories: () => void;
|
||||
};
|
||||
|
||||
export const BudgetTotals = memo(function BudgetTotals({
|
||||
MonthComponent,
|
||||
toggleHiddenCategories,
|
||||
expandAllCategories,
|
||||
collapseAllCategories,
|
||||
@@ -57,6 +57,8 @@ export const BudgetTotals = memo(function BudgetTotals({
|
||||
}
|
||||
};
|
||||
|
||||
const { BudgetTotalsComponent: MonthComponent } = useBudgetComponents();
|
||||
|
||||
return (
|
||||
<View
|
||||
data-testid="budget-totals"
|
||||
@@ -177,7 +179,9 @@ export const BudgetTotals = memo(function BudgetTotals({
|
||||
/>
|
||||
</Popover>
|
||||
</View>
|
||||
<RenderMonths component={MonthComponent} />
|
||||
<RenderMonths>
|
||||
<MonthComponent />
|
||||
</RenderMonths>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,12 +31,12 @@ function getNumPossibleMonths(width: number, categoryWidth: number) {
|
||||
return 6;
|
||||
}
|
||||
|
||||
type DynamicBudgetTableInnerProps = {
|
||||
type DynamicBudgetTableProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
} & DynamicBudgetTableProps;
|
||||
} & AutoSizingBudgetTableProps;
|
||||
|
||||
const DynamicBudgetTableInner = ({
|
||||
const DynamicBudgetTable = ({
|
||||
type,
|
||||
width,
|
||||
height,
|
||||
@@ -46,7 +46,7 @@ const DynamicBudgetTableInner = ({
|
||||
monthBounds,
|
||||
onMonthSelect,
|
||||
...props
|
||||
}: DynamicBudgetTableInnerProps) => {
|
||||
}: DynamicBudgetTableProps) => {
|
||||
const { setDisplayMax } = useBudgetMonthCount();
|
||||
const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState');
|
||||
const categoryExpandedState = categoryExpandedStatePref ?? 0;
|
||||
@@ -150,9 +150,9 @@ const DynamicBudgetTableInner = ({
|
||||
);
|
||||
};
|
||||
|
||||
DynamicBudgetTableInner.displayName = 'DynamicBudgetTableInner';
|
||||
DynamicBudgetTable.displayName = 'DynamicBudgetTable';
|
||||
|
||||
type DynamicBudgetTableProps = Omit<
|
||||
type AutoSizingBudgetTableProps = Omit<
|
||||
ComponentProps<typeof BudgetTable>,
|
||||
'numMonths'
|
||||
> & {
|
||||
@@ -160,14 +160,14 @@ type DynamicBudgetTableProps = Omit<
|
||||
onMonthSelect: (month: string, numMonths: number) => void;
|
||||
};
|
||||
|
||||
export const DynamicBudgetTable = (props: DynamicBudgetTableProps) => {
|
||||
export const AutoSizingBudgetTable = (props: AutoSizingBudgetTableProps) => {
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<DynamicBudgetTableInner width={width} height={height} {...props} />
|
||||
<DynamicBudgetTable width={width} height={height} {...props} />
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
||||
|
||||
DynamicBudgetTable.displayName = 'DynamicBudgetTable';
|
||||
AutoSizingBudgetTable.displayName = 'AutoSizingBudgetTable';
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
import { SidebarCategory } from './SidebarCategory';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import {
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
@@ -28,7 +30,6 @@ type ExpenseCategoryProps = {
|
||||
categoryGroup?: CategoryGroupEntity;
|
||||
editingCell: { id: string; cell: string } | null;
|
||||
dragState: DragState<CategoryEntity> | DragState<CategoryGroupEntity> | null;
|
||||
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
|
||||
onEditName?: ComponentProps<typeof SidebarCategory>['onEditName'];
|
||||
onEditMonth?: (id: CategoryEntity['id'], month: string) => void;
|
||||
onSave?: ComponentProps<typeof SidebarCategory>['onSave'];
|
||||
@@ -44,7 +45,6 @@ export function ExpenseCategory({
|
||||
categoryGroup,
|
||||
editingCell,
|
||||
dragState,
|
||||
MonthComponent,
|
||||
onEditName,
|
||||
onEditMonth,
|
||||
onSave,
|
||||
@@ -74,6 +74,8 @@ export function ExpenseCategory({
|
||||
onDrop: onReorder,
|
||||
});
|
||||
|
||||
const { ExpenseCategoryComponent: MonthComponent } = useBudgetComponents();
|
||||
|
||||
return (
|
||||
<Row
|
||||
innerRef={dropRef}
|
||||
@@ -102,18 +104,22 @@ export function ExpenseCategory({
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
<RenderMonths
|
||||
component={MonthComponent}
|
||||
editingMonth={
|
||||
editingCell && editingCell.id === cat.id && editingCell.cell
|
||||
}
|
||||
args={{
|
||||
category: cat,
|
||||
onEdit: onEditMonth,
|
||||
onBudgetAction,
|
||||
onShowActivity,
|
||||
}}
|
||||
/>
|
||||
<RenderMonths>
|
||||
{({ month }) => (
|
||||
<MonthComponent
|
||||
month={month}
|
||||
editing={
|
||||
editingCell &&
|
||||
editingCell.id === cat.id &&
|
||||
editingCell.cell === month
|
||||
}
|
||||
category={cat}
|
||||
onEdit={onEditMonth}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivity}
|
||||
/>
|
||||
)}
|
||||
</RenderMonths>
|
||||
</View>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
import { SidebarGroup } from './SidebarGroup';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import {
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
@@ -28,7 +30,6 @@ type ExpenseGroupProps = {
|
||||
collapsed: boolean;
|
||||
editingCell: { id: string; cell: string } | null;
|
||||
dragState: DragState<CategoryEntity> | DragState<CategoryGroupEntity> | null;
|
||||
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
|
||||
onEditName?: ComponentProps<typeof SidebarGroup>['onEdit'];
|
||||
onSave?: ComponentProps<typeof SidebarGroup>['onSave'];
|
||||
onDelete?: ComponentProps<typeof SidebarGroup>['onDelete'];
|
||||
@@ -49,7 +50,6 @@ export function ExpenseGroup({
|
||||
collapsed,
|
||||
editingCell,
|
||||
dragState,
|
||||
MonthComponent,
|
||||
onEditName,
|
||||
onSave,
|
||||
onDelete,
|
||||
@@ -87,6 +87,8 @@ export function ExpenseGroup({
|
||||
},
|
||||
});
|
||||
|
||||
const { ExpenseGroupComponent: MonthComponent } = useBudgetComponents();
|
||||
|
||||
return (
|
||||
<Row
|
||||
collapsed={true}
|
||||
@@ -140,7 +142,9 @@ export function ExpenseGroup({
|
||||
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
/>
|
||||
<RenderMonths component={MonthComponent} args={{ group }} />
|
||||
<RenderMonths>
|
||||
{({ month }) => <MonthComponent month={month} group={group} />}
|
||||
</RenderMonths>
|
||||
</View>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ import { type CategoryEntity } from 'loot-core/types/models';
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
import { SidebarCategory } from './SidebarCategory';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import {
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
@@ -20,7 +22,6 @@ type IncomeCategoryProps = {
|
||||
cat: CategoryEntity;
|
||||
isLast?: boolean;
|
||||
editingCell: { id: CategoryEntity['id']; cell: string } | null;
|
||||
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
|
||||
onEditName: ComponentProps<typeof SidebarCategory>['onEditName'];
|
||||
onEditMonth?: (id: CategoryEntity['id'], month: string) => void;
|
||||
onSave: ComponentProps<typeof SidebarCategory>['onSave'];
|
||||
@@ -35,7 +36,6 @@ export function IncomeCategory({
|
||||
cat,
|
||||
isLast,
|
||||
editingCell,
|
||||
MonthComponent,
|
||||
onEditName,
|
||||
onEditMonth,
|
||||
onSave,
|
||||
@@ -59,6 +59,8 @@ export function IncomeCategory({
|
||||
onDrop: onReorder,
|
||||
});
|
||||
|
||||
const { IncomeCategoryComponent: MonthComponent } = useBudgetComponents();
|
||||
|
||||
return (
|
||||
<Row
|
||||
innerRef={dropRef}
|
||||
@@ -82,19 +84,23 @@ export function IncomeCategory({
|
||||
onSave={onSave}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
<RenderMonths
|
||||
component={MonthComponent}
|
||||
editingMonth={
|
||||
editingCell && editingCell.id === cat.id && editingCell.cell
|
||||
}
|
||||
args={{
|
||||
category: cat,
|
||||
onEdit: onEditMonth,
|
||||
isLast,
|
||||
onShowActivity,
|
||||
onBudgetAction,
|
||||
}}
|
||||
/>
|
||||
<RenderMonths>
|
||||
{({ month }) => (
|
||||
<MonthComponent
|
||||
month={month}
|
||||
editing={
|
||||
editingCell &&
|
||||
editingCell.id === cat.id &&
|
||||
editingCell.cell === month
|
||||
}
|
||||
category={cat}
|
||||
isLast={isLast}
|
||||
onEdit={onEditMonth}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivity}
|
||||
/>
|
||||
)}
|
||||
</RenderMonths>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { type JSX } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
|
||||
@@ -8,13 +8,14 @@ import { type CategoryGroupEntity } from 'loot-core/types/models';
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
import { SidebarGroup } from './SidebarGroup';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import { Row } from '@desktop-client/components/table';
|
||||
|
||||
type IncomeGroupProps = {
|
||||
group: CategoryGroupEntity;
|
||||
editingCell: { id: CategoryGroupEntity['id']; cell: string } | null;
|
||||
collapsed: boolean;
|
||||
MonthComponent: () => JSX.Element;
|
||||
onEditName: (id: CategoryGroupEntity['id']) => void;
|
||||
onSave: (group: CategoryGroupEntity) => void;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
@@ -25,12 +26,12 @@ export function IncomeGroup({
|
||||
group,
|
||||
editingCell,
|
||||
collapsed,
|
||||
MonthComponent,
|
||||
onEditName,
|
||||
onSave,
|
||||
onToggleCollapse,
|
||||
onShowNewCategory,
|
||||
}: IncomeGroupProps) {
|
||||
const { IncomeGroupComponent: MonthComponent } = useBudgetComponents();
|
||||
return (
|
||||
<Row
|
||||
collapsed={true}
|
||||
@@ -52,7 +53,9 @@ export function IncomeGroup({
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
/>
|
||||
<RenderMonths component={MonthComponent} args={{ group }} />
|
||||
<RenderMonths>
|
||||
{({ month }) => <MonthComponent month={month} group={group} />}
|
||||
</RenderMonths>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type JSX } from 'react';
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
@@ -6,19 +6,18 @@ import { View } from '@actual-app/components/view';
|
||||
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
|
||||
import { useBudgetComponents } from '.';
|
||||
|
||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||
|
||||
type IncomeHeaderProps = {
|
||||
MonthComponent?: () => JSX.Element;
|
||||
onShowNewGroup: () => void;
|
||||
};
|
||||
|
||||
export function IncomeHeader({
|
||||
MonthComponent,
|
||||
onShowNewGroup,
|
||||
}: IncomeHeaderProps) {
|
||||
export function IncomeHeader({ onShowNewGroup }: IncomeHeaderProps) {
|
||||
const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState');
|
||||
const categoryExpandedState = categoryExpandedStatePref ?? 0;
|
||||
const { IncomeHeaderComponent: MonthComponent } = useBudgetComponents();
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', flex: 1 }}>
|
||||
<View
|
||||
@@ -32,10 +31,9 @@ export function IncomeHeader({
|
||||
<Trans>Add group</Trans>
|
||||
</Button>
|
||||
</View>
|
||||
<RenderMonths
|
||||
component={MonthComponent}
|
||||
style={{ border: 0, justifyContent: 'flex-end' }}
|
||||
/>
|
||||
<RenderMonths style={{ border: 0, justifyContent: 'flex-end' }}>
|
||||
<MonthComponent />
|
||||
</RenderMonths>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import React, {
|
||||
useContext,
|
||||
type CSSProperties,
|
||||
type ComponentType,
|
||||
type JSX,
|
||||
} from 'react';
|
||||
import React, { type ReactNode, useContext, type CSSProperties } from 'react';
|
||||
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
@@ -16,35 +10,24 @@ import { MonthsContext } from './MonthsContext';
|
||||
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
|
||||
|
||||
type RenderMonthsProps = {
|
||||
component?: ComponentType<{ month: string; editing: boolean }>;
|
||||
editingMonth?: string;
|
||||
args?: object;
|
||||
children: ReactNode | (({ month }: { month: string }) => ReactNode);
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function RenderMonths({
|
||||
component: Component,
|
||||
editingMonth,
|
||||
args,
|
||||
style,
|
||||
}: RenderMonthsProps) {
|
||||
export function RenderMonths({ children, style }: RenderMonthsProps) {
|
||||
const { months } = useContext(MonthsContext);
|
||||
|
||||
return months.map((month, index) => {
|
||||
const editing = editingMonth === month;
|
||||
|
||||
return (
|
||||
<SheetNameProvider key={index} name={monthUtils.sheetForMonth(month)}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderLeft: '1px solid ' + theme.tableBorder,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Component month={month} editing={editing} {...args} />
|
||||
</View>
|
||||
</SheetNameProvider>
|
||||
);
|
||||
}) as unknown as JSX.Element;
|
||||
return months.map((month, index) => (
|
||||
<SheetNameProvider key={index} name={monthUtils.sheetForMonth(month)}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderLeft: '1px solid ' + theme.tableBorder,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{typeof children === 'function' ? children({ month }) : children}
|
||||
</View>
|
||||
</SheetNameProvider>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ import { css } from '@emotion/css';
|
||||
import { evalArithmetic } from 'loot-core/shared/arithmetic';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { integerToCurrency, amountToInteger } from 'loot-core/shared/util';
|
||||
import {
|
||||
type CategoryGroupEntity,
|
||||
type CategoryEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { type CategoryGroupMonthProps, type CategoryMonthProps } from '..';
|
||||
|
||||
import { BalanceMovementMenu } from './BalanceMovementMenu';
|
||||
import { BudgetMenu } from './BudgetMenu';
|
||||
@@ -152,14 +150,10 @@ export function IncomeHeaderMonth() {
|
||||
);
|
||||
}
|
||||
|
||||
type ExpenseGroupMonthProps = {
|
||||
month: string;
|
||||
group: CategoryGroupEntity;
|
||||
};
|
||||
export const ExpenseGroupMonth = memo(function ExpenseGroupMonth({
|
||||
month,
|
||||
group,
|
||||
}: ExpenseGroupMonthProps) {
|
||||
}: CategoryGroupMonthProps) {
|
||||
const { id } = group;
|
||||
|
||||
return (
|
||||
@@ -210,14 +204,6 @@ export const ExpenseGroupMonth = memo(function ExpenseGroupMonth({
|
||||
);
|
||||
});
|
||||
|
||||
type ExpenseCategoryMonthProps = {
|
||||
month: string;
|
||||
category: CategoryEntity;
|
||||
editing: boolean;
|
||||
onEdit: (id: CategoryEntity['id'] | null, month?: string) => void;
|
||||
onBudgetAction: (month: string, action: string, arg?: unknown) => void;
|
||||
onShowActivity: (id: CategoryEntity['id'], month: string) => void;
|
||||
};
|
||||
export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
||||
month,
|
||||
category,
|
||||
@@ -225,7 +211,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
||||
onEdit,
|
||||
onBudgetAction,
|
||||
onShowActivity,
|
||||
}: ExpenseCategoryMonthProps) {
|
||||
}: CategoryMonthProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const budgetMenuTriggerRef = useRef(null);
|
||||
@@ -550,20 +536,13 @@ export function IncomeGroupMonth({ month }: IncomeGroupMonthProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeCategoryMonthProps = {
|
||||
category: CategoryEntity;
|
||||
isLast: boolean;
|
||||
month: string;
|
||||
onShowActivity: (id: CategoryEntity['id'], month: string) => void;
|
||||
onBudgetAction: (month: string, action: string, arg?: unknown) => void;
|
||||
};
|
||||
export function IncomeCategoryMonth({
|
||||
category,
|
||||
isLast,
|
||||
month,
|
||||
onShowActivity,
|
||||
onBudgetAction,
|
||||
}: IncomeCategoryMonthProps) {
|
||||
}: CategoryMonthProps) {
|
||||
const incomeMenuTriggerRef = useRef(null);
|
||||
const {
|
||||
setMenuOpen: setIncomeMenuOpen,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { useMemo, useState, useEffect, type ComponentType } from 'react';
|
||||
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { DynamicBudgetTable } from './DynamicBudgetTable';
|
||||
import { AutoSizingBudgetTable } from './DynamicBudgetTable';
|
||||
import * as envelopeBudget from './envelope/EnvelopeBudgetComponents';
|
||||
import { EnvelopeBudgetProvider } from './envelope/EnvelopeBudgetContext';
|
||||
import * as trackingBudget from './tracking/TrackingBudgetComponents';
|
||||
@@ -17,59 +20,21 @@ import { prewarmAllMonths, prewarmMonth } from './util';
|
||||
|
||||
import {
|
||||
applyBudgetAction,
|
||||
createCategory,
|
||||
createCategoryGroup,
|
||||
deleteCategory,
|
||||
deleteCategoryGroup,
|
||||
getCategories,
|
||||
moveCategory,
|
||||
moveCategoryGroup,
|
||||
updateCategory,
|
||||
updateCategoryGroup,
|
||||
} from '@desktop-client/budget/budgetSlice';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useCategoryActions } from '@desktop-client/hooks/useCategoryActions';
|
||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
|
||||
import { useSpreadsheet } from '@desktop-client/hooks/useSpreadsheet';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
|
||||
type TrackingReportComponents = {
|
||||
SummaryComponent: typeof trackingBudget.BudgetSummary;
|
||||
ExpenseCategoryComponent: typeof trackingBudget.ExpenseCategoryMonth;
|
||||
ExpenseGroupComponent: typeof trackingBudget.ExpenseGroupMonth;
|
||||
IncomeCategoryComponent: typeof trackingBudget.IncomeCategoryMonth;
|
||||
IncomeGroupComponent: typeof trackingBudget.IncomeGroupMonth;
|
||||
BudgetTotalsComponent: typeof trackingBudget.BudgetTotalsMonth;
|
||||
IncomeHeaderComponent: typeof trackingBudget.IncomeHeaderMonth;
|
||||
};
|
||||
|
||||
type EnvelopeBudgetComponents = {
|
||||
SummaryComponent: typeof envelopeBudget.BudgetSummary;
|
||||
ExpenseCategoryComponent: typeof envelopeBudget.ExpenseCategoryMonth;
|
||||
ExpenseGroupComponent: typeof envelopeBudget.ExpenseGroupMonth;
|
||||
IncomeCategoryComponent: typeof envelopeBudget.IncomeCategoryMonth;
|
||||
IncomeGroupComponent: typeof envelopeBudget.IncomeGroupMonth;
|
||||
BudgetTotalsComponent: typeof envelopeBudget.BudgetTotalsMonth;
|
||||
IncomeHeaderComponent: typeof envelopeBudget.IncomeHeaderMonth;
|
||||
};
|
||||
|
||||
type BudgetInnerProps = {
|
||||
accountId?: string;
|
||||
trackingComponents: TrackingReportComponents;
|
||||
envelopeComponents: EnvelopeBudgetComponents;
|
||||
};
|
||||
|
||||
function BudgetInner(props: BudgetInnerProps) {
|
||||
const { t } = useTranslation();
|
||||
export function Budget() {
|
||||
const currentMonth = monthUtils.currentMonth();
|
||||
const spreadsheet = useSpreadsheet();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref(
|
||||
'budget.summaryCollapsed',
|
||||
);
|
||||
@@ -111,7 +76,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
setBounds({ start, end });
|
||||
}
|
||||
});
|
||||
}, [props.accountId]);
|
||||
}, []);
|
||||
|
||||
const onMonthSelect = async (month, numDisplayed) => {
|
||||
setStartMonthPref(month);
|
||||
@@ -145,116 +110,6 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const categoryNameAlreadyExistsNotification = name => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
notification: {
|
||||
type: 'error',
|
||||
message: t(
|
||||
'Category “{{name}}” already exists in group (it may be hidden)',
|
||||
{ name },
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveCategory = async category => {
|
||||
const cats = await send('get-categories');
|
||||
const exists =
|
||||
cats.grouped
|
||||
.filter(g => g.id === category.group)[0]
|
||||
.categories.filter(
|
||||
c => c.name.toUpperCase() === category.name.toUpperCase(),
|
||||
)
|
||||
.filter(c => (category.id === 'new' ? true : c.id !== category.id))
|
||||
.length > 0;
|
||||
|
||||
if (exists) {
|
||||
categoryNameAlreadyExistsNotification(category.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (category.id === 'new') {
|
||||
dispatch(
|
||||
createCategory({
|
||||
name: category.name,
|
||||
groupId: category.group,
|
||||
isIncome: category.is_income,
|
||||
isHidden: category.hidden,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(updateCategory({ category }));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteCategory = async id => {
|
||||
const mustTransfer = await send('must-category-transfer', { id });
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'confirm-category-delete',
|
||||
options: {
|
||||
category: id,
|
||||
onDelete: transferCategory => {
|
||||
if (id !== transferCategory) {
|
||||
dispatch(
|
||||
deleteCategory({ id, transferId: transferCategory }),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(deleteCategory({ id }));
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveGroup = group => {
|
||||
if (group.id === 'new') {
|
||||
dispatch(createCategoryGroup({ name: group.name }));
|
||||
} else {
|
||||
dispatch(updateCategoryGroup({ group }));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteGroup = async id => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
|
||||
let mustTransfer = false;
|
||||
for (const category of group.categories) {
|
||||
if (await send('must-category-transfer', { id: category.id })) {
|
||||
mustTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'confirm-category-delete',
|
||||
options: {
|
||||
group: id,
|
||||
onDelete: transferCategory => {
|
||||
dispatch(
|
||||
deleteCategoryGroup({ id, transferId: transferCategory }),
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(deleteCategoryGroup({ id }));
|
||||
}
|
||||
};
|
||||
|
||||
const onApplyBudgetTemplatesInGroup = async categories => {
|
||||
dispatch(
|
||||
applyBudgetAction({
|
||||
@@ -271,62 +126,19 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
dispatch(applyBudgetAction({ month, type, args }));
|
||||
};
|
||||
|
||||
const onShowActivity = (categoryId, month) => {
|
||||
const filterConditions = [
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
{
|
||||
field: 'date',
|
||||
op: 'is',
|
||||
value: month,
|
||||
options: { month: true },
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
filterConditions,
|
||||
categoryId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onReorderCategory = async sortInfo => {
|
||||
const cats = await send('get-categories');
|
||||
const moveCandidate = cats.list.filter(c => c.id === sortInfo.id)[0];
|
||||
const exists =
|
||||
cats.grouped
|
||||
.filter(g => g.id === sortInfo.groupId)[0]
|
||||
.categories.filter(
|
||||
c => c.name.toUpperCase() === moveCandidate.name.toUpperCase(),
|
||||
)
|
||||
.filter(c => c.id !== moveCandidate.id).length > 0;
|
||||
|
||||
if (exists) {
|
||||
categoryNameAlreadyExistsNotification(moveCandidate.name);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: sortInfo.id,
|
||||
groupId: sortInfo.groupId,
|
||||
targetId: sortInfo.targetId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onReorderGroup = async sortInfo => {
|
||||
dispatch(
|
||||
moveCategoryGroup({ id: sortInfo.id, targetId: sortInfo.targetId }),
|
||||
);
|
||||
};
|
||||
|
||||
const onToggleCollapse = () => {
|
||||
setSummaryCollapsedPref(!summaryCollapsed);
|
||||
};
|
||||
|
||||
const { trackingComponents, envelopeComponents } = props;
|
||||
const {
|
||||
onSaveCategory,
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
onDeleteGroup,
|
||||
onShowActivity,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
} = useCategoryActions();
|
||||
|
||||
if (!initialized || !categoryGroups) {
|
||||
return null;
|
||||
@@ -340,14 +152,12 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
onBudgetAction={onBudgetAction}
|
||||
onToggleSummaryCollapse={onToggleCollapse}
|
||||
>
|
||||
<DynamicBudgetTable
|
||||
<AutoSizingBudgetTable
|
||||
type={budgetType}
|
||||
prewarmStartMonth={startMonth}
|
||||
startMonth={startMonth}
|
||||
monthBounds={bounds}
|
||||
maxMonths={maxMonths}
|
||||
// @ts-expect-error fix me
|
||||
dataComponents={trackingComponents}
|
||||
onMonthSelect={onMonthSelect}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
@@ -368,14 +178,12 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
onBudgetAction={onBudgetAction}
|
||||
onToggleSummaryCollapse={onToggleCollapse}
|
||||
>
|
||||
<DynamicBudgetTable
|
||||
<AutoSizingBudgetTable
|
||||
type={budgetType}
|
||||
prewarmStartMonth={startMonth}
|
||||
startMonth={startMonth}
|
||||
monthBounds={bounds}
|
||||
maxMonths={maxMonths}
|
||||
// @ts-expect-error fix me
|
||||
dataComponents={envelopeComponents}
|
||||
onMonthSelect={onMonthSelect}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
@@ -393,13 +201,65 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
|
||||
return (
|
||||
<SheetNameProvider name={monthUtils.sheetForMonth(startMonth)}>
|
||||
<View style={{ flex: 1 }}>{table}</View>
|
||||
{/*
|
||||
In a previous iteration, the wrapper needs `overflow: hidden` for
|
||||
some reason. Without it at certain dimensions the width/height
|
||||
that autosizer gives us is slightly wrong, causing scrollbars to
|
||||
appear. We might not need it anymore?
|
||||
*/}
|
||||
<View
|
||||
style={{
|
||||
...styles.page,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>{table}</View>
|
||||
</View>
|
||||
</SheetNameProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function Budget() {
|
||||
const trackingComponents = useMemo<TrackingReportComponents>(
|
||||
export type BudgetSummaryProps = {
|
||||
month: string;
|
||||
};
|
||||
|
||||
export type CategoryMonthProps = {
|
||||
month: string;
|
||||
category: CategoryEntity;
|
||||
editing: boolean;
|
||||
isLast?: boolean;
|
||||
onEdit: (id: CategoryEntity['id'] | null, month?: string) => void;
|
||||
onBudgetAction: (month: string, action: string, arg: unknown) => void;
|
||||
onShowActivity: (id: CategoryEntity['id'], month: string) => void;
|
||||
};
|
||||
|
||||
export type CategoryGroupMonthProps = {
|
||||
month: string;
|
||||
group: CategoryGroupEntity;
|
||||
};
|
||||
|
||||
export type BudgetComponents = {
|
||||
SummaryComponent: ComponentType<BudgetSummaryProps>;
|
||||
ExpenseCategoryComponent: ComponentType<CategoryMonthProps>;
|
||||
ExpenseGroupComponent: ComponentType<CategoryGroupMonthProps>;
|
||||
IncomeCategoryComponent: ComponentType<CategoryMonthProps>;
|
||||
IncomeGroupComponent: ComponentType<CategoryGroupMonthProps>;
|
||||
BudgetTotalsComponent: ComponentType;
|
||||
IncomeHeaderComponent: ComponentType;
|
||||
};
|
||||
|
||||
export function useBudgetComponents(): BudgetComponents {
|
||||
const [budgetType = 'envelope'] = useSyncedPref('budgetType');
|
||||
const envelopeComponents = useEnvelopeBudgetComponents();
|
||||
const trackingComponents = useTrackingBudgetComponents();
|
||||
|
||||
return budgetType === 'envelope' ? envelopeComponents : trackingComponents;
|
||||
}
|
||||
|
||||
function useTrackingBudgetComponents(): BudgetComponents {
|
||||
return useMemo(
|
||||
() => ({
|
||||
SummaryComponent: trackingBudget.BudgetSummary,
|
||||
ExpenseCategoryComponent: trackingBudget.ExpenseCategoryMonth,
|
||||
@@ -409,10 +269,12 @@ export function Budget() {
|
||||
BudgetTotalsComponent: trackingBudget.BudgetTotalsMonth,
|
||||
IncomeHeaderComponent: trackingBudget.IncomeHeaderMonth,
|
||||
}),
|
||||
[trackingBudget],
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
const envelopeComponents = useMemo<EnvelopeBudgetComponents>(
|
||||
function useEnvelopeBudgetComponents(): BudgetComponents {
|
||||
return useMemo(
|
||||
() => ({
|
||||
SummaryComponent: envelopeBudget.BudgetSummary,
|
||||
ExpenseCategoryComponent: envelopeBudget.ExpenseCategoryMonth,
|
||||
@@ -422,26 +284,6 @@ export function Budget() {
|
||||
BudgetTotalsComponent: envelopeBudget.BudgetTotalsMonth,
|
||||
IncomeHeaderComponent: envelopeBudget.IncomeHeaderMonth,
|
||||
}),
|
||||
[envelopeBudget],
|
||||
);
|
||||
|
||||
// In a previous iteration, the wrapper needs `overflow: hidden` for
|
||||
// some reason. Without it at certain dimensions the width/height
|
||||
// that autosizer gives us is slightly wrong, causing scrollbars to
|
||||
// appear. We might not need it anymore?
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...styles.page,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<BudgetInner
|
||||
trackingComponents={trackingComponents}
|
||||
envelopeComponents={envelopeComponents}
|
||||
/>
|
||||
</View>
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,10 +24,8 @@ import { css } from '@emotion/css';
|
||||
import { evalArithmetic } from 'loot-core/shared/arithmetic';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { integerToCurrency, amountToInteger } from 'loot-core/shared/util';
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { type CategoryGroupMonthProps, type CategoryMonthProps } from '..';
|
||||
|
||||
import { BalanceMenu } from './BalanceMenu';
|
||||
import { BudgetMenu } from './BudgetMenu';
|
||||
@@ -149,14 +147,10 @@ export function IncomeHeaderMonth() {
|
||||
);
|
||||
}
|
||||
|
||||
type GroupMonthProps = {
|
||||
month: string;
|
||||
group: CategoryGroupEntity;
|
||||
};
|
||||
export const GroupMonth = memo(function GroupMonth({
|
||||
month,
|
||||
group,
|
||||
}: GroupMonthProps) {
|
||||
}: CategoryGroupMonthProps) {
|
||||
const { id } = group;
|
||||
|
||||
return (
|
||||
@@ -209,14 +203,6 @@ export const GroupMonth = memo(function GroupMonth({
|
||||
);
|
||||
});
|
||||
|
||||
type CategoryMonthProps = {
|
||||
month: string;
|
||||
category: CategoryEntity;
|
||||
editing: boolean;
|
||||
onEdit: (id: CategoryEntity['id'] | null, month?: string) => void;
|
||||
onBudgetAction: (month: string, action: string, arg: unknown) => void;
|
||||
onShowActivity: (id: CategoryEntity['id'], month: string) => void;
|
||||
};
|
||||
export const CategoryMonth = memo(function CategoryMonth({
|
||||
month,
|
||||
category,
|
||||
|
||||
228
packages/desktop-client/src/hooks/useCategoryActions.ts
Normal file
228
packages/desktop-client/src/hooks/useCategoryActions.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { useCategories } from './useCategories';
|
||||
import { useNavigate } from './useNavigate';
|
||||
|
||||
import {
|
||||
createCategory,
|
||||
createCategoryGroup,
|
||||
deleteCategory,
|
||||
deleteCategoryGroup,
|
||||
moveCategory,
|
||||
moveCategoryGroup,
|
||||
updateCategory,
|
||||
updateCategoryGroup,
|
||||
} from '@desktop-client/budget/budgetSlice';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
|
||||
export function useCategoryActions() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
|
||||
const categoryNameAlreadyExistsNotification = (
|
||||
name: CategoryEntity['name'],
|
||||
) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
notification: {
|
||||
type: 'error',
|
||||
message: t(
|
||||
'Category “{{name}}” already exists in group (it may be hidden)',
|
||||
{ name },
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveCategory = async (category: CategoryEntity) => {
|
||||
const { grouped: categoryGroups = [] } = await send('get-categories');
|
||||
|
||||
const group = categoryGroups.find(g => g.id === category.group);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCategories = group.categories ?? [];
|
||||
|
||||
const exists =
|
||||
groupCategories
|
||||
.filter(c => c.name.toUpperCase() === category.name.toUpperCase())
|
||||
.filter(c => (category.id === 'new' ? true : c.id !== category.id))
|
||||
.length > 0;
|
||||
|
||||
if (exists) {
|
||||
categoryNameAlreadyExistsNotification(category.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (category.id === 'new') {
|
||||
dispatch(
|
||||
createCategory({
|
||||
name: category.name,
|
||||
groupId: category.group,
|
||||
isIncome: !!category.is_income,
|
||||
isHidden: !!category.hidden,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(updateCategory({ category }));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteCategory = async (id: CategoryEntity['id']) => {
|
||||
const mustTransfer = await send('must-category-transfer', { id });
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'confirm-category-delete',
|
||||
options: {
|
||||
category: id,
|
||||
onDelete: transferCategory => {
|
||||
if (id !== transferCategory) {
|
||||
dispatch(
|
||||
deleteCategory({ id, transferId: transferCategory }),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(deleteCategory({ id }));
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveGroup = (group: CategoryGroupEntity) => {
|
||||
if (group.id === 'new') {
|
||||
dispatch(createCategoryGroup({ name: group.name }));
|
||||
} else {
|
||||
dispatch(updateCategoryGroup({ group }));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteGroup = async (id: CategoryGroupEntity['id']) => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCategories = group.categories ?? [];
|
||||
|
||||
let mustTransfer = false;
|
||||
for (const category of groupCategories) {
|
||||
if (await send('must-category-transfer', { id: category.id })) {
|
||||
mustTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'confirm-category-delete',
|
||||
options: {
|
||||
group: id,
|
||||
onDelete: transferCategory => {
|
||||
dispatch(
|
||||
deleteCategoryGroup({ id, transferId: transferCategory }),
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(deleteCategoryGroup({ id }));
|
||||
}
|
||||
};
|
||||
|
||||
const onShowActivity = (categoryId: CategoryEntity['id'], month: string) => {
|
||||
const filterConditions = [
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
{
|
||||
field: 'date',
|
||||
op: 'is',
|
||||
value: month,
|
||||
options: { month: true },
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
filterConditions,
|
||||
categoryId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onReorderCategory = async (sortInfo: {
|
||||
id: CategoryEntity['id'];
|
||||
groupId?: CategoryGroupEntity['id'];
|
||||
targetId: CategoryEntity['id'] | null;
|
||||
}) => {
|
||||
const { grouped: categoryGroups = [], list: categories = [] } =
|
||||
await send('get-categories');
|
||||
|
||||
const moveCandidate = categories.find(c => c.id === sortInfo.id);
|
||||
const group = categoryGroups.find(g => g.id === sortInfo.groupId);
|
||||
|
||||
if (!moveCandidate || !group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCategories = group.categories ?? [];
|
||||
|
||||
const exists =
|
||||
groupCategories
|
||||
.filter(c => c.name.toUpperCase() === moveCandidate.name.toUpperCase())
|
||||
.filter(c => c.id !== moveCandidate.id).length > 0;
|
||||
|
||||
if (exists) {
|
||||
categoryNameAlreadyExistsNotification(moveCandidate.name);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: moveCandidate.id,
|
||||
groupId: group.id,
|
||||
targetId: sortInfo.targetId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onReorderGroup = async (sortInfo: {
|
||||
id: CategoryGroupEntity['id'];
|
||||
targetId: CategoryGroupEntity['id'] | null;
|
||||
}) => {
|
||||
dispatch(
|
||||
moveCategoryGroup({ id: sortInfo.id, targetId: sortInfo.targetId }),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
onSaveCategory,
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
onDeleteGroup,
|
||||
onShowActivity,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
};
|
||||
}
|
||||
6
upcoming-release-notes/6022.md
Normal file
6
upcoming-release-notes/6022.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Update types for budget table components + new hooks to reduce prop drilling
|
||||
Reference in New Issue
Block a user