From d3de35f536c99cc8f9c78e9b35df91a3fd04056b Mon Sep 17 00:00:00 2001 From: Dany Khalife Date: Sat, 14 Dec 2024 15:16:23 -0800 Subject: [PATCH 1/6] initial attempt --- ...odal.jsx => SelectLinkedAccountsModal.tsx} | 76 +++++++++++++------ 1 file changed, 54 insertions(+), 22 deletions(-) rename packages/desktop-client/src/components/modals/{SelectLinkedAccountsModal.jsx => SelectLinkedAccountsModal.tsx} (79%) diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx similarity index 79% rename from packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx rename to packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx index 61a1aabd28a..53d3b1c3446 100644 --- a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx +++ b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { @@ -8,6 +8,10 @@ import { linkAccountSimpleFin, unlinkAccount, } from 'loot-core/client/actions'; +import { + type AccountEntity, + type AccountSyncSource, +} from 'loot-core/types/models/account'; import { useAccounts } from '../../hooks/useAccounts'; import { theme } from '../../style'; @@ -17,7 +21,7 @@ import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; -import { TableHeader, Table, Row, Field } from '../table'; +import { Field, Row, Table, TableHeader } from '../table'; const addOnBudgetAccountOption = { id: 'new-on', name: 'Create new account' }; const addOffBudgetAccountOption = { @@ -25,16 +29,32 @@ const addOffBudgetAccountOption = { name: 'Create new account (off budget)', }; +type SelectLinkedAccountsModalProps = { + requisitionId: string; + externalAccounts: AccountEntity[]; + syncSource: AccountSyncSource; +}; + +type LinkedAccountIds = { [key: string]: string }; +type LinkedAccountIdsSetter = ( + fn: (value: LinkedAccountIds) => LinkedAccountIds, +) => void; + export function SelectLinkedAccountsModal({ requisitionId, externalAccounts, syncSource, -}) { +}: SelectLinkedAccountsModalProps) { externalAccounts.sort((a, b) => a.name.localeCompare(b.name)); const { t } = useTranslation(); const dispatch = useDispatch(); - const localAccounts = useAccounts().filter(a => a.closed === 0); - const [chosenAccounts, setChosenAccounts] = useState(() => { + const localAccounts: AccountEntity[] = useAccounts().filter( + a => a.closed === 0, + ); + const [chosenAccounts, setChosenAccounts]: [ + LinkedAccountIds, + LinkedAccountIdsSetter, + ] = useState(() => { return Object.fromEntries( localAccounts .filter(acc => acc.account_id) @@ -43,7 +63,7 @@ export function SelectLinkedAccountsModal({ }); async function onNext() { - const chosenLocalAccountIds = Object.values(chosenAccounts); + const chosenLocalAccountIds: string[] = Object.values(chosenAccounts); // Unlink accounts that were previously linked, but the user // chose to remove the bank-sync @@ -101,9 +121,9 @@ export function SelectLinkedAccountsModal({ account => !Object.values(chosenAccounts).includes(account.id), ); - function onSetLinkedAccount(externalAccount, localAccountId) { - setChosenAccounts(accounts => { - const updatedAccounts = { ...accounts }; + function onSetLinkedAccount(externalAccount: AccountEntity, localAccountId: string) { + setChosenAccounts((accounts: LinkedAccountIds): LinkedAccountIds => { + const updatedAccounts: LinkedAccountIds = { ...accounts }; if (localAccountId) { updatedAccounts[externalAccount.account_id] = localAccountId; @@ -138,21 +158,26 @@ export function SelectLinkedAccountsModal({ border: '1px solid ' + theme.tableBorder, }} > - - + + + Bank Account To Sync + + + Balance + + + Account in Actual + + + Actions + + index} - renderItem={({ key, item }) => ( - + getItemKey={index => externalAccounts[index].id} + renderItem={({ item }) => ( + void; +}; + function TableRow({ externalAccount, chosenAccount, unlinkedAccounts, onSetLinkedAccount, -}) { +}: TableRowProps) { const { t } = useTranslation(); const [focusedField, setFocusedField] = useState(null); From 1c51a5c69391f2d9a57d6a1e17dcedc346f4d636 Mon Sep 17 00:00:00 2001 From: Dany Khalife Date: Sat, 14 Dec 2024 15:18:30 -0800 Subject: [PATCH 2/6] release notes --- upcoming-release-notes/3984.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/3984.md diff --git a/upcoming-release-notes/3984.md b/upcoming-release-notes/3984.md new file mode 100644 index 00000000000..05e0e77b9b6 --- /dev/null +++ b/upcoming-release-notes/3984.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [dkhalife] +--- + +Migrate SelectLinkedAccountsModal to TypeScript \ No newline at end of file From 2bf3b70699042fc7ae7d5d330fa888cbe01f888b Mon Sep 17 00:00:00 2001 From: Dany Khalife Date: Sat, 14 Dec 2024 16:04:06 -0800 Subject: [PATCH 3/6] type fixes --- .../components/modals/CreateAccountModal.tsx | 22 ++++++------- .../modals/SelectLinkedAccountsModal.tsx | 33 +++++++++++-------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx index 6ba131bf6dd..6e785b41b7a 100644 --- a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx @@ -22,6 +22,15 @@ import { Popover } from '../common/Popover'; import { Text } from '../common/Text'; import { View } from '../common/View'; +export type NormalizedAccount = { + id: string; + name: string; + institution: string; + orgDomain: string; + orgId: string; + balance: number; +}; + type CreateAccountProps = { upgradingAccountId?: string; }; @@ -70,20 +79,11 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { throw new Error(results.reason); } - const newAccounts = []; - - type NormalizedAccount = { - account_id: string; - name: string; - institution: string; - orgDomain: string; - orgId: string; - balance: number; - }; + const newAccounts: NormalizedAccount[] = []; for (const oldAccount of results.accounts) { const newAccount: NormalizedAccount = { - account_id: oldAccount.id, + id: oldAccount.id, name: oldAccount.name, institution: oldAccount.org.name, orgDomain: oldAccount.org.domain, diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx index 53d3b1c3446..7e4dc8820c1 100644 --- a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx +++ b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx @@ -23,6 +23,8 @@ import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; import { Field, Row, Table, TableHeader } from '../table'; +import { type NormalizedAccount } from './CreateAccountModal'; + const addOnBudgetAccountOption = { id: 'new-on', name: 'Create new account' }; const addOffBudgetAccountOption = { id: 'new-off', @@ -31,7 +33,7 @@ const addOffBudgetAccountOption = { type SelectLinkedAccountsModalProps = { requisitionId: string; - externalAccounts: AccountEntity[]; + externalAccounts: NormalizedAccount[]; syncSource: AccountSyncSource; }; @@ -76,7 +78,7 @@ export function SelectLinkedAccountsModal({ Object.entries(chosenAccounts).forEach( ([chosenExternalAccountId, chosenLocalAccountId]) => { const externalAccount = externalAccounts.find( - account => account.account_id === chosenExternalAccountId, + account => account.id === chosenExternalAccountId, ); const offBudget = chosenLocalAccountId === addOffBudgetAccountOption.id; @@ -121,14 +123,17 @@ export function SelectLinkedAccountsModal({ account => !Object.values(chosenAccounts).includes(account.id), ); - function onSetLinkedAccount(externalAccount: AccountEntity, localAccountId: string) { + function onSetLinkedAccount( + externalAccount: NormalizedAccount, + localAccountId: string | null, + ) { setChosenAccounts((accounts: LinkedAccountIds): LinkedAccountIds => { const updatedAccounts: LinkedAccountIds = { ...accounts }; if (localAccountId) { - updatedAccounts[externalAccount.account_id] = localAccountId; + updatedAccounts[externalAccount.id] = localAccountId; } else { - delete updatedAccounts[externalAccount.account_id]; + delete updatedAccounts[externalAccount.id]; } return updatedAccounts; @@ -181,14 +186,13 @@ export function SelectLinkedAccountsModal({ chosenAccounts[item.account_id] === acc.id, + acc => chosenAccounts[item.id] === acc.id, ) } unlinkedAccounts={unlinkedAccounts} @@ -221,10 +225,13 @@ export function SelectLinkedAccountsModal({ } type TableRowProps = { - externalAccount: AccountEntity; - chosenAccount: any; + externalAccount: NormalizedAccount; + chosenAccount: string; unlinkedAccounts: AccountEntity[]; - onSetLinkedAccount: (account: AccountEntity, localAccountId: string) => void; + onSetLinkedAccount: ( + account: NormalizedAccount, + localAccountId: string | null, + ) => void; }; function TableRow({ @@ -234,7 +241,7 @@ function TableRow({ onSetLinkedAccount, }: TableRowProps) { const { t } = useTranslation(); - const [focusedField, setFocusedField] = useState(null); + const [focusedField, setFocusedField] = useState(null); const availableAccountOptions = [ ...unlinkedAccounts, From 126c4bdb9efa5940ada15f110cb00d07a5055260 Mon Sep 17 00:00:00 2001 From: Dany Khalife Date: Sat, 14 Dec 2024 16:33:06 -0800 Subject: [PATCH 4/6] hardening --- .../modals/SelectLinkedAccountsModal.tsx | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx index 7e4dc8820c1..66b73fc5d61 100644 --- a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx +++ b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx @@ -25,8 +25,17 @@ import { Field, Row, Table, TableHeader } from '../table'; import { type NormalizedAccount } from './CreateAccountModal'; -const addOnBudgetAccountOption = { id: 'new-on', name: 'Create new account' }; -const addOffBudgetAccountOption = { +type LinkAccountOption = { + id: string; + name: string; +}; + +const addOnBudgetAccountOption: LinkAccountOption = { + id: 'new-on', + name: 'Create new account', +}; + +const addOffBudgetAccountOption: LinkAccountOption = { id: 'new-off', name: 'Create new account (off budget)', }; @@ -125,7 +134,7 @@ export function SelectLinkedAccountsModal({ function onSetLinkedAccount( externalAccount: NormalizedAccount, - localAccountId: string | null, + localAccountId: string | undefined, ) { setChosenAccounts((accounts: LinkedAccountIds): LinkedAccountIds => { const updatedAccounts: LinkedAccountIds = { ...accounts }; @@ -226,11 +235,11 @@ export function SelectLinkedAccountsModal({ type TableRowProps = { externalAccount: NormalizedAccount; - chosenAccount: string; - unlinkedAccounts: AccountEntity[]; + chosenAccount: LinkAccountOption | undefined; + unlinkedAccounts: LinkAccountOption[]; onSetLinkedAccount: ( account: NormalizedAccount, - localAccountId: string | null, + localAccountId: string | undefined, ) => void; }; @@ -243,13 +252,16 @@ function TableRow({ const { t } = useTranslation(); const [focusedField, setFocusedField] = useState(null); - const availableAccountOptions = [ + const availableAccountOptions: LinkAccountOption[] = [ ...unlinkedAccounts, - chosenAccount?.id !== addOnBudgetAccountOption.id && chosenAccount, addOnBudgetAccountOption, addOffBudgetAccountOption, ].filter(Boolean); + if (chosenAccount && chosenAccount.id !== addOnBudgetAccountOption.id) { + availableAccountOptions.push(chosenAccount); + } + return ( {externalAccount.name} @@ -267,7 +279,7 @@ function TableRow({ strict highlightFirst suggestions={availableAccountOptions} - onSelect={value => { + onSelect={(value: string | undefined) => { onSetLinkedAccount(externalAccount, value); }} inputProps={{ @@ -283,7 +295,7 @@ function TableRow({ {chosenAccount ? (