mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-29 17:30:04 -05:00
♻️ (typescript) Convert BudgetCategories to TypeScript (#5961)
This commit is contained in:
committed by
GitHub
parent
1a845583ef
commit
b690302998
@@ -1,9 +1,19 @@
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
import React, {
|
||||
memo,
|
||||
useState,
|
||||
useMemo,
|
||||
type ComponentPropsWithoutRef,
|
||||
} from 'react';
|
||||
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { ExpenseCategory } from './ExpenseCategory';
|
||||
import { ExpenseGroup } from './ExpenseGroup';
|
||||
import { IncomeCategory } from './IncomeCategory';
|
||||
@@ -13,11 +23,66 @@ import { SidebarCategory } from './SidebarCategory';
|
||||
import { SidebarGroup } from './SidebarGroup';
|
||||
import { separateGroups } from './util';
|
||||
|
||||
import { DropHighlightPosContext } from '@desktop-client/components/sort';
|
||||
import {
|
||||
DropHighlightPosContext,
|
||||
type DragState,
|
||||
type OnDropCallback,
|
||||
} from '@desktop-client/components/sort';
|
||||
import { Row } from '@desktop-client/components/table';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
|
||||
export const BudgetCategories = memo(
|
||||
type BudgetItem =
|
||||
| { type: 'new-group' }
|
||||
| { type: 'new-category' }
|
||||
| { type: 'expense-group'; value: CategoryGroupEntity }
|
||||
| {
|
||||
type: 'expense-category';
|
||||
value: CategoryEntity;
|
||||
group: CategoryGroupEntity;
|
||||
}
|
||||
| { type: 'income-separator' }
|
||||
| { type: 'income-group'; value: CategoryGroupEntity }
|
||||
| { type: 'income-category'; value: CategoryEntity };
|
||||
|
||||
type LocalDragState =
|
||||
| DragState<CategoryEntity>
|
||||
| DragState<CategoryGroupEntity>
|
||||
| null;
|
||||
|
||||
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;
|
||||
onDeleteCategory: (id: CategoryEntity['id']) => void;
|
||||
onDeleteGroup?: (id: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup?: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onReorderCategory: OnDropCallback;
|
||||
onReorderGroup: OnDropCallback;
|
||||
};
|
||||
|
||||
export const BudgetCategories = memo<BudgetCategoriesProps>(
|
||||
({
|
||||
categoryGroups,
|
||||
editingCell,
|
||||
@@ -37,27 +102,31 @@ export const BudgetCategories = memo(
|
||||
const [collapsedGroupIds = [], setCollapsedGroupIdsPref] =
|
||||
useLocalPref('budget.collapsed');
|
||||
const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories');
|
||||
function onCollapse(value) {
|
||||
function onCollapse(value: Array<CategoryGroupEntity['id']>) {
|
||||
setCollapsedGroupIdsPref(value);
|
||||
}
|
||||
|
||||
const [isAddingGroup, setIsAddingGroup] = useState(false);
|
||||
const [newCategoryForGroup, setNewCategoryForGroup] = useState(null);
|
||||
const items = useMemo(() => {
|
||||
const [newCategoryForGroup, setNewCategoryForGroup] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const items: BudgetItem[] = useMemo(() => {
|
||||
const [expenseGroups, incomeGroup] = separateGroups(categoryGroups);
|
||||
|
||||
let items = Array.prototype.concat.apply(
|
||||
let items: BudgetItem[] = Array.prototype.concat.apply(
|
||||
[],
|
||||
expenseGroups.map(group => {
|
||||
if (group.hidden && !showHiddenCategories) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupCategories = group.categories.filter(
|
||||
const groupCategories = group.categories?.filter(
|
||||
cat => showHiddenCategories || !cat.hidden,
|
||||
);
|
||||
|
||||
const items = [{ type: 'expense-group', value: { ...group } }];
|
||||
const items: BudgetItem[] = [
|
||||
{ type: 'expense-group', value: { ...group } },
|
||||
];
|
||||
|
||||
if (newCategoryForGroup === group.id) {
|
||||
items.push({ type: 'new-category' });
|
||||
@@ -67,12 +136,14 @@ export const BudgetCategories = memo(
|
||||
...items,
|
||||
...(collapsedGroupIds.includes(group.id)
|
||||
? []
|
||||
: groupCategories
|
||||
).map(cat => ({
|
||||
type: 'expense-category',
|
||||
value: cat,
|
||||
group,
|
||||
})),
|
||||
: groupCategories || []
|
||||
).map(
|
||||
(cat): BudgetItem => ({
|
||||
type: 'expense-category',
|
||||
value: cat,
|
||||
group,
|
||||
}),
|
||||
),
|
||||
];
|
||||
}),
|
||||
);
|
||||
@@ -82,22 +153,30 @@ export const BudgetCategories = memo(
|
||||
}
|
||||
|
||||
if (incomeGroup) {
|
||||
items = items.concat(
|
||||
[
|
||||
{ type: 'income-separator' },
|
||||
{ type: 'income-group', value: incomeGroup },
|
||||
newCategoryForGroup === incomeGroup.id && { type: 'new-category' },
|
||||
...(collapsedGroupIds.includes(incomeGroup.id)
|
||||
? []
|
||||
: incomeGroup.categories.filter(
|
||||
cat => showHiddenCategories || !cat.hidden,
|
||||
)
|
||||
).map(cat => ({
|
||||
const incomeCategoryItems: BudgetItem[] = [
|
||||
{ type: 'income-separator' },
|
||||
{ type: 'income-group', value: incomeGroup },
|
||||
];
|
||||
|
||||
if (newCategoryForGroup === incomeGroup.id) {
|
||||
incomeCategoryItems.push({ type: 'new-category' });
|
||||
}
|
||||
|
||||
incomeCategoryItems.push(
|
||||
...(collapsedGroupIds.includes(incomeGroup.id)
|
||||
? []
|
||||
: incomeGroup.categories?.filter(
|
||||
cat => showHiddenCategories || !cat.hidden,
|
||||
) || []
|
||||
).map(
|
||||
(cat): BudgetItem => ({
|
||||
type: 'income-category',
|
||||
value: cat,
|
||||
})),
|
||||
].filter(x => x),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
items = items.concat(incomeCategoryItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -109,15 +188,20 @@ export const BudgetCategories = memo(
|
||||
showHiddenCategories,
|
||||
]);
|
||||
|
||||
const [dragState, setDragState] = useState(null);
|
||||
const [savedCollapsed, setSavedCollapsed] = useState(null);
|
||||
const [dragState, setDragState] = useState<LocalDragState>(null);
|
||||
const [savedCollapsed, setSavedCollapsed] = useState<Array<
|
||||
CategoryGroupEntity['id']
|
||||
> | null>(null);
|
||||
|
||||
// TODO: If we turn this into a reducer, we could probably memoize
|
||||
// each item in the list for better perf
|
||||
function onDragChange(newDragState) {
|
||||
function onDragChange(
|
||||
newDragState: DragState<CategoryEntity> | DragState<CategoryGroupEntity>,
|
||||
) {
|
||||
const { state } = newDragState;
|
||||
|
||||
if (state === 'start-preview') {
|
||||
// @ts-expect-error fix me
|
||||
setDragState({
|
||||
type: newDragState.type,
|
||||
item: newDragState.item,
|
||||
@@ -131,19 +215,13 @@ export const BudgetCategories = memo(
|
||||
});
|
||||
setSavedCollapsed(collapsedGroupIds);
|
||||
}
|
||||
} else if (state === 'hover') {
|
||||
setDragState({
|
||||
...dragState,
|
||||
hoveredId: newDragState.id,
|
||||
hoveredPos: newDragState.pos,
|
||||
});
|
||||
} else if (state === 'end') {
|
||||
setDragState(null);
|
||||
onCollapse(savedCollapsed || []);
|
||||
}
|
||||
}
|
||||
|
||||
function onToggleCollapse(id) {
|
||||
function onToggleCollapse(id: CategoryGroupEntity['id']) {
|
||||
if (collapsedGroupIds.includes(id)) {
|
||||
onCollapse(collapsedGroupIds.filter(id_ => id_ !== id));
|
||||
} else {
|
||||
@@ -159,14 +237,14 @@ export const BudgetCategories = memo(
|
||||
setIsAddingGroup(false);
|
||||
}
|
||||
|
||||
function _onSaveGroup(group) {
|
||||
function _onSaveGroup(group: CategoryGroupEntity) {
|
||||
onSaveGroup?.(group);
|
||||
if (group.id === 'new') {
|
||||
onHideNewGroup();
|
||||
}
|
||||
}
|
||||
|
||||
function onShowNewCategory(groupId) {
|
||||
function onShowNewCategory(groupId: CategoryGroupEntity['id']) {
|
||||
onCollapse(collapsedGroupIds.filter(c => c !== groupId));
|
||||
setNewCategoryForGroup(groupId);
|
||||
}
|
||||
@@ -175,7 +253,7 @@ export const BudgetCategories = memo(
|
||||
setNewCategoryForGroup(null);
|
||||
}
|
||||
|
||||
function _onSaveCategory(category) {
|
||||
function _onSaveCategory(category: CategoryEntity) {
|
||||
onSaveCategory?.(category);
|
||||
if (category.id === 'new') {
|
||||
onHideNewCategory();
|
||||
@@ -203,6 +281,7 @@ export const BudgetCategories = memo(
|
||||
>
|
||||
<SidebarGroup
|
||||
group={{ id: 'new', name: '' }}
|
||||
collapsed={false}
|
||||
editing={true}
|
||||
onSave={_onSaveGroup}
|
||||
onHideNewGroup={onHideNewGroup}
|
||||
@@ -215,18 +294,20 @@ export const BudgetCategories = memo(
|
||||
content = (
|
||||
<Row>
|
||||
<SidebarCategory
|
||||
innerRef={null}
|
||||
category={{
|
||||
name: '',
|
||||
group: newCategoryForGroup,
|
||||
group: newCategoryForGroup!,
|
||||
is_income:
|
||||
newCategoryForGroup ===
|
||||
categoryGroups.find(g => g.is_income).id,
|
||||
categoryGroups.find(g => g.is_income)?.id,
|
||||
id: 'new',
|
||||
}}
|
||||
editing={true}
|
||||
onSave={_onSaveCategory}
|
||||
onDelete={async () => {}}
|
||||
onHideNewCategory={onHideNewCategory}
|
||||
onEditName={onEditName}
|
||||
onEditName={onEditName!}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
@@ -293,10 +374,10 @@ export const BudgetCategories = memo(
|
||||
editingCell={editingCell}
|
||||
MonthComponent={dataComponents.IncomeGroupComponent}
|
||||
collapsed={collapsedGroupIds.includes(item.value.id)}
|
||||
onEditName={onEditName}
|
||||
onEditName={onEditName!}
|
||||
onSave={_onSaveGroup}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
onShowNewCategory={onShowNewCategory!}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -307,7 +388,7 @@ export const BudgetCategories = memo(
|
||||
editingCell={editingCell}
|
||||
isLast={idx === items.length - 1}
|
||||
MonthComponent={dataComponents.IncomeCategoryComponent}
|
||||
onEditName={onEditName}
|
||||
onEditName={onEditName!}
|
||||
onEditMonth={onEditMonth}
|
||||
onSave={_onSaveCategory}
|
||||
onDelete={onDeleteCategory}
|
||||
@@ -319,6 +400,7 @@ export const BudgetCategories = memo(
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// @ts-expect-error Error is expected here because "item.type" is "never"
|
||||
throw new Error('Unknown item type: ' + item.type);
|
||||
}
|
||||
|
||||
@@ -328,7 +410,7 @@ export const BudgetCategories = memo(
|
||||
return (
|
||||
<DropHighlightPosContext.Provider
|
||||
key={
|
||||
item.value
|
||||
'value' in item
|
||||
? item.value.id
|
||||
: item.type === 'income-separator'
|
||||
? 'separator'
|
||||
@@ -338,9 +420,11 @@ export const BudgetCategories = memo(
|
||||
>
|
||||
<View
|
||||
style={
|
||||
!dragState && {
|
||||
':hover': { backgroundColor: theme.tableBackground },
|
||||
}
|
||||
dragState
|
||||
? {}
|
||||
: {
|
||||
':hover': { backgroundColor: theme.tableBackground },
|
||||
}
|
||||
}
|
||||
>
|
||||
{content}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {
|
||||
type ComponentPropsWithoutRef,
|
||||
type ComponentProps,
|
||||
type KeyboardEvent,
|
||||
useMemo,
|
||||
useState,
|
||||
@@ -45,12 +46,12 @@ type BudgetTableProps = {
|
||||
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: (groupId: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onReorderCategory: (params: {
|
||||
id: CategoryEntity['id'];
|
||||
groupId?: CategoryGroupEntity['id'];
|
||||
@@ -297,7 +298,6 @@ export function BudgetTable(props: BudgetTableProps) {
|
||||
>
|
||||
<SchedulesProvider query={schedulesQuery}>
|
||||
<BudgetCategories
|
||||
// @ts-expect-error Fix when migrating BudgetCategories to ts
|
||||
categoryGroups={categoryGroups}
|
||||
editingCell={editing}
|
||||
dataComponents={dataComponents}
|
||||
|
||||
@@ -27,15 +27,15 @@ type ExpenseCategoryProps = {
|
||||
cat: CategoryEntity;
|
||||
categoryGroup?: CategoryGroupEntity;
|
||||
editingCell: { id: string; cell: string } | null;
|
||||
dragState: DragState<CategoryEntity>;
|
||||
dragState: DragState<CategoryEntity> | DragState<CategoryGroupEntity> | null;
|
||||
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
|
||||
onEditName?: ComponentProps<typeof SidebarCategory>['onEditName'];
|
||||
onEditMonth?: (id: string, month: string) => void;
|
||||
onEditMonth?: (id: CategoryEntity['id'], month: string) => void;
|
||||
onSave?: ComponentProps<typeof SidebarCategory>['onSave'];
|
||||
onDelete?: ComponentProps<typeof SidebarCategory>['onDelete'];
|
||||
onDragChange: OnDragChangeCallback<CategoryEntity>;
|
||||
onBudgetAction: (month: number, action: string, arg: unknown) => void;
|
||||
onShowActivity: (id: string, month: string) => void;
|
||||
onBudgetAction: (month: string, action: string, arg: unknown) => void;
|
||||
onShowActivity: (id: CategoryEntity['id'], month: string) => void;
|
||||
onReorder: OnDropCallback;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ import React, { type ComponentProps } from 'react';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { RenderMonths } from './RenderMonths';
|
||||
import { SidebarGroup } from './SidebarGroup';
|
||||
|
||||
@@ -22,7 +27,7 @@ type ExpenseGroupProps = {
|
||||
group: ComponentProps<typeof SidebarGroup>['group'];
|
||||
collapsed: boolean;
|
||||
editingCell: { id: string; cell: string } | null;
|
||||
dragState: DragState<ComponentProps<typeof SidebarGroup>['group']>;
|
||||
dragState: DragState<CategoryEntity> | DragState<CategoryGroupEntity> | null;
|
||||
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
|
||||
onEditName?: ComponentProps<typeof SidebarGroup>['onEdit'];
|
||||
onSave?: ComponentProps<typeof SidebarGroup>['onSave'];
|
||||
|
||||
@@ -16,7 +16,7 @@ type IncomeGroupProps = {
|
||||
collapsed: boolean;
|
||||
MonthComponent: () => JSX.Element;
|
||||
onEditName: (id: CategoryGroupEntity['id']) => void;
|
||||
onSave: (group: CategoryGroupEntity) => Promise<void>;
|
||||
onSave: (group: CategoryGroupEntity) => void;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
onShowNewCategory: (groupId: CategoryGroupEntity['id']) => void;
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ type SidebarCategoryProps = {
|
||||
isLast?: boolean;
|
||||
onEditName: (id: CategoryEntity['id']) => void;
|
||||
onSave: (category: CategoryEntity) => void;
|
||||
onDelete: (id: CategoryEntity['id']) => Promise<void>;
|
||||
onDelete: (id: CategoryEntity['id']) => void;
|
||||
onHideNewCategory?: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ type SidebarGroupProps = {
|
||||
innerRef?: RefCallback<HTMLDivElement>;
|
||||
style?: CSSProperties;
|
||||
onEdit?: (id: CategoryGroupEntity['id']) => void;
|
||||
onSave?: (group: CategoryGroupEntity) => Promise<void>;
|
||||
onDelete?: (id: CategoryGroupEntity['id']) => Promise<void>;
|
||||
onSave?: (group: CategoryGroupEntity) => void;
|
||||
onDelete?: (id: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup?: (
|
||||
categories: Array<CategoryEntity['id']>,
|
||||
) => void;
|
||||
|
||||
@@ -346,6 +346,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
startMonth={startMonth}
|
||||
monthBounds={bounds}
|
||||
maxMonths={maxMonths}
|
||||
// @ts-expect-error fix me
|
||||
dataComponents={trackingComponents}
|
||||
onMonthSelect={onMonthSelect}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
@@ -373,6 +374,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
startMonth={startMonth}
|
||||
monthBounds={bounds}
|
||||
maxMonths={maxMonths}
|
||||
// @ts-expect-error fix me
|
||||
dataComponents={envelopeComponents}
|
||||
onMonthSelect={onMonthSelect}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
|
||||
@@ -76,14 +76,14 @@ export function useDraggable<T>({
|
||||
export type OnDropCallback = (
|
||||
id: string,
|
||||
dropPos: DropPosition,
|
||||
targetId: unknown,
|
||||
targetId: string,
|
||||
) => Promise<void> | void;
|
||||
|
||||
type OnLongHoverCallback = () => Promise<void> | void;
|
||||
|
||||
type UseDroppableArgs = {
|
||||
types: string | string[];
|
||||
id: unknown;
|
||||
id: string;
|
||||
onDrop: OnDropCallback;
|
||||
onLongHover?: OnLongHoverCallback;
|
||||
};
|
||||
@@ -95,7 +95,7 @@ export function useDroppable<T extends { id: string }>({
|
||||
onLongHover,
|
||||
}: UseDroppableArgs) {
|
||||
const ref = useRef(null);
|
||||
const [dropPos, setDropPos] = useState<DropPosition>(null);
|
||||
const [dropPos, setDropPos] = useState<DropPosition | null>(null);
|
||||
|
||||
const [{ isOver }, dropRef] = useDrop<
|
||||
{ item: T },
|
||||
@@ -137,7 +137,7 @@ export function useDroppable<T extends { id: string }>({
|
||||
};
|
||||
}
|
||||
|
||||
type ItemPosition = 'first' | 'last';
|
||||
type ItemPosition = 'first' | 'last' | null;
|
||||
export const DropHighlightPosContext: Context<ItemPosition> =
|
||||
createContext(null);
|
||||
|
||||
|
||||
7
upcoming-release-notes/5961.md
Normal file
7
upcoming-release-notes/5961.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Convert BudgetCategories component to TypeScript.
|
||||
|
||||
Reference in New Issue
Block a user