Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] Migrate Schedules Table to typescript #1691

Merged
Merged
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) => {
MatissJanis marked this conversation as resolved.
Show resolved Hide resolved
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 });
Copy link
Contributor Author

@muhsinkamil muhsinkamil Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, had to cast it as the items is a mix of ScheduleEntity & CompletedScheduleItem

But not very bad tho, as the implementation still remains the same.,
( except I renamed type === 'show-completed' -> id === 'show-completed to satisfy ts 😃 )

}

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