From f329fe21af6d055b3fb9c90adee0df1c9f782377 Mon Sep 17 00:00:00 2001 From: Jed Fox <git@jedfox.com> Date: Tue, 18 Jul 2023 15:55:14 -0400 Subject: [PATCH] Add clear typings to the modals (#1359) --- .../src/components/{Modals.js => Modals.tsx} | 59 +++++-------- .../desktop-client/src/hooks/useActions.ts | 1 - .../loot-core/src/client/actions/budgets.ts | 1 + .../loot-core/src/client/actions/modals.ts | 19 +++- .../loot-core/src/client/reducers/modals.ts | 7 +- .../src/client/state-types/modals.d.ts | 88 +++++++++++++++++-- upcoming-release-notes/1359.md | 6 ++ 7 files changed, 127 insertions(+), 54 deletions(-) rename packages/desktop-client/src/components/{Modals.js => Modals.tsx} (82%) create mode 100644 upcoming-release-notes/1359.md diff --git a/packages/desktop-client/src/components/Modals.js b/packages/desktop-client/src/components/Modals.tsx similarity index 82% rename from packages/desktop-client/src/components/Modals.js rename to packages/desktop-client/src/components/Modals.tsx index 502201f25..b26cc34ef 100644 --- a/packages/desktop-client/src/components/Modals.js +++ b/packages/desktop-client/src/components/Modals.tsx @@ -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'; @@ -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, @@ -61,7 +58,6 @@ function Modals({ return ( <CreateAccount modalProps={modalProps} - actions={actions} syncServerStatus={syncServerStatus} /> ); @@ -91,7 +87,6 @@ function Modals({ externalAccounts={options.accounts} requisitionId={options.requisitionId} localAccounts={accounts.filter(acct => acct.closed === 0)} - upgradingAccountId={options.upgradingAccountId} actions={actions} /> ); @@ -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} /> @@ -115,6 +115,7 @@ function Modals({ budgetId={budgetId} modalProps={modalProps} actions={actions} + backupDisabled={false} /> ); @@ -148,7 +149,6 @@ function Modals({ return ( <PlaidExternalMsg modalProps={modalProps} - actions={actions} onMoveExternal={options.onMoveExternal} onClose={() => { options.onClose?.(); @@ -170,7 +170,6 @@ function Modals({ return ( <NordigenExternalMsg modalProps={modalProps} - actions={actions} onMoveExternal={options.onMoveExternal} onClose={() => { options.onClose?.(); @@ -217,8 +216,6 @@ function Modals({ key={name} modalProps={modalProps} month={options.month} - actions={actions} - isGoalTemplatesEnabled={isGoalTemplatesEnabled} /> ); @@ -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); diff --git a/packages/desktop-client/src/hooks/useActions.ts b/packages/desktop-client/src/hooks/useActions.ts index b60a9abe0..2078a4d8c 100644 --- a/packages/desktop-client/src/hooks/useActions.ts +++ b/packages/desktop-client/src/hooks/useActions.ts @@ -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(() => { diff --git a/packages/loot-core/src/client/actions/budgets.ts b/packages/loot-core/src/client/actions/budgets.ts index 498541c62..d9a54920c 100644 --- a/packages/loot-core/src/client/actions/budgets.ts +++ b/packages/loot-core/src/client/actions/budgets.ts @@ -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 { diff --git a/packages/loot-core/src/client/actions/modals.ts b/packages/loot-core/src/client/actions/modals.ts index 27b33df09..87c1abb1b 100644 --- a/packages/loot-core/src/client/actions/modals.ts +++ b/packages/loot-core/src/client/actions/modals.ts @@ -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 { diff --git a/packages/loot-core/src/client/reducers/modals.ts b/packages/loot-core/src/client/reducers/modals.ts index 2c40d0914..eeb743869 100644 --- a/packages/loot-core/src/client/reducers/modals.ts +++ b/packages/loot-core/src/client/reducers/modals.ts @@ -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) }; diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index c3c9bbe4c..3f77b3532 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -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 = { diff --git a/upcoming-release-notes/1359.md b/upcoming-release-notes/1359.md new file mode 100644 index 000000000..c2c17458d --- /dev/null +++ b/upcoming-release-notes/1359.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [j-f1] +--- + +Port the modal infrastructure to TypeScript -- GitLab