Skip to content

Commit

Permalink
[refactor] Migrate Schedules Table to typescript (actualbudget#1691)
Browse files Browse the repository at this point in the history
  • Loading branch information
muhsinkamil authored Sep 17, 2023
1 parent f182bf5 commit dc47c2d
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 161 deletions.
2 changes: 1 addition & 1 deletion packages/desktop-client/src/components/common/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type MenuProps = {
header?: ReactNode;
footer?: ReactNode;
items: Array<MenuItem | typeof Menu.line>;
onMenuSelect;
onMenuSelect: (itemName: MenuItem['name']) => void;
};

export default function Menu({
Expand Down
187 changes: 126 additions & 61 deletions packages/desktop-client/src/components/schedules/SchedulesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, type CSSProperties } from 'react';
import { useSelector } from 'react-redux';

import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
import * as monthUtils from 'loot-core/src/shared/months';
import {
type ScheduleStatusType,
type ScheduleStatuses,
} from 'loot-core/src/client/data-hooks/schedules';
import { format as monthUtilFormat } from 'loot-core/src/shared/months';
import { getScheduledAmount } from 'loot-core/src/shared/schedules';
import { integerToCurrency } from 'loot-core/src/shared/util';
import { type ScheduleEntity } from 'loot-core/src/types/models';

import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple';
import Check from '../../icons/v2/Check';
Expand All @@ -21,10 +26,73 @@ import DisplayId from '../util/DisplayId';

import { StatusBadge } from './StatusBadge';

export let ROW_HEIGHT = 43;
type SchedulesTableProps = {
schedules: ScheduleEntity[];
statuses: ScheduleStatuses;
filter: string;
allowCompleted: boolean;
onSelect: (id: ScheduleEntity['id']) => void;
onAction: (actionName: ScheduleItemAction, id: ScheduleEntity['id']) => void;
style: CSSProperties;
minimal?: boolean;
tableStyle?: CSSProperties;
};

function OverflowMenu({ schedule, status, onAction }) {
let [open, setOpen] = useState(false);
type CompletedScheduleItem = { id: 'show-completed' };
type SchedulesTableItem = ScheduleEntity | CompletedScheduleItem;

export type ScheduleItemAction =
| 'post-transaction'
| 'skip'
| 'complete'
| 'restart'
| 'delete';

export const ROW_HEIGHT = 43;

function OverflowMenu({
schedule,
status,
onAction,
}: {
schedule: ScheduleEntity;
status: ScheduleStatusType;
onAction: SchedulesTableProps['onAction'];
}) {
const [open, setOpen] = useState(false);

const getMenuItems = () => {
const menuItems: { name: ScheduleItemAction; text: string }[] = [];

if (status === 'due') {
menuItems.push({
name: 'post-transaction',
text: 'Post transaction',
});
}

if (status === 'completed') {
menuItems.push({
name: 'restart',
text: 'Restart',
});
} else {
menuItems.push(
{
name: 'skip',
text: 'Skip next date',
},
{
name: 'complete',
text: 'Complete',
},
);
}

menuItems.push({ name: 'delete', text: 'Delete' });

return menuItems;
};

return (
<View>
Expand All @@ -49,34 +117,28 @@ function OverflowMenu({ schedule, status, onAction }) {
onClose={() => setOpen(false)}
>
<Menu
onMenuSelect={name => {
onMenuSelect={(name: ScheduleItemAction) => {
onAction(name, schedule.id);
setOpen(false);
}}
items={[
status === 'due' && {
name: 'post-transaction',
text: 'Post transaction',
},
...(schedule.completed
? [{ name: 'restart', text: 'Restart' }]
: [
{ name: 'skip', text: 'Skip next date' },
{ name: 'complete', text: 'Complete' },
]),
{ name: 'delete', text: 'Delete' },
]}
items={getMenuItems()}
/>
</Tooltip>
)}
</View>
);
}

export function ScheduleAmountCell({ amount, op }) {
let num = getScheduledAmount(amount);
let str = integerToCurrency(Math.abs(num || 0));
let isApprox = op === 'isapprox' || op === 'isbetween';
export function ScheduleAmountCell({
amount,
op,
}: {
amount: ScheduleEntity['_amount'];
op: ScheduleEntity['_amountOp'];
}) {
const num = getScheduledAmount(amount);
const str = integerToCurrency(Math.abs(num || 0));
const isApprox = op === 'isapprox' || op === 'isbetween';

return (
<Cell
Expand Down Expand Up @@ -129,38 +191,38 @@ export function SchedulesTable({
onSelect,
onAction,
tableStyle,
}) {
let dateFormat = useSelector(state => {
}: SchedulesTableProps) {
const dateFormat = useSelector(state => {
return state.prefs.local.dateFormat || 'MM/dd/yyyy';
});

let [showCompleted, setShowCompleted] = useState(false);
const [showCompleted, setShowCompleted] = useState(false);

let payees = useCachedPayees();
let accounts = useCachedAccounts();
const payees = useCachedPayees();
const accounts = useCachedAccounts();

let filteredSchedules = useMemo(() => {
const filteredSchedules = useMemo(() => {
if (!filter) {
return schedules;
}
const filterIncludes = str =>
const filterIncludes = (str: string) =>
str
? str.toLowerCase().includes(filter.toLowerCase()) ||
filter.toLowerCase().includes(str.toLowerCase())
: false;

return schedules.filter(schedule => {
let payee = payees.find(p => schedule._payee === p.id);
let account = accounts.find(a => schedule._account === a.id);
let amount = getScheduledAmount(schedule._amount);
let amountStr =
const payee = payees.find(p => schedule._payee === p.id);
const account = accounts.find(a => schedule._account === a.id);
const amount = getScheduledAmount(schedule._amount);
const amountStr =
(schedule._amountOp === 'isapprox' || schedule._amountOp === 'isbetween'
? '~'
: '') +
(amount > 0 ? '+' : '') +
integerToCurrency(Math.abs(amount || 0));
let dateStr = schedule.next_date
? monthUtils.format(schedule.next_date, dateFormat)
const dateStr = schedule.next_date
? monthUtilFormat(schedule.next_date, dateFormat)
: null;

return (
Expand All @@ -174,26 +236,29 @@ export function SchedulesTable({
});
}, [schedules, filter, statuses]);

let items = useMemo(() => {
const items: SchedulesTableItem[] = useMemo(() => {
const unCompletedSchedules = filteredSchedules.filter(s => !s.completed);

if (!allowCompleted) {
return filteredSchedules.filter(s => !s.completed);
return unCompletedSchedules;
}
if (showCompleted) {
return filteredSchedules;
}
let arr = filteredSchedules.filter(s => !s.completed);
if (filteredSchedules.find(s => s.completed)) {
arr.push({ type: 'show-completed' });
}
return arr;

const hasCompletedSchedule = filteredSchedules.find(s => s.completed);

if (!hasCompletedSchedule) return unCompletedSchedules;

return [...unCompletedSchedules, { id: 'show-completed' }];
}, [filteredSchedules, showCompleted, allowCompleted]);

function renderSchedule({ item }) {
function renderSchedule({ schedule }: { schedule: ScheduleEntity }) {
return (
<Row
height={ROW_HEIGHT}
inset={15}
onClick={() => onSelect(item.id)}
onClick={() => onSelect(schedule.id)}
style={{
cursor: 'pointer',
backgroundColor: theme.tableBackground,
Expand All @@ -204,42 +269,42 @@ export function SchedulesTable({
<Field width="flex" name="name">
<Text
style={
item.name == null
schedule.name == null
? { color: theme.buttonNormalDisabledText }
: null
}
title={item.name ? item.name : ''}
title={schedule.name ? schedule.name : ''}
>
{item.name ? item.name : 'None'}
{schedule.name ? schedule.name : 'None'}
</Text>
</Field>
<Field width="flex" name="payee">
<DisplayId type="payees" id={item._payee} />
<DisplayId type="payees" id={schedule._payee} />
</Field>
<Field width="flex" name="account">
<DisplayId type="accounts" id={item._account} />
<DisplayId type="accounts" id={schedule._account} />
</Field>
<Field width={110} name="date">
{item.next_date
? monthUtils.format(item.next_date, dateFormat)
{schedule.next_date
? monthUtilFormat(schedule.next_date, dateFormat)
: null}
</Field>
<Field width={120} name="status" style={{ alignItems: 'flex-start' }}>
<StatusBadge status={statuses.get(item.id)} />
<StatusBadge status={statuses.get(schedule.id)} />
</Field>
<ScheduleAmountCell amount={item._amount} op={item._amountOp} />
<ScheduleAmountCell amount={schedule._amount} op={schedule._amountOp} />
{!minimal && (
<Field width={80} style={{ textAlign: 'center' }}>
{item._date && item._date.frequency && (
{schedule._date && schedule._date.frequency && (
<Check style={{ width: 13, height: 13 }} />
)}
</Field>
)}
{!minimal && (
<Field width={40} name="actions">
<OverflowMenu
schedule={item}
status={statuses.get(item.id)}
schedule={schedule}
status={statuses.get(schedule.id)}
onAction={onAction}
/>
</Field>
Expand All @@ -248,8 +313,8 @@ export function SchedulesTable({
);
}

function renderItem({ item }) {
if (item.type === 'show-completed') {
function renderItem({ item }: { item: SchedulesTableItem }) {
if (item.id === 'show-completed') {
return (
<Row
height={ROW_HEIGHT}
Expand All @@ -274,7 +339,7 @@ export function SchedulesTable({
</Row>
);
}
return renderSchedule({ item });
return renderSchedule({ schedule: item as ScheduleEntity });
}

return (
Expand All @@ -300,7 +365,7 @@ export function SchedulesTable({
backgroundColor="transparent"
version="v2"
style={{ flex: 1, backgroundColor: 'transparent', ...style }}
items={items}
items={items as ScheduleEntity[]}
renderItem={renderItem}
renderEmpty={filter ? 'No matching schedules' : 'No schedules'}
allowPopupsEscape={items.length < 6}
Expand Down
Loading

0 comments on commit dc47c2d

Please sign in to comment.