mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-10 16:26:43 -05:00
[refactor] Migrate Schedules Table to typescript (#1691)
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
@@ -49,23 +117,11 @@ 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>
|
||||
)}
|
||||
@@ -73,10 +129,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 (
|
||||
<Cell
|
||||
@@ -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 (
|
||||
@@ -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,
|
||||
@@ -204,33 +269,33 @@ 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>
|
||||
@@ -238,8 +303,8 @@ export function SchedulesTable({
|
||||
{!minimal && (
|
||||
<Field width={40} name="actions">
|
||||
<OverflowMenu
|
||||
schedule={item}
|
||||
status={statuses.get(item.id)}
|
||||
schedule={schedule}
|
||||
status={statuses.get(schedule.id)}
|
||||
onAction={onAction}
|
||||
/>
|
||||
</Field>
|
||||
@@ -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}
|
||||
@@ -274,7 +339,7 @@ export function SchedulesTable({
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return renderSchedule({ item });
|
||||
return renderSchedule({ schedule: item as ScheduleEntity });
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { type ScheduleStatusType } from 'loot-core/src/client/data-hooks/schedules';
|
||||
import { titleFirst } from 'loot-core/src/shared/util';
|
||||
|
||||
import AlertTriangle from '../../icons/v2/AlertTriangle';
|
||||
@@ -9,81 +10,73 @@ 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 } from '../../style';
|
||||
import Text from '../common/Text';
|
||||
import View from '../common/View';
|
||||
|
||||
export function getStatusProps(status: Status) {
|
||||
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: theme.altErrorText,
|
||||
backgroundColor: theme.altErrorBackground,
|
||||
Icon: EditSkull1,
|
||||
};
|
||||
case 'due':
|
||||
color = theme.altWarningText;
|
||||
backgroundColor = theme.altWarningBackground;
|
||||
Icon = AlertTriangle;
|
||||
break;
|
||||
return {
|
||||
color: theme.altWarningText,
|
||||
backgroundColor: theme.altWarningBackground,
|
||||
Icon: AlertTriangle,
|
||||
};
|
||||
case 'upcoming':
|
||||
color = theme.upcomingText;
|
||||
backgroundColor = theme.upcomingBackground;
|
||||
Icon = CalendarIcon;
|
||||
break;
|
||||
return {
|
||||
color: theme.upcomingText,
|
||||
backgroundColor: theme.upcomingBackground,
|
||||
Icon: CalendarIcon,
|
||||
};
|
||||
case 'paid':
|
||||
color = theme.alt2NoticeText;
|
||||
backgroundColor = theme.altNoticeBackground;
|
||||
Icon = ValidationCheck;
|
||||
break;
|
||||
return {
|
||||
color: theme.alt2NoticeText,
|
||||
backgroundColor: theme.altNoticeBackground,
|
||||
Icon: ValidationCheck,
|
||||
};
|
||||
case 'completed':
|
||||
color = theme.alt2TableText;
|
||||
backgroundColor = theme.altTableBackground;
|
||||
Icon = FavoriteStar;
|
||||
break;
|
||||
return {
|
||||
color: theme.alt2TableText,
|
||||
backgroundColor: theme.altTableBackground,
|
||||
Icon: FavoriteStar,
|
||||
};
|
||||
case 'pending':
|
||||
color = theme.alt3NoticeText;
|
||||
backgroundColor = theme.alt2NoticeBackground;
|
||||
Icon = CalendarIcon;
|
||||
break;
|
||||
return {
|
||||
color: theme.alt3NoticeText,
|
||||
backgroundColor: theme.alt2NoticeBackground,
|
||||
Icon: CalendarIcon,
|
||||
};
|
||||
case 'scheduled':
|
||||
color = theme.menuItemText;
|
||||
backgroundColor = theme.altTableBackground;
|
||||
Icon = CalendarIcon;
|
||||
break;
|
||||
return {
|
||||
color: theme.menuItemText,
|
||||
backgroundColor: theme.altTableBackground,
|
||||
Icon: CalendarIcon,
|
||||
};
|
||||
case 'cleared':
|
||||
color = theme.noticeText;
|
||||
backgroundColor = theme.altTableBackground;
|
||||
Icon = CheckCircle1;
|
||||
break;
|
||||
return {
|
||||
color: theme.noticeText,
|
||||
backgroundColor: theme.altTableBackground,
|
||||
Icon: CheckCircle1,
|
||||
};
|
||||
default:
|
||||
color = theme.buttonNormalDisabledText;
|
||||
backgroundColor = theme.altTableBackground;
|
||||
Icon = CheckCircleHollow;
|
||||
break;
|
||||
return {
|
||||
color: theme.buttonNormalDisabledText,
|
||||
backgroundColor: theme.altTableBackground,
|
||||
Icon: CheckCircleHollow,
|
||||
};
|
||||
}
|
||||
|
||||
return { color, backgroundColor, Icon };
|
||||
}
|
||||
|
||||
type Status =
|
||||
| 'missed'
|
||||
| 'due'
|
||||
| 'upcoming'
|
||||
| 'paid'
|
||||
| 'completed'
|
||||
| 'pending'
|
||||
| 'scheduled'
|
||||
| 'cleared';
|
||||
|
||||
type StatusBadgeProps = {
|
||||
status: Status;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, style }: StatusBadgeProps) {
|
||||
let { color, backgroundColor, Icon } = getStatusProps(status);
|
||||
export function StatusBadge({ status }: { status: ScheduleStatusType }) {
|
||||
const { color, backgroundColor, Icon } = getStatusProps(status);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -94,7 +87,6 @@ export function StatusBadge({ status, style }: StatusBadgeProps) {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -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,
|
||||
type ScheduleItemAction,
|
||||
} from './SchedulesTable';
|
||||
|
||||
export default function Schedules() {
|
||||
const { pushModal } = useActions();
|
||||
@@ -37,8 +41,7 @@ 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':
|
||||
await send('schedule/post-transaction', { id });
|
||||
@@ -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}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import React, {
|
||||
type ReactNode,
|
||||
type KeyboardEvent,
|
||||
type UIEvent,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
import { useStore } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
@@ -833,7 +835,7 @@ let rowStyle: CSSProperties = {
|
||||
type TableHandleRef = {
|
||||
scrollTo: (id: number, alignment?: string) => void;
|
||||
scrollToTop: () => void;
|
||||
getScrolledItem: () => number;
|
||||
getScrolledItem: () => TableItem['id'];
|
||||
setRowAnimation: (flag) => void;
|
||||
edit(id: number, field, shouldScroll): void;
|
||||
anchor(): void;
|
||||
@@ -852,10 +854,10 @@ export const TableWithNavigator = forwardRef<
|
||||
return <Table {...props} navigator={navigator} />;
|
||||
});
|
||||
|
||||
type TableItem = { id: number };
|
||||
type TableItem = { id: number | string };
|
||||
|
||||
type TableProps = {
|
||||
items: TableItem[];
|
||||
type TableProps<T = TableItem> = {
|
||||
items: T[];
|
||||
count?: number;
|
||||
headers?: ReactNode | TableHeaderProps['headers'];
|
||||
contentHeader?: ReactNode;
|
||||
@@ -863,7 +865,7 @@ type TableProps = {
|
||||
rowHeight?: number;
|
||||
backgroundColor?: string;
|
||||
renderItem: (arg: {
|
||||
item: TableItem;
|
||||
item: T;
|
||||
editing: boolean;
|
||||
focusedField: unknown;
|
||||
onEdit: (id, field) => void;
|
||||
@@ -875,6 +877,7 @@ type TableProps = {
|
||||
loadMore?: () => void;
|
||||
style?: CSSProperties;
|
||||
navigator?: ReturnType<typeof useTableNavigator>;
|
||||
listRef?: unknown;
|
||||
onScroll?: () => void;
|
||||
version?: string;
|
||||
allowPopupsEscape?: boolean;
|
||||
@@ -882,7 +885,9 @@ type TableProps = {
|
||||
saveScrollWidth?: (parent, child) => void;
|
||||
};
|
||||
|
||||
export const Table = forwardRef<TableHandleRef, TableProps>(
|
||||
export const Table: <T extends TableItem>(
|
||||
props: TableProps<T> & { ref?: Ref<TableHandleRef> },
|
||||
) => ReactElement = forwardRef<TableHandleRef, TableProps>(
|
||||
(
|
||||
{
|
||||
items,
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
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';
|
||||
|
||||
function useAccounts() {
|
||||
function useAccounts(): AccountEntity[] {
|
||||
return useLiveQuery(() => q('accounts').select('*'), []);
|
||||
}
|
||||
|
||||
let AccountsContext = createContext(null);
|
||||
const AccountsContext = createContext<AccountEntity[]>(null);
|
||||
|
||||
export function AccountsProvider({ children }) {
|
||||
let data = useAccounts();
|
||||
const data = useAccounts();
|
||||
return <AccountsContext.Provider value={data} children={children} />;
|
||||
}
|
||||
|
||||
export function CachedAccounts({ children, idKey }) {
|
||||
let data = useCachedAccounts({ idKey });
|
||||
const data = useCachedAccounts({ idKey });
|
||||
return children(data);
|
||||
}
|
||||
|
||||
export function useCachedAccounts({ idKey }: { idKey? } = {}) {
|
||||
let data = useContext(AccountsContext);
|
||||
export function useCachedAccounts(): AccountEntity[];
|
||||
export function useCachedAccounts({
|
||||
idKey,
|
||||
}: {
|
||||
idKey: boolean;
|
||||
}): Record<AccountEntity['id'], AccountEntity>;
|
||||
export function useCachedAccounts({ idKey }: { idKey?: boolean } = {}) {
|
||||
const data = useContext(AccountsContext);
|
||||
return idKey && data ? getAccountsById(data) : data;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
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';
|
||||
|
||||
function usePayees() {
|
||||
function usePayees(): PayeeEntity[] {
|
||||
return useLiveQuery(() => q('payees').select('*'), []);
|
||||
}
|
||||
|
||||
let PayeesContext = createContext(null);
|
||||
const PayeesContext = createContext<PayeeEntity[]>(null);
|
||||
|
||||
export function PayeesProvider({ children }) {
|
||||
let data = usePayees();
|
||||
const data = usePayees();
|
||||
return <PayeesContext.Provider value={data} children={children} />;
|
||||
}
|
||||
|
||||
export function CachedPayees({ children, idKey }) {
|
||||
let data = useCachedPayees({ idKey });
|
||||
const data = useCachedPayees({ idKey });
|
||||
return children(data);
|
||||
}
|
||||
|
||||
export function useCachedPayees({ idKey }: { idKey? } = {}) {
|
||||
let data = useContext(PayeesContext);
|
||||
export function useCachedPayees(): PayeeEntity[];
|
||||
export function useCachedPayees({
|
||||
idKey,
|
||||
}: {
|
||||
idKey: boolean;
|
||||
}): Record<PayeeEntity['id'], PayeeEntity>;
|
||||
export function useCachedPayees({ idKey }: { idKey?: boolean } = {}) {
|
||||
const data = useContext(PayeesContext);
|
||||
return idKey && data ? getPayeesById(data) : data;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { getStatus, getHasTransactionsQuery } from '../../shared/schedules';
|
||||
import { type ScheduleEntity } from '../../types/models';
|
||||
import q, { liveQuery } from '../query-helpers';
|
||||
|
||||
export type ScheduleStatusType = ReturnType<typeof getStatus>;
|
||||
export type ScheduleStatuses = Map<ScheduleEntity['id'], ScheduleStatusType>;
|
||||
|
||||
function loadStatuses(schedules: ScheduleEntity[], onData) {
|
||||
return liveQuery(getHasTransactionsQuery(schedules), onData, {
|
||||
mapper: data => {
|
||||
@@ -23,13 +26,15 @@ function loadStatuses(schedules: ScheduleEntity[], onData) {
|
||||
type UseSchedulesArgs = { transform?: (q: Query) => Query };
|
||||
type UseSchedulesReturnType = {
|
||||
schedules: ScheduleEntity[];
|
||||
statuses: Record<string, ReturnType<typeof getStatus>>;
|
||||
statuses: ScheduleStatuses;
|
||||
} | null;
|
||||
export function useSchedules({ transform }: UseSchedulesArgs = {}) {
|
||||
let [data, setData] = useState<UseSchedulesReturnType | null>(null);
|
||||
export function useSchedules({
|
||||
transform,
|
||||
}: UseSchedulesArgs = {}): UseSchedulesReturnType {
|
||||
const [data, setData] = useState<UseSchedulesReturnType>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let query = q('schedules').select('*');
|
||||
const query = q('schedules').select('*');
|
||||
let scheduleQuery, statusQuery;
|
||||
|
||||
scheduleQuery = liveQuery(
|
||||
@@ -40,10 +45,8 @@ export function useSchedules({ transform }: UseSchedulesArgs = {}) {
|
||||
statusQuery.unsubscribe();
|
||||
}
|
||||
|
||||
statusQuery = loadStatuses(
|
||||
schedules,
|
||||
(statuses: Record<string, ReturnType<typeof getStatus>>) =>
|
||||
setData({ schedules, statuses }),
|
||||
statusQuery = loadStatuses(schedules, (statuses: ScheduleStatuses) =>
|
||||
setData({ schedules, statuses }),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -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 <SchedulesContext.Provider value={data} children={children} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ export function fastSetMerge(set1, set2) {
|
||||
return finalSet;
|
||||
}
|
||||
|
||||
export function titleFirst(str) {
|
||||
export function titleFirst(str: string) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { PayeeEntity } from './payee';
|
||||
import type { RuleEntity } from './rule';
|
||||
|
||||
export interface ScheduleEntity {
|
||||
id?: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
rule: RuleEntity;
|
||||
rule: RuleEntity['id'];
|
||||
next_date: string;
|
||||
completed: boolean;
|
||||
posts_transaction: boolean;
|
||||
@@ -13,11 +13,21 @@ export interface ScheduleEntity {
|
||||
|
||||
// These are special fields that are actually pulled from the
|
||||
// underlying rule
|
||||
_payee: PayeeEntity;
|
||||
_account: AccountEntity;
|
||||
_payee: PayeeEntity['id'];
|
||||
_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;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/1691.md
Normal file
6
upcoming-release-notes/1691.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [muhsinkamil]
|
||||
---
|
||||
|
||||
Refactor SchedulesTable and its components to tsx.
|
||||
Reference in New Issue
Block a user