Skip to content

Commit

Permalink
Add clear typings to the modals (actualbudget#1359)
Browse files Browse the repository at this point in the history
  • Loading branch information
j-f1 authored Jul 18, 2023
1 parent 790b18d commit 4f396d7
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import React from 'react';
import { connect } from 'react-redux';
import { useSelector } from 'react-redux';

import { bindActionCreators } from 'redux';

import * as actions from 'loot-core/src/client/actions';
import { send } from 'loot-core/src/platform/client/fetch';

import useFeatureFlag from '../hooks/useFeatureFlag';
import { useActions } from '../hooks/useActions';
import useSyncServerStatus from '../hooks/useSyncServerStatus';

import BudgetSummary from './modals/BudgetSummary';
Expand All @@ -27,21 +24,21 @@ import NordigenInitialise from './modals/NordigenInitialise';
import PlaidExternalMsg from './modals/PlaidExternalMsg';
import SelectLinkedAccounts from './modals/SelectLinkedAccounts';

function Modals({
modalStack,
isHidden,
accounts,
categoryGroups,
categories,
budgetId,
actions,
}) {
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
export default function Modals() {
const modalStack = useSelector(state => state.modals.modalStack);
const isHidden = useSelector(state => state.modals.isHidden);
const accounts = useSelector(state => state.queries.accounts);
const categoryGroups = useSelector(state => state.queries.categories.grouped);
const categories = useSelector(state => state.queries.categories.list);
const budgetId = useSelector(
state => state.prefs.local && state.prefs.local.id,
);
const actions = useActions();

const syncServerStatus = useSyncServerStatus();

return modalStack
.map(({ name, options = {} }, idx) => {
.map(({ name, options }, idx) => {
const modalProps = {
onClose: actions.popModal,
onBack: actions.popModal,
Expand All @@ -61,7 +58,6 @@ function Modals({
return (
<CreateAccount
modalProps={modalProps}
actions={actions}
syncServerStatus={syncServerStatus}
/>
);
Expand Down Expand Up @@ -91,7 +87,6 @@ function Modals({
externalAccounts={options.accounts}
requisitionId={options.requisitionId}
localAccounts={accounts.filter(acct => acct.closed === 0)}
upgradingAccountId={options.upgradingAccountId}
actions={actions}
/>
);
Expand All @@ -100,9 +95,14 @@ function Modals({
return (
<ConfirmCategoryDelete
modalProps={modalProps}
actions={actions}
category={categories.find(c => c.id === options.category)}
group={categoryGroups.find(g => g.id === options.group)}
category={
'category' in options &&
categories.find(c => c.id === options.category)
}
group={
'group' in options &&
categoryGroups.find(g => g.id === options.group)
}
categoryGroups={categoryGroups}
onDelete={options.onDelete}
/>
Expand All @@ -115,6 +115,7 @@ function Modals({
budgetId={budgetId}
modalProps={modalProps}
actions={actions}
backupDisabled={false}
/>
);

Expand Down Expand Up @@ -148,7 +149,6 @@ function Modals({
return (
<PlaidExternalMsg
modalProps={modalProps}
actions={actions}
onMoveExternal={options.onMoveExternal}
onClose={() => {
options.onClose?.();
Expand All @@ -170,7 +170,6 @@ function Modals({
return (
<NordigenExternalMsg
modalProps={modalProps}
actions={actions}
onMoveExternal={options.onMoveExternal}
onClose={() => {
options.onClose?.();
Expand Down Expand Up @@ -217,8 +216,6 @@ function Modals({
key={name}
modalProps={modalProps}
month={options.month}
actions={actions}
isGoalTemplatesEnabled={isGoalTemplatesEnabled}
/>
);

Expand All @@ -231,15 +228,3 @@ function Modals({
<React.Fragment key={modalStack[idx].name}>{modal}</React.Fragment>
));
}

export default connect(
state => ({
modalStack: state.modals.modalStack,
isHidden: state.modals.isHidden,
accounts: state.queries.accounts,
categoryGroups: state.queries.categories.grouped,
categories: state.queries.categories.list,
budgetId: state.prefs.local && state.prefs.local.id,
}),
dispatch => ({ actions: bindActionCreators(actions, dispatch) }),
)(Modals);
1 change: 0 additions & 1 deletion packages/desktop-client/src/hooks/useActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { bindActionCreators } from 'redux';
import * as actions from 'loot-core/src/client/actions';

// https://react-redux.js.org/api/hooks#recipe-useactions
// eslint-disable-next-line import/no-unused-modules
export function useActions() {
const dispatch = useDispatch();
return useMemo(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/loot-core/src/client/actions/budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function loadBudget(
);

if (showBackups) {
// @ts-expect-error manager modals are not yet typed
dispatch(pushModal('load-backup', { budgetId: id }));
}
} else {
Expand Down
19 changes: 15 additions & 4 deletions packages/loot-core/src/client/actions/modals.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import * as constants from '../constants';
import type { Modal } from '../state-types/modals';

import type { ActionResult } from './types';

export function pushModal(name: string, options: unknown): ActionResult {
return { type: constants.PUSH_MODAL, name, options };
export function pushModal<M extends Modal>(
name: M['name'],
options: M['options'],
): ActionResult {
// @ts-expect-error TS is unable to determine that `name` and `options` match
let modal: M = { name, options };
return { type: constants.PUSH_MODAL, modal };
}

export function replaceModal(name: string, options: unknown): ActionResult {
return { type: constants.REPLACE_MODAL, name, options };
export function replaceModal<M extends Modal>(
name: M['name'],
options: M['options'],
): ActionResult {
// @ts-expect-error TS is unable to determine that `name` and `options` match
let modal: M = { name, options };
return { type: constants.REPLACE_MODAL, modal };
}

export function popModal(): ActionResult {
Expand Down
7 changes: 2 additions & 5 deletions packages/loot-core/src/client/reducers/modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@ function update(state = initialState, action: Action): ModalsState {
case constants.PUSH_MODAL:
return {
...state,
modalStack: [
...state.modalStack,
{ name: action.name, options: action.options },
],
modalStack: [...state.modalStack, action.modal],
};
case constants.REPLACE_MODAL:
return {
...state,
modalStack: [{ name: action.name, options: action.options }],
modalStack: [action.modal],
};
case constants.POP_MODAL:
return { ...state, modalStack: state.modalStack.slice(0, -1) };
Expand Down
88 changes: 81 additions & 7 deletions packages/loot-core/src/client/state-types/modals.d.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,95 @@
import type { AccountEntity } from '../../types/models';
import type { RuleEntity } from '../../types/models/rule';
import type * as constants from '../constants';

// TODO: type this more throughly
type Modal = {
name: string;
options: unknown;
[K in keyof FinanceModals]: {
name: K;
options: FinanceModals[K];
};
}[keyof FinanceModals];

// There is a separate (overlapping!) set of modals for the management app. Fun!
type FinanceModals = {
'import-transactions': {
accountId: string;
filename: string;
onImported: (didChange: boolean) => void;
};

'add-account': null;
'add-local-account': null;
'close-account': {
account: AccountEntity;
balance: number;
canDelete: boolean;
};
'select-linked-accounts': {
accounts: unknown[];
requisitionId: string;
upgradingAccountId: string;
};
'configure-linked-accounts': never;

'confirm-category-delete': { onDelete: () => void } & (
| { category: string }
| { group: string }
);

'load-backup': null;

'manage-rules': { payeeId: string } | null;
'edit-rule': {
rule: RuleEntity;
onSave: (rule: RuleEntity) => void;
};
'merge-unused-payees': {
payeeIds: string[];
targetPayeeId: string;
};

'plaid-external-msg': {
onMoveExternal: () => Promise<void>;
onClose?: () => void;
onSuccess: (data: unknown) => Promise<void>;
};

'nordigen-init': {
onSuccess: () => void;
};
'nordigen-external-msg': {
onMoveExternal: (arg: {
institutionId: string;
}) => Promise<{ error: string } | { data: unknown }>;
onClose?: () => void;
onSuccess: (data: unknown) => Promise<void>;
};

'create-encryption-key': { recreate: boolean } | null;
'fix-encryption-key': {
hasExistingKey: boolean;
cloudFileId: string;
onSuccess?: () => void;
};

'edit-field': {
name: string;
onSubmit: (name: string, value: string) => void;
};

'budget-summary': {
month: string;
};
};

export type PushModalAction = {
type: typeof constants.PUSH_MODAL;
name: string;
options: unknown;
modal: Modal;
};

export type ReplaceModalAction = {
type: typeof constants.REPLACE_MODAL;
name: string;
options: unknown;
modal: Modal;
};

export type PopModalAction = {
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/1359.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [j-f1]
---

Port the modal infrastructure to TypeScript

0 comments on commit 4f396d7

Please sign in to comment.