mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
807 lines
23 KiB
TypeScript
807 lines
23 KiB
TypeScript
import React, {
|
|
type ComponentProps,
|
|
type ReactNode,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { Dialog, DialogTrigger } from 'react-aria-components';
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
import { Trans, useTranslation } from 'react-i18next';
|
|
|
|
import { Button } from '@actual-app/components/button';
|
|
import { AnimatedLoading } from '@actual-app/components/icons/AnimatedLoading';
|
|
import {
|
|
SvgAdd,
|
|
SvgDotsHorizontalTriple,
|
|
} from '@actual-app/components/icons/v1';
|
|
import {
|
|
SvgArrowsExpand3,
|
|
SvgArrowsShrink3,
|
|
SvgDownloadThickBottom,
|
|
SvgLockClosed,
|
|
SvgPencil1,
|
|
} from '@actual-app/components/icons/v2';
|
|
import { InitialFocus } from '@actual-app/components/initial-focus';
|
|
import { Input } from '@actual-app/components/input';
|
|
import { Menu } from '@actual-app/components/menu';
|
|
import { Popover } from '@actual-app/components/popover';
|
|
import { Stack } from '@actual-app/components/stack';
|
|
import { styles } from '@actual-app/components/styles';
|
|
import { theme } from '@actual-app/components/theme';
|
|
import { Tooltip } from '@actual-app/components/tooltip';
|
|
import { View } from '@actual-app/components/view';
|
|
import { format as formatDate } from 'date-fns';
|
|
|
|
import { tsToRelativeTime } from 'loot-core/shared/util';
|
|
import {
|
|
type AccountEntity,
|
|
type RuleConditionEntity,
|
|
type TransactionEntity,
|
|
type TransactionFilterEntity,
|
|
} from 'loot-core/types/models';
|
|
|
|
import { type TableRef } from './Account';
|
|
import { Balances } from './Balance';
|
|
import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
|
|
|
|
import { AnimatedRefresh } from '@desktop-client/components/AnimatedRefresh';
|
|
import { Search } from '@desktop-client/components/common/Search';
|
|
import { FilterButton } from '@desktop-client/components/filters/FiltersMenu';
|
|
import { FiltersStack } from '@desktop-client/components/filters/FiltersStack';
|
|
import { type SavedFilter } from '@desktop-client/components/filters/SavedFilterMenuButton';
|
|
import { NotesButton } from '@desktop-client/components/NotesButton';
|
|
import { SelectedTransactionsButton } from '@desktop-client/components/transactions/SelectedTransactionsButton';
|
|
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
|
import { useLocale } from '@desktop-client/hooks/useLocale';
|
|
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
|
import { useSplitsExpanded } from '@desktop-client/hooks/useSplitsExpanded';
|
|
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
|
|
|
|
type AccountHeaderProps = {
|
|
tableRef: TableRef;
|
|
isNameEditable: boolean;
|
|
workingHard: boolean;
|
|
accountName: string;
|
|
account?: AccountEntity;
|
|
filterId?: SavedFilter;
|
|
savedFilters: TransactionFilterEntity[];
|
|
accountsSyncing: string[];
|
|
failedAccounts: AccountSyncSidebarProps['failedAccounts'];
|
|
accounts: AccountEntity[];
|
|
transactions: TransactionEntity[];
|
|
showBalances: boolean;
|
|
showExtraBalances: boolean;
|
|
showCleared: boolean;
|
|
showReconciled: boolean;
|
|
showEmptyMessage: boolean;
|
|
balanceQuery: ComponentProps<typeof ReconcilingMessage>['balanceQuery'];
|
|
reconcileAmount?: number | null;
|
|
canCalculateBalance?: () => boolean;
|
|
isFiltered: boolean;
|
|
filteredAmount?: number | null;
|
|
isSorted: boolean;
|
|
search: string;
|
|
filterConditions: RuleConditionEntity[];
|
|
filterConditionsOp: 'and' | 'or';
|
|
onSearch: (newSearch: string) => void;
|
|
onAddTransaction: () => void;
|
|
onShowTransactions: ComponentProps<
|
|
typeof SelectedTransactionsButton
|
|
>['onShow'];
|
|
onDoneReconciling: ComponentProps<typeof ReconcilingMessage>['onDone'];
|
|
onCreateReconciliationTransaction: ComponentProps<
|
|
typeof ReconcilingMessage
|
|
>['onCreateTransaction'];
|
|
onToggleExtraBalances: ComponentProps<
|
|
typeof Balances
|
|
>['onToggleExtraBalances'];
|
|
onSaveName: AccountNameFieldProps['onSaveName'];
|
|
saveNameError: AccountNameFieldProps['saveNameError'];
|
|
onSync: () => void;
|
|
onImport: () => void;
|
|
onMenuSelect: AccountMenuProps['onMenuSelect'];
|
|
onReconcile: ComponentProps<typeof ReconcileMenu>['onReconcile'];
|
|
onBatchEdit: ComponentProps<typeof SelectedTransactionsButton>['onEdit'];
|
|
onRunRules: ComponentProps<typeof SelectedTransactionsButton>['onRunRules'];
|
|
onBatchDelete: ComponentProps<typeof SelectedTransactionsButton>['onDelete'];
|
|
onBatchDuplicate: ComponentProps<
|
|
typeof SelectedTransactionsButton
|
|
>['onDuplicate'];
|
|
onBatchLinkSchedule: ComponentProps<
|
|
typeof SelectedTransactionsButton
|
|
>['onLinkSchedule'];
|
|
onBatchUnlinkSchedule: ComponentProps<
|
|
typeof SelectedTransactionsButton
|
|
>['onUnlinkSchedule'];
|
|
onApplyFilter: (filter: RuleConditionEntity) => void;
|
|
} & Pick<
|
|
ComponentProps<typeof SelectedTransactionsButton>,
|
|
| 'onCreateRule'
|
|
| 'onScheduleAction'
|
|
| 'onSetTransfer'
|
|
| 'onMakeAsSplitTransaction'
|
|
| 'onMakeAsNonSplitTransactions'
|
|
| 'onMergeTransactions'
|
|
> &
|
|
Pick<
|
|
ComponentProps<typeof FiltersStack>,
|
|
| 'onUpdateFilter'
|
|
| 'onDeleteFilter'
|
|
| 'onConditionsOpChange'
|
|
| 'onClearFilters'
|
|
| 'onReloadSavedFilter'
|
|
>;
|
|
|
|
export function AccountHeader({
|
|
tableRef,
|
|
isNameEditable,
|
|
workingHard,
|
|
accountName,
|
|
account,
|
|
filterId,
|
|
savedFilters,
|
|
accountsSyncing,
|
|
failedAccounts,
|
|
accounts,
|
|
transactions,
|
|
showBalances,
|
|
showExtraBalances,
|
|
showCleared,
|
|
showReconciled,
|
|
showEmptyMessage,
|
|
balanceQuery,
|
|
reconcileAmount,
|
|
canCalculateBalance,
|
|
isFiltered,
|
|
filteredAmount,
|
|
isSorted,
|
|
search,
|
|
filterConditions,
|
|
filterConditionsOp,
|
|
onSearch,
|
|
onAddTransaction,
|
|
onShowTransactions,
|
|
onDoneReconciling,
|
|
onCreateReconciliationTransaction,
|
|
onToggleExtraBalances,
|
|
onSaveName,
|
|
saveNameError,
|
|
onSync,
|
|
onImport,
|
|
onMenuSelect,
|
|
onReconcile,
|
|
onBatchDelete,
|
|
onBatchDuplicate,
|
|
onBatchEdit,
|
|
onBatchLinkSchedule,
|
|
onBatchUnlinkSchedule,
|
|
onCreateRule,
|
|
onApplyFilter,
|
|
onUpdateFilter,
|
|
onClearFilters,
|
|
onReloadSavedFilter,
|
|
onConditionsOpChange,
|
|
onDeleteFilter,
|
|
onScheduleAction,
|
|
onSetTransfer,
|
|
onRunRules,
|
|
onMakeAsSplitTransaction,
|
|
onMakeAsNonSplitTransactions,
|
|
onMergeTransactions,
|
|
}: AccountHeaderProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const [reconcileOpen, setReconcileOpen] = useState(false);
|
|
const searchInput = useRef<HTMLInputElement>(null);
|
|
const reconcileRef = useRef(null);
|
|
const splitsExpanded = useSplitsExpanded();
|
|
const syncServerStatus = useSyncServerStatus();
|
|
const isUsingServer = syncServerStatus !== 'no-server';
|
|
const isServerOffline = syncServerStatus === 'offline';
|
|
const [_, setExpandSplitsPref] = useLocalPref('expand-splits');
|
|
|
|
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
|
const locale = useLocale();
|
|
|
|
let canSync = !!(account?.account_id && isUsingServer);
|
|
if (!account) {
|
|
// All accounts - check for any syncable account
|
|
canSync = !!accounts.find(account => !!account.account_id) && isUsingServer;
|
|
}
|
|
|
|
// Only show the ability to make linked transfers on multi-account views.
|
|
const showMakeTransfer = !account;
|
|
|
|
function onToggleSplits() {
|
|
if (tableRef.current) {
|
|
splitsExpanded.dispatch({
|
|
type: 'switch-mode',
|
|
id: tableRef.current.getScrolledItem(),
|
|
});
|
|
|
|
setExpandSplitsPref(!(splitsExpanded.state.mode === 'expand'));
|
|
}
|
|
}
|
|
|
|
useHotkeys(
|
|
'ctrl+f, cmd+f, meta+f',
|
|
() => {
|
|
if (searchInput.current) {
|
|
searchInput.current.focus();
|
|
}
|
|
},
|
|
{
|
|
enableOnFormTags: true,
|
|
preventDefault: true,
|
|
scopes: ['app'],
|
|
},
|
|
[searchInput],
|
|
);
|
|
useHotkeys(
|
|
't',
|
|
() => onAddTransaction(),
|
|
{
|
|
preventDefault: true,
|
|
scopes: ['app'],
|
|
},
|
|
[onAddTransaction],
|
|
);
|
|
useHotkeys(
|
|
'ctrl+i, cmd+i, meta+i',
|
|
() => onImport(),
|
|
{
|
|
scopes: ['app'],
|
|
},
|
|
[onImport],
|
|
);
|
|
useHotkeys(
|
|
'ctrl+b, cmd+b, meta+b',
|
|
() => onSync(),
|
|
{
|
|
enabled: canSync && !isServerOffline,
|
|
preventDefault: true,
|
|
scopes: ['app'],
|
|
},
|
|
[onSync],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<View style={{ ...styles.pageContent, paddingBottom: 10, flexShrink: 0 }}>
|
|
<View
|
|
style={{ marginTop: 2, marginBottom: 10, alignItems: 'flex-start' }}
|
|
>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 3,
|
|
}}
|
|
>
|
|
{!!account?.bank && (
|
|
<AccountSyncSidebar
|
|
account={account}
|
|
failedAccounts={failedAccounts}
|
|
accountsSyncing={accountsSyncing}
|
|
/>
|
|
)}
|
|
<AccountNameField
|
|
account={account}
|
|
accountName={accountName}
|
|
isNameEditable={isNameEditable}
|
|
saveNameError={saveNameError}
|
|
onSaveName={onSaveName}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<Balances
|
|
balanceQuery={balanceQuery}
|
|
showExtraBalances={showExtraBalances}
|
|
onToggleExtraBalances={onToggleExtraBalances}
|
|
account={account}
|
|
isFiltered={isFiltered}
|
|
filteredAmount={filteredAmount}
|
|
/>
|
|
|
|
<Stack
|
|
spacing={2}
|
|
direction="row"
|
|
align="center"
|
|
style={{ marginTop: 12 }}
|
|
>
|
|
{canSync && (
|
|
<Button
|
|
variant="bare"
|
|
onPress={onSync}
|
|
isDisabled={isServerOffline}
|
|
>
|
|
<AnimatedRefresh
|
|
width={13}
|
|
height={13}
|
|
animating={
|
|
account
|
|
? accountsSyncing.includes(account.id)
|
|
: accountsSyncing.length > 0
|
|
}
|
|
/>{' '}
|
|
{isServerOffline ? t('Bank Sync Offline') : t('Bank Sync')}
|
|
</Button>
|
|
)}
|
|
|
|
{account && !account.closed && (
|
|
<Button variant="bare" onPress={onImport}>
|
|
<SvgDownloadThickBottom
|
|
width={13}
|
|
height={13}
|
|
style={{ marginRight: 4 }}
|
|
/>{' '}
|
|
<Trans>Import</Trans>
|
|
</Button>
|
|
)}
|
|
|
|
{!showEmptyMessage && (
|
|
<Button variant="bare" onPress={onAddTransaction}>
|
|
<SvgAdd width={10} height={10} style={{ marginRight: 3 }} />
|
|
<Trans>Add New</Trans>
|
|
</Button>
|
|
)}
|
|
<View style={{ flexShrink: 0 }}>
|
|
{/* @ts-expect-error fix me */}
|
|
<FilterButton onApply={onApplyFilter} />
|
|
</View>
|
|
<View style={{ flex: 1 }} />
|
|
<Search
|
|
placeholder={t('Search')}
|
|
value={search}
|
|
onChange={onSearch}
|
|
inputRef={searchInput}
|
|
// Remove marginRight magically being added by Stack...
|
|
// We need to refactor the Stack component
|
|
style={{ marginRight: 0 }}
|
|
/>
|
|
{workingHard ? (
|
|
<View>
|
|
<AnimatedLoading style={{ width: 16, height: 16 }} />
|
|
</View>
|
|
) : (
|
|
<SelectedTransactionsButton
|
|
getTransaction={id => transactions.find(t => t.id === id)}
|
|
onShow={onShowTransactions}
|
|
onDuplicate={onBatchDuplicate}
|
|
onDelete={onBatchDelete}
|
|
onEdit={onBatchEdit}
|
|
onRunRules={onRunRules}
|
|
onLinkSchedule={onBatchLinkSchedule}
|
|
onUnlinkSchedule={onBatchUnlinkSchedule}
|
|
onCreateRule={onCreateRule}
|
|
onSetTransfer={onSetTransfer}
|
|
onScheduleAction={onScheduleAction}
|
|
showMakeTransfer={showMakeTransfer}
|
|
onMakeAsSplitTransaction={onMakeAsSplitTransaction}
|
|
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
|
|
onMergeTransactions={onMergeTransactions}
|
|
/>
|
|
)}
|
|
<View style={{ flex: '0 0 auto', marginLeft: 10 }}>
|
|
{account && (
|
|
<Tooltip
|
|
style={{
|
|
...styles.tooltip,
|
|
marginBottom: 10,
|
|
}}
|
|
content={
|
|
account?.last_reconciled
|
|
? t('Reconciled {{relativeTimeAgo}} ({{absoluteDate}})', {
|
|
relativeTimeAgo: tsToRelativeTime(
|
|
account.last_reconciled,
|
|
locale,
|
|
),
|
|
absoluteDate: formatDate(
|
|
new Date(
|
|
parseInt(account.last_reconciled ?? '0', 10),
|
|
),
|
|
dateFormat,
|
|
{ locale },
|
|
),
|
|
})
|
|
: t('Not yet reconciled')
|
|
}
|
|
placement="top"
|
|
triggerProps={{
|
|
isDisabled: reconcileOpen,
|
|
}}
|
|
>
|
|
<Button
|
|
ref={reconcileRef}
|
|
variant="bare"
|
|
aria-label={t('Reconcile')}
|
|
style={{ padding: 6 }}
|
|
onPress={() => {
|
|
setReconcileOpen(true);
|
|
}}
|
|
>
|
|
<View>
|
|
<SvgLockClosed width={14} height={14} />
|
|
</View>
|
|
</Button>
|
|
<Popover
|
|
placement="bottom"
|
|
triggerRef={reconcileRef}
|
|
style={{ width: 275 }}
|
|
isOpen={reconcileOpen}
|
|
onOpenChange={() => setReconcileOpen(false)}
|
|
>
|
|
<ReconcileMenu
|
|
account={account}
|
|
onClose={() => setReconcileOpen(false)}
|
|
onReconcile={onReconcile}
|
|
/>
|
|
</Popover>
|
|
</Tooltip>
|
|
)}
|
|
</View>
|
|
<Button
|
|
variant="bare"
|
|
aria-label={
|
|
splitsExpanded.state.mode === 'collapse'
|
|
? t('Collapse split transactions')
|
|
: t('Expand split transactions')
|
|
}
|
|
style={{ padding: 6 }}
|
|
onPress={onToggleSplits}
|
|
>
|
|
<View
|
|
title={
|
|
splitsExpanded.state.mode === 'collapse'
|
|
? t('Collapse split transactions')
|
|
: t('Expand split transactions')
|
|
}
|
|
>
|
|
{splitsExpanded.state.mode === 'collapse' ? (
|
|
<SvgArrowsShrink3 style={{ width: 14, height: 14 }} />
|
|
) : (
|
|
<SvgArrowsExpand3 style={{ width: 14, height: 14 }} />
|
|
)}
|
|
</View>
|
|
</Button>
|
|
{account ? (
|
|
<View style={{ flex: '0 0 auto' }}>
|
|
<DialogTrigger>
|
|
<Button variant="bare" aria-label={t('Account menu')}>
|
|
<SvgDotsHorizontalTriple
|
|
width={15}
|
|
height={15}
|
|
style={{ transform: 'rotateZ(90deg)' }}
|
|
/>
|
|
</Button>
|
|
|
|
<Popover style={{ width: 275 }}>
|
|
<Dialog>
|
|
<AccountMenu
|
|
account={account}
|
|
canSync={canSync}
|
|
canShowBalances={
|
|
canCalculateBalance ? canCalculateBalance() : false
|
|
}
|
|
isSorted={isSorted}
|
|
showBalances={showBalances}
|
|
showCleared={showCleared}
|
|
showReconciled={showReconciled}
|
|
onMenuSelect={onMenuSelect}
|
|
/>
|
|
</Dialog>
|
|
</Popover>
|
|
</DialogTrigger>
|
|
</View>
|
|
) : (
|
|
<View style={{ flex: '0 0 auto' }}>
|
|
<DialogTrigger>
|
|
<Button variant="bare" aria-label={t('Account menu')}>
|
|
<SvgDotsHorizontalTriple
|
|
width={15}
|
|
height={15}
|
|
style={{ transform: 'rotateZ(90deg)' }}
|
|
/>
|
|
</Button>
|
|
|
|
<Popover>
|
|
<Dialog>
|
|
<Menu
|
|
slot="close"
|
|
onMenuSelect={onMenuSelect}
|
|
items={[
|
|
...(isSorted
|
|
? [
|
|
{
|
|
name: 'remove-sorting',
|
|
text: t('Remove all sorting'),
|
|
} as const,
|
|
]
|
|
: []),
|
|
{ name: 'export', text: t('Export') },
|
|
]}
|
|
/>
|
|
</Dialog>
|
|
</Popover>
|
|
</DialogTrigger>
|
|
</View>
|
|
)}
|
|
</Stack>
|
|
|
|
{filterConditions?.length > 0 && (
|
|
<FiltersStack
|
|
conditions={filterConditions}
|
|
conditionsOp={filterConditionsOp}
|
|
onUpdateFilter={onUpdateFilter}
|
|
onDeleteFilter={onDeleteFilter}
|
|
onClearFilters={onClearFilters}
|
|
onReloadSavedFilter={onReloadSavedFilter}
|
|
filterId={filterId}
|
|
savedFilters={savedFilters}
|
|
onConditionsOpChange={onConditionsOpChange}
|
|
/>
|
|
)}
|
|
</View>
|
|
{reconcileAmount != null && (
|
|
<ReconcilingMessage
|
|
targetBalance={reconcileAmount}
|
|
balanceQuery={balanceQuery}
|
|
onDone={onDoneReconciling}
|
|
onCreateTransaction={onCreateReconciliationTransaction}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
type AccountSyncSidebarProps = {
|
|
account: AccountEntity;
|
|
failedAccounts: Map<
|
|
string,
|
|
{
|
|
type: string;
|
|
code: string;
|
|
}
|
|
>;
|
|
accountsSyncing: string[];
|
|
};
|
|
|
|
function AccountSyncSidebar({
|
|
account,
|
|
failedAccounts,
|
|
accountsSyncing,
|
|
}: AccountSyncSidebarProps) {
|
|
return (
|
|
<View
|
|
style={{
|
|
backgroundColor: accountsSyncing.includes(account.id)
|
|
? theme.sidebarItemBackgroundPending
|
|
: failedAccounts.has(account.id)
|
|
? theme.sidebarItemBackgroundFailed
|
|
: theme.sidebarItemBackgroundPositive,
|
|
marginRight: '4px',
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 8,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type AccountNameFieldProps = {
|
|
account?: AccountEntity;
|
|
accountName: string;
|
|
isNameEditable: boolean;
|
|
saveNameError?: ReactNode;
|
|
onSaveName: (newName: string) => void;
|
|
};
|
|
|
|
function AccountNameField({
|
|
account,
|
|
accountName,
|
|
isNameEditable,
|
|
saveNameError,
|
|
onSaveName,
|
|
}: AccountNameFieldProps) {
|
|
const { t } = useTranslation();
|
|
const [editingName, setEditingName] = useState(false);
|
|
|
|
const handleSave = (newName: string) => {
|
|
onSaveName(newName);
|
|
setEditingName(false);
|
|
};
|
|
|
|
if (editingName) {
|
|
return (
|
|
<>
|
|
<InitialFocus>
|
|
<Input
|
|
defaultValue={accountName}
|
|
onEnter={handleSave}
|
|
onUpdate={handleSave}
|
|
onEscape={() => setEditingName(false)}
|
|
style={{
|
|
fontSize: 25,
|
|
fontWeight: 500,
|
|
marginTop: -3,
|
|
marginBottom: -4,
|
|
marginLeft: -6,
|
|
paddingTop: 2,
|
|
paddingBottom: 2,
|
|
width: Math.max(20, accountName.length) + 'ch',
|
|
}}
|
|
/>
|
|
</InitialFocus>
|
|
{saveNameError && (
|
|
<View style={{ color: theme.warningText }}>{saveNameError}</View>
|
|
)}
|
|
</>
|
|
);
|
|
} else {
|
|
if (isNameEditable) {
|
|
return (
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 3,
|
|
'& .hover-visible': {
|
|
opacity: 0,
|
|
transition: 'opacity .25s',
|
|
},
|
|
'&:hover .hover-visible': {
|
|
opacity: 1,
|
|
},
|
|
}}
|
|
>
|
|
<View
|
|
style={{
|
|
fontSize: 25,
|
|
fontWeight: 500,
|
|
marginRight: 5,
|
|
marginBottom: -1,
|
|
}}
|
|
data-testid="account-name"
|
|
>
|
|
{account && account.closed
|
|
? t('Closed: {{ accountName }}', { accountName })
|
|
: accountName}
|
|
</View>
|
|
|
|
{account && (
|
|
<NotesButton
|
|
id={`account-${account.id}`}
|
|
defaultColor={theme.pageTextSubdued}
|
|
/>
|
|
)}
|
|
<Button
|
|
variant="bare"
|
|
aria-label={t('Edit account name')}
|
|
className="hover-visible"
|
|
onPress={() => setEditingName(true)}
|
|
>
|
|
<SvgPencil1
|
|
style={{
|
|
width: 11,
|
|
height: 11,
|
|
color: theme.pageTextSubdued,
|
|
}}
|
|
/>
|
|
</Button>
|
|
</View>
|
|
);
|
|
} else {
|
|
return (
|
|
<View
|
|
style={{ fontSize: 25, fontWeight: 500, marginBottom: -1 }}
|
|
data-testid="account-name"
|
|
>
|
|
{account && account.closed
|
|
? t('Closed: {{ accountName }}', { accountName })
|
|
: accountName}
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
type AccountMenuProps = {
|
|
account: AccountEntity;
|
|
canSync: boolean;
|
|
showBalances: boolean;
|
|
canShowBalances: boolean;
|
|
showCleared: boolean;
|
|
showReconciled: boolean;
|
|
isSorted: boolean;
|
|
onMenuSelect: (
|
|
item:
|
|
| 'link'
|
|
| 'unlink'
|
|
| 'close'
|
|
| 'reopen'
|
|
| 'export'
|
|
| 'toggle-balance'
|
|
| 'remove-sorting'
|
|
| 'toggle-cleared'
|
|
| 'toggle-reconciled',
|
|
) => void;
|
|
};
|
|
|
|
function AccountMenu({
|
|
account,
|
|
canSync,
|
|
showBalances,
|
|
canShowBalances,
|
|
showCleared,
|
|
showReconciled,
|
|
isSorted,
|
|
onMenuSelect,
|
|
}: AccountMenuProps) {
|
|
const { t } = useTranslation();
|
|
const syncServerStatus = useSyncServerStatus();
|
|
|
|
return (
|
|
<Menu
|
|
slot="close"
|
|
onMenuSelect={item => {
|
|
onMenuSelect(item);
|
|
}}
|
|
items={[
|
|
...(isSorted
|
|
? [
|
|
{
|
|
name: 'remove-sorting',
|
|
text: t('Remove all sorting'),
|
|
} as const,
|
|
]
|
|
: []),
|
|
...(canShowBalances
|
|
? [
|
|
{
|
|
name: 'toggle-balance',
|
|
text: showBalances
|
|
? t('Hide running balance')
|
|
: t('Show running balance'),
|
|
} as const,
|
|
]
|
|
: []),
|
|
{
|
|
name: 'toggle-cleared',
|
|
text: showCleared
|
|
? t('Hide “cleared” checkboxes')
|
|
: t('Show “cleared” checkboxes'),
|
|
},
|
|
{
|
|
name: 'toggle-reconciled',
|
|
text: showReconciled
|
|
? t('Hide reconciled transactions')
|
|
: t('Show reconciled transactions'),
|
|
},
|
|
{ name: 'export', text: t('Export') },
|
|
...(account && !account.closed
|
|
? canSync
|
|
? [
|
|
{
|
|
name: 'unlink',
|
|
text: t('Unlink account'),
|
|
} as const,
|
|
]
|
|
: syncServerStatus === 'online'
|
|
? [
|
|
{
|
|
name: 'link',
|
|
text: t('Link account'),
|
|
} as const,
|
|
]
|
|
: []
|
|
: []),
|
|
|
|
...(account.closed
|
|
? [{ name: 'reopen', text: t('Reopen account') } as const]
|
|
: [{ name: 'close', text: t('Close account') } as const]),
|
|
]}
|
|
/>
|
|
);
|
|
}
|