mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-24 15:23:52 -05:00
Compare commits
25 Commits
js-proxy
...
accounts-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c285392c3c | ||
|
|
3fe5b5aaf0 | ||
|
|
4fc91c6b4c | ||
|
|
45777cad8d | ||
|
|
72644d3d51 | ||
|
|
5e805085b0 | ||
|
|
4e6331c7f0 | ||
|
|
2ec46d8dec | ||
|
|
bb17f6a6f1 | ||
|
|
ba70fca304 | ||
|
|
3ace7d199d | ||
|
|
2a93b173e0 | ||
|
|
e9175951dd | ||
|
|
3ba94642d9 | ||
|
|
bc3b96c9b2 | ||
|
|
ccff8412d3 | ||
|
|
df61e42fda | ||
|
|
0726760084 | ||
|
|
a7f65532fb | ||
|
|
89059bf5da | ||
|
|
b9c167d5d6 | ||
|
|
60dac66898 | ||
|
|
b9a43b992a | ||
|
|
a7b90a0945 | ||
|
|
fbc2ccd2e7 |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 105 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useHover } from 'usehooks-ts';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { isPreviewId } from 'loot-core/shared/transactions';
|
import { isPreviewId } from 'loot-core/shared/transactions';
|
||||||
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||||
@@ -12,7 +12,7 @@ import { type AccountEntity } from 'loot-core/types/models';
|
|||||||
import { useSelectedItems } from '../../hooks/useSelected';
|
import { useSelectedItems } from '../../hooks/useSelected';
|
||||||
import { SvgArrowButtonRight1 } from '../../icons/v2';
|
import { SvgArrowButtonRight1 } from '../../icons/v2';
|
||||||
import { theme } from '../../style';
|
import { theme } from '../../style';
|
||||||
import { Button } from '../common/Button2';
|
import { ButtonWithLoading } from '../common/Button2';
|
||||||
import { Text } from '../common/Text';
|
import { Text } from '../common/Text';
|
||||||
import { View } from '../common/View';
|
import { View } from '../common/View';
|
||||||
import { PrivacyFilter } from '../PrivacyFilter';
|
import { PrivacyFilter } from '../PrivacyFilter';
|
||||||
@@ -56,10 +56,10 @@ function DetailedBalance({
|
|||||||
|
|
||||||
type SelectedBalanceProps = {
|
type SelectedBalanceProps = {
|
||||||
selectedItems: Set<string>;
|
selectedItems: Set<string>;
|
||||||
account?: AccountEntity;
|
accountId?: AccountEntity['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
function SelectedBalance({ selectedItems, account }: SelectedBalanceProps) {
|
function SelectedBalance({ selectedItems, accountId }: SelectedBalanceProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const name = `selected-balance-${[...selectedItems].join('-')}`;
|
const name = `selected-balance-${[...selectedItems].join('-')}`;
|
||||||
@@ -104,7 +104,7 @@ function SelectedBalance({ selectedItems, account }: SelectedBalanceProps) {
|
|||||||
isExactBalance = false;
|
isExactBalance = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account || account.id === s._account) {
|
if (accountId !== s._account) {
|
||||||
scheduleBalance += getScheduledAmount(s._amount);
|
scheduleBalance += getScheduledAmount(s._amount);
|
||||||
} else {
|
} else {
|
||||||
scheduleBalance -= getScheduledAmount(s._amount);
|
scheduleBalance -= getScheduledAmount(s._amount);
|
||||||
@@ -128,39 +128,53 @@ function SelectedBalance({ selectedItems, account }: SelectedBalanceProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FilteredBalanceProps = {
|
type FilteredBalanceProps = {
|
||||||
filteredAmount?: number | null;
|
transactionsQuery: Query;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FilteredBalance({ filteredAmount }: FilteredBalanceProps) {
|
function FilteredBalance({ transactionsQuery }: FilteredBalanceProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const filteredBalance = useSheetValue<'balance', 'filtered-balance'>({
|
||||||
|
name: 'filtered-balance',
|
||||||
|
query: transactionsQuery.calculate({ $sum: '$amount' }),
|
||||||
|
value: 0,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DetailedBalance
|
<DetailedBalance
|
||||||
name={t('Filtered balance:')}
|
name={t('Filtered balance:')}
|
||||||
balance={filteredAmount ?? 0}
|
balance={filteredBalance || 0}
|
||||||
isExactBalance={true}
|
isExactBalance={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type MoreBalancesProps = {
|
type MoreBalancesProps = {
|
||||||
balanceQuery: { name: `balance-query-${string}`; query: Query };
|
accountId: AccountEntity['id'] | string;
|
||||||
|
balanceQuery: Query;
|
||||||
};
|
};
|
||||||
|
|
||||||
function MoreBalances({ balanceQuery }: MoreBalancesProps) {
|
function MoreBalances({ accountId, balanceQuery }: MoreBalancesProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const clearedQuery = useMemo(
|
||||||
|
() => balanceQuery.filter({ cleared: true }),
|
||||||
|
[balanceQuery],
|
||||||
|
);
|
||||||
const cleared = useSheetValue<'balance', `balance-query-${string}-cleared`>({
|
const cleared = useSheetValue<'balance', `balance-query-${string}-cleared`>({
|
||||||
name: (balanceQuery.name + '-cleared') as `balance-query-${string}-cleared`,
|
name: `balance-query-${accountId}-cleared`,
|
||||||
query: balanceQuery.query.filter({ cleared: true }),
|
query: clearedQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unclearedQuery = useMemo(
|
||||||
|
() => balanceQuery.filter({ cleared: false }),
|
||||||
|
[balanceQuery],
|
||||||
|
);
|
||||||
const uncleared = useSheetValue<
|
const uncleared = useSheetValue<
|
||||||
'balance',
|
'balance',
|
||||||
`balance-query-${string}-uncleared`
|
`balance-query-${string}-uncleared`
|
||||||
>({
|
>({
|
||||||
name: (balanceQuery.name +
|
name: `balance-query-${accountId}-uncleared`,
|
||||||
'-uncleared') as `balance-query-${string}-uncleared`,
|
query: unclearedQuery,
|
||||||
query: balanceQuery.query.filter({ cleared: false }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -172,25 +186,31 @@ function MoreBalances({ balanceQuery }: MoreBalancesProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BalancesProps = {
|
type BalancesProps = {
|
||||||
balanceQuery: { name: `balance-query-${string}`; query: Query };
|
accountId?: AccountEntity['id'] | string;
|
||||||
|
showFilteredBalance: boolean;
|
||||||
|
transactionsQuery?: Query;
|
||||||
|
balanceQuery: Query;
|
||||||
showExtraBalances: boolean;
|
showExtraBalances: boolean;
|
||||||
onToggleExtraBalances: () => void;
|
onToggleExtraBalances: () => void;
|
||||||
account?: AccountEntity;
|
|
||||||
isFiltered: boolean;
|
|
||||||
filteredAmount?: number | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Balances({
|
export function Balances({
|
||||||
|
accountId,
|
||||||
balanceQuery,
|
balanceQuery,
|
||||||
|
transactionsQuery,
|
||||||
|
showFilteredBalance,
|
||||||
showExtraBalances,
|
showExtraBalances,
|
||||||
onToggleExtraBalances,
|
onToggleExtraBalances,
|
||||||
account,
|
|
||||||
isFiltered,
|
|
||||||
filteredAmount,
|
|
||||||
}: BalancesProps) {
|
}: BalancesProps) {
|
||||||
const selectedItems = useSelectedItems();
|
const selectedItems = useSelectedItems();
|
||||||
const buttonRef = useRef(null);
|
const balanceBinding = useMemo<Binding<'balance', `balance-query-${string}`>>(
|
||||||
const isButtonHovered = useHover(buttonRef);
|
() => ({
|
||||||
|
name: `balance-query-${accountId}`,
|
||||||
|
query: balanceQuery,
|
||||||
|
value: 0,
|
||||||
|
}),
|
||||||
|
[accountId, balanceQuery],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -201,25 +221,28 @@ export function Balances({
|
|||||||
marginLeft: -5,
|
marginLeft: -5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<ButtonWithLoading
|
||||||
ref={buttonRef}
|
isLoading={!balanceQuery}
|
||||||
data-testid="account-balance"
|
data-testid="account-balance"
|
||||||
variant="bare"
|
variant="bare"
|
||||||
onPress={onToggleExtraBalances}
|
onPress={onToggleExtraBalances}
|
||||||
style={{
|
className={css({
|
||||||
paddingTop: 1,
|
paddingTop: 1,
|
||||||
paddingBottom: 1,
|
paddingBottom: 1,
|
||||||
}}
|
[`& svg`]: {
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
marginLeft: 10,
|
||||||
|
color: theme.pillText,
|
||||||
|
transform: showExtraBalances ? 'rotateZ(180deg)' : 'rotateZ(0)',
|
||||||
|
opacity: selectedItems.size > 0 || showExtraBalances ? 1 : 0,
|
||||||
|
},
|
||||||
|
[`&[data-hovered] svg`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<CellValue
|
<CellValue binding={balanceBinding} type="financial">
|
||||||
binding={
|
|
||||||
{ ...balanceQuery, value: 0 } as Binding<
|
|
||||||
'balance',
|
|
||||||
`balance-query-${string}`
|
|
||||||
>
|
|
||||||
}
|
|
||||||
type="financial"
|
|
||||||
>
|
|
||||||
{props => (
|
{props => (
|
||||||
<CellValueText
|
<CellValueText
|
||||||
{...props}
|
{...props}
|
||||||
@@ -237,26 +260,17 @@ export function Balances({
|
|||||||
)}
|
)}
|
||||||
</CellValue>
|
</CellValue>
|
||||||
|
|
||||||
<SvgArrowButtonRight1
|
<SvgArrowButtonRight1 />
|
||||||
style={{
|
</ButtonWithLoading>
|
||||||
width: 10,
|
{showExtraBalances && accountId && balanceQuery && (
|
||||||
height: 10,
|
<MoreBalances accountId={accountId} balanceQuery={balanceQuery} />
|
||||||
marginLeft: 10,
|
)}
|
||||||
color: theme.pillText,
|
{selectedItems.size > 0 && (
|
||||||
transform: showExtraBalances ? 'rotateZ(180deg)' : 'rotateZ(0)',
|
<SelectedBalance selectedItems={selectedItems} accountId={accountId} />
|
||||||
opacity:
|
)}
|
||||||
isButtonHovered || selectedItems.size > 0 || showExtraBalances
|
{showFilteredBalance && transactionsQuery && (
|
||||||
? 1
|
<FilteredBalance transactionsQuery={transactionsQuery} />
|
||||||
: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
{showExtraBalances && <MoreBalances balanceQuery={balanceQuery} />}
|
|
||||||
|
|
||||||
{selectedItems.size > 0 && (
|
|
||||||
<SelectedBalance selectedItems={selectedItems} account={account} />
|
|
||||||
)}
|
)}
|
||||||
{isFiltered && <FilteredBalance filteredAmount={filteredAmount} />}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import React, {
|
|||||||
Fragment,
|
Fragment,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { type Query } from 'loot-core/shared/query';
|
||||||
import {
|
import {
|
||||||
type AccountEntity,
|
type AccountEntity,
|
||||||
type RuleConditionEntity,
|
type RuleConditionEntity,
|
||||||
@@ -18,7 +21,7 @@ import {
|
|||||||
import { useLocalPref } from '../../hooks/useLocalPref';
|
import { useLocalPref } from '../../hooks/useLocalPref';
|
||||||
import { useSplitsExpanded } from '../../hooks/useSplitsExpanded';
|
import { useSplitsExpanded } from '../../hooks/useSplitsExpanded';
|
||||||
import { useSyncServerStatus } from '../../hooks/useSyncServerStatus';
|
import { useSyncServerStatus } from '../../hooks/useSyncServerStatus';
|
||||||
import { AnimatedLoading } from '../../icons/AnimatedLoading';
|
// import { AnimatedLoading } from '../../icons/AnimatedLoading';
|
||||||
import { SvgAdd } from '../../icons/v1';
|
import { SvgAdd } from '../../icons/v1';
|
||||||
import {
|
import {
|
||||||
SvgArrowsExpand3,
|
SvgArrowsExpand3,
|
||||||
@@ -40,7 +43,6 @@ import { Stack } from '../common/Stack';
|
|||||||
import { View } from '../common/View';
|
import { View } from '../common/View';
|
||||||
import { FilterButton } from '../filters/FiltersMenu';
|
import { FilterButton } from '../filters/FiltersMenu';
|
||||||
import { FiltersStack } from '../filters/FiltersStack';
|
import { FiltersStack } from '../filters/FiltersStack';
|
||||||
import { type SavedFilter } from '../filters/SavedFilterMenuButton';
|
|
||||||
import { NotesButton } from '../NotesButton';
|
import { NotesButton } from '../NotesButton';
|
||||||
import { SelectedTransactionsButton } from '../transactions/SelectedTransactionsButton';
|
import { SelectedTransactionsButton } from '../transactions/SelectedTransactionsButton';
|
||||||
|
|
||||||
@@ -52,29 +54,28 @@ type AccountHeaderProps = {
|
|||||||
tableRef: TableRef;
|
tableRef: TableRef;
|
||||||
editingName: boolean;
|
editingName: boolean;
|
||||||
isNameEditable: boolean;
|
isNameEditable: boolean;
|
||||||
workingHard: boolean;
|
isLoading: boolean;
|
||||||
accountName: string;
|
accountId?: AccountEntity['id'] | string;
|
||||||
|
accountName: string | null;
|
||||||
account?: AccountEntity;
|
account?: AccountEntity;
|
||||||
filterId?: SavedFilter;
|
activeFilter?: TransactionFilterEntity;
|
||||||
savedFilters: TransactionFilterEntity[];
|
dirtyFilter?: TransactionFilterEntity;
|
||||||
accountsSyncing: string[];
|
accountsSyncing: string[];
|
||||||
failedAccounts: AccountSyncSidebarProps['failedAccounts'];
|
failedAccounts: AccountSyncSidebarProps['failedAccounts'];
|
||||||
accounts: AccountEntity[];
|
accounts: AccountEntity[];
|
||||||
transactions: TransactionEntity[];
|
transactions: readonly TransactionEntity[];
|
||||||
showBalances: boolean;
|
showBalances: boolean;
|
||||||
showExtraBalances: boolean;
|
showExtraBalances: boolean;
|
||||||
showCleared: boolean;
|
showCleared: boolean;
|
||||||
showReconciled: boolean;
|
showReconciled: boolean;
|
||||||
showEmptyMessage: boolean;
|
showEmptyMessage: boolean;
|
||||||
balanceQuery: ComponentProps<typeof ReconcilingMessage>['balanceQuery'];
|
balanceQuery: Query;
|
||||||
reconcileAmount?: number | null;
|
transactionsQuery?: Query;
|
||||||
canCalculateBalance?: () => boolean;
|
reconcileAmount: number | null;
|
||||||
isFiltered: boolean;
|
showFilteredBalance: boolean;
|
||||||
filteredAmount?: number | null;
|
|
||||||
isSorted: boolean;
|
isSorted: boolean;
|
||||||
search: string;
|
filterConditions: readonly RuleConditionEntity[];
|
||||||
filterConditions: RuleConditionEntity[];
|
filterConditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
filterConditionsOp: 'and' | 'or';
|
|
||||||
onSearch: (newSearch: string) => void;
|
onSearch: (newSearch: string) => void;
|
||||||
onAddTransaction: () => void;
|
onAddTransaction: () => void;
|
||||||
onShowTransactions: ComponentProps<
|
onShowTransactions: ComponentProps<
|
||||||
@@ -127,11 +128,12 @@ export function AccountHeader({
|
|||||||
tableRef,
|
tableRef,
|
||||||
editingName,
|
editingName,
|
||||||
isNameEditable,
|
isNameEditable,
|
||||||
workingHard,
|
isLoading,
|
||||||
|
accountId,
|
||||||
accountName,
|
accountName,
|
||||||
account,
|
account,
|
||||||
filterId,
|
activeFilter,
|
||||||
savedFilters,
|
dirtyFilter,
|
||||||
accountsSyncing,
|
accountsSyncing,
|
||||||
failedAccounts,
|
failedAccounts,
|
||||||
accounts,
|
accounts,
|
||||||
@@ -143,11 +145,9 @@ export function AccountHeader({
|
|||||||
showEmptyMessage,
|
showEmptyMessage,
|
||||||
balanceQuery,
|
balanceQuery,
|
||||||
reconcileAmount,
|
reconcileAmount,
|
||||||
canCalculateBalance,
|
showFilteredBalance,
|
||||||
isFiltered,
|
transactionsQuery,
|
||||||
filteredAmount,
|
|
||||||
isSorted,
|
isSorted,
|
||||||
search,
|
|
||||||
filterConditions,
|
filterConditions,
|
||||||
filterConditionsOp,
|
filterConditionsOp,
|
||||||
onSearch,
|
onSearch,
|
||||||
@@ -191,6 +191,7 @@ export function AccountHeader({
|
|||||||
const isUsingServer = syncServerStatus !== 'no-server';
|
const isUsingServer = syncServerStatus !== 'no-server';
|
||||||
const isServerOffline = syncServerStatus === 'offline';
|
const isServerOffline = syncServerStatus === 'offline';
|
||||||
const [_, setExpandSplitsPref] = useLocalPref('expand-splits');
|
const [_, setExpandSplitsPref] = useLocalPref('expand-splits');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
let canSync = !!(account?.account_id && isUsingServer);
|
let canSync = !!(account?.account_id && isUsingServer);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@@ -254,6 +255,19 @@ export function AccountHeader({
|
|||||||
[onSync],
|
[onSync],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSearchChange = useCallback(
|
||||||
|
(search: string) => {
|
||||||
|
setSearch(search);
|
||||||
|
onSearch?.(search);
|
||||||
|
},
|
||||||
|
[onSearch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactionsMap = useMemo(
|
||||||
|
() => new Map(transactions.map(t => [t.id, t])),
|
||||||
|
[transactions],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={{ ...styles.pageContent, paddingBottom: 10, flexShrink: 0 }}>
|
<View style={{ ...styles.pageContent, paddingBottom: 10, flexShrink: 0 }}>
|
||||||
@@ -276,7 +290,7 @@ export function AccountHeader({
|
|||||||
)}
|
)}
|
||||||
<AccountNameField
|
<AccountNameField
|
||||||
account={account}
|
account={account}
|
||||||
accountName={accountName}
|
accountName={accountName || ''}
|
||||||
isNameEditable={isNameEditable}
|
isNameEditable={isNameEditable}
|
||||||
editingName={editingName}
|
editingName={editingName}
|
||||||
saveNameError={saveNameError}
|
saveNameError={saveNameError}
|
||||||
@@ -287,12 +301,12 @@ export function AccountHeader({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Balances
|
<Balances
|
||||||
|
accountId={accountId}
|
||||||
balanceQuery={balanceQuery}
|
balanceQuery={balanceQuery}
|
||||||
showExtraBalances={showExtraBalances}
|
transactionsQuery={transactionsQuery}
|
||||||
|
showFilteredBalance={showFilteredBalance}
|
||||||
|
showExtraBalances={!showFilteredBalance && showExtraBalances}
|
||||||
onToggleExtraBalances={onToggleExtraBalances}
|
onToggleExtraBalances={onToggleExtraBalances}
|
||||||
account={account}
|
|
||||||
isFiltered={isFiltered}
|
|
||||||
filteredAmount={filteredAmount}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
@@ -345,30 +359,25 @@ export function AccountHeader({
|
|||||||
<Search
|
<Search
|
||||||
placeholder={t('Search')}
|
placeholder={t('Search')}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={onSearch}
|
onChange={onSearchChange}
|
||||||
inputRef={searchInput}
|
inputRef={searchInput}
|
||||||
/>
|
/>
|
||||||
{workingHard ? (
|
<SelectedTransactionsButton
|
||||||
<View>
|
isLoading={isLoading}
|
||||||
<AnimatedLoading style={{ width: 16, height: 16 }} />
|
getTransaction={id => transactionsMap.get(id)}
|
||||||
</View>
|
onShow={onShowTransactions}
|
||||||
) : (
|
onDuplicate={onBatchDuplicate}
|
||||||
<SelectedTransactionsButton
|
onDelete={onBatchDelete}
|
||||||
getTransaction={id => transactions.find(t => t.id === id)}
|
onEdit={onBatchEdit}
|
||||||
onShow={onShowTransactions}
|
onLinkSchedule={onBatchLinkSchedule}
|
||||||
onDuplicate={onBatchDuplicate}
|
onUnlinkSchedule={onBatchUnlinkSchedule}
|
||||||
onDelete={onBatchDelete}
|
onCreateRule={onCreateRule}
|
||||||
onEdit={onBatchEdit}
|
onSetTransfer={onSetTransfer}
|
||||||
onLinkSchedule={onBatchLinkSchedule}
|
onScheduleAction={onScheduleAction}
|
||||||
onUnlinkSchedule={onBatchUnlinkSchedule}
|
showMakeTransfer={showMakeTransfer}
|
||||||
onCreateRule={onCreateRule}
|
onMakeAsSplitTransaction={onMakeAsSplitTransaction}
|
||||||
onSetTransfer={onSetTransfer}
|
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
|
||||||
onScheduleAction={onScheduleAction}
|
/>
|
||||||
showMakeTransfer={showMakeTransfer}
|
|
||||||
onMakeAsSplitTransaction={onMakeAsSplitTransaction}
|
|
||||||
onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<View style={{ flex: '0 0 auto' }}>
|
<View style={{ flex: '0 0 auto' }}>
|
||||||
{account && (
|
{account && (
|
||||||
<>
|
<>
|
||||||
@@ -443,9 +452,7 @@ export function AccountHeader({
|
|||||||
<AccountMenu
|
<AccountMenu
|
||||||
account={account}
|
account={account}
|
||||||
canSync={canSync}
|
canSync={canSync}
|
||||||
canShowBalances={
|
// canShowBalances={canCalculateBalance()}
|
||||||
canCalculateBalance ? canCalculateBalance() : false
|
|
||||||
}
|
|
||||||
isSorted={isSorted}
|
isSorted={isSorted}
|
||||||
showBalances={showBalances}
|
showBalances={showBalances}
|
||||||
showCleared={showCleared}
|
showCleared={showCleared}
|
||||||
@@ -500,14 +507,15 @@ export function AccountHeader({
|
|||||||
onDeleteFilter={onDeleteFilter}
|
onDeleteFilter={onDeleteFilter}
|
||||||
onClearFilters={onClearFilters}
|
onClearFilters={onClearFilters}
|
||||||
onReloadSavedFilter={onReloadSavedFilter}
|
onReloadSavedFilter={onReloadSavedFilter}
|
||||||
filterId={filterId}
|
filter={activeFilter}
|
||||||
savedFilters={savedFilters}
|
dirtyFilter={dirtyFilter}
|
||||||
onConditionsOpChange={onConditionsOpChange}
|
onConditionsOpChange={onConditionsOpChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{reconcileAmount != null && (
|
{reconcileAmount != null && (
|
||||||
<ReconcilingMessage
|
<ReconcilingMessage
|
||||||
|
accountId={accountId}
|
||||||
targetBalance={reconcileAmount}
|
targetBalance={reconcileAmount}
|
||||||
balanceQuery={balanceQuery}
|
balanceQuery={balanceQuery}
|
||||||
onDone={onDoneReconciling}
|
onDone={onDoneReconciling}
|
||||||
@@ -590,7 +598,7 @@ function AccountNameField({
|
|||||||
marginLeft: -6,
|
marginLeft: -6,
|
||||||
paddingTop: 2,
|
paddingTop: 2,
|
||||||
paddingBottom: 2,
|
paddingBottom: 2,
|
||||||
width: Math.max(20, accountName.length) + 'ch',
|
width: Math.max(20, accountName?.length ?? 0) + 'ch',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</InitialFocus>
|
</InitialFocus>
|
||||||
@@ -671,7 +679,7 @@ type AccountMenuProps = {
|
|||||||
account: AccountEntity;
|
account: AccountEntity;
|
||||||
canSync: boolean;
|
canSync: boolean;
|
||||||
showBalances: boolean;
|
showBalances: boolean;
|
||||||
canShowBalances: boolean;
|
// canShowBalances: boolean;
|
||||||
showCleared: boolean;
|
showCleared: boolean;
|
||||||
showReconciled: boolean;
|
showReconciled: boolean;
|
||||||
isSorted: boolean;
|
isSorted: boolean;
|
||||||
@@ -693,7 +701,7 @@ function AccountMenu({
|
|||||||
account,
|
account,
|
||||||
canSync,
|
canSync,
|
||||||
showBalances,
|
showBalances,
|
||||||
canShowBalances,
|
// canShowBalances,
|
||||||
showCleared,
|
showCleared,
|
||||||
showReconciled,
|
showReconciled,
|
||||||
isSorted,
|
isSorted,
|
||||||
@@ -716,16 +724,22 @@ function AccountMenu({
|
|||||||
} as const,
|
} as const,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(canShowBalances
|
// ...(canShowBalances
|
||||||
? [
|
// ? [
|
||||||
{
|
// {
|
||||||
name: 'toggle-balance',
|
// name: 'toggle-balance',
|
||||||
text: showBalances
|
// text: showBalances
|
||||||
? t('Hide running balance')
|
// ? t('Hide running balance')
|
||||||
: t('Show running balance'),
|
// : t('Show running balance'),
|
||||||
} as const,
|
// } as const,
|
||||||
]
|
// ]
|
||||||
: []),
|
// : []),[
|
||||||
|
{
|
||||||
|
name: 'toggle-balance',
|
||||||
|
text: showBalances
|
||||||
|
? t('Hide running balance')
|
||||||
|
: t('Show running balance'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'toggle-cleared',
|
name: 'toggle-cleared',
|
||||||
text: showCleared
|
text: showCleared
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ import { useFormat } from '../spreadsheet/useFormat';
|
|||||||
import { useSheetValue } from '../spreadsheet/useSheetValue';
|
import { useSheetValue } from '../spreadsheet/useSheetValue';
|
||||||
|
|
||||||
type ReconcilingMessageProps = {
|
type ReconcilingMessageProps = {
|
||||||
balanceQuery: { name: `balance-query-${string}`; query: Query };
|
accountId?: AccountEntity['id'] | string;
|
||||||
|
balanceQuery: Query;
|
||||||
targetBalance: number;
|
targetBalance: number;
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
onCreateTransaction: (targetDiff: number) => void;
|
onCreateTransaction: (targetDiff: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ReconcilingMessage({
|
export function ReconcilingMessage({
|
||||||
|
accountId,
|
||||||
balanceQuery,
|
balanceQuery,
|
||||||
targetBalance,
|
targetBalance,
|
||||||
onDone,
|
onDone,
|
||||||
@@ -31,11 +33,10 @@ export function ReconcilingMessage({
|
|||||||
}: ReconcilingMessageProps) {
|
}: ReconcilingMessageProps) {
|
||||||
const cleared =
|
const cleared =
|
||||||
useSheetValue<'balance', `balance-query-${string}-cleared`>({
|
useSheetValue<'balance', `balance-query-${string}-cleared`>({
|
||||||
name: (balanceQuery.name +
|
name: `balance-query-${accountId}-cleared`,
|
||||||
'-cleared') as `balance-query-${string}-cleared`,
|
|
||||||
value: 0,
|
value: 0,
|
||||||
query: balanceQuery.query.filter({ cleared: true }),
|
query: balanceQuery.filter({ cleared: true }),
|
||||||
}) ?? 0;
|
} as const) ?? 0;
|
||||||
const format = useFormat();
|
const format = useFormat();
|
||||||
const targetDiff = targetBalance - cleared;
|
const targetDiff = targetBalance - cleared;
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ import { ConditionsOpMenu } from './ConditionsOpMenu';
|
|||||||
import { FilterExpression } from './FilterExpression';
|
import { FilterExpression } from './FilterExpression';
|
||||||
|
|
||||||
type AppliedFiltersProps = {
|
type AppliedFiltersProps = {
|
||||||
conditions: RuleConditionEntity[];
|
conditions: readonly RuleConditionEntity[];
|
||||||
onUpdate: (
|
onUpdate: (
|
||||||
filter: RuleConditionEntity,
|
filterCondition: RuleConditionEntity,
|
||||||
newFilter: RuleConditionEntity,
|
newFilterCondition: RuleConditionEntity,
|
||||||
|
) => void;
|
||||||
|
onDelete: (filterCondition: RuleConditionEntity) => void;
|
||||||
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
|
onConditionsOpChange: (
|
||||||
|
filterConditionsOp: RuleConditionEntity['conditionsOp'],
|
||||||
) => void;
|
) => void;
|
||||||
onDelete: (filter: RuleConditionEntity) => void;
|
|
||||||
conditionsOp: string;
|
|
||||||
onConditionsOpChange: (value: 'and' | 'or') => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppliedFilters({
|
export function AppliedFilters({
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export function ConditionsOpMenu({
|
|||||||
onChange,
|
onChange,
|
||||||
conditions,
|
conditions,
|
||||||
}: {
|
}: {
|
||||||
conditionsOp: string;
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
onChange: (value: 'and' | 'or') => void;
|
onChange: (op: RuleConditionEntity['conditionsOp']) => void;
|
||||||
conditions: RuleConditionEntity[];
|
conditions: readonly RuleConditionEntity[];
|
||||||
}) {
|
}) {
|
||||||
return conditions.length > 1 ? (
|
return conditions.length > 1 ? (
|
||||||
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
|
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { type TransactionFilterEntity } from 'loot-core/types/models';
|
||||||
|
|
||||||
import { Menu } from '../common/Menu';
|
import { Menu } from '../common/Menu';
|
||||||
|
|
||||||
import { type SavedFilter } from './SavedFilterMenuButton';
|
|
||||||
|
|
||||||
export function FilterMenu({
|
export function FilterMenu({
|
||||||
filterId,
|
filter,
|
||||||
|
dirtyFilter,
|
||||||
onFilterMenuSelect,
|
onFilterMenuSelect,
|
||||||
}: {
|
}: {
|
||||||
filterId?: SavedFilter;
|
filter?: TransactionFilterEntity;
|
||||||
|
dirtyFilter?: TransactionFilterEntity;
|
||||||
onFilterMenuSelect: (item: string) => void;
|
onFilterMenuSelect: (item: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -20,12 +22,12 @@ export function FilterMenu({
|
|||||||
onFilterMenuSelect(item);
|
onFilterMenuSelect(item);
|
||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
!filterId?.id
|
!filter?.id
|
||||||
? [
|
? [
|
||||||
{ name: 'save-filter', text: t('Save new filter') },
|
{ name: 'save-filter', text: t('Save new filter') },
|
||||||
{ name: 'clear-filter', text: t('Clear all conditions') },
|
{ name: 'clear-filter', text: t('Clear all filter conditions') },
|
||||||
]
|
]
|
||||||
: filterId?.id !== null && filterId?.status === 'saved'
|
: filter?.id !== null && !dirtyFilter
|
||||||
? [
|
? [
|
||||||
{ name: 'rename-filter', text: t('Rename') },
|
{ name: 'rename-filter', text: t('Rename') },
|
||||||
{ name: 'delete-filter', text: t('Delete') },
|
{ name: 'delete-filter', text: t('Delete') },
|
||||||
@@ -35,16 +37,22 @@ export function FilterMenu({
|
|||||||
text: t('Save new filter'),
|
text: t('Save new filter'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{ name: 'clear-filter', text: t('Clear all conditions') },
|
{
|
||||||
|
name: 'clear-filter',
|
||||||
|
text: t('Clear all filter conditions'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ name: 'rename-filter', text: t('Rename') },
|
{ name: 'rename-filter', text: t('Rename') },
|
||||||
{ name: 'update-filter', text: t('Update condtions') },
|
{ name: 'update-filter', text: t('Update filter conditions') },
|
||||||
{ name: 'reload-filter', text: t('Revert changes') },
|
{ name: 'reload-filter', text: t('Revert changes') },
|
||||||
{ name: 'delete-filter', text: t('Delete') },
|
{ name: 'delete-filter', text: t('Delete') },
|
||||||
Menu.line,
|
Menu.line,
|
||||||
{ name: 'save-filter', text: t('Save new filter') },
|
{ name: 'save-filter', text: t('Save new filter') },
|
||||||
{ name: 'clear-filter', text: t('Clear all conditions') },
|
{
|
||||||
|
name: 'clear-filter',
|
||||||
|
text: t('Clear all filter conditions'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,10 +7,27 @@ import { Stack } from '../common/Stack';
|
|||||||
import { View } from '../common/View';
|
import { View } from '../common/View';
|
||||||
|
|
||||||
import { AppliedFilters } from './AppliedFilters';
|
import { AppliedFilters } from './AppliedFilters';
|
||||||
import {
|
import { SavedFilterMenuButton } from './SavedFilterMenuButton';
|
||||||
type SavedFilter,
|
|
||||||
SavedFilterMenuButton,
|
type FiltersStackProps = {
|
||||||
} from './SavedFilterMenuButton';
|
conditions: readonly RuleConditionEntity[];
|
||||||
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
|
onUpdateFilter: (
|
||||||
|
filterCondition: RuleConditionEntity,
|
||||||
|
newFilterCondition: RuleConditionEntity,
|
||||||
|
) => void;
|
||||||
|
onDeleteFilter: (filterCondition: RuleConditionEntity) => void;
|
||||||
|
onClearFilters: () => void;
|
||||||
|
onReloadSavedFilter: (
|
||||||
|
savedFilter: TransactionFilterEntity,
|
||||||
|
action?: 'reload' | 'update',
|
||||||
|
) => void;
|
||||||
|
filter?: TransactionFilterEntity;
|
||||||
|
dirtyFilter?: TransactionFilterEntity;
|
||||||
|
onConditionsOpChange: (
|
||||||
|
conditionsOp: RuleConditionEntity['conditionsOp'],
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export function FiltersStack({
|
export function FiltersStack({
|
||||||
conditions,
|
conditions,
|
||||||
@@ -19,23 +36,10 @@ export function FiltersStack({
|
|||||||
onDeleteFilter,
|
onDeleteFilter,
|
||||||
onClearFilters,
|
onClearFilters,
|
||||||
onReloadSavedFilter,
|
onReloadSavedFilter,
|
||||||
filterId,
|
filter,
|
||||||
savedFilters,
|
dirtyFilter,
|
||||||
onConditionsOpChange,
|
onConditionsOpChange,
|
||||||
}: {
|
}: FiltersStackProps) {
|
||||||
conditions: RuleConditionEntity[];
|
|
||||||
conditionsOp: 'and' | 'or';
|
|
||||||
onUpdateFilter: (
|
|
||||||
filter: RuleConditionEntity,
|
|
||||||
newFilter: RuleConditionEntity,
|
|
||||||
) => void;
|
|
||||||
onDeleteFilter: (filter: RuleConditionEntity) => void;
|
|
||||||
onClearFilters: () => void;
|
|
||||||
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
|
|
||||||
filterId?: SavedFilter;
|
|
||||||
savedFilters: TransactionFilterEntity[];
|
|
||||||
onConditionsOpChange: (value: 'and' | 'or') => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Stack
|
<Stack
|
||||||
@@ -55,10 +59,10 @@ export function FiltersStack({
|
|||||||
<SavedFilterMenuButton
|
<SavedFilterMenuButton
|
||||||
conditions={conditions}
|
conditions={conditions}
|
||||||
conditionsOp={conditionsOp}
|
conditionsOp={conditionsOp}
|
||||||
filterId={filterId}
|
filter={filter}
|
||||||
|
dirtyFilter={dirtyFilter}
|
||||||
onClearFilters={onClearFilters}
|
onClearFilters={onClearFilters}
|
||||||
onReloadSavedFilter={onReloadSavedFilter}
|
onReloadSavedFilter={onReloadSavedFilter}
|
||||||
savedFilters={savedFilters}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useFilters } from 'loot-core/client/data-hooks/filters';
|
||||||
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
|
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
|
||||||
import { type TransactionFilterEntity } from 'loot-core/types/models';
|
import { type TransactionFilterEntity } from 'loot-core/types/models';
|
||||||
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
||||||
@@ -14,29 +15,26 @@ import { View } from '../common/View';
|
|||||||
import { FilterMenu } from './FilterMenu';
|
import { FilterMenu } from './FilterMenu';
|
||||||
import { NameFilter } from './NameFilter';
|
import { NameFilter } from './NameFilter';
|
||||||
|
|
||||||
export type SavedFilter = {
|
type SavedFilterMenuButtonProps = {
|
||||||
conditions?: RuleConditionEntity[];
|
conditions: readonly RuleConditionEntity[];
|
||||||
conditionsOp?: 'and' | 'or';
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
id?: string;
|
filter?: TransactionFilterEntity;
|
||||||
name: string;
|
dirtyFilter?: TransactionFilterEntity;
|
||||||
status?: string;
|
onClearFilters: () => void;
|
||||||
|
onReloadSavedFilter: (
|
||||||
|
savedFilter: TransactionFilterEntity,
|
||||||
|
action?: 'reload' | 'update',
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SavedFilterMenuButton({
|
export function SavedFilterMenuButton({
|
||||||
conditions,
|
conditions,
|
||||||
conditionsOp,
|
conditionsOp,
|
||||||
filterId,
|
filter,
|
||||||
|
dirtyFilter,
|
||||||
onClearFilters,
|
onClearFilters,
|
||||||
onReloadSavedFilter,
|
onReloadSavedFilter,
|
||||||
savedFilters,
|
}: SavedFilterMenuButtonProps) {
|
||||||
}: {
|
|
||||||
conditions: RuleConditionEntity[];
|
|
||||||
conditionsOp: 'and' | 'or';
|
|
||||||
filterId?: SavedFilter;
|
|
||||||
onClearFilters: () => void;
|
|
||||||
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
|
|
||||||
savedFilters: TransactionFilterEntity[];
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [nameOpen, setNameOpen] = useState(false);
|
const [nameOpen, setNameOpen] = useState(false);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
@@ -44,9 +42,8 @@ export function SavedFilterMenuButton({
|
|||||||
const triggerRef = useRef(null);
|
const triggerRef = useRef(null);
|
||||||
const [err, setErr] = useState(null);
|
const [err, setErr] = useState(null);
|
||||||
const [menuItem, setMenuItem] = useState('');
|
const [menuItem, setMenuItem] = useState('');
|
||||||
const [name, setName] = useState(filterId?.name ?? '');
|
const [name, setName] = useState(filter?.name ?? '');
|
||||||
const id = filterId?.id;
|
const savedFilters = useFilters();
|
||||||
let savedFilter: SavedFilter;
|
|
||||||
|
|
||||||
const onFilterMenuSelect = async (item: string) => {
|
const onFilterMenuSelect = async (item: string) => {
|
||||||
setMenuItem(item);
|
setMenuItem(item);
|
||||||
@@ -59,23 +56,22 @@ export function SavedFilterMenuButton({
|
|||||||
break;
|
break;
|
||||||
case 'delete-filter':
|
case 'delete-filter':
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
await send('filter-delete', id);
|
if (filter?.id) {
|
||||||
|
await send('filter-delete', filter.id);
|
||||||
|
}
|
||||||
onClearFilters();
|
onClearFilters();
|
||||||
break;
|
break;
|
||||||
case 'update-filter':
|
case 'update-filter':
|
||||||
setErr(null);
|
setErr(null);
|
||||||
setAdding(false);
|
setAdding(false);
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
savedFilter = {
|
if (!filter || !dirtyFilter) {
|
||||||
conditions,
|
// No active filter or filter is not dirty, nothing to update.
|
||||||
conditionsOp,
|
return;
|
||||||
id: filterId?.id,
|
}
|
||||||
name: filterId?.name ?? '',
|
|
||||||
status: 'saved',
|
|
||||||
};
|
|
||||||
const response = await sendCatch('filter-update', {
|
const response = await sendCatch('filter-update', {
|
||||||
state: savedFilter,
|
state: dirtyFilter,
|
||||||
filters: [...savedFilters],
|
filters: savedFilters,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
@@ -84,7 +80,7 @@ export function SavedFilterMenuButton({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onReloadSavedFilter(savedFilter, 'update');
|
onReloadSavedFilter(dirtyFilter, 'update');
|
||||||
break;
|
break;
|
||||||
case 'save-filter':
|
case 'save-filter':
|
||||||
setErr(null);
|
setErr(null);
|
||||||
@@ -94,11 +90,9 @@ export function SavedFilterMenuButton({
|
|||||||
break;
|
break;
|
||||||
case 'reload-filter':
|
case 'reload-filter':
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
savedFilter = {
|
if (filter) {
|
||||||
...savedFilter,
|
onReloadSavedFilter(filter, 'reload');
|
||||||
status: 'saved',
|
}
|
||||||
};
|
|
||||||
onReloadSavedFilter(savedFilter, 'reload');
|
|
||||||
break;
|
break;
|
||||||
case 'clear-filter':
|
case 'clear-filter':
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
@@ -111,10 +105,9 @@ export function SavedFilterMenuButton({
|
|||||||
async function onAddUpdate() {
|
async function onAddUpdate() {
|
||||||
if (adding) {
|
if (adding) {
|
||||||
const newSavedFilter = {
|
const newSavedFilter = {
|
||||||
conditions,
|
conditions: [...conditions],
|
||||||
conditionsOp,
|
conditionsOp: conditionsOp || 'and',
|
||||||
name,
|
name,
|
||||||
status: 'saved',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await sendCatch('filter-create', {
|
const response = await sendCatch('filter-create', {
|
||||||
@@ -132,30 +125,32 @@ export function SavedFilterMenuButton({
|
|||||||
onReloadSavedFilter({
|
onReloadSavedFilter({
|
||||||
...newSavedFilter,
|
...newSavedFilter,
|
||||||
id: response.data,
|
id: response.data,
|
||||||
|
tombstone: false,
|
||||||
});
|
});
|
||||||
return;
|
} else {
|
||||||
|
if (!filter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFilter = {
|
||||||
|
...filter,
|
||||||
|
...dirtyFilter,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await sendCatch('filter-update', {
|
||||||
|
state: updatedFilter,
|
||||||
|
filters: [...savedFilters],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
setErr(response.error.message);
|
||||||
|
setNameOpen(true);
|
||||||
|
} else {
|
||||||
|
setNameOpen(false);
|
||||||
|
onReloadSavedFilter(updatedFilter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedFilter = {
|
|
||||||
conditions: filterId?.conditions,
|
|
||||||
conditionsOp: filterId?.conditionsOp,
|
|
||||||
id: filterId?.id,
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await sendCatch('filter-update', {
|
|
||||||
state: updatedFilter,
|
|
||||||
filters: [...savedFilters],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
setErr(response.error.message);
|
|
||||||
setNameOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setNameOpen(false);
|
|
||||||
onReloadSavedFilter(updatedFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -178,9 +173,9 @@ export function SavedFilterMenuButton({
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!filterId?.id ? t('Unsaved filter') : filterId?.name}
|
{!filter?.id ? t('Unsaved filter') : filter?.name}
|
||||||
</Text>
|
</Text>
|
||||||
{filterId?.id && filterId?.status !== 'saved' && (
|
{filter?.id && !!dirtyFilter && (
|
||||||
<Text>
|
<Text>
|
||||||
<Trans>(modified)</Trans>
|
<Trans>(modified)</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -196,7 +191,8 @@ export function SavedFilterMenuButton({
|
|||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
>
|
>
|
||||||
<FilterMenu
|
<FilterMenu
|
||||||
filterId={filterId}
|
filter={filter}
|
||||||
|
dirtyFilter={dirtyFilter}
|
||||||
onFilterMenuSelect={onFilterMenuSelect}
|
onFilterMenuSelect={onFilterMenuSelect}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from 'loot-core/client/actions';
|
} from 'loot-core/client/actions';
|
||||||
import { amountToInteger } from 'loot-core/src/shared/util';
|
import { amountToInteger } from 'loot-core/src/shared/util';
|
||||||
|
|
||||||
|
import { useCategories } from '../../../hooks/useCategories';
|
||||||
import { useDateFormat } from '../../../hooks/useDateFormat';
|
import { useDateFormat } from '../../../hooks/useDateFormat';
|
||||||
import { useSyncedPrefs } from '../../../hooks/useSyncedPrefs';
|
import { useSyncedPrefs } from '../../../hooks/useSyncedPrefs';
|
||||||
import { useDispatch } from '../../../redux';
|
import { useDispatch } from '../../../redux';
|
||||||
@@ -157,7 +158,8 @@ export function ImportTransactionsModal({ options }) {
|
|||||||
const [flipAmount, setFlipAmount] = useState(false);
|
const [flipAmount, setFlipAmount] = useState(false);
|
||||||
const [multiplierEnabled, setMultiplierEnabled] = useState(false);
|
const [multiplierEnabled, setMultiplierEnabled] = useState(false);
|
||||||
const [reconcile, setReconcile] = useState(true);
|
const [reconcile, setReconcile] = useState(true);
|
||||||
const { accountId, categories, onImported } = options;
|
const { accountId, onImported } = options;
|
||||||
|
const { list: categories } = useCategories();
|
||||||
|
|
||||||
// This cannot be set after parsing the file, because changing it
|
// This cannot be set after parsing the file, because changing it
|
||||||
// requires re-parsing the file. This is different from the other
|
// requires re-parsing the file. This is different from the other
|
||||||
@@ -241,7 +243,7 @@ export function ImportTransactionsModal({ options }) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const category_id = parseCategoryFields(trans, categories.list);
|
const category_id = parseCategoryFields(trans, categories);
|
||||||
if (category_id != null) {
|
if (category_id != null) {
|
||||||
trans.category = category_id;
|
trans.category = category_id;
|
||||||
}
|
}
|
||||||
@@ -295,7 +297,7 @@ export function ImportTransactionsModal({ options }) {
|
|||||||
// add the updated existing transaction in the list, with the
|
// add the updated existing transaction in the list, with the
|
||||||
// isMatchedTransaction flag to identify it in display and not send it again
|
// isMatchedTransaction flag to identify it in display and not send it again
|
||||||
existing_trx.isMatchedTransaction = true;
|
existing_trx.isMatchedTransaction = true;
|
||||||
existing_trx.category = categories.list.find(
|
existing_trx.category = categories.find(
|
||||||
cat => cat.id === existing_trx.category,
|
cat => cat.id === existing_trx.category,
|
||||||
)?.name;
|
)?.name;
|
||||||
// add parent transaction attribute to mimic behaviour
|
// add parent transaction attribute to mimic behaviour
|
||||||
@@ -310,7 +312,7 @@ export function ImportTransactionsModal({ options }) {
|
|||||||
return next;
|
return next;
|
||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
[accountId, categories.list, clearOnImport, dispatch],
|
[accountId, categories, clearOnImport, dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const parse = useCallback(
|
const parse = useCallback(
|
||||||
@@ -584,7 +586,7 @@ export function ImportTransactionsModal({ options }) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const category_id = parseCategoryFields(trans, categories.list);
|
const category_id = parseCategoryFields(trans, categories);
|
||||||
trans.category = category_id;
|
trans.category = category_id;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -784,7 +786,7 @@ export function ImportTransactionsModal({ options }) {
|
|||||||
outValue={outValue}
|
outValue={outValue}
|
||||||
flipAmount={flipAmount}
|
flipAmount={flipAmount}
|
||||||
multiplierAmount={multiplierAmount}
|
multiplierAmount={multiplierAmount}
|
||||||
categories={categories.list}
|
categories={categories}
|
||||||
onCheckTransaction={onCheckTransaction}
|
onCheckTransaction={onCheckTransaction}
|
||||||
reconcile={reconcile}
|
reconcile={reconcile}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type HeaderProps = {
|
|||||||
mode: TimeFrame['mode'],
|
mode: TimeFrame['mode'],
|
||||||
) => void;
|
) => void;
|
||||||
filters?: RuleConditionEntity[];
|
filters?: RuleConditionEntity[];
|
||||||
conditionsOp: 'and' | 'or';
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
onApply?: (conditions: RuleConditionEntity) => void;
|
onApply?: (conditions: RuleConditionEntity) => void;
|
||||||
onUpdateFilter: ComponentProps<typeof AppliedFilters>['onUpdate'];
|
onUpdateFilter: ComponentProps<typeof AppliedFilters>['onUpdate'];
|
||||||
onDeleteFilter: ComponentProps<typeof AppliedFilters>['onDelete'];
|
onDeleteFilter: ComponentProps<typeof AppliedFilters>['onDelete'];
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import { useMergedRefs } from '../../../hooks/useMergedRefs';
|
|||||||
import { useNavigate } from '../../../hooks/useNavigate';
|
import { useNavigate } from '../../../hooks/useNavigate';
|
||||||
import { usePayees } from '../../../hooks/usePayees';
|
import { usePayees } from '../../../hooks/usePayees';
|
||||||
import { useResizeObserver } from '../../../hooks/useResizeObserver';
|
import { useResizeObserver } from '../../../hooks/useResizeObserver';
|
||||||
import { SelectedProviderWithItems } from '../../../hooks/useSelected';
|
|
||||||
import { SplitsExpandedProvider } from '../../../hooks/useSplitsExpanded';
|
import { SplitsExpandedProvider } from '../../../hooks/useSplitsExpanded';
|
||||||
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
||||||
import {
|
import {
|
||||||
@@ -563,141 +562,129 @@ function CalendarInner({ widget, parameters }: CalendarInnerProps) {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<SelectedProviderWithItems
|
<SchedulesProvider query={undefined}>
|
||||||
name="transactions"
|
<View
|
||||||
items={[]}
|
style={{
|
||||||
fetchAllIds={async () => []}
|
width: '100%',
|
||||||
registerDispatch={() => {}}
|
flexGrow: 1,
|
||||||
selectAllFilter={(item: TransactionEntity) =>
|
overflow: isNarrowWidth ? 'auto' : 'hidden',
|
||||||
!item._unmatched && !item.is_parent
|
}}
|
||||||
}
|
ref={table}
|
||||||
>
|
>
|
||||||
<SchedulesProvider query={undefined}>
|
{!isNarrowWidth ? (
|
||||||
<View
|
<SplitsExpandedProvider initialMode="collapse">
|
||||||
style={{
|
<TransactionList
|
||||||
width: '100%',
|
tableRef={table}
|
||||||
flexGrow: 1,
|
account={undefined}
|
||||||
overflow: isNarrowWidth ? 'auto' : 'hidden',
|
transactions={transactionsGrouped}
|
||||||
}}
|
allTransactions={allTransactions}
|
||||||
ref={table}
|
loadMoreTransactions={loadMoreTransactions}
|
||||||
>
|
accounts={accounts}
|
||||||
{!isNarrowWidth ? (
|
category={undefined}
|
||||||
<SplitsExpandedProvider initialMode="collapse">
|
categoryGroups={categoryGroups}
|
||||||
<TransactionList
|
payees={payees}
|
||||||
headerContent={undefined}
|
balances={null}
|
||||||
tableRef={table}
|
showBalances={false}
|
||||||
account={undefined}
|
showReconciled={true}
|
||||||
transactions={transactionsGrouped}
|
showCleared={false}
|
||||||
allTransactions={allTransactions}
|
showAccount={true}
|
||||||
loadMoreTransactions={loadMoreTransactions}
|
isAdding={false}
|
||||||
accounts={accounts}
|
isNew={() => false}
|
||||||
category={undefined}
|
isMatched={() => false}
|
||||||
categoryGroups={categoryGroups}
|
isFiltered={() => true}
|
||||||
payees={payees}
|
dateFormat={dateFormat}
|
||||||
balances={null}
|
hideFraction={false}
|
||||||
showBalances={false}
|
renderEmpty={() => (
|
||||||
showReconciled={true}
|
<View
|
||||||
showCleared={false}
|
style={{
|
||||||
showAccount={true}
|
color: theme.tableText,
|
||||||
isAdding={false}
|
marginTop: 20,
|
||||||
isNew={() => false}
|
textAlign: 'center',
|
||||||
isMatched={() => false}
|
fontStyle: 'italic',
|
||||||
isFiltered={() => true}
|
}}
|
||||||
dateFormat={dateFormat}
|
>
|
||||||
hideFraction={false}
|
<Trans>No transactions</Trans>
|
||||||
addNotification={addNotification}
|
</View>
|
||||||
renderEmpty={() => (
|
)}
|
||||||
<View
|
onSort={onSort}
|
||||||
style={{
|
sortField={sortField}
|
||||||
color: theme.tableText,
|
ascDesc={ascDesc}
|
||||||
marginTop: 20,
|
onChange={() => {}}
|
||||||
textAlign: 'center',
|
onRefetch={() => setDirty(true)}
|
||||||
fontStyle: 'italic',
|
onCloseAddTransaction={() => {}}
|
||||||
}}
|
onCreatePayee={() => {}}
|
||||||
>
|
onApplyFilter={() => {}}
|
||||||
<Trans>No transactions</Trans>
|
onBatchDelete={() => {}}
|
||||||
</View>
|
onBatchDuplicate={() => {}}
|
||||||
)}
|
onBatchLinkSchedule={() => {}}
|
||||||
onSort={onSort}
|
onBatchUnlinkSchedule={() => {}}
|
||||||
sortField={sortField}
|
onCreateRule={() => {}}
|
||||||
ascDesc={ascDesc}
|
onScheduleAction={() => {}}
|
||||||
onChange={() => {}}
|
onMakeAsNonSplitTransactions={() => {}}
|
||||||
onRefetch={() => setDirty(true)}
|
showSelection={false}
|
||||||
onCloseAddTransaction={() => {}}
|
allowSplitTransaction={false}
|
||||||
onCreatePayee={() => {}}
|
/>
|
||||||
onApplyFilter={() => {}}
|
</SplitsExpandedProvider>
|
||||||
onBatchDelete={() => {}}
|
) : (
|
||||||
onBatchDuplicate={() => {}}
|
<animated.div
|
||||||
onBatchLinkSchedule={() => {}}
|
{...bind()}
|
||||||
onBatchUnlinkSchedule={() => {}}
|
style={{
|
||||||
onCreateRule={() => {}}
|
y,
|
||||||
onScheduleAction={() => {}}
|
touchAction: 'pan-x',
|
||||||
onMakeAsNonSplitTransactions={() => {}}
|
backgroundColor: theme.mobileNavBackground,
|
||||||
showSelection={false}
|
borderTop: `1px solid ${theme.menuBorder}`,
|
||||||
allowSplitTransaction={false}
|
...styles.shadow,
|
||||||
/>
|
height: totalHeight + CHEVRON_HEIGHT,
|
||||||
</SplitsExpandedProvider>
|
width: '100%',
|
||||||
) : (
|
position: 'fixed',
|
||||||
<animated.div
|
zIndex: 100,
|
||||||
{...bind()}
|
bottom: 0,
|
||||||
style={{
|
display: isNarrowWidth ? 'flex' : 'none',
|
||||||
y,
|
flexDirection: 'column',
|
||||||
touchAction: 'pan-x',
|
alignItems: 'center',
|
||||||
backgroundColor: theme.mobileNavBackground,
|
}}
|
||||||
borderTop: `1px solid ${theme.menuBorder}`,
|
>
|
||||||
...styles.shadow,
|
<Button
|
||||||
height: totalHeight + CHEVRON_HEIGHT,
|
variant="bare"
|
||||||
width: '100%',
|
onPress={() =>
|
||||||
position: 'fixed',
|
!mobileTransactionsOpen
|
||||||
zIndex: 100,
|
? open({ canceled: false })
|
||||||
bottom: 0,
|
: close()
|
||||||
display: isNarrowWidth ? 'flex' : 'none',
|
}
|
||||||
flexDirection: 'column',
|
className={css({
|
||||||
alignItems: 'center',
|
color: theme.pageTextSubdued,
|
||||||
}}
|
height: 42,
|
||||||
|
'&[data-pressed]': { backgroundColor: 'transparent' },
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<Button
|
{!mobileTransactionsOpen && (
|
||||||
variant="bare"
|
<>
|
||||||
onPress={() =>
|
<SvgCheveronUp width={16} height={16} />
|
||||||
!mobileTransactionsOpen
|
<Trans>Show transactions</Trans>
|
||||||
? open({ canceled: false })
|
</>
|
||||||
: close()
|
)}
|
||||||
}
|
{mobileTransactionsOpen && (
|
||||||
className={css({
|
<>
|
||||||
color: theme.pageTextSubdued,
|
<SvgCheveronDown width={16} height={16} />
|
||||||
height: 42,
|
<Trans>Hide transactions</Trans>
|
||||||
'&[data-pressed]': { backgroundColor: 'transparent' },
|
</>
|
||||||
})}
|
)}
|
||||||
>
|
</Button>
|
||||||
{!mobileTransactionsOpen && (
|
<View
|
||||||
<>
|
style={{ height: '100%', width: '100%', overflow: 'auto' }}
|
||||||
<SvgCheveronUp width={16} height={16} />
|
>
|
||||||
<Trans>Show transactions</Trans>
|
<TransactionListMobile
|
||||||
</>
|
isLoading={false}
|
||||||
)}
|
onLoadMore={loadMoreTransactions}
|
||||||
{mobileTransactionsOpen && (
|
transactions={allTransactions}
|
||||||
<>
|
onOpenTransaction={onOpenTransaction}
|
||||||
<SvgCheveronDown width={16} height={16} />
|
isLoadingMore={false}
|
||||||
<Trans>Hide transactions</Trans>
|
/>
|
||||||
</>
|
</View>
|
||||||
)}
|
</animated.div>
|
||||||
</Button>
|
)}
|
||||||
<View
|
</View>
|
||||||
style={{ height: '100%', width: '100%', overflow: 'auto' }}
|
</SchedulesProvider>
|
||||||
>
|
|
||||||
<TransactionListMobile
|
|
||||||
isLoading={false}
|
|
||||||
onLoadMore={loadMoreTransactions}
|
|
||||||
transactions={allTransactions}
|
|
||||||
onOpenTransaction={onOpenTransaction}
|
|
||||||
isLoadingMore={false}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</animated.div>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</SchedulesProvider>
|
|
||||||
</SelectedProviderWithItems>
|
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function simpleCashFlow(
|
|||||||
startMonth: string,
|
startMonth: string,
|
||||||
endMonth: string,
|
endMonth: string,
|
||||||
conditions: RuleConditionEntity[] = [],
|
conditions: RuleConditionEntity[] = [],
|
||||||
conditionsOp: 'and' | 'or' = 'and',
|
conditionsOp: RuleConditionEntity['conditionsOp'] = 'and',
|
||||||
) {
|
) {
|
||||||
const start = monthUtils.firstDayOfMonth(startMonth);
|
const start = monthUtils.firstDayOfMonth(startMonth);
|
||||||
const end = monthUtils.lastDayOfMonth(endMonth);
|
const end = monthUtils.lastDayOfMonth(endMonth);
|
||||||
@@ -71,7 +71,7 @@ export function cashFlowByDate(
|
|||||||
endMonth: string,
|
endMonth: string,
|
||||||
isConcise: boolean,
|
isConcise: boolean,
|
||||||
conditions: RuleConditionEntity[] = [],
|
conditions: RuleConditionEntity[] = [],
|
||||||
conditionsOp: 'and' | 'or',
|
conditionsOp: RuleConditionEntity['conditionsOp'],
|
||||||
) {
|
) {
|
||||||
const start = monthUtils.firstDayOfMonth(startMonth);
|
const start = monthUtils.firstDayOfMonth(startMonth);
|
||||||
const end = monthUtils.lastDayOfMonth(endMonth);
|
const end = monthUtils.lastDayOfMonth(endMonth);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function createSpreadsheet(
|
|||||||
end: string,
|
end: string,
|
||||||
accounts: AccountEntity[],
|
accounts: AccountEntity[],
|
||||||
conditions: RuleConditionEntity[] = [],
|
conditions: RuleConditionEntity[] = [],
|
||||||
conditionsOp: 'and' | 'or' = 'and',
|
conditionsOp: RuleConditionEntity['conditionsOp'] = 'and',
|
||||||
) {
|
) {
|
||||||
return async (
|
return async (
|
||||||
spreadsheet: ReturnType<typeof useSpreadsheet>,
|
spreadsheet: ReturnType<typeof useSpreadsheet>,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function summarySpreadsheet(
|
|||||||
start: string,
|
start: string,
|
||||||
end: string,
|
end: string,
|
||||||
conditions: RuleConditionEntity[] = [],
|
conditions: RuleConditionEntity[] = [],
|
||||||
conditionsOp: 'and' | 'or' = 'and',
|
conditionsOp: RuleConditionEntity['conditionsOp'] = 'and',
|
||||||
summaryContent: SummaryContent,
|
summaryContent: SummaryContent,
|
||||||
) {
|
) {
|
||||||
return async (
|
return async (
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export function Value<T>({
|
|||||||
return value ? 'true' : 'false';
|
return value ? 'true' : 'false';
|
||||||
} else {
|
} else {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
|
case 'id':
|
||||||
|
return value;
|
||||||
case 'amount':
|
case 'amount':
|
||||||
return integerToCurrency(value);
|
return integerToCurrency(value);
|
||||||
case 'date':
|
case 'date':
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import React, { useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { pushModal } from 'loot-core/client/actions';
|
import { pushModal } from 'loot-core/client/actions';
|
||||||
|
import { q } from 'loot-core/shared/query';
|
||||||
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||||
import { send } from 'loot-core/src/platform/client/fetch';
|
import { send } from 'loot-core/src/platform/client/fetch';
|
||||||
import { q } from 'loot-core/src/shared/query';
|
|
||||||
import {
|
import {
|
||||||
type ScheduleEntity,
|
type ScheduleEntity,
|
||||||
type TransactionEntity,
|
type TransactionEntity,
|
||||||
@@ -37,6 +37,7 @@ export function ScheduleLink({
|
|||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [filter, setFilter] = useState(accountName || '');
|
const [filter, setFilter] = useState(accountName || '');
|
||||||
|
|
||||||
const schedulesQuery = useMemo(
|
const schedulesQuery = useMemo(
|
||||||
() => q('schedules').filter({ completed: false }).select('*'),
|
() => q('schedules').filter({ completed: false }).select('*'),
|
||||||
[],
|
[],
|
||||||
@@ -47,25 +48,26 @@ export function ScheduleLink({
|
|||||||
statuses,
|
statuses,
|
||||||
} = useSchedules({ query: schedulesQuery });
|
} = useSchedules({ query: schedulesQuery });
|
||||||
|
|
||||||
const searchInput = useRef(null);
|
const onSelect = useCallback(
|
||||||
|
async (scheduleId: string) => {
|
||||||
|
if (ids?.length > 0) {
|
||||||
|
await send('transactions-batch-update', {
|
||||||
|
updated: ids.map(id => ({ id, schedule: scheduleId })),
|
||||||
|
});
|
||||||
|
onScheduleLinked?.(schedules.find(s => s.id === scheduleId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ids, onScheduleLinked, schedules],
|
||||||
|
);
|
||||||
|
|
||||||
async function onSelect(scheduleId: string) {
|
const onCreate = useCallback(() => {
|
||||||
if (ids?.length > 0) {
|
|
||||||
await send('transactions-batch-update', {
|
|
||||||
updated: ids.map(id => ({ id, schedule: scheduleId })),
|
|
||||||
});
|
|
||||||
onScheduleLinked?.(schedules.find(s => s.id === scheduleId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onCreate() {
|
|
||||||
dispatch(
|
dispatch(
|
||||||
pushModal('schedule-edit', {
|
pushModal('schedule-edit', {
|
||||||
id: null,
|
id: null,
|
||||||
transaction: getTransaction(ids[0]),
|
transaction: getTransaction(ids[0]),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}, [dispatch, getTransaction, ids]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -98,7 +100,6 @@ export function ScheduleLink({
|
|||||||
</Text>
|
</Text>
|
||||||
<InitialFocus>
|
<InitialFocus>
|
||||||
<Search
|
<Search
|
||||||
inputRef={searchInput}
|
|
||||||
isInModal
|
isInModal
|
||||||
width={300}
|
width={300}
|
||||||
placeholder={t('Filter schedules…')}
|
placeholder={t('Filter schedules…')}
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ function ScheduleRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SchedulesTable({
|
export function SchedulesTable({
|
||||||
isLoading,
|
isLoading = false,
|
||||||
schedules,
|
schedules,
|
||||||
statuses,
|
statuses,
|
||||||
filter,
|
filter,
|
||||||
|
|||||||
@@ -72,13 +72,16 @@ export type Spreadsheets = {
|
|||||||
goal: number;
|
goal: number;
|
||||||
'long-goal': number;
|
'long-goal': number;
|
||||||
};
|
};
|
||||||
[`balance`]: {
|
balance: {
|
||||||
// Common fields
|
// Common fields
|
||||||
'uncategorized-amount': number;
|
'uncategorized-amount': number;
|
||||||
'uncategorized-balance': number;
|
'uncategorized-balance': number;
|
||||||
|
'filtered-balance': number;
|
||||||
|
|
||||||
// Balance fields
|
// Balance fields
|
||||||
[key: `balance-query-${string}`]: number;
|
[key: `balance-query-${string}`]: number;
|
||||||
|
[key: `balance-query-${string}-cleared`]: number;
|
||||||
|
[key: `balance-query-${string}-uncleared`]: number;
|
||||||
[key: `selected-transactions-${string}`]: Array<{ id: string }>;
|
[key: `selected-transactions-${string}`]: Array<{ id: string }>;
|
||||||
[key: `selected-balance-${string}`]: number;
|
[key: `selected-balance-${string}`]: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { SvgDelete, SvgExpandArrow } from '../icons/v0';
|
|||||||
import { SvgCheckmark } from '../icons/v1';
|
import { SvgCheckmark } from '../icons/v1';
|
||||||
import { styles, theme } from '../style';
|
import { styles, theme } from '../style';
|
||||||
|
|
||||||
import { Button } from './common/Button2';
|
import { ButtonWithLoading } from './common/Button2';
|
||||||
import { Input } from './common/Input';
|
import { Input } from './common/Input';
|
||||||
import { Menu, type MenuItem } from './common/Menu';
|
import { Menu, type MenuItem } from './common/Menu';
|
||||||
import { Popover } from './common/Popover';
|
import { Popover } from './common/Popover';
|
||||||
@@ -815,6 +815,7 @@ type SelectedItemsButtonProps<Name extends string> = {
|
|||||||
name: ((count: number) => string) | string;
|
name: ((count: number) => string) | string;
|
||||||
items: MenuItem<Name>[];
|
items: MenuItem<Name>[];
|
||||||
onSelect: (name: Name, items: string[]) => void;
|
onSelect: (name: Name, items: string[]) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SelectedItemsButton<Name extends string>({
|
export function SelectedItemsButton<Name extends string>({
|
||||||
@@ -822,6 +823,7 @@ export function SelectedItemsButton<Name extends string>({
|
|||||||
name,
|
name,
|
||||||
items,
|
items,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
isLoading = false,
|
||||||
}: SelectedItemsButtonProps<Name>) {
|
}: SelectedItemsButtonProps<Name>) {
|
||||||
const selectedItems = useSelectedItems();
|
const selectedItems = useSelectedItems();
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
@@ -836,7 +838,8 @@ export function SelectedItemsButton<Name extends string>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginLeft: 10, flexShrink: 0 }}>
|
<View style={{ marginLeft: 10, flexShrink: 0 }}>
|
||||||
<Button
|
<ButtonWithLoading
|
||||||
|
isLoading={isLoading}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
variant="bare"
|
variant="bare"
|
||||||
style={{ color: theme.pageTextPositive }}
|
style={{ color: theme.pageTextPositive }}
|
||||||
@@ -849,7 +852,7 @@ export function SelectedItemsButton<Name extends string>({
|
|||||||
style={{ marginRight: 5, color: theme.pageText }}
|
style={{ marginRight: 5, color: theme.pageText }}
|
||||||
/>
|
/>
|
||||||
{buttonLabel}
|
{buttonLabel}
|
||||||
</Button>
|
</ButtonWithLoading>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
triggerRef={triggerRef}
|
triggerRef={triggerRef}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type SelectedTransactionsButtonProps = {
|
|||||||
showMakeTransfer: boolean;
|
showMakeTransfer: boolean;
|
||||||
onMakeAsSplitTransaction: (selectedIds: string[]) => void;
|
onMakeAsSplitTransaction: (selectedIds: string[]) => void;
|
||||||
onMakeAsNonSplitTransactions: (selectedIds: string[]) => void;
|
onMakeAsNonSplitTransactions: (selectedIds: string[]) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SelectedTransactionsButton({
|
export function SelectedTransactionsButton({
|
||||||
@@ -55,6 +56,7 @@ export function SelectedTransactionsButton({
|
|||||||
showMakeTransfer,
|
showMakeTransfer,
|
||||||
onMakeAsSplitTransaction,
|
onMakeAsSplitTransaction,
|
||||||
onMakeAsNonSplitTransactions,
|
onMakeAsNonSplitTransactions,
|
||||||
|
isLoading = false,
|
||||||
}: SelectedTransactionsButtonProps) {
|
}: SelectedTransactionsButtonProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -206,6 +208,7 @@ export function SelectedTransactionsButton({
|
|||||||
return (
|
return (
|
||||||
<SelectedItemsButton
|
<SelectedItemsButton
|
||||||
id="transactions"
|
id="transactions"
|
||||||
|
isLoading={isLoading}
|
||||||
name={count => t('{{count}} transactions', { count })}
|
name={count => t('{{count}} transactions', { count })}
|
||||||
// @ts-expect-error fix me
|
// @ts-expect-error fix me
|
||||||
items={[
|
items={[
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ async function saveDiffAndApply(diff, changes, onChange) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TransactionList({
|
export function TransactionList({
|
||||||
|
isLoading = false,
|
||||||
tableRef,
|
tableRef,
|
||||||
transactions,
|
transactions,
|
||||||
allTransactions,
|
allTransactions,
|
||||||
@@ -71,14 +72,12 @@ export function TransactionList({
|
|||||||
showReconciled,
|
showReconciled,
|
||||||
showCleared,
|
showCleared,
|
||||||
showAccount,
|
showAccount,
|
||||||
headerContent,
|
|
||||||
isAdding,
|
isAdding,
|
||||||
isNew,
|
isNew,
|
||||||
isMatched,
|
isMatched,
|
||||||
isFiltered,
|
isFiltered,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
hideFraction,
|
hideFraction,
|
||||||
addNotification,
|
|
||||||
renderEmpty,
|
renderEmpty,
|
||||||
onSort,
|
onSort,
|
||||||
sortField,
|
sortField,
|
||||||
@@ -110,7 +109,6 @@ export function TransactionList({
|
|||||||
newTransactions = realizeTempTransactions(newTransactions);
|
newTransactions = realizeTempTransactions(newTransactions);
|
||||||
|
|
||||||
await saveDiff({ added: newTransactions });
|
await saveDiff({ added: newTransactions });
|
||||||
onRefetch();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSave = useCallback(async transaction => {
|
const onSave = useCallback(async transaction => {
|
||||||
@@ -208,6 +206,7 @@ export function TransactionList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TransactionTable
|
<TransactionTable
|
||||||
|
isLoading={isLoading}
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
transactions={allTransactions}
|
transactions={allTransactions}
|
||||||
loadMoreTransactions={loadMoreTransactions}
|
loadMoreTransactions={loadMoreTransactions}
|
||||||
@@ -228,8 +227,6 @@ export function TransactionList({
|
|||||||
isFiltered={isFiltered}
|
isFiltered={isFiltered}
|
||||||
dateFormat={dateFormat}
|
dateFormat={dateFormat}
|
||||||
hideFraction={hideFraction}
|
hideFraction={hideFraction}
|
||||||
addNotification={addNotification}
|
|
||||||
headerContent={headerContent}
|
|
||||||
renderEmpty={renderEmpty}
|
renderEmpty={renderEmpty}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onApplyRules={onApplyRules}
|
onApplyRules={onApplyRules}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
createElement,
|
createElement,
|
||||||
createRef,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
useState,
|
useState,
|
||||||
@@ -20,7 +19,7 @@ import {
|
|||||||
isValid as isDateValid,
|
isValid as isDateValid,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
|
|
||||||
import { pushModal } from 'loot-core/client/actions';
|
import { addNotification, pushModal } from 'loot-core/client/actions';
|
||||||
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||||
import {
|
import {
|
||||||
getAccountsById,
|
getAccountsById,
|
||||||
@@ -1831,6 +1830,7 @@ function NewTransaction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TransactionTableInner({
|
function TransactionTableInner({
|
||||||
|
isLoading,
|
||||||
tableNavigator,
|
tableNavigator,
|
||||||
tableRef,
|
tableRef,
|
||||||
listContainerRef,
|
listContainerRef,
|
||||||
@@ -1840,7 +1840,7 @@ function TransactionTableInner({
|
|||||||
onScroll,
|
onScroll,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const containerRef = createRef();
|
const containerRef = useRef();
|
||||||
const isAddingPrev = usePrevious(props.isAdding);
|
const isAddingPrev = usePrevious(props.isAdding);
|
||||||
const [scrollWidth, setScrollWidth] = useState(0);
|
const [scrollWidth, setScrollWidth] = useState(0);
|
||||||
|
|
||||||
@@ -2070,6 +2070,7 @@ function TransactionTableInner({
|
|||||||
data-testid="transaction-table"
|
data-testid="transaction-table"
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
|
loading={isLoading}
|
||||||
navigator={tableNavigator}
|
navigator={tableNavigator}
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
listContainerRef={listContainerRef}
|
listContainerRef={listContainerRef}
|
||||||
@@ -2103,6 +2104,7 @@ function TransactionTableInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TransactionTable = forwardRef((props, ref) => {
|
export const TransactionTable = forwardRef((props, ref) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const [newTransactions, setNewTransactions] = useState(null);
|
const [newTransactions, setNewTransactions] = useState(null);
|
||||||
const [prevIsAdding, setPrevIsAdding] = useState(false);
|
const [prevIsAdding, setPrevIsAdding] = useState(false);
|
||||||
const splitsExpanded = useSplitsExpanded();
|
const splitsExpanded = useSplitsExpanded();
|
||||||
@@ -2235,10 +2237,12 @@ export const TransactionTable = forwardRef((props, ref) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldAdd.current) {
|
if (shouldAdd.current) {
|
||||||
if (newTransactions[0].account == null) {
|
if (newTransactions[0].account == null) {
|
||||||
props.addNotification({
|
dispatch(
|
||||||
type: 'error',
|
addNotification({
|
||||||
message: 'Account is a required field',
|
type: 'error',
|
||||||
});
|
message: 'Account is a required field',
|
||||||
|
}),
|
||||||
|
);
|
||||||
newNavigator.onEdit('temp', 'account');
|
newNavigator.onEdit('temp', 'account');
|
||||||
} else {
|
} else {
|
||||||
const transactions = latestState.current.newTransactions;
|
const transactions = latestState.current.newTransactions;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { integerToCurrency } from 'loot-core/src/shared/util';
|
import { integerToCurrency } from 'loot-core/src/shared/util';
|
||||||
|
|
||||||
import { AuthProvider } from '../../auth/AuthProvider';
|
import { AuthProvider } from '../../auth/AuthProvider';
|
||||||
import { SelectedProviderWithItems } from '../../hooks/useSelected';
|
import { SelectedProvider, useSelected } from '../../hooks/useSelected';
|
||||||
import { SplitsExpandedProvider } from '../../hooks/useSplitsExpanded';
|
import { SplitsExpandedProvider } from '../../hooks/useSplitsExpanded';
|
||||||
import { TestProvider } from '../../redux/mock';
|
import { TestProvider } from '../../redux/mock';
|
||||||
import { ResponsiveProvider } from '../responsive/ResponsiveProvider';
|
import { ResponsiveProvider } from '../responsive/ResponsiveProvider';
|
||||||
@@ -110,6 +110,20 @@ function generateTransactions(count, splitAtIndexes = [], showError = false) {
|
|||||||
return transactions;
|
return transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is needed because useSelected needs redux access.
|
||||||
|
// This provider needs to be a child of the redux TestProvider.
|
||||||
|
function TestSelectedProvider({ transactions, children }) {
|
||||||
|
const selectedInst = useSelected('transactions', transactions);
|
||||||
|
return (
|
||||||
|
<SelectedProvider
|
||||||
|
instance={selectedInst}
|
||||||
|
fetchAllIds={() => transactions.map(t => t.id)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectedProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function LiveTransactionTable(props) {
|
function LiveTransactionTable(props) {
|
||||||
const [transactions, setTransactions] = useState(props.transactions);
|
const [transactions, setTransactions] = useState(props.transactions);
|
||||||
|
|
||||||
@@ -152,11 +166,7 @@ function LiveTransactionTable(props) {
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SpreadsheetProvider>
|
<SpreadsheetProvider>
|
||||||
<SchedulesProvider>
|
<SchedulesProvider>
|
||||||
<SelectedProviderWithItems
|
<TestSelectedProvider transactions={transactions}>
|
||||||
name="transactions"
|
|
||||||
items={transactions}
|
|
||||||
fetchAllIds={() => transactions.map(t => t.id)}
|
|
||||||
>
|
|
||||||
<SplitsExpandedProvider>
|
<SplitsExpandedProvider>
|
||||||
<TransactionTable
|
<TransactionTable
|
||||||
{...props}
|
{...props}
|
||||||
@@ -164,17 +174,14 @@ function LiveTransactionTable(props) {
|
|||||||
loadMoreTransactions={() => {}}
|
loadMoreTransactions={() => {}}
|
||||||
commonPayees={[]}
|
commonPayees={[]}
|
||||||
payees={payees}
|
payees={payees}
|
||||||
addNotification={n => console.log(n)}
|
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onSplit={onSplit}
|
onSplit={onSplit}
|
||||||
onAdd={onAdd}
|
onAdd={onAdd}
|
||||||
onAddSplit={onAddSplit}
|
onAddSplit={onAddSplit}
|
||||||
onCreatePayee={onCreatePayee}
|
onCreatePayee={onCreatePayee}
|
||||||
showSelection={true}
|
|
||||||
allowSplitTransaction={true}
|
|
||||||
/>
|
/>
|
||||||
</SplitsExpandedProvider>
|
</SplitsExpandedProvider>
|
||||||
</SelectedProviderWithItems>
|
</TestSelectedProvider>
|
||||||
</SchedulesProvider>
|
</SchedulesProvider>
|
||||||
</SpreadsheetProvider>
|
</SpreadsheetProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type BoundActions = {
|
|||||||
* @deprecated please use actions directly with `useAppDispatch`
|
* @deprecated please use actions directly with `useAppDispatch`
|
||||||
* @see https://github.com/reduxjs/react-redux/issues/1252#issuecomment-488160930
|
* @see https://github.com/reduxjs/react-redux/issues/1252#issuecomment-488160930
|
||||||
**/
|
**/
|
||||||
|
|
||||||
export function useActions() {
|
export function useActions() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
|||||||
@@ -4,19 +4,22 @@ import { type RuleConditionEntity } from 'loot-core/types/models/rule';
|
|||||||
|
|
||||||
export function useFilters<T extends RuleConditionEntity>(
|
export function useFilters<T extends RuleConditionEntity>(
|
||||||
initialConditions: T[] = [],
|
initialConditions: T[] = [],
|
||||||
initialConditionsOp: 'and' | 'or' = 'and',
|
initialConditionsOp: RuleConditionEntity['conditionsOp'] = 'and',
|
||||||
) {
|
) {
|
||||||
const [conditions, setConditions] = useState<T[]>(initialConditions);
|
const [conditions, setConditions] = useState<T[]>(initialConditions);
|
||||||
const [conditionsOp, setConditionsOp] = useState<'and' | 'or'>(
|
const [conditionsOp, setConditionsOp] =
|
||||||
initialConditionsOp,
|
useState<RuleConditionEntity['conditionsOp']>(initialConditionsOp);
|
||||||
);
|
|
||||||
const [saved, setSaved] = useState<T[] | null>(null);
|
const [saved, setSaved] = useState<T[] | null>(null);
|
||||||
|
|
||||||
const onApply = useCallback(
|
const onApply = useCallback(
|
||||||
(
|
(
|
||||||
conditionsOrSavedFilter:
|
conditionsOrSavedFilter:
|
||||||
| null
|
| null
|
||||||
| { conditions: T[]; conditionsOp: 'and' | 'or'; id: T[] | null }
|
| {
|
||||||
|
conditions: T[];
|
||||||
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
|
id: T[] | null;
|
||||||
|
}
|
||||||
| T,
|
| T,
|
||||||
) => {
|
) => {
|
||||||
if (conditionsOrSavedFilter === null) {
|
if (conditionsOrSavedFilter === null) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
type Dispatch,
|
|
||||||
type ReactElement,
|
type ReactElement,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
@@ -47,12 +46,12 @@ type SelectAllAction = {
|
|||||||
isRangeSelect?: boolean;
|
isRangeSelect?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Actions = SelectAction | SelectNoneAction | SelectAllAction;
|
type Actions = SelectAction | SelectNoneAction | SelectAllAction;
|
||||||
|
|
||||||
export function useSelected<T extends Item>(
|
export function useSelected<T extends Item>(
|
||||||
name: string,
|
name: string,
|
||||||
items: T[],
|
items: readonly T[],
|
||||||
initialSelectedIds: string[],
|
initialSelectedIds: readonly string[],
|
||||||
selectAllFilter?: (item: T) => boolean,
|
selectAllFilter?: (item: T) => boolean,
|
||||||
) {
|
) {
|
||||||
const [state, dispatch] = useReducer(
|
const [state, dispatch] = useReducer(
|
||||||
@@ -304,42 +303,3 @@ export function SelectedProvider<T extends Item>({
|
|||||||
</SelectedItems.Provider>
|
</SelectedItems.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectedProviderWithItemsProps<T extends Item> = {
|
|
||||||
name: string;
|
|
||||||
items: T[];
|
|
||||||
initialSelectedIds?: string[];
|
|
||||||
fetchAllIds: () => Promise<string[]>;
|
|
||||||
registerDispatch?: (dispatch: Dispatch<Actions>) => void;
|
|
||||||
selectAllFilter?: (item: T) => boolean;
|
|
||||||
children: ReactElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This can be helpful in class components if you cannot use the
|
|
||||||
// custom hook
|
|
||||||
export function SelectedProviderWithItems<T extends Item>({
|
|
||||||
name,
|
|
||||||
items,
|
|
||||||
initialSelectedIds = [],
|
|
||||||
fetchAllIds,
|
|
||||||
registerDispatch,
|
|
||||||
selectAllFilter,
|
|
||||||
children,
|
|
||||||
}: SelectedProviderWithItemsProps<T>) {
|
|
||||||
const selected = useSelected<T>(
|
|
||||||
name,
|
|
||||||
items,
|
|
||||||
initialSelectedIds,
|
|
||||||
selectAllFilter,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
registerDispatch?.(selected.dispatch);
|
|
||||||
}, [registerDispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectedProvider<T> instance={selected} fetchAllIds={fetchAllIds}>
|
|
||||||
{children}
|
|
||||||
</SelectedProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function useSchedules({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(query !== null);
|
||||||
|
|
||||||
scheduleQueryRef.current = liveQuery<ScheduleEntity>(query, {
|
scheduleQueryRef.current = liveQuery<ScheduleEntity>(query, {
|
||||||
onData: async schedules => {
|
onData: async schedules => {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { type Query } from '../../shared/query';
|
|||||||
import { getScheduledAmount } from '../../shared/schedules';
|
import { getScheduledAmount } from '../../shared/schedules';
|
||||||
import { ungroupTransactions } from '../../shared/transactions';
|
import { ungroupTransactions } from '../../shared/transactions';
|
||||||
import {
|
import {
|
||||||
|
type TransactionFilterEntity,
|
||||||
|
type RuleConditionEntity,
|
||||||
type ScheduleEntity,
|
type ScheduleEntity,
|
||||||
type TransactionEntity,
|
type TransactionEntity,
|
||||||
} from '../../types/models';
|
} from '../../types/models';
|
||||||
@@ -233,20 +235,26 @@ export function useTransactionsSearch({
|
|||||||
}: UseTransactionsSearchProps): UseTransactionsSearchResult {
|
}: UseTransactionsSearchProps): UseTransactionsSearchResult {
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
const updateQueryRef = useRef(updateQuery);
|
||||||
|
updateQueryRef.current = updateQuery;
|
||||||
|
|
||||||
|
const resetQueryRef = useRef(resetQuery);
|
||||||
|
resetQueryRef.current = resetQuery;
|
||||||
|
|
||||||
const updateSearchQuery = useMemo(
|
const updateSearchQuery = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((searchText: string) => {
|
debounce((searchText: string) => {
|
||||||
if (searchText === '') {
|
if (searchText === '') {
|
||||||
resetQuery();
|
resetQueryRef.current?.();
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
} else if (searchText) {
|
} else if (searchText) {
|
||||||
updateQuery(previousQuery =>
|
updateQueryRef.current?.(previousQuery =>
|
||||||
queries.transactionsSearch(previousQuery, searchText, dateFormat),
|
queries.transactionsSearch(previousQuery, searchText, dateFormat),
|
||||||
);
|
);
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
}
|
}
|
||||||
}, delayMs),
|
}, delayMs),
|
||||||
[dateFormat, delayMs, resetQuery, updateQuery],
|
[dateFormat, delayMs],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -259,6 +267,206 @@ export function useTransactionsSearch({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UseTransactionsFilterProps = {
|
||||||
|
updateQuery?: (updateFn: (filterQuery: Query) => Query) => void;
|
||||||
|
resetQuery?: () => void;
|
||||||
|
initialConditions?: RuleConditionEntity[];
|
||||||
|
initialConditionsOp?: RuleConditionEntity['conditionsOp'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseTransactionsFilterResult = {
|
||||||
|
isFiltered: boolean;
|
||||||
|
activeFilter?: TransactionFilterEntity;
|
||||||
|
dirtyFilter?: TransactionFilterEntity;
|
||||||
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
|
updateConditionsOp: (op: RuleConditionEntity['conditionsOp']) => void;
|
||||||
|
conditions: readonly RuleConditionEntity[];
|
||||||
|
updateConditions: (
|
||||||
|
conditions:
|
||||||
|
| RuleConditionEntity[]
|
||||||
|
| ((conditions: RuleConditionEntity[]) => RuleConditionEntity[]),
|
||||||
|
) => void;
|
||||||
|
clear: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
applyFilter: (
|
||||||
|
savedFilter: TransactionFilterEntity,
|
||||||
|
clearConditions?: boolean,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTransactionsFilter({
|
||||||
|
updateQuery,
|
||||||
|
resetQuery,
|
||||||
|
initialConditions = [],
|
||||||
|
initialConditionsOp = 'and',
|
||||||
|
}: UseTransactionsFilterProps): UseTransactionsFilterResult {
|
||||||
|
const [isFiltered, setIsFiltered] = useState(false);
|
||||||
|
const [activeFilter, setActiveFilter] = useState<
|
||||||
|
TransactionFilterEntity | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [dirtyFilter, setDirtyFilter] = useState<
|
||||||
|
TransactionFilterEntity | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [conditions, setConditions] =
|
||||||
|
useState<RuleConditionEntity[]>(initialConditions);
|
||||||
|
const [conditionsOp, setConditionsOp] =
|
||||||
|
useState<RuleConditionEntity['conditionsOp']>(initialConditionsOp);
|
||||||
|
|
||||||
|
const updateQueryRef = useRef(updateQuery);
|
||||||
|
updateQueryRef.current = updateQuery;
|
||||||
|
|
||||||
|
const resetQueryRef = useRef(resetQuery);
|
||||||
|
resetQueryRef.current = resetQuery;
|
||||||
|
|
||||||
|
const updateQueryFilter = useCallback(
|
||||||
|
async (conditions: RuleConditionEntity[]) => {
|
||||||
|
const { filters } = await send('make-filters-from-conditions', {
|
||||||
|
conditions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
[conditionsOp === 'and' ? '$and' : '$or']: filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateQueryRef.current?.(previousQuery =>
|
||||||
|
previousQuery.unfilter().filter(filter),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[conditionsOp],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isUnmounted = false;
|
||||||
|
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
resetQueryRef.current?.();
|
||||||
|
setIsFiltered(false);
|
||||||
|
} else {
|
||||||
|
updateQueryFilter(conditions).then(() => {
|
||||||
|
if (!isUnmounted) {
|
||||||
|
setIsFiltered(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
isUnmounted = true;
|
||||||
|
};
|
||||||
|
}, [conditions, updateQueryFilter]);
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
setConditionsOp(initialConditionsOp);
|
||||||
|
setConditions([]);
|
||||||
|
setActiveFilter(undefined);
|
||||||
|
setDirtyFilter(undefined);
|
||||||
|
}, [initialConditionsOp]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setConditionsOp(initialConditionsOp);
|
||||||
|
setConditions(initialConditions);
|
||||||
|
setActiveFilter(undefined);
|
||||||
|
setDirtyFilter(undefined);
|
||||||
|
}, [initialConditions, initialConditionsOp]);
|
||||||
|
|
||||||
|
const applyFilter = useCallback(
|
||||||
|
(savedFilter: TransactionFilterEntity, clearConditions = false) => {
|
||||||
|
setActiveFilter(savedFilter);
|
||||||
|
setDirtyFilter(undefined);
|
||||||
|
setConditionsOp(savedFilter.conditionsOp);
|
||||||
|
if (clearConditions) {
|
||||||
|
setConditions(savedFilter.conditions);
|
||||||
|
} else {
|
||||||
|
setConditions(previousConditions => [
|
||||||
|
...previousConditions,
|
||||||
|
...savedFilter.conditions,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateConditionsOp = useCallback(
|
||||||
|
(op: RuleConditionEntity['conditionsOp'] = 'and') => {
|
||||||
|
setConditionsOp(op);
|
||||||
|
if (activeFilter && activeFilter.conditionsOp !== op) {
|
||||||
|
setDirtyFilter({ ...activeFilter, conditionsOp: op });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateActiveFilterIfNeeded = useCallback(
|
||||||
|
({
|
||||||
|
activeFilter,
|
||||||
|
conditions,
|
||||||
|
}: {
|
||||||
|
activeFilter?: TransactionFilterEntity;
|
||||||
|
conditions: RuleConditionEntity[];
|
||||||
|
}) => {
|
||||||
|
if (activeFilter) {
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
setActiveFilter(undefined);
|
||||||
|
setDirtyFilter(undefined);
|
||||||
|
} else if (activeFilter.conditions !== conditions) {
|
||||||
|
setDirtyFilter({ ...activeFilter, conditions });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateConditions = useCallback(
|
||||||
|
(conditions: Parameters<typeof setConditions>[0]) => {
|
||||||
|
setConditions(previousConditions => {
|
||||||
|
const maybeNewConditions =
|
||||||
|
typeof conditions === 'function'
|
||||||
|
? (conditions(previousConditions) ?? [])
|
||||||
|
: (conditions ?? []);
|
||||||
|
|
||||||
|
updateActiveFilterIfNeeded({
|
||||||
|
activeFilter,
|
||||||
|
conditions: maybeNewConditions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return maybeNewConditions;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[activeFilter, updateActiveFilterIfNeeded],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFiltered,
|
||||||
|
activeFilter,
|
||||||
|
dirtyFilter,
|
||||||
|
applyFilter,
|
||||||
|
conditionsOp,
|
||||||
|
updateConditionsOp,
|
||||||
|
conditions,
|
||||||
|
updateConditions,
|
||||||
|
clear,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransactionFilter = RuleConditionEntity | TransactionFilterEntity;
|
||||||
|
|
||||||
|
export function isTransactionFilterEntity(
|
||||||
|
filter: TransactionFilter,
|
||||||
|
): filter is TransactionFilterEntity {
|
||||||
|
return 'id' in filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFilterConditions(
|
||||||
|
filter: TransactionFilter,
|
||||||
|
): RuleConditionEntity[] {
|
||||||
|
if (isTransactionFilterEntity(filter)) {
|
||||||
|
// This is a saved transaction filter.
|
||||||
|
return filter.conditions;
|
||||||
|
} else {
|
||||||
|
// This is a rule condition.
|
||||||
|
return [filter];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isForPreview(schedule: ScheduleEntity, statuses: ScheduleStatuses) {
|
function isForPreview(schedule: ScheduleEntity, statuses: ScheduleStatuses) {
|
||||||
const status = statuses.get(schedule.id);
|
const status = statuses.get(schedule.id);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useEffect, type DependencyList } from 'react';
|
import { type DependencyList, useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { type Query } from '../shared/query';
|
import { type Query } from '../shared/query';
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type FieldInfoConstraint = Record<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const FIELD_INFO = {
|
const FIELD_INFO = {
|
||||||
|
id: { type: 'id' },
|
||||||
imported_payee: {
|
imported_payee: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
disallowedOps: new Set(['hasTags']),
|
disallowedOps: new Set(['hasTags']),
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ export function recalculateSplit(trans: TransactionEntity) {
|
|||||||
} as TransactionEntityWithError;
|
} as TransactionEntityWithError;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findParentIndex(transactions: TransactionEntity[], idx: number) {
|
function findParentIndex(
|
||||||
|
transactions: readonly TransactionEntity[],
|
||||||
|
idx: number,
|
||||||
|
) {
|
||||||
// This relies on transactions being sorted in a way where parents
|
// This relies on transactions being sorted in a way where parents
|
||||||
// are always before children, which is enforced in the db layer.
|
// are always before children, which is enforced in the db layer.
|
||||||
// Walk backwards and find the last parent;
|
// Walk backwards and find the last parent;
|
||||||
@@ -104,7 +107,10 @@ function findParentIndex(transactions: TransactionEntity[], idx: number) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSplit(transactions: TransactionEntity[], parentIndex: number) {
|
function getSplit(
|
||||||
|
transactions: readonly TransactionEntity[],
|
||||||
|
parentIndex: number,
|
||||||
|
) {
|
||||||
const split = [transactions[parentIndex]];
|
const split = [transactions[parentIndex]];
|
||||||
let curr = parentIndex + 1;
|
let curr = parentIndex + 1;
|
||||||
while (curr < transactions.length && transactions[curr].is_child) {
|
while (curr < transactions.length && transactions[curr].is_child) {
|
||||||
@@ -114,7 +120,9 @@ function getSplit(transactions: TransactionEntity[], parentIndex: number) {
|
|||||||
return split;
|
return split;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ungroupTransactions(transactions: TransactionEntity[]) {
|
export function ungroupTransactions(
|
||||||
|
transactions: readonly TransactionEntity[],
|
||||||
|
) {
|
||||||
return transactions.reduce<TransactionEntity[]>((list, parent) => {
|
return transactions.reduce<TransactionEntity[]>((list, parent) => {
|
||||||
const { subtransactions, ...trans } = parent;
|
const { subtransactions, ...trans } = parent;
|
||||||
const _subtransactions = subtransactions || [];
|
const _subtransactions = subtransactions || [];
|
||||||
@@ -128,7 +136,7 @@ export function ungroupTransactions(transactions: TransactionEntity[]) {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupTransaction(split: TransactionEntity[]) {
|
export function groupTransaction(split: readonly TransactionEntity[]) {
|
||||||
return { ...split[0], subtransactions: split.slice(1) } as TransactionEntity;
|
return { ...split[0], subtransactions: split.slice(1) } as TransactionEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +160,7 @@ export function applyTransactionDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function replaceTransactions(
|
function replaceTransactions(
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
id: string,
|
id: string,
|
||||||
func: (
|
func: (
|
||||||
transaction: TransactionEntity,
|
transaction: TransactionEntity,
|
||||||
@@ -218,7 +226,7 @@ function replaceTransactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addSplitTransaction(
|
export function addSplitTransaction(
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
id: string,
|
id: string,
|
||||||
) {
|
) {
|
||||||
return replaceTransactions(transactions, id, trans => {
|
return replaceTransactions(transactions, id, trans => {
|
||||||
@@ -237,7 +245,7 @@ export function addSplitTransaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateTransaction(
|
export function updateTransaction(
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
transaction: TransactionEntity,
|
transaction: TransactionEntity,
|
||||||
) {
|
) {
|
||||||
return replaceTransactions(transactions, transaction.id, trans => {
|
return replaceTransactions(transactions, transaction.id, trans => {
|
||||||
@@ -270,7 +278,7 @@ export function updateTransaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTransaction(
|
export function deleteTransaction(
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
id: string,
|
id: string,
|
||||||
) {
|
) {
|
||||||
return replaceTransactions(transactions, id, trans => {
|
return replaceTransactions(transactions, id, trans => {
|
||||||
@@ -295,7 +303,7 @@ export function deleteTransaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function splitTransaction(
|
export function splitTransaction(
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
id: string,
|
id: string,
|
||||||
createSubtransactions?: (
|
createSubtransactions?: (
|
||||||
parentTransaction: TransactionEntity,
|
parentTransaction: TransactionEntity,
|
||||||
@@ -323,7 +331,7 @@ export function splitTransaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function realizeTempTransactions(
|
export function realizeTempTransactions(
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
): TransactionEntity[] {
|
): TransactionEntity[] {
|
||||||
const parent = {
|
const parent = {
|
||||||
...transactions.find(t => !t.is_child),
|
...transactions.find(t => !t.is_child),
|
||||||
@@ -344,8 +352,8 @@ export function realizeTempTransactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function makeAsNonChildTransactions(
|
export function makeAsNonChildTransactions(
|
||||||
childTransactionsToUpdate: TransactionEntity[],
|
childTransactionsToUpdate: readonly TransactionEntity[],
|
||||||
transactions: TransactionEntity[],
|
transactions: readonly TransactionEntity[],
|
||||||
) {
|
) {
|
||||||
const [parentTransaction, ...childTransactions] = transactions;
|
const [parentTransaction, ...childTransactions] = transactions;
|
||||||
const newNonChildTransactions = childTransactionsToUpdate.map(t =>
|
const newNonChildTransactions = childTransactionsToUpdate.map(t =>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type NetWorthWidget = AbstractWidget<
|
|||||||
{
|
{
|
||||||
name?: string;
|
name?: string;
|
||||||
conditions?: RuleConditionEntity[];
|
conditions?: RuleConditionEntity[];
|
||||||
conditionsOp?: 'and' | 'or';
|
conditionsOp?: RuleConditionEntity['conditionsOp'];
|
||||||
timeFrame?: TimeFrame;
|
timeFrame?: TimeFrame;
|
||||||
} | null
|
} | null
|
||||||
>;
|
>;
|
||||||
@@ -35,7 +35,7 @@ export type CashFlowWidget = AbstractWidget<
|
|||||||
{
|
{
|
||||||
name?: string;
|
name?: string;
|
||||||
conditions?: RuleConditionEntity[];
|
conditions?: RuleConditionEntity[];
|
||||||
conditionsOp?: 'and' | 'or';
|
conditionsOp?: RuleConditionEntity['conditionsOp'];
|
||||||
timeFrame?: TimeFrame;
|
timeFrame?: TimeFrame;
|
||||||
showBalance?: boolean;
|
showBalance?: boolean;
|
||||||
} | null
|
} | null
|
||||||
@@ -45,7 +45,7 @@ export type SpendingWidget = AbstractWidget<
|
|||||||
{
|
{
|
||||||
name?: string;
|
name?: string;
|
||||||
conditions?: RuleConditionEntity[];
|
conditions?: RuleConditionEntity[];
|
||||||
conditionsOp?: 'and' | 'or';
|
conditionsOp?: RuleConditionEntity['conditionsOp'];
|
||||||
compare?: string;
|
compare?: string;
|
||||||
compareTo?: string;
|
compareTo?: string;
|
||||||
isLive?: boolean;
|
isLive?: boolean;
|
||||||
@@ -96,7 +96,7 @@ export type SummaryWidget = AbstractWidget<
|
|||||||
{
|
{
|
||||||
name?: string;
|
name?: string;
|
||||||
conditions?: RuleConditionEntity[];
|
conditions?: RuleConditionEntity[];
|
||||||
conditionsOp?: 'and' | 'or';
|
conditionsOp?: RuleConditionEntity['conditionsOp'];
|
||||||
timeFrame?: TimeFrame;
|
timeFrame?: TimeFrame;
|
||||||
content?: string;
|
content?: string;
|
||||||
} | null
|
} | null
|
||||||
@@ -110,7 +110,7 @@ export type BaseSummaryContent = {
|
|||||||
export type PercentageSummaryContent = {
|
export type PercentageSummaryContent = {
|
||||||
type: 'percentage';
|
type: 'percentage';
|
||||||
divisorConditions: RuleConditionEntity[];
|
divisorConditions: RuleConditionEntity[];
|
||||||
divisorConditionsOp: 'and' | 'or';
|
divisorConditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
divisorAllTimeDateRange?: boolean;
|
divisorAllTimeDateRange?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface CustomReportEntity {
|
|||||||
showUncategorized: boolean;
|
showUncategorized: boolean;
|
||||||
graphType: string;
|
graphType: string;
|
||||||
conditions?: RuleConditionEntity[];
|
conditions?: RuleConditionEntity[];
|
||||||
conditionsOp: 'and' | 'or';
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
data?: GroupedEntity;
|
data?: GroupedEntity;
|
||||||
tombstone?: boolean;
|
tombstone?: boolean;
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ export interface CustomReportData {
|
|||||||
show_uncategorized: number;
|
show_uncategorized: number;
|
||||||
graph_type: string;
|
graph_type: string;
|
||||||
conditions?: RuleConditionEntity[];
|
conditions?: RuleConditionEntity[];
|
||||||
conditions_op: 'and' | 'or';
|
conditions_op: RuleConditionEntity['conditionsOp'];
|
||||||
metadata?: GroupedEntity;
|
metadata?: GroupedEntity;
|
||||||
interval: string;
|
interval: string;
|
||||||
color_scheme?: string;
|
color_scheme?: string;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type RuleConditionOp =
|
|||||||
| 'offBudget';
|
| 'offBudget';
|
||||||
|
|
||||||
export type FieldValueTypes = {
|
export type FieldValueTypes = {
|
||||||
|
id: string;
|
||||||
account: string;
|
account: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -64,13 +65,13 @@ type BaseConditionEntity<
|
|||||||
month?: boolean;
|
month?: boolean;
|
||||||
year?: boolean;
|
year?: boolean;
|
||||||
};
|
};
|
||||||
conditionsOp?: string;
|
conditionsOp?: RuleConditionEntity['conditionsOp'];
|
||||||
type?: 'id' | 'boolean' | 'date' | 'number' | 'string';
|
type?: 'id' | 'boolean' | 'date' | 'number' | 'string';
|
||||||
customName?: string;
|
customName?: string;
|
||||||
queryFilter?: Record<string, { $oneof: string[] }>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RuleConditionEntity =
|
export type RuleConditionEntity =
|
||||||
|
| BaseConditionEntity<'id', 'oneOf'>
|
||||||
| BaseConditionEntity<
|
| BaseConditionEntity<
|
||||||
'account',
|
'account',
|
||||||
| 'is'
|
| 'is'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type RuleConditionEntity } from './rule';
|
|||||||
export interface TransactionFilterEntity {
|
export interface TransactionFilterEntity {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
conditionsOp: 'and' | 'or';
|
conditionsOp: RuleConditionEntity['conditionsOp'];
|
||||||
conditions: RuleConditionEntity[];
|
conditions: RuleConditionEntity[];
|
||||||
tombstone: boolean;
|
tombstone: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
6
upcoming-release-notes/3708.md
Normal file
6
upcoming-release-notes/3708.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
category: Maintenance
|
||||||
|
authors: [joel-jeremy]
|
||||||
|
---
|
||||||
|
|
||||||
|
Convert Account component to a functional component and use useTransactions hook.
|
||||||
Reference in New Issue
Block a user