diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.tsx similarity index 65% rename from packages/desktop-client/src/components/budget/BudgetTable.jsx rename to packages/desktop-client/src/components/budget/BudgetTable.tsx index 785457f2ab5..66481fd64af 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.tsx @@ -1,14 +1,24 @@ -import React, { useState } from 'react'; +import React, { + type ComponentPropsWithoutRef, + type KeyboardEvent, + useState, +} from 'react'; + +import { + type CategoryEntity, + type CategoryGroupEntity, +} from 'loot-core/types/models'; import { useCategories } from '../../hooks/useCategories'; import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; import { View } from '../common/View'; +import { type DropPosition } from '../sort'; import { BudgetCategories } from './BudgetCategories'; import { BudgetSummaries } from './BudgetSummaries'; import { BudgetTotals } from './BudgetTotals'; -import { MonthsProvider } from './MonthsContext'; +import { type MonthBounds, MonthsProvider } from './MonthsContext'; import { findSortDown, findSortUp, @@ -16,7 +26,39 @@ import { separateGroups, } from './util'; -export function BudgetTable(props) { +type BudgetTableProps = { + type: string; + prewarmStartMonth: string; + startMonth: string; + numMonths: number; + monthBounds: MonthBounds; + dataComponents: { + SummaryComponent: ComponentPropsWithoutRef< + typeof BudgetSummaries + >['SummaryComponent']; + BudgetTotalsComponent: ComponentPropsWithoutRef< + typeof BudgetTotals + >['MonthComponent']; + }; + onSaveCategory: (category: CategoryEntity) => void; + onDeleteCategory: (id: CategoryEntity['id']) => void; + onSaveGroup: (group: CategoryGroupEntity) => void; + onDeleteGroup: (id: CategoryGroupEntity['id']) => void; + onApplyBudgetTemplatesInGroup: (groupId: CategoryGroupEntity['id']) => void; + onReorderCategory: (params: { + id: CategoryEntity['id']; + groupId?: CategoryGroupEntity['id']; + targetId: CategoryEntity['id'] | null; + }) => void; + onReorderGroup: (params: { + id: CategoryGroupEntity['id']; + targetId: CategoryEntity['id'] | null; + }) => void; + onShowActivity: (id: CategoryEntity['id'], month?: string) => void; + onBudgetAction: (month: string, type: string, args: unknown) => void; +}; + +export function BudgetTable(props: BudgetTableProps) { const { type, prewarmStartMonth, @@ -35,23 +77,29 @@ export function BudgetTable(props) { onBudgetAction, } = props; - const { grouped: categoryGroups } = useCategories(); + const { grouped: categoryGroups = [] } = useCategories(); const [collapsedGroupIds = [], setCollapsedGroupIdsPref] = useLocalPref('budget.collapsed'); const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref( 'budget.showHiddenCategories', ); - const [editing, setEditing] = useState(null); + const [editing, setEditing] = useState<{ id: string; cell: string } | null>( + null, + ); - const onEditMonth = (id, month) => { + const onEditMonth = (id: string, month: string) => { setEditing(id ? { id, cell: month } : null); }; - const onEditName = id => { + const onEditName = (id: string) => { setEditing(id ? { id, cell: 'name' } : null); }; - const _onReorderCategory = (id, dropPos, targetId) => { + const _onReorderCategory = ( + id: string, + dropPos: DropPosition, + targetId: string, + ) => { const isGroup = !!categoryGroups.find(g => g.id === targetId); if (isGroup) { @@ -63,7 +111,7 @@ export function BudgetTable(props) { const group = categoryGroups.find(g => g.id === groupId); if (group) { - const { categories } = group; + const { categories = [] } = group; onReorderCategory({ id, groupId: group.id, @@ -77,7 +125,7 @@ export function BudgetTable(props) { let targetGroup; for (const group of categoryGroups) { - if (group.categories.find(cat => cat.id === targetId)) { + if (group.categories?.find(cat => cat.id === targetId)) { targetGroup = group; break; } @@ -85,13 +133,17 @@ export function BudgetTable(props) { onReorderCategory({ id, - groupId: targetGroup.id, - ...findSortDown(targetGroup.categories, dropPos, targetId), + groupId: targetGroup?.id, + ...findSortDown(targetGroup?.categories || [], dropPos, targetId), }); } }; - const _onReorderGroup = (id, dropPos, targetId) => { + const _onReorderGroup = ( + id: string, + dropPos: DropPosition, + targetId: string, + ) => { const [expenseGroups] = separateGroups(categoryGroups); // exclude Income group from sortable groups to fix off-by-one error onReorderGroup({ id, @@ -99,13 +151,21 @@ export function BudgetTable(props) { }); }; - const moveVertically = dir => { - const flattened = categoryGroups.reduce((all, group) => { - if (collapsedGroupIds.includes(group.id)) { - return all.concat({ id: group.id, isGroup: true }); - } - return all.concat([{ id: group.id, isGroup: true }, ...group.categories]); - }, []); + const moveVertically = (dir: 1 | -1) => { + const flattened = categoryGroups.reduce( + (all, group) => { + if (collapsedGroupIds.includes(group.id)) { + return all.concat({ id: group.id, isGroup: true }); + } + return all.concat([ + { id: group.id, isGroup: true }, + ...(group?.categories || []), + ]); + }, + [] as Array< + { id: CategoryGroupEntity['id']; isGroup: boolean } | CategoryEntity + >, + ); if (editing) { const idx = flattened.findIndex(item => item.id === editing.id); @@ -114,10 +174,13 @@ export function BudgetTable(props) { while (nextIdx >= 0 && nextIdx < flattened.length) { const next = flattened[nextIdx]; - if (next.isGroup) { + if ('isGroup' in next && next.isGroup) { nextIdx += dir; continue; - } else if (type === 'report' || !next.is_income) { + } else if ( + type === 'report' || + ('is_income' in next && !next.is_income) + ) { onEditMonth(next.id, editing.cell); return; } else { @@ -127,7 +190,7 @@ export function BudgetTable(props) { } }; - const onKeyDown = e => { + const onKeyDown = (e: KeyboardEvent) => { if (!editing) { return null; } @@ -138,7 +201,7 @@ export function BudgetTable(props) { } }; - const onCollapse = collapsedIds => { + const onCollapse = (collapsedIds: string[]) => { setCollapsedGroupIdsPref(collapsedIds); }; @@ -223,6 +286,7 @@ export function BudgetTable(props) { onKeyDown={onKeyDown} > ; +type DynamicBudgetTableProps = Omit< + ComponentProps, + 'numMonths' +> & { + maxMonths: number; + onMonthSelect: (month: string, numMonths: number) => void; +}; export const DynamicBudgetTable = (props: DynamicBudgetTableProps) => { return ( diff --git a/packages/desktop-client/src/components/budget/MonthPicker.tsx b/packages/desktop-client/src/components/budget/MonthPicker.tsx index 501bafd79ff..66f44423871 100644 --- a/packages/desktop-client/src/components/budget/MonthPicker.tsx +++ b/packages/desktop-client/src/components/budget/MonthPicker.tsx @@ -7,12 +7,12 @@ import { useResizeObserver } from '../../hooks/useResizeObserver'; import { styles, theme } from '../../style'; import { View } from '../common/View'; -import { type BoundsProps } from './MonthsContext'; +import { type MonthBounds } from './MonthsContext'; type MonthPickerProps = { startMonth: string; numDisplayed: number; - monthBounds: BoundsProps; + monthBounds: MonthBounds; style: CSSProperties; onSelect: (month: string) => void; }; diff --git a/packages/desktop-client/src/components/budget/MonthsContext.tsx b/packages/desktop-client/src/components/budget/MonthsContext.tsx index 2d5e377402a..dad9b264028 100644 --- a/packages/desktop-client/src/components/budget/MonthsContext.tsx +++ b/packages/desktop-client/src/components/budget/MonthsContext.tsx @@ -3,13 +3,13 @@ import React, { createContext, type ReactNode } from 'react'; import * as monthUtils from 'loot-core/src/shared/months'; -export type BoundsProps = { +export type MonthBounds = { start: string; end: string; }; export function getValidMonthBounds( - bounds: BoundsProps, + bounds: MonthBounds, startMonth: undefined | string, endMonth: string, ) { @@ -29,7 +29,7 @@ export const MonthsContext = createContext(null); type MonthsProviderProps = { startMonth: string | undefined; numMonths: number; - monthBounds: BoundsProps; + monthBounds: MonthBounds; type: string; children: ReactNode; }; diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index 8abaf8de55f..0012f46f0a7 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -355,6 +355,7 @@ function BudgetInner(props: BudgetInnerProps) { onShowActivity={onShowActivity} onReorderCategory={onReorderCategory} onReorderGroup={onReorderGroup} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} /> ); @@ -375,13 +376,13 @@ function BudgetInner(props: BudgetInnerProps) { onMonthSelect={onMonthSelect} onDeleteCategory={onDeleteCategory} onDeleteGroup={onDeleteGroup} - onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} onSaveCategory={onSaveCategory} onSaveGroup={onSaveGroup} onBudgetAction={onBudgetAction} onShowActivity={onShowActivity} onReorderCategory={onReorderCategory} onReorderGroup={onReorderGroup} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} /> ); diff --git a/upcoming-release-notes/3899.md b/upcoming-release-notes/3899.md new file mode 100644 index 00000000000..f330782869e --- /dev/null +++ b/upcoming-release-notes/3899.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Convert BudgetTable.jsx to TypeScript