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

Support type-checking on spreadsheet fields (part 1) #3093

Merged
Merged
10 changes: 5 additions & 5 deletions packages/desktop-client/src/components/sidebar/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
type OnDragChangeCallback,
type OnDropCallback,
} from '../sort';
import { type Binding } from '../spreadsheet';
import { type SheetFields, type Binding } from '../spreadsheet';
import { CellValue } from '../spreadsheet/CellValue';

export const accountNameStyle: CSSProperties = {
Expand All @@ -37,10 +37,10 @@ export const accountNameStyle: CSSProperties = {
...styles.smallText,
};

type AccountProps = {
type AccountProps<FieldName extends SheetFields<'account'>> = {
name: string;
to: string;
query: Binding;
query: Binding<'account', FieldName>;
account?: AccountEntity;
connected?: boolean;
pending?: boolean;
Expand All @@ -52,7 +52,7 @@ type AccountProps = {
onDrop?: OnDropCallback;
};

export function Account({
export function Account<FieldName extends SheetFields<'account'>>({
name,
account,
connected,
Expand All @@ -65,7 +65,7 @@ export function Account({
outerStyle,
onDragChange,
onDrop,
}: AccountProps) {
}: AccountProps<FieldName>) {
const type = account
? account.closed
? 'account-closed'
Expand Down
40 changes: 38 additions & 2 deletions packages/desktop-client/src/components/spreadsheet/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
// @ts-strict-ignore
import { type Query } from 'loot-core/src/shared/query';

export type Binding = string | { name: string; value?; query?: Query };
export type Spreadsheets = {
account: {
// Common fields
'uncategorized-amount': number;
'uncategorized-balance': number;

// Account fields
balance: number;
'accounts-balance': number;
'budgeted-accounts-balance': number;
'offbudget-accounts-balance': number;
balanceCleared: number;
balanceUncleared: number;
};
};

export type SheetNames = keyof Spreadsheets & string;

export type SheetFields<SheetName extends SheetNames> =
keyof Spreadsheets[SheetName] & string;

export type Binding<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
SheetName extends SheetNames = any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
SheetFieldName extends SheetFields<SheetName> = any,
> =
| SheetFieldName
| {
name: SheetFieldName;
value?: Spreadsheets[SheetName][SheetFieldName];
query?: Query;
};
export const parametrizedField =
<SheetName extends SheetNames>() =>
<SheetFieldName extends SheetFields<SheetName>>(field: SheetFieldName) =>
(id: string): SheetFieldName =>
`${field}-${id}` as SheetFieldName;
51 changes: 36 additions & 15 deletions packages/desktop-client/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ import {
ConditionalPrivacyFilter,
mergeConditionalPrivacyFilterProps,
} from './PrivacyFilter';
import { type Binding } from './spreadsheet';
import {
type Spreadsheets,
type SheetFields,
type SheetNames,
type Binding,
} from './spreadsheet';
import { type FormatType, useFormat } from './spreadsheet/useFormat';
import { useSheetValue } from './spreadsheet/useSheetValue';

Expand Down Expand Up @@ -311,7 +316,7 @@ const readonlyInputStyle = {
'::selection': { backgroundColor: theme.formInputTextReadOnlySelection },
};

type InputValueProps = ComponentProps<typeof Input> & {
type InputValueProps = Omit<ComponentProps<typeof Input>, 'value'> & {
value?: string;
};

Expand Down Expand Up @@ -671,31 +676,47 @@ export function SelectCell({
);
}

type SheetCellValueProps = {
binding: Binding;
type SheetCellValueProps<
SheetName extends SheetNames,
FieldName extends SheetFields<SheetName>,
> = {
binding: Binding<SheetName, FieldName>;
type: FormatType;
getValueStyle?: (value: string | number) => CSSProperties;
formatExpr?: (value) => string;
getValueStyle?: (value: Spreadsheets[SheetName][FieldName]) => CSSProperties;
formatExpr?: (value: Spreadsheets[SheetName][FieldName]) => string;
unformatExpr?: (value: string) => unknown;
privacyFilter?: ComponentProps<
typeof ConditionalPrivacyFilter
>['privacyFilter'];
};

type SheetCellProps = ComponentProps<typeof Cell> & {
valueProps: SheetCellValueProps;
inputProps?: Omit<ComponentProps<typeof InputValue>, 'value' | 'onUpdate'>;
type SheetCellProps<
SheetName extends SheetNames,
FieldName extends SheetFields<SheetName>,
> = ComponentProps<typeof Cell> & {
valueProps: SheetCellValueProps<SheetName, FieldName>;
inputProps?: Omit<
ComponentProps<typeof InputValue>,
'value' | 'onUpdate' | 'onBlur'
> & {
onBlur?: () => void;
};
onSave?: (value) => void;
textAlign?: CSSProperties['textAlign'];
};
export function SheetCell({
export function SheetCell<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
SheetName extends SheetNames = any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
FieldName extends SheetFields<SheetName> = any,
>({
valueProps,
valueStyle,
inputProps,
textAlign,
onSave,
...props
}: SheetCellProps) {
}: SheetCellProps<SheetName, FieldName>) {
const {
binding,
type,
Expand All @@ -705,10 +726,10 @@ export function SheetCell({
privacyFilter,
} = valueProps;

const sheetValue = useSheetValue(binding, e => {
const sheetValue = useSheetValue(binding, () => {
// "close" the cell if it's editing
if (props.exposed && inputProps && inputProps.onBlur) {
inputProps.onBlur(e);
inputProps.onBlur();
}
});
const format = useFormat();
Expand All @@ -722,7 +743,7 @@ export function SheetCell({
}
textAlign={textAlign}
{...props}
value={sheetValue}
value={String(sheetValue ?? '')}
formatter={value =>
props.formatter ? props.formatter(value, type) : format(value, type)
}
Expand All @@ -738,7 +759,7 @@ export function SheetCell({
{() => {
return (
<InputValue
value={formatExpr ? formatExpr(sheetValue) : sheetValue}
value={formatExpr ? formatExpr(sheetValue) : sheetValue.toString()}
onUpdate={value => {
onSave(unformatExpr ? unformatExpr(value) : value);
}}
Expand Down
68 changes: 51 additions & 17 deletions packages/loot-core/src/client/queries.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
// @ts-strict-ignore
import { parse as parseDate, isValid as isDateValid } from 'date-fns';

import {
parametrizedField,
type Binding,
type SheetNames,
} from '../../../desktop-client/src/components/spreadsheet';
import {
dayFromDate,
getDayMonthRegex,
getDayMonthFormat,
getShortYearRegex,
getShortYearFormat,
} from '../shared/months';
import { q } from '../shared/query';
import { q, type Query } from '../shared/query';
import { currencyToAmount, amountToInteger } from '../shared/util';
import { type CategoryEntity, type AccountEntity } from '../types/models';
import { type LocalPrefs } from '../types/prefs';

const accountParametrizedField = parametrizedField<'account'>();

export function getAccountFilter(accountId, field = 'account') {
export function getAccountFilter(accountId: string, field = 'account') {
if (accountId) {
if (accountId === 'budgeted') {
return {
Expand Down Expand Up @@ -47,7 +56,7 @@ export function getAccountFilter(accountId, field = 'account') {
return null;
}

export function makeTransactionsQuery(accountId) {
export function makeTransactionsQuery(accountId: string) {
let query = q('transactions').options({ splits: 'grouped' });

const filter = getAccountFilter(accountId);
Expand All @@ -58,7 +67,11 @@ export function makeTransactionsQuery(accountId) {
return query;
}

export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
export function makeTransactionSearchQuery(
currentQuery: Query,
search: string,
dateFormat: LocalPrefs['dateFormat'],
) {
const amount = currencyToAmount(search);

// Support various date formats
Expand Down Expand Up @@ -94,37 +107,43 @@ export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
});
}

export function accountBalance(acct) {
export function accountBalance(
acct: AccountEntity,
): Binding<'account', 'balance'> {
return {
name: `balance-${acct.id}`,
name: accountParametrizedField('balance')(acct.id),
query: q('transactions')
.filter({ account: acct.id })
.options({ splits: 'none' })
.calculate({ $sum: '$amount' }),
};
}

export function accountBalanceCleared(acct) {
export function accountBalanceCleared(
acct: AccountEntity,
): Binding<'account', 'balanceCleared'> {
return {
name: `balanceCleared-${acct.id}`,
name: accountParametrizedField('balanceCleared')(acct.id),
query: q('transactions')
.filter({ account: acct.id, cleared: true })
.options({ splits: 'none' })
.calculate({ $sum: '$amount' }),
};
}

export function accountBalanceUncleared(acct) {
export function accountBalanceUncleared(
acct: AccountEntity,
): Binding<'account', 'balanceUncleared'> {
return {
name: `balanceUncleared-${acct.id}`,
name: accountParametrizedField('balanceUncleared')(acct.id),
query: q('transactions')
.filter({ account: acct.id, cleared: false })
.options({ splits: 'none' })
.calculate({ $sum: '$amount' }),
};
}

export function allAccountBalance() {
export function allAccountBalance(): Binding<'account', 'accounts-balance'> {
return {
query: q('transactions')
.filter({ 'account.closed': false })
Expand All @@ -133,7 +152,10 @@ export function allAccountBalance() {
};
}

export function budgetedAccountBalance() {
export function budgetedAccountBalance(): Binding<
'account',
'budgeted-accounts-balance'
> {
return {
name: `budgeted-accounts-balance`,
query: q('transactions')
Expand All @@ -142,7 +164,10 @@ export function budgetedAccountBalance() {
};
}

export function offbudgetAccountBalance() {
export function offbudgetAccountBalance(): Binding<
'account',
'offbudget-accounts-balance'
> {
return {
name: `offbudget-accounts-balance`,
query: q('transactions')
Expand All @@ -151,7 +176,7 @@ export function offbudgetAccountBalance() {
};
}

export function categoryBalance(category, month) {
export function categoryBalance(category: CategoryEntity, month: string) {
return {
name: `balance-${category.id}`,
query: q('transactions')
Expand All @@ -164,7 +189,10 @@ export function categoryBalance(category, month) {
};
}

export function categoryBalanceCleared(category, month) {
export function categoryBalanceCleared(
category: CategoryEntity,
month: string,
) {
return {
name: `balanceCleared-${category.id}`,
query: q('transactions')
Expand All @@ -178,7 +206,10 @@ export function categoryBalanceCleared(category, month) {
};
}

export function categoryBalanceUncleared(category, month) {
export function categoryBalanceUncleared(
category: CategoryEntity,
month: string,
) {
return {
name: `balanceUncleared-${category.id}`,
query: q('transactions')
Expand Down Expand Up @@ -210,7 +241,10 @@ export function uncategorizedBalance() {
};
}

export function uncategorizedCount() {
export function uncategorizedCount<SheetName extends SheetNames>(): Binding<
SheetName,
'uncategorized-amount'
> {
return {
name: 'uncategorized-amount',
query: uncategorizedQuery.calculate({ $count: '$id' }),
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/3093.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [jfdoming]
---

Support type-checking on spreadsheet fields (part 1)
Loading