mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
Add clear typings to the modals (#1359)
This commit is contained in:
@@ -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);
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
6
upcoming-release-notes/1359.md
Normal file
6
upcoming-release-notes/1359.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [j-f1]
|
||||
---
|
||||
|
||||
Port the modal infrastructure to TypeScript
|
||||
Reference in New Issue
Block a user