From 23fad8ca80d6649cffac4683fc6a85dea7e8eea2 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Tue, 5 Sep 2023 22:06:38 +0200 Subject: [PATCH 01/11] refactor: change schedules table to ts file --- .../components/schedules/SchedulesTable.tsx | 48 ++++++++++++------- .../src/components/schedules/StatusBadge.tsx | 21 ++------ .../desktop-client/src/components/table.tsx | 5 +- .../src/client/data-hooks/schedules.tsx | 14 +++--- .../loot-core/src/types/models/schedule.d.ts | 2 +- 5 files changed, 46 insertions(+), 44 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index e95348718ef..272a14fc46b 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -1,11 +1,13 @@ -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 { type ScheduleStatuses } from 'loot-core/src/client/data-hooks/schedules'; import * as monthUtils 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'; @@ -21,6 +23,20 @@ import DisplayId from '../util/DisplayId'; import { StatusBadge } from './StatusBadge'; +type SchedulesTableProps = { + schedules: ScheduleEntity[]; + statuses: ScheduleStatuses; + filter: string; + allowCompleted: boolean; + onSelect: (id: ScheduleEntity['id']) => void; + onAction: (actionName: string, id: ScheduleEntity['id']) => void; + style: CSSProperties; + minimal?: boolean; + tableStyle?: CSSProperties; +}; + +type CompletedScheduleItem = { type: 'show-completed' }; + export let ROW_HEIGHT = 43; function OverflowMenu({ schedule, status, onAction }) { @@ -129,17 +145,17 @@ 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; } @@ -150,16 +166,16 @@ export function SchedulesTable({ : 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 + const dateStr = schedule.next_date ? monthUtils.format(schedule.next_date, dateFormat) : null; @@ -174,21 +190,21 @@ export function SchedulesTable({ }); }, [schedules, filter, statuses]); - let items = useMemo(() => { + const items: ScheduleEntity[] | CompletedScheduleItem = useMemo(() => { if (!allowCompleted) { return filteredSchedules.filter(s => !s.completed); } if (showCompleted) { return filteredSchedules; } - let arr = filteredSchedules.filter(s => !s.completed); + const arr = filteredSchedules.filter(s => !s.completed); if (filteredSchedules.find(s => s.completed)) { - arr.push({ type: 'show-completed' }); + arr.push({ type: 'show-completed' } as CompletedScheduleItem); } return arr; }, [filteredSchedules, showCompleted, allowCompleted]); - function renderSchedule({ item }) { + function renderSchedule({ item }: { item: ScheduleEntity }) { return ( void; scrollToTop: () => void; - getScrolledItem: () => number; + getScrolledItem: () => TableItem['id']; setRowAnimation: (flag) => void; edit(id: number, field, shouldScroll): void; anchor(): void; @@ -852,7 +852,7 @@ export const TableWithNavigator = forwardRef< return ; }); -type TableItem = { id: number }; +type TableItem = { id: number | string }; type TableProps = { items: TableItem[]; @@ -875,6 +875,7 @@ type TableProps = { loadMore?: () => void; style?: CSSProperties; navigator?: ReturnType; + listRef?: unknown; onScroll?: () => void; version?: string; allowPopupsEscape?: boolean; diff --git a/packages/loot-core/src/client/data-hooks/schedules.tsx b/packages/loot-core/src/client/data-hooks/schedules.tsx index 9131d3c96b1..d994d1b3842 100644 --- a/packages/loot-core/src/client/data-hooks/schedules.tsx +++ b/packages/loot-core/src/client/data-hooks/schedules.tsx @@ -5,6 +5,8 @@ import { getStatus, getHasTransactionsQuery } from '../../shared/schedules'; import { type ScheduleEntity } from '../../types/models'; import q, { liveQuery } from '../query-helpers'; +export type ScheduleStatusType = ReturnType; +export type ScheduleStatuses = Map; function loadStatuses(schedules: ScheduleEntity[], onData) { return liveQuery(getHasTransactionsQuery(schedules), onData, { mapper: data => { @@ -23,13 +25,13 @@ function loadStatuses(schedules: ScheduleEntity[], onData) { type UseSchedulesArgs = { transform?: (q: Query) => Query }; type UseSchedulesReturnType = { schedules: ScheduleEntity[]; - statuses: Record>; + statuses: ScheduleStatuses; } | null; export function useSchedules({ transform }: UseSchedulesArgs = {}) { - let [data, setData] = useState(null); + const [data, setData] = useState(null); useEffect(() => { - let query = q('schedules').select('*'); + const query = q('schedules').select('*'); let scheduleQuery, statusQuery; scheduleQuery = liveQuery( @@ -40,10 +42,8 @@ export function useSchedules({ transform }: UseSchedulesArgs = {}) { statusQuery.unsubscribe(); } - statusQuery = loadStatuses( - schedules, - (statuses: Record>) => - setData({ schedules, statuses }), + statusQuery = loadStatuses(schedules, (statuses: ScheduleStatuses) => + setData({ schedules, statuses }), ); } }, diff --git a/packages/loot-core/src/types/models/schedule.d.ts b/packages/loot-core/src/types/models/schedule.d.ts index f170b736a8f..7021417fe94 100644 --- a/packages/loot-core/src/types/models/schedule.d.ts +++ b/packages/loot-core/src/types/models/schedule.d.ts @@ -3,7 +3,7 @@ import type { PayeeEntity } from './payee'; import type { RuleEntity } from './rule'; export interface ScheduleEntity { - id?: string; + id: string; name?: string; rule: RuleEntity; next_date: string; From 95382b3c2f5f81382c7bda433026eef99b3c5922 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Thu, 7 Sep 2023 01:05:17 +0200 Subject: [PATCH 02/11] refactor: add generic types to table component --- .../components/schedules/SchedulesTable.tsx | 61 +++++++++++++------ .../desktop-client/src/components/table.tsx | 11 ++-- .../src/client/data-hooks/accounts.tsx | 11 +++- .../loot-core/src/types/models/schedule.d.ts | 2 +- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 272a14fc46b..004223169e4 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -3,7 +3,10 @@ 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 { type ScheduleStatuses } from 'loot-core/src/client/data-hooks/schedules'; +import { + type ScheduleStatusType, + type ScheduleStatuses, +} from 'loot-core/src/client/data-hooks/schedules'; import * as monthUtils from 'loot-core/src/shared/months'; import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { integerToCurrency } from 'loot-core/src/shared/util'; @@ -35,11 +38,20 @@ type SchedulesTableProps = { tableStyle?: CSSProperties; }; -type CompletedScheduleItem = { type: 'show-completed' }; +type CompletedScheduleItem = { id: 'show-completed' }; +type SchedulesTableItem = ScheduleEntity | CompletedScheduleItem; export let ROW_HEIGHT = 43; -function OverflowMenu({ schedule, status, onAction }) { +function OverflowMenu({ + schedule, + status, + onAction, +}: { + schedule: ScheduleEntity; + status: ScheduleStatusType; + onAction: SchedulesTableProps['onAction']; +}) { let [open, setOpen] = useState(false); return ( @@ -89,10 +101,16 @@ function OverflowMenu({ schedule, status, onAction }) { ); } -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 ( + const filterIncludes = (str: string) => str ? str.toLowerCase().includes(filter.toLowerCase()) || filter.toLowerCase().includes(str.toLowerCase()) @@ -190,18 +208,21 @@ export function SchedulesTable({ }); }, [schedules, filter, statuses]); - const items: ScheduleEntity[] | CompletedScheduleItem = 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; } - const arr = filteredSchedules.filter(s => !s.completed); - if (filteredSchedules.find(s => s.completed)) { - arr.push({ type: 'show-completed' } as CompletedScheduleItem); - } - return arr; + + const hasCompletedSchedule = filteredSchedules.find(s => s.completed); + + if (!hasCompletedSchedule) return unCompletedSchedules; + + return [...unCompletedSchedules, { id: 'show-completed' }]; }, [filteredSchedules, showCompleted, allowCompleted]); function renderSchedule({ item }: { item: ScheduleEntity }) { @@ -246,7 +267,7 @@ export function SchedulesTable({ {!minimal && ( - {item._date && item._date.frequency && ( + {item._date && (item._date as any).frequency && ( )} @@ -264,8 +285,8 @@ export function SchedulesTable({ ); } - function renderItem({ item }) { - if (item.type === 'show-completed') { + function renderItem({ item }: { item: SchedulesTableItem }) { + if (item.id === 'show-completed') { return ( ); } - return renderSchedule({ item }); + return renderSchedule({ item: item as ScheduleEntity }); } return ( @@ -316,7 +337,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} diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx index 9bd8aa9778a..bae92be46c5 100644 --- a/packages/desktop-client/src/components/table.tsx +++ b/packages/desktop-client/src/components/table.tsx @@ -11,6 +11,7 @@ import React, { type ReactNode, type KeyboardEvent, type UIEvent, + type ReactElement, } from 'react'; import { useStore } from 'react-redux'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -854,8 +855,8 @@ export const TableWithNavigator = forwardRef< type TableItem = { id: number | string }; -type TableProps = { - items: TableItem[]; +type TableProps = { + items: T[]; count?: number; headers?: ReactNode | TableHeaderProps['headers']; contentHeader?: ReactNode; @@ -863,7 +864,7 @@ type TableProps = { rowHeight?: number; backgroundColor?: string; renderItem: (arg: { - item: TableItem; + item: T; editing: boolean; focusedField: unknown; onEdit: (id, field) => void; @@ -883,7 +884,9 @@ type TableProps = { saveScrollWidth?: (parent, child) => void; }; -export const Table = forwardRef( +export const Table: ( + props: TableProps, +) => ReactElement = forwardRef( ( { items, diff --git a/packages/loot-core/src/client/data-hooks/accounts.tsx b/packages/loot-core/src/client/data-hooks/accounts.tsx index 147e545c6f8..fd3d4fd002b 100644 --- a/packages/loot-core/src/client/data-hooks/accounts.tsx +++ b/packages/loot-core/src/client/data-hooks/accounts.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext } from 'react'; +import { type AccountEntity } from '../../types/models'; import q from '../query-helpers'; import { useLiveQuery } from '../query-hooks'; import { getAccountsById } from '../reducers/queries'; @@ -20,7 +21,13 @@ export function CachedAccounts({ children, idKey }) { return children(data); } -export function useCachedAccounts({ idKey }: { idKey? } = {}) { - let data = useContext(AccountsContext); +export function useCachedAccounts(): AccountEntity[]; +export function useCachedAccounts({ + idKey, +}: { + idKey: string; +}): Record; +export function useCachedAccounts({ idKey }: { idKey?: string } = {}) { + const data: AccountEntity[] = useContext(AccountsContext); return idKey && data ? getAccountsById(data) : data; } diff --git a/packages/loot-core/src/types/models/schedule.d.ts b/packages/loot-core/src/types/models/schedule.d.ts index 7021417fe94..03dd826213a 100644 --- a/packages/loot-core/src/types/models/schedule.d.ts +++ b/packages/loot-core/src/types/models/schedule.d.ts @@ -14,7 +14,7 @@ export interface ScheduleEntity { // These are special fields that are actually pulled from the // underlying rule _payee: PayeeEntity; - _account: AccountEntity; + _account: AccountEntity['id']; _amount: unknown; _amountOp: string; _date: unknown; From 3e09fe137a6ea10503c684e5619a9ad4363cd5a4 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Thu, 7 Sep 2023 20:18:26 +0200 Subject: [PATCH 03/11] refactor: add types to cached accounts & payees --- .../src/components/common/Menu.tsx | 4 ++-- .../src/components/schedules/SchedulesTable.tsx | 16 +++++++--------- .../loot-core/src/client/data-hooks/accounts.tsx | 6 +++--- .../loot-core/src/client/data-hooks/payees.tsx | 13 ++++++++++--- .../loot-core/src/types/models/schedule.d.ts | 2 +- tsconfig.json | 1 + 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx index 84ced272fbf..6012cf8e33f 100644 --- a/packages/desktop-client/src/components/common/Menu.tsx +++ b/packages/desktop-client/src/components/common/Menu.tsx @@ -23,7 +23,7 @@ function Keybinding({ keyName }: KeybindingProps) { ); } -type MenuItem = { +export type MenuItem = { type?: string | symbol; name: string; disabled?: boolean; @@ -37,7 +37,7 @@ type MenuProps = { header?: ReactNode; footer?: ReactNode; items: Array; - onMenuSelect; + onMenuSelect: (itemName: MenuItem['name']) => void; }; export default function Menu({ diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 004223169e4..9605975d190 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -7,7 +7,7 @@ import { type ScheduleStatusType, type ScheduleStatuses, } from 'loot-core/src/client/data-hooks/schedules'; -import * as monthUtils from 'loot-core/src/shared/months'; +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'; @@ -16,7 +16,7 @@ import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple'; import Check from '../../icons/v2/Check'; import { theme } from '../../style'; import Button from '../common/Button'; -import Menu from '../common/Menu'; +import Menu, { type MenuItem } from '../common/Menu'; import Text from '../common/Text'; import View from '../common/View'; import PrivacyFilter from '../PrivacyFilter'; @@ -41,7 +41,7 @@ type SchedulesTableProps = { type CompletedScheduleItem = { id: 'show-completed' }; type SchedulesTableItem = ScheduleEntity | CompletedScheduleItem; -export let ROW_HEIGHT = 43; +export const ROW_HEIGHT = 43; function OverflowMenu({ schedule, @@ -52,7 +52,7 @@ function OverflowMenu({ status: ScheduleStatusType; onAction: SchedulesTableProps['onAction']; }) { - let [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); return ( @@ -77,7 +77,7 @@ function OverflowMenu({ onClose={() => setOpen(false)} > { + onMenuSelect={(name: MenuItem['name']) => { onAction(name, schedule.id); setOpen(false); }} @@ -194,7 +194,7 @@ export function SchedulesTable({ (amount > 0 ? '+' : '') + integerToCurrency(Math.abs(amount || 0)); const dateStr = schedule.next_date - ? monthUtils.format(schedule.next_date, dateFormat) + ? monthUtilFormat(schedule.next_date, dateFormat) : null; return ( @@ -257,9 +257,7 @@ export function SchedulesTable({ - {item.next_date - ? monthUtils.format(item.next_date, dateFormat) - : null} + {item.next_date ? monthUtilFormat(item.next_date, dateFormat) : null} diff --git a/packages/loot-core/src/client/data-hooks/accounts.tsx b/packages/loot-core/src/client/data-hooks/accounts.tsx index fd3d4fd002b..9ed2b18497a 100644 --- a/packages/loot-core/src/client/data-hooks/accounts.tsx +++ b/packages/loot-core/src/client/data-hooks/accounts.tsx @@ -12,7 +12,7 @@ function useAccounts() { let AccountsContext = createContext(null); export function AccountsProvider({ children }) { - let data = useAccounts(); + const data: AccountEntity[] = useAccounts(); return ; } @@ -25,9 +25,9 @@ export function useCachedAccounts(): AccountEntity[]; export function useCachedAccounts({ idKey, }: { - idKey: string; + idKey: boolean; }): Record; -export function useCachedAccounts({ idKey }: { idKey?: string } = {}) { +export function useCachedAccounts({ idKey }: { idKey?: boolean } = {}) { const data: AccountEntity[] = useContext(AccountsContext); return idKey && data ? getAccountsById(data) : data; } diff --git a/packages/loot-core/src/client/data-hooks/payees.tsx b/packages/loot-core/src/client/data-hooks/payees.tsx index 2adba3bb71f..bdf73189634 100644 --- a/packages/loot-core/src/client/data-hooks/payees.tsx +++ b/packages/loot-core/src/client/data-hooks/payees.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext } from 'react'; +import { type PayeeEntity } from '../../types/models'; import q from '../query-helpers'; import { useLiveQuery } from '../query-hooks'; import { getPayeesById } from '../reducers/queries'; @@ -11,7 +12,7 @@ function usePayees() { let PayeesContext = createContext(null); export function PayeesProvider({ children }) { - let data = usePayees(); + const data: PayeeEntity[] = usePayees(); return ; } @@ -20,7 +21,13 @@ export function CachedPayees({ children, idKey }) { return children(data); } -export function useCachedPayees({ idKey }: { idKey? } = {}) { - let data = useContext(PayeesContext); +export function useCachedPayees(): PayeeEntity[]; +export function useCachedPayees({ + idKey, +}: { + idKey: boolean; +}): Record; +export function useCachedPayees({ idKey }: { idKey?: boolean } = {}) { + const data: PayeeEntity[] = useContext(PayeesContext); return idKey && data ? getPayeesById(data) : data; } diff --git a/packages/loot-core/src/types/models/schedule.d.ts b/packages/loot-core/src/types/models/schedule.d.ts index 03dd826213a..57bad1bd52c 100644 --- a/packages/loot-core/src/types/models/schedule.d.ts +++ b/packages/loot-core/src/types/models/schedule.d.ts @@ -13,7 +13,7 @@ export interface ScheduleEntity { // These are special fields that are actually pulled from the // underlying rule - _payee: PayeeEntity; + _payee: PayeeEntity['id']; _account: AccountEntity['id']; _amount: unknown; _amountOp: string; diff --git a/tsconfig.json b/tsconfig.json index 1a4dce7bf8f..9002ba8e283 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ // TODO: enable once every file is ts // "strict": true, "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, "skipLibCheck": true, "jsx": "preserve", // Check JS files too From d2b6b3f3fefc77a32500eb047c193632a969cd5d Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Thu, 7 Sep 2023 22:10:00 +0200 Subject: [PATCH 04/11] refactor: add types and refactor StatusBadge file --- .../src/components/schedules/StatusBadge.tsx | 96 ++++++++++--------- packages/loot-core/src/shared/util.ts | 2 +- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/StatusBadge.tsx b/packages/desktop-client/src/components/schedules/StatusBadge.tsx index 87074b73edc..65cdf0119c7 100644 --- a/packages/desktop-client/src/components/schedules/StatusBadge.tsx +++ b/packages/desktop-client/src/components/schedules/StatusBadge.tsx @@ -10,66 +10,74 @@ import CheckCircleHollow from '../../icons/v2/CheckCircleHollow'; import EditSkull1 from '../../icons/v2/EditSkull1'; import FavoriteStar from '../../icons/v2/FavoriteStar'; import ValidationCheck from '../../icons/v2/ValidationCheck'; -import { theme, type CSSProperties } from '../../style'; +import { theme, type CSSProperties, colors } from '../../style'; import Text from '../common/Text'; import View from '../common/View'; -export function getStatusProps(status: ScheduleStatusType) { - let color, backgroundColor, Icon; - +// Consists of Schedule Statuses + Transaction statuses +type StatusTypes = ScheduleStatusType | 'cleared' | 'pending'; +export function getStatusProps(status: StatusTypes) { switch (status) { case 'missed': - color = theme.altErrorText; - backgroundColor = theme.altErrorBackground; - Icon = EditSkull1; - break; + return { + color: colors.r1, + backgroundColor: colors.r10, + Icon: EditSkull1, + }; case 'due': - color = theme.altWarningText; - backgroundColor = theme.altWarningBackground; - Icon = AlertTriangle; - break; + return { + color: colors.y1, + backgroundColor: colors.y9, + Icon: AlertTriangle, + }; case 'upcoming': - color = theme.upcomingText; - backgroundColor = theme.upcomingBackground; - Icon = CalendarIcon; - break; + return { + color: colors.p1, + backgroundColor: colors.p10, + Icon: CalendarIcon, + }; case 'paid': - color = theme.alt2NoticeText; - backgroundColor = theme.altNoticeBackground; - Icon = ValidationCheck; - break; + return { + color: colors.g2, + backgroundColor: colors.g10, + Icon: ValidationCheck, + }; case 'completed': - color = theme.alt2TableText; - backgroundColor = theme.altTableBackground; - Icon = FavoriteStar; - break; + return { + color: colors.n4, + backgroundColor: colors.n11, + Icon: FavoriteStar, + }; + // @todo: Check if 'pending' is still a valid status in Transaction case 'pending': - color = theme.alt3NoticeText; - backgroundColor = theme.alt2NoticeBackground; - Icon = CalendarIcon; - break; + return { + color: colors.g4, + backgroundColor: colors.g11, + Icon: CalendarIcon, + }; case 'scheduled': - color = theme.menuItemText; - backgroundColor = theme.altTableBackground; - Icon = CalendarIcon; - break; + return { + color: colors.n1, + backgroundColor: colors.n11, + Icon: CalendarIcon, + }; case 'cleared': - color = theme.noticeText; - backgroundColor = theme.altTableBackground; - Icon = CheckCircle1; - break; + return { + color: colors.g5, + backgroundColor: colors.n11, + Icon: CheckCircle1, + }; default: - color = theme.buttonNormalDisabledText; - backgroundColor = theme.altTableBackground; - Icon = CheckCircleHollow; - break; + return { + color: colors.n7, + backgroundColor: colors.n11, + Icon: CheckCircleHollow, + }; } - - return { color, backgroundColor, Icon }; } -export function StatusBadge({ status }) { - let { color, backgroundColor, Icon } = getStatusProps(status); +export function StatusBadge({ status }: { status: ScheduleStatusType }) { + const { color, backgroundColor, Icon } = getStatusProps(status); return ( Date: Fri, 8 Sep 2023 01:05:31 +0200 Subject: [PATCH 05/11] refactor: add types to table component ref --- .../components/schedules/SchedulesTable.tsx | 30 ++++++++++--------- .../desktop-client/src/components/table.tsx | 3 +- .../src/client/data-hooks/schedules.tsx | 9 ++++-- .../loot-core/src/types/models/schedule.d.ts | 2 +- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 9605975d190..20be3a03b75 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -225,12 +225,12 @@ export function SchedulesTable({ return [...unCompletedSchedules, { id: 'show-completed' }]; }, [filteredSchedules, showCompleted, allowCompleted]); - function renderSchedule({ item }: { item: ScheduleEntity }) { + function renderSchedule({ schedule }: { schedule: ScheduleEntity }) { return ( onSelect(item.id)} + onClick={() => onSelect(schedule.id)} style={{ cursor: 'pointer', backgroundColor: theme.tableBackground, @@ -241,31 +241,33 @@ export function SchedulesTable({ - {item.name ? item.name : 'None'} + {schedule.name ? schedule.name : 'None'} - + - + - {item.next_date ? monthUtilFormat(item.next_date, dateFormat) : null} + {schedule.next_date + ? monthUtilFormat(schedule.next_date, dateFormat) + : null} - + - + {!minimal && ( - {item._date && (item._date as any).frequency && ( + {schedule._date && (schedule._date as any).frequency && ( )} @@ -273,8 +275,8 @@ export function SchedulesTable({ {!minimal && ( @@ -309,7 +311,7 @@ export function SchedulesTable({ ); } - return renderSchedule({ item: item as ScheduleEntity }); + return renderSchedule({ schedule: item as ScheduleEntity }); } return ( diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx index bae92be46c5..7d0d2aca3cb 100644 --- a/packages/desktop-client/src/components/table.tsx +++ b/packages/desktop-client/src/components/table.tsx @@ -12,6 +12,7 @@ import React, { type KeyboardEvent, type UIEvent, type ReactElement, + type Ref, } from 'react'; import { useStore } from 'react-redux'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -885,7 +886,7 @@ type TableProps = { }; export const Table: ( - props: TableProps, + props: TableProps & { ref?: Ref }, ) => ReactElement = forwardRef( ( { diff --git a/packages/loot-core/src/client/data-hooks/schedules.tsx b/packages/loot-core/src/client/data-hooks/schedules.tsx index d994d1b3842..a11bb1c7f0e 100644 --- a/packages/loot-core/src/client/data-hooks/schedules.tsx +++ b/packages/loot-core/src/client/data-hooks/schedules.tsx @@ -6,7 +6,8 @@ import { type ScheduleEntity } from '../../types/models'; import q, { liveQuery } from '../query-helpers'; export type ScheduleStatusType = ReturnType; -export type ScheduleStatuses = Map; +export type ScheduleStatuses = Map; + function loadStatuses(schedules: ScheduleEntity[], onData) { return liveQuery(getHasTransactionsQuery(schedules), onData, { mapper: data => { @@ -27,7 +28,9 @@ type UseSchedulesReturnType = { schedules: ScheduleEntity[]; statuses: ScheduleStatuses; } | null; -export function useSchedules({ transform }: UseSchedulesArgs = {}) { +export function useSchedules({ + transform, +}: UseSchedulesArgs = {}): UseSchedulesReturnType { const [data, setData] = useState(null); useEffect(() => { @@ -65,7 +68,7 @@ export function useSchedules({ transform }: UseSchedulesArgs = {}) { let SchedulesContext = createContext(null); export function SchedulesProvider({ transform, children }) { - let data = useSchedules({ transform }); + const data = useSchedules({ transform }); return ; } diff --git a/packages/loot-core/src/types/models/schedule.d.ts b/packages/loot-core/src/types/models/schedule.d.ts index 57bad1bd52c..bf74638c41d 100644 --- a/packages/loot-core/src/types/models/schedule.d.ts +++ b/packages/loot-core/src/types/models/schedule.d.ts @@ -5,7 +5,7 @@ import type { RuleEntity } from './rule'; export interface ScheduleEntity { id: string; name?: string; - rule: RuleEntity; + rule: RuleEntity['id']; next_date: string; completed: boolean; posts_transaction: boolean; From 20dd8577e314d3dbb0761bf6ed2b37c7010dff83 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Wed, 13 Sep 2023 23:01:01 +0200 Subject: [PATCH 06/11] refactor: change colors to themes --- .../src/components/schedules/StatusBadge.tsx | 39 +++++++++---------- tsconfig.json | 1 - 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/StatusBadge.tsx b/packages/desktop-client/src/components/schedules/StatusBadge.tsx index 65cdf0119c7..d0b7ae84cba 100644 --- a/packages/desktop-client/src/components/schedules/StatusBadge.tsx +++ b/packages/desktop-client/src/components/schedules/StatusBadge.tsx @@ -10,7 +10,7 @@ import CheckCircleHollow from '../../icons/v2/CheckCircleHollow'; import EditSkull1 from '../../icons/v2/EditSkull1'; import FavoriteStar from '../../icons/v2/FavoriteStar'; import ValidationCheck from '../../icons/v2/ValidationCheck'; -import { theme, type CSSProperties, colors } from '../../style'; +import { theme } from '../../style'; import Text from '../common/Text'; import View from '../common/View'; @@ -20,57 +20,56 @@ export function getStatusProps(status: StatusTypes) { switch (status) { case 'missed': return { - color: colors.r1, - backgroundColor: colors.r10, + color: theme.altErrorText, + backgroundColor: theme.altErrorBackground, Icon: EditSkull1, }; case 'due': return { - color: colors.y1, - backgroundColor: colors.y9, + color: theme.altWarningText, + backgroundColor: theme.altWarningBackground, Icon: AlertTriangle, }; case 'upcoming': return { - color: colors.p1, - backgroundColor: colors.p10, + color: theme.upcomingText, + backgroundColor: theme.upcomingBackground, Icon: CalendarIcon, }; case 'paid': return { - color: colors.g2, - backgroundColor: colors.g10, + color: theme.alt2NoticeText, + backgroundColor: theme.altNoticeBackground, Icon: ValidationCheck, }; case 'completed': return { - color: colors.n4, - backgroundColor: colors.n11, + color: theme.alt2TableText, + backgroundColor: theme.altTableBackground, Icon: FavoriteStar, }; - // @todo: Check if 'pending' is still a valid status in Transaction case 'pending': return { - color: colors.g4, - backgroundColor: colors.g11, + color: theme.alt3NoticeText, + backgroundColor: theme.alt2NoticeBackground, Icon: CalendarIcon, }; case 'scheduled': return { - color: colors.n1, - backgroundColor: colors.n11, + color: theme.menuItemText, + backgroundColor: theme.altTableBackground, Icon: CalendarIcon, }; case 'cleared': return { - color: colors.g5, - backgroundColor: colors.n11, + color: theme.noticeText, + backgroundColor: theme.altTableBackground, Icon: CheckCircle1, }; default: return { - color: colors.n7, - backgroundColor: colors.n11, + color: theme.buttonNormalDisabledText, + backgroundColor: theme.altTableBackground, Icon: CheckCircleHollow, }; } diff --git a/tsconfig.json b/tsconfig.json index 9002ba8e283..1a4dce7bf8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,6 @@ // TODO: enable once every file is ts // "strict": true, "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, "skipLibCheck": true, "jsx": "preserve", // Check JS files too From 025c0b888063a68f1162e06dcf925e27dddd2b12 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Wed, 13 Sep 2023 23:19:52 +0200 Subject: [PATCH 07/11] chore: add release note --- upcoming-release-notes/1691.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/1691.md diff --git a/upcoming-release-notes/1691.md b/upcoming-release-notes/1691.md new file mode 100644 index 00000000000..3af55f81084 --- /dev/null +++ b/upcoming-release-notes/1691.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [muhsinkamil] +--- + +Refactor SchedulesTable and its components to tsx. From 23ed104c812e68a1bb41f5e32014c4f8931be570 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Wed, 13 Sep 2023 23:44:40 +0200 Subject: [PATCH 08/11] refactor: add types to Schedule item actions --- .../components/schedules/SchedulesTable.tsx | 29 +++++++++++++++---- .../src/components/schedules/index.tsx | 22 +++++++------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 20be3a03b75..01445264e2b 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -41,6 +41,14 @@ type SchedulesTableProps = { type CompletedScheduleItem = { id: 'show-completed' }; type SchedulesTableItem = ScheduleEntity | CompletedScheduleItem; +export enum ScheduleItemAction { + PostTransaction = 'post-transaction', + SkipSchedule = 'skip', + CompleteSchedule = 'complete', + RestartSchedule = 'restart', + DeleteSchedule = 'delete', +} + export const ROW_HEIGHT = 43; function OverflowMenu({ @@ -83,16 +91,27 @@ function OverflowMenu({ }} items={[ status === 'due' && { - name: 'post-transaction', + name: ScheduleItemAction.PostTransaction, text: 'Post transaction', }, ...(schedule.completed - ? [{ name: 'restart', text: 'Restart' }] + ? [ + { + name: ScheduleItemAction.RestartSchedule, + text: 'Restart', + }, + ] : [ - { name: 'skip', text: 'Skip next date' }, - { name: 'complete', text: 'Complete' }, + { + name: ScheduleItemAction.SkipSchedule, + text: 'Skip next date', + }, + { + name: ScheduleItemAction.CompleteSchedule, + text: 'Complete', + }, ]), - { name: 'delete', text: 'Delete' }, + { name: ScheduleItemAction.DeleteSchedule, text: 'Delete' }, ]} /> diff --git a/packages/desktop-client/src/components/schedules/index.tsx b/packages/desktop-client/src/components/schedules/index.tsx index f72b3937cb5..a8010e4ade7 100644 --- a/packages/desktop-client/src/components/schedules/index.tsx +++ b/packages/desktop-client/src/components/schedules/index.tsx @@ -11,7 +11,11 @@ import Search from '../common/Search'; import View from '../common/View'; import { Page } from '../Page'; -import { SchedulesTable, ROW_HEIGHT } from './SchedulesTable'; +import { + SchedulesTable, + ROW_HEIGHT, + ScheduleItemAction, +} from './SchedulesTable'; export default function Schedules() { const { pushModal } = useActions(); @@ -37,27 +41,26 @@ export default function Schedules() { pushModal('schedules-discover'); } - // @todo: replace name: string with enum - async function onAction(name: string, id: ScheduleEntity['id']) { + async function onAction(name: ScheduleItemAction, id: ScheduleEntity['id']) { switch (name) { - case 'post-transaction': + case ScheduleItemAction.PostTransaction: await send('schedule/post-transaction', { id }); break; - case 'skip': + case ScheduleItemAction.SkipSchedule: await send('schedule/skip-next-date', { id }); break; - case 'complete': + case ScheduleItemAction.CompleteSchedule: await send('schedule/update', { schedule: { id, completed: true }, }); break; - case 'restart': + case ScheduleItemAction.RestartSchedule: await send('schedule/update', { schedule: { id, completed: false }, resetNextDate: true, }); break; - case 'delete': + case ScheduleItemAction.DeleteSchedule: await send('schedule/delete', { id }); break; default: @@ -88,9 +91,6 @@ export default function Schedules() { onSelect={onEdit} onAction={onAction} style={{ backgroundColor: theme.tableBackground }} - // @todo: Remove following props after typing SchedulesTable - minimal={undefined} - tableStyle={undefined} /> From 7649afe8cdbc7559284b43cc313b548421cf267e Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Wed, 13 Sep 2023 23:51:00 +0200 Subject: [PATCH 09/11] refactor: type with Item action enum for onAction in SchedulesTable --- packages/desktop-client/src/components/common/Menu.tsx | 2 +- .../src/components/schedules/SchedulesTable.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx index 6012cf8e33f..43aabf4dce9 100644 --- a/packages/desktop-client/src/components/common/Menu.tsx +++ b/packages/desktop-client/src/components/common/Menu.tsx @@ -23,7 +23,7 @@ function Keybinding({ keyName }: KeybindingProps) { ); } -export type MenuItem = { +type MenuItem = { type?: string | symbol; name: string; disabled?: boolean; diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 01445264e2b..a9bfaa72e3f 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -16,7 +16,7 @@ import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple'; import Check from '../../icons/v2/Check'; import { theme } from '../../style'; import Button from '../common/Button'; -import Menu, { type MenuItem } from '../common/Menu'; +import Menu from '../common/Menu'; import Text from '../common/Text'; import View from '../common/View'; import PrivacyFilter from '../PrivacyFilter'; @@ -32,7 +32,7 @@ type SchedulesTableProps = { filter: string; allowCompleted: boolean; onSelect: (id: ScheduleEntity['id']) => void; - onAction: (actionName: string, id: ScheduleEntity['id']) => void; + onAction: (actionName: ScheduleItemAction, id: ScheduleEntity['id']) => void; style: CSSProperties; minimal?: boolean; tableStyle?: CSSProperties; @@ -85,7 +85,7 @@ function OverflowMenu({ onClose={() => setOpen(false)} > { + onMenuSelect={(name: ScheduleItemAction) => { onAction(name, schedule.id); setOpen(false); }} From b991f677bad003fd64ef70357e3bd43f3058981a Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Thu, 14 Sep 2023 23:21:46 +0200 Subject: [PATCH 10/11] refactor: add types to schedules date --- .../src/components/schedules/SchedulesTable.tsx | 2 +- packages/loot-core/src/types/models/schedule.d.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index a9bfaa72e3f..8daa359ca89 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -286,7 +286,7 @@ export function SchedulesTable({ {!minimal && ( - {schedule._date && (schedule._date as any).frequency && ( + {schedule._date && schedule._date.frequency && ( )} diff --git a/packages/loot-core/src/types/models/schedule.d.ts b/packages/loot-core/src/types/models/schedule.d.ts index bf74638c41d..5384eab2161 100644 --- a/packages/loot-core/src/types/models/schedule.d.ts +++ b/packages/loot-core/src/types/models/schedule.d.ts @@ -17,7 +17,17 @@ export interface ScheduleEntity { _account: AccountEntity['id']; _amount: unknown; _amountOp: string; - _date: unknown; + _date: { + interval: number; + patterns: { + value: number; + type: 'SU' | 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'day'; + }[]; + skipWeekend: boolean; + start: string; + weekendSolveMode: 'before' | 'after'; + frequency: 'daily' | 'weekly' | 'monthly' | 'yearly'; + }; _conditions: unknown; _actions: unknown; } From d45ed4737d37d1644dd44f54284750de6dc32603 Mon Sep 17 00:00:00 2001 From: Mohamed Muhsin Kamil Date: Sat, 16 Sep 2023 21:16:56 +0200 Subject: [PATCH 11/11] refactor: refactor type definition to data-hooks --- .../components/schedules/SchedulesTable.tsx | 71 +++++++++++-------- .../src/components/schedules/index.tsx | 12 ++-- .../src/client/data-hooks/accounts.tsx | 10 +-- .../src/client/data-hooks/payees.tsx | 10 +-- 4 files changed, 56 insertions(+), 47 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 8daa359ca89..8bfb8062e2f 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -41,13 +41,12 @@ type SchedulesTableProps = { type CompletedScheduleItem = { id: 'show-completed' }; type SchedulesTableItem = ScheduleEntity | CompletedScheduleItem; -export enum ScheduleItemAction { - PostTransaction = 'post-transaction', - SkipSchedule = 'skip', - CompleteSchedule = 'complete', - RestartSchedule = 'restart', - DeleteSchedule = 'delete', -} +export type ScheduleItemAction = + | 'post-transaction' + | 'skip' + | 'complete' + | 'restart' + | 'delete'; export const ROW_HEIGHT = 43; @@ -62,6 +61,39 @@ function OverflowMenu({ }) { 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 (