mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-29 19:14:22 -05:00
Compare commits
11 Commits
cursor/ana
...
v25.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7846d2e787 | ||
|
|
ca6d80461a | ||
|
|
fa14cbb697 | ||
|
|
1210a74b4a | ||
|
|
534c1e6680 | ||
|
|
14d436712a | ||
|
|
e9f3925124 | ||
|
|
f28229be99 | ||
|
|
1fc922c672 | ||
|
|
c712217a7c | ||
|
|
3559b2df3a |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description:
|
||||
description:
|
||||
globs: *.ts,*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
@@ -21,7 +21,7 @@ Naming Conventions
|
||||
|
||||
TypeScript Usage
|
||||
|
||||
- Use TypeScript for all code; prefer interfaces over types.
|
||||
- Use TypeScript for all code; prefer types over interfaces.
|
||||
- Avoid enums; use objects or maps instead.
|
||||
- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead.
|
||||
- Avoid type assertions with `as` or `!`; prefer using `satisfies`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.9.0",
|
||||
"version": "25.10.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
|
||||
@@ -154,4 +154,10 @@ export const styles: Record<string, any> = {
|
||||
borderRadius: 4,
|
||||
padding: '3px 5px',
|
||||
},
|
||||
mobileListItem: {
|
||||
borderBottom: `1px solid ${theme.tableBorder}`,
|
||||
backgroundColor: theme.tableBackground,
|
||||
padding: 16,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "25.9.0",
|
||||
"version": "25.10.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
|
||||
@@ -22,7 +22,7 @@ import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
|
||||
import { LoadingIndicator } from '@desktop-client/components/reports/LoadingIndicator';
|
||||
import { useLocale } from '@desktop-client/hooks/useLocale';
|
||||
import * as query from '@desktop-client/queries';
|
||||
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
|
||||
import { liveQuery } from '@desktop-client/queries/liveQuery';
|
||||
|
||||
const LABEL_WIDTH = 70;
|
||||
|
||||
@@ -32,11 +32,6 @@ type BalanceHistoryGraphProps = {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
};
|
||||
|
||||
type Balance = {
|
||||
date: string;
|
||||
balance: number;
|
||||
};
|
||||
|
||||
export function BalanceHistoryGraph({
|
||||
accountId,
|
||||
style,
|
||||
@@ -51,6 +46,11 @@ export function BalanceHistoryGraph({
|
||||
date: string;
|
||||
balance: number;
|
||||
} | null>(null);
|
||||
const [startingBalance, setStartingBalance] = useState<number | null>(null);
|
||||
const [monthlyTotals, setMonthlyTotals] = useState<Array<{
|
||||
date: string;
|
||||
balance: number;
|
||||
}> | null>(null);
|
||||
|
||||
const percentageChange = useMemo(() => {
|
||||
if (balanceData.length < 2) return 0;
|
||||
@@ -65,7 +65,70 @@ export function BalanceHistoryGraph({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchBalanceHistory() {
|
||||
// Reset state when accountId changes
|
||||
setStartingBalance(null);
|
||||
setMonthlyTotals(null);
|
||||
setLoading(true);
|
||||
|
||||
const endDate = new Date();
|
||||
const startDate = subMonths(endDate, 12);
|
||||
|
||||
const startingBalanceQuery = query
|
||||
.transactions(accountId)
|
||||
.filter({
|
||||
date: { $lt: monthUtils.firstDayOfMonth(startDate) },
|
||||
})
|
||||
.calculate({ $sum: '$amount' });
|
||||
const monthlyTotalsQuery = query
|
||||
.transactions(accountId)
|
||||
.filter({
|
||||
$and: [
|
||||
{ date: { $gte: monthUtils.firstDayOfMonth(startDate) } },
|
||||
{ date: { $lte: monthUtils.lastDayOfMonth(endDate) } },
|
||||
],
|
||||
})
|
||||
.groupBy({ $month: '$date' })
|
||||
.select([{ date: { $month: '$date' } }, { amount: { $sum: '$amount' } }]);
|
||||
|
||||
const startingBalanceLive: ReturnType<typeof liveQuery<number>> = liveQuery(
|
||||
startingBalanceQuery,
|
||||
{
|
||||
onData: (data: number[]) => {
|
||||
setStartingBalance(data[0] || 0);
|
||||
},
|
||||
onError: error => {
|
||||
console.error('Error fetching starting balance:', error);
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const monthlyTotalsLive: ReturnType<
|
||||
typeof liveQuery<{ date: string; amount: number }>
|
||||
> = liveQuery(monthlyTotalsQuery, {
|
||||
onData: (data: Array<{ date: string; amount: number }>) => {
|
||||
setMonthlyTotals(
|
||||
data.map(d => ({
|
||||
date: d.date,
|
||||
balance: d.amount,
|
||||
})),
|
||||
);
|
||||
},
|
||||
onError: error => {
|
||||
console.error('Error fetching monthly totals:', error);
|
||||
setLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
startingBalanceLive?.unsubscribe();
|
||||
monthlyTotalsLive?.unsubscribe();
|
||||
};
|
||||
}, [accountId, locale]);
|
||||
|
||||
// Process data when both startingBalance and monthlyTotals are available
|
||||
useEffect(() => {
|
||||
if (startingBalance !== null && monthlyTotals !== null) {
|
||||
const endDate = new Date();
|
||||
const startDate = subMonths(endDate, 12);
|
||||
const months = eachMonthOfInterval({
|
||||
@@ -73,99 +136,70 @@ export function BalanceHistoryGraph({
|
||||
end: endDate,
|
||||
}).map(m => format(m, 'yyyy-MM'));
|
||||
|
||||
const [starting, totals]: [number, Balance[]] = await Promise.all([
|
||||
aqlQuery(
|
||||
query
|
||||
.transactions(accountId)
|
||||
.filter({
|
||||
date: { $lt: monthUtils.firstDayOfMonth(startDate) },
|
||||
})
|
||||
.calculate({ $sum: '$amount' }),
|
||||
).then(({ data }) => data),
|
||||
|
||||
aqlQuery(
|
||||
query
|
||||
.transactions(accountId)
|
||||
.filter({
|
||||
$and: [
|
||||
{ date: { $gte: monthUtils.firstDayOfMonth(startDate) } },
|
||||
{ date: { $lte: monthUtils.lastDayOfMonth(endDate) } },
|
||||
],
|
||||
})
|
||||
.groupBy({ $month: '$date' })
|
||||
.select([
|
||||
{ date: { $month: '$date' } },
|
||||
{ amount: { $sum: '$amount' } },
|
||||
]),
|
||||
).then(({ data }) =>
|
||||
data.map((d: { date: string; amount: number }) => {
|
||||
return {
|
||||
date: d.date,
|
||||
balance: d.amount,
|
||||
};
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
// calculate balances from sum of transactions
|
||||
let currentBalance = starting;
|
||||
totals.reverse().forEach(month => {
|
||||
currentBalance = currentBalance + month.balance;
|
||||
month.balance = currentBalance;
|
||||
});
|
||||
|
||||
// if the account doesn't have recent transactions
|
||||
// then the empty months will be missing from our data
|
||||
// so add in entries for those here
|
||||
if (totals.length === 0) {
|
||||
//handle case of no transactions in the last year
|
||||
months.forEach(expectedMonth =>
|
||||
totals.push({
|
||||
date: expectedMonth,
|
||||
balance: starting,
|
||||
}),
|
||||
);
|
||||
} else if (totals.length < months.length) {
|
||||
// iterate through each array together and add in missing data
|
||||
let totalsIndex = 0;
|
||||
let mostRecent = starting;
|
||||
months.forEach(expectedMonth => {
|
||||
if (totalsIndex > totals.length - 1) {
|
||||
// fill in the data at the end of the window
|
||||
totals.push({
|
||||
date: expectedMonth,
|
||||
balance: mostRecent,
|
||||
});
|
||||
} else if (totals[totalsIndex].date === expectedMonth) {
|
||||
// a matched month
|
||||
mostRecent = totals[totalsIndex].balance;
|
||||
totalsIndex += 1;
|
||||
} else {
|
||||
// a missing month in the middle
|
||||
totals.push({
|
||||
date: expectedMonth,
|
||||
balance: mostRecent,
|
||||
});
|
||||
}
|
||||
function processData(
|
||||
startingBalanceValue: number,
|
||||
monthlyTotalsValue: Array<{ date: string; balance: number }>,
|
||||
) {
|
||||
let currentBalance = startingBalanceValue;
|
||||
const totals = [...monthlyTotalsValue];
|
||||
totals.reverse().forEach(month => {
|
||||
currentBalance = currentBalance + month.balance;
|
||||
month.balance = currentBalance;
|
||||
});
|
||||
|
||||
// if the account doesn't have recent transactions
|
||||
// then the empty months will be missing from our data
|
||||
// so add in entries for those here
|
||||
if (totals.length === 0) {
|
||||
//handle case of no transactions in the last year
|
||||
months.forEach(expectedMonth =>
|
||||
totals.push({
|
||||
date: expectedMonth,
|
||||
balance: startingBalanceValue,
|
||||
}),
|
||||
);
|
||||
} else if (totals.length < months.length) {
|
||||
// iterate through each array together and add in missing data
|
||||
let totalsIndex = 0;
|
||||
let mostRecent = startingBalanceValue;
|
||||
months.forEach(expectedMonth => {
|
||||
if (totalsIndex > totals.length - 1) {
|
||||
// fill in the data at the end of the window
|
||||
totals.push({
|
||||
date: expectedMonth,
|
||||
balance: mostRecent,
|
||||
});
|
||||
} else if (totals[totalsIndex].date === expectedMonth) {
|
||||
// a matched month
|
||||
mostRecent = totals[totalsIndex].balance;
|
||||
totalsIndex += 1;
|
||||
} else {
|
||||
// a missing month in the middle
|
||||
totals.push({
|
||||
date: expectedMonth,
|
||||
balance: mostRecent,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const balances = totals
|
||||
.sort((a, b) => monthUtils.differenceInCalendarMonths(a.date, b.date))
|
||||
.map(t => {
|
||||
return {
|
||||
balance: t.balance,
|
||||
date: monthUtils.format(t.date, 'MMM yyyy', locale),
|
||||
};
|
||||
});
|
||||
|
||||
setBalanceData(balances);
|
||||
setHoveredValue(balances[balances.length - 1]);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const balances = totals
|
||||
.sort((a, b) => monthUtils.differenceInCalendarMonths(a.date, b.date))
|
||||
.map(t => {
|
||||
return {
|
||||
balance: t.balance,
|
||||
date: monthUtils.format(t.date, 'MMM yyyy', locale),
|
||||
};
|
||||
});
|
||||
|
||||
setBalanceData(balances);
|
||||
setHoveredValue(balances[balances.length - 1]);
|
||||
setLoading(false);
|
||||
processData(startingBalance, monthlyTotals);
|
||||
}
|
||||
|
||||
fetchBalanceHistory();
|
||||
}, [accountId, locale]);
|
||||
}, [startingBalance, monthlyTotals, locale]);
|
||||
|
||||
// State to track if the chart is hovered (used to conditionally render PrivacyFilter)
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -204,7 +203,7 @@ export function AccountHeader({
|
||||
const isUsingServer = syncServerStatus !== 'no-server';
|
||||
const isServerOffline = syncServerStatus === 'offline';
|
||||
const [_, setExpandSplitsPref] = useLocalPref('expand-splits');
|
||||
const [showNetWorthChartPref, setShowNetWorthChartPref] = useSyncedPref(
|
||||
const [showNetWorthChartPref, _setShowNetWorthChartPref] = useSyncedPref(
|
||||
`show-account-${accountId}-net-worth-chart`,
|
||||
);
|
||||
const showNetWorthChart = showNetWorthChartPref === 'true';
|
||||
@@ -233,30 +232,6 @@ export function AccountHeader({
|
||||
}
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const ele = graphRef.current;
|
||||
if (!ele) return;
|
||||
const clone = ele.cloneNode(true) as HTMLDivElement;
|
||||
Object.assign(clone.style, {
|
||||
visibility: 'hidden',
|
||||
display: 'flex',
|
||||
});
|
||||
ele.after(clone);
|
||||
if (clone.clientHeight < window.innerHeight * 0.15) {
|
||||
setShowNetWorthChartPref('true');
|
||||
} else {
|
||||
setShowNetWorthChartPref('false');
|
||||
}
|
||||
clone.remove();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [setShowNetWorthChartPref]);
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+f, cmd+f, meta+f',
|
||||
|
||||
@@ -171,34 +171,54 @@ function PayeeList({
|
||||
// entered
|
||||
|
||||
const { newPayee, suggestedPayees, payees, transferPayees } = useMemo(() => {
|
||||
return items.reduce(
|
||||
(acc, item, index) => {
|
||||
let currentIndex = 0;
|
||||
const result = items.reduce(
|
||||
(acc, item) => {
|
||||
if (item.id === 'new') {
|
||||
acc.newPayee = { ...item, highlightedIndex: index };
|
||||
acc.newPayee = { ...item };
|
||||
} else if (item.itemType === 'common_payee') {
|
||||
acc.suggestedPayees.push({ ...item, highlightedIndex: index });
|
||||
acc.suggestedPayees.push({ ...item });
|
||||
} else if (item.itemType === 'payee') {
|
||||
acc.payees.push({ ...item, highlightedIndex: index });
|
||||
acc.payees.push({ ...item });
|
||||
} else if (item.itemType === 'account') {
|
||||
acc.transferPayees.push({ ...item, highlightedIndex: index });
|
||||
acc.transferPayees.push({ ...item });
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
newPayee: null as PayeeAutocompleteItem & {
|
||||
highlightedIndex: number;
|
||||
},
|
||||
suggestedPayees: [] as Array<
|
||||
PayeeAutocompleteItem & { highlightedIndex: number }
|
||||
>,
|
||||
payees: [] as Array<
|
||||
PayeeAutocompleteItem & { highlightedIndex: number }
|
||||
>,
|
||||
transferPayees: [] as Array<
|
||||
PayeeAutocompleteItem & { highlightedIndex: number }
|
||||
>,
|
||||
newPayee: null as PayeeAutocompleteItem | null,
|
||||
suggestedPayees: [] as Array<PayeeAutocompleteItem>,
|
||||
payees: [] as Array<PayeeAutocompleteItem>,
|
||||
transferPayees: [] as Array<PayeeAutocompleteItem>,
|
||||
},
|
||||
);
|
||||
|
||||
// assign indexes in render order
|
||||
const newPayeeWithIndex = result.newPayee
|
||||
? { ...result.newPayee, highlightedIndex: currentIndex++ }
|
||||
: null;
|
||||
|
||||
const suggestedPayeesWithIndex = result.suggestedPayees.map(item => ({
|
||||
...item,
|
||||
highlightedIndex: currentIndex++,
|
||||
}));
|
||||
|
||||
const payeesWithIndex = result.payees.map(item => ({
|
||||
...item,
|
||||
highlightedIndex: currentIndex++,
|
||||
}));
|
||||
|
||||
const transferPayeesWithIndex = result.transferPayees.map(item => ({
|
||||
...item,
|
||||
highlightedIndex: currentIndex++,
|
||||
}));
|
||||
|
||||
return {
|
||||
newPayee: newPayeeWithIndex,
|
||||
suggestedPayees: suggestedPayeesWithIndex,
|
||||
payees: payeesWithIndex,
|
||||
transferPayees: transferPayeesWithIndex,
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
// We limit the number of payees shown to 100.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, type ComponentProps } from 'react';
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
@@ -27,7 +27,7 @@ export function ReportList<T extends { id: string; name: string }>({
|
||||
...(!embedded && { maxHeight: 175 }),
|
||||
}}
|
||||
>
|
||||
<Fragment>{ItemHeader({ title: t('Saved Reports') })}</Fragment>
|
||||
<ItemHeader title={t('Saved Reports')} />
|
||||
{items.map((item, idx) => {
|
||||
return [
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from 'react-i18next';
|
||||
import { GridList } from 'react-aria-components';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { AnimatedLoading } from '@actual-app/components/icons/AnimatedLoading';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
@@ -24,6 +25,8 @@ export function PayeesList({
|
||||
isLoading = false,
|
||||
onPayeePress,
|
||||
}: PayeesListProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isLoading && payees.length === 0) {
|
||||
return (
|
||||
<View
|
||||
@@ -63,22 +66,30 @@ export function PayeesList({
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ flex: 1, paddingBottom: MOBILE_NAV_HEIGHT, overflow: 'auto' }}
|
||||
>
|
||||
{payees.map(payee => (
|
||||
<PayeesListItem
|
||||
key={payee.id}
|
||||
payee={payee}
|
||||
ruleCount={ruleCounts.get(payee.id) ?? 0}
|
||||
onPress={() => onPayeePress(payee)}
|
||||
/>
|
||||
))}
|
||||
<View style={{ flex: 1 }}>
|
||||
<GridList
|
||||
aria-label={t('Payees')}
|
||||
aria-busy={isLoading || undefined}
|
||||
items={payees}
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingBottom: MOBILE_NAV_HEIGHT,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{payee => (
|
||||
<PayeesListItem
|
||||
value={payee}
|
||||
ruleCount={ruleCounts.get(payee.id) ?? 0}
|
||||
onAction={() => onPayeePress(payee)}
|
||||
/>
|
||||
)}
|
||||
</GridList>
|
||||
{isLoading && (
|
||||
<View
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
paddingTop: 20,
|
||||
}}
|
||||
>
|
||||
<AnimatedLoading style={{ width: 20, height: 20 }} />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { GridListItem, type GridListItemProps } from 'react-aria-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { SvgBookmark } from '@actual-app/components/icons/v1';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
|
||||
import { type PayeeEntity } from 'loot-core/types/models';
|
||||
@@ -11,84 +12,82 @@ import { type PayeeEntity } from 'loot-core/types/models';
|
||||
import { PayeeRuleCountLabel } from '@desktop-client/components/payees/PayeeRuleCountLabel';
|
||||
|
||||
type PayeesListItemProps = {
|
||||
payee: PayeeEntity;
|
||||
value: PayeeEntity;
|
||||
ruleCount: number;
|
||||
onPress: () => void;
|
||||
};
|
||||
} & Omit<GridListItemProps<PayeeEntity>, 'value'>;
|
||||
|
||||
export function PayeesListItem({
|
||||
payee,
|
||||
export const PayeesListItem = memo(function PayeeListItem({
|
||||
value: payee,
|
||||
ruleCount,
|
||||
onPress,
|
||||
...props
|
||||
}: PayeesListItemProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
minHeight: 56,
|
||||
width: '100%',
|
||||
borderRadius: 0,
|
||||
borderWidth: '0 0 1px 0',
|
||||
borderColor: theme.tableBorder,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: theme.tableBackground,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '12px 16px',
|
||||
gap: 5,
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{payee.favorite && (
|
||||
<SvgBookmark
|
||||
width={15}
|
||||
height={15}
|
||||
style={{
|
||||
color: theme.pageText,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SpaceBetween
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
color: payee.transfer_acct ? theme.pageTextSubdued : theme.pageText,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
title={payee.name}
|
||||
>
|
||||
{(payee.transfer_acct ? t('Transfer: ') : '') + payee.name}
|
||||
</span>
|
||||
const label = payee.transfer_acct
|
||||
? t('Transfer: {{name}}', { name: payee.name })
|
||||
: payee.name;
|
||||
|
||||
<span
|
||||
return (
|
||||
<GridListItem
|
||||
id={payee.id}
|
||||
value={payee}
|
||||
textValue={label}
|
||||
style={styles.mobileListItem}
|
||||
{...props}
|
||||
>
|
||||
<SpaceBetween gap={5}>
|
||||
{payee.favorite && (
|
||||
<SvgBookmark
|
||||
aria-hidden
|
||||
focusable={false}
|
||||
width={15}
|
||||
height={15}
|
||||
style={{
|
||||
color: theme.pageText,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SpaceBetween
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
padding: '3px 6px',
|
||||
backgroundColor: theme.noticeBackground,
|
||||
border: '1px solid ' + theme.noticeBackground,
|
||||
color: theme.noticeTextDark,
|
||||
fontSize: 12,
|
||||
flexShrink: 0,
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<PayeeRuleCountLabel count={ruleCount} style={{ fontSize: 12 }} />
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
color: payee.transfer_acct
|
||||
? theme.pageTextSubdued
|
||||
: theme.pageText,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
padding: '3px 6px',
|
||||
backgroundColor: theme.noticeBackground,
|
||||
border: '1px solid ' + theme.noticeBackground,
|
||||
color: theme.noticeTextDark,
|
||||
fontSize: 12,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<PayeeRuleCountLabel count={ruleCount} style={{ fontSize: 12 }} />
|
||||
</span>
|
||||
</SpaceBetween>
|
||||
</SpaceBetween>
|
||||
</Button>
|
||||
</GridListItem>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,7 +180,7 @@ export class LiveQuery<TResponse = unknown> {
|
||||
|
||||
protected fetchData = async (
|
||||
runQuery: () => Promise<{
|
||||
data: Data<TResponse>;
|
||||
data: Data<TResponse> | TResponse;
|
||||
dependencies: string[];
|
||||
}>,
|
||||
) => {
|
||||
@@ -205,7 +205,15 @@ export class LiveQuery<TResponse = unknown> {
|
||||
// still subscribed (`this.unsubscribeSyncEvent` will exist)
|
||||
if (this.inflightRequestId === reqId && this._unsubscribeSyncEvent) {
|
||||
const previousData = this.data;
|
||||
this.data = data;
|
||||
|
||||
// For calculate queries, data is a raw value, not an array
|
||||
// Convert it to an array format to maintain consistency
|
||||
if (this._query.state.calculation) {
|
||||
this.data = [data as TResponse];
|
||||
} else {
|
||||
this.data = data as Data<TResponse>;
|
||||
}
|
||||
|
||||
this.onData(this.data, previousData);
|
||||
this.inflightRequestId = null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"author": "Actual",
|
||||
"productName": "Actual",
|
||||
"description": "A simple and powerful personal finance system",
|
||||
"version": "25.9.0",
|
||||
"version": "25.10.0",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"update-client": "bin/update-client",
|
||||
|
||||
@@ -434,10 +434,6 @@ export class CategoryTemplateContext {
|
||||
t.type === 'limit' ||
|
||||
t.type === 'remainder',
|
||||
)) {
|
||||
if (this.limitCheck) {
|
||||
throw new Error('Only one `up to` allowed per category');
|
||||
}
|
||||
|
||||
let limitDef;
|
||||
if (template.type === 'limit') {
|
||||
limitDef = template;
|
||||
@@ -449,6 +445,10 @@ export class CategoryTemplateContext {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.limitCheck) {
|
||||
throw new Error('Only one `up to` allowed per category');
|
||||
}
|
||||
|
||||
if (limitDef.period === 'daily') {
|
||||
const numDays = monthUtils.differenceInCalendarDays(
|
||||
monthUtils.addMonths(this.month, 1),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { captureBreadcrumb } from '../../platform/exceptions';
|
||||
import * as connection from '../../platform/server/connection';
|
||||
import { logger } from '../../platform/server/log';
|
||||
import { currentDay, dayFromDate, parseDate } from '../../shared/months';
|
||||
import { q } from '../../shared/query';
|
||||
import {
|
||||
@@ -559,8 +560,12 @@ app.events.on('sync', ({ type }) => {
|
||||
type === 'success' || type === 'error' || type === 'unauthorized';
|
||||
|
||||
if (completeEvent && prefs.getPrefs()) {
|
||||
const { lastScheduleRun } = prefs.getPrefs();
|
||||
if (!db.getDatabase()) {
|
||||
logger.info('database is not available, skipping schedule service');
|
||||
return;
|
||||
}
|
||||
|
||||
const { lastScheduleRun } = prefs.getPrefs();
|
||||
if (lastScheduleRun !== currentDay()) {
|
||||
runMutator(() => advanceSchedulesService(type === 'success'));
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ import { t } from 'i18next';
|
||||
|
||||
import { FieldValueTypes, RuleConditionOp } from '../types/models';
|
||||
|
||||
import { integerToAmount, amountToInteger, currencyToAmount } from './util';
|
||||
import {
|
||||
integerToAmount,
|
||||
amountToInteger,
|
||||
currencyToAmount,
|
||||
getNumberFormat,
|
||||
} from './util';
|
||||
|
||||
// For now, this info is duplicated from the backend. Figure out how
|
||||
// to share it later.
|
||||
@@ -374,10 +379,22 @@ export function makeValue(value, cond) {
|
||||
switch (cond.type) {
|
||||
case 'number': {
|
||||
if (cond.op !== 'isbetween') {
|
||||
const stringValue = String(value);
|
||||
const { decimalSeparator } = getNumberFormat();
|
||||
|
||||
// preserve trailing decimal separator to allow decimal input during typing
|
||||
if (stringValue && stringValue.endsWith(decimalSeparator)) {
|
||||
return {
|
||||
...cond,
|
||||
error: null,
|
||||
value: stringValue,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...cond,
|
||||
error: null,
|
||||
value: value ? currencyToAmount(String(value)) || 0 : 0,
|
||||
value: value ? currencyToAmount(stringValue) || 0 : 0,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/sync-server",
|
||||
"version": "25.9.0",
|
||||
"version": "25.10.0",
|
||||
"license": "MIT",
|
||||
"description": "actual syncing server",
|
||||
"bin": {
|
||||
|
||||
@@ -162,8 +162,8 @@ function getAccountResponse(results, accountId, startDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const needsAttention = results.sferrors.find(
|
||||
e => e === `Connection to ${account.org.name} may need attention`,
|
||||
const needsAttention = results.sferrors.find(e =>
|
||||
e.startsWith(`Connection to ${account.org.name} may need attention`),
|
||||
);
|
||||
if (needsAttention) {
|
||||
logAccountError(results, accountId, {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [passabilities]
|
||||
---
|
||||
|
||||
Adds net worth graph for each account page.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [Triscal]
|
||||
---
|
||||
|
||||
Improved the handling of schedules that have the same payee, amount, account, and date when posted by the schedule automatically or manually via the previews.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Implement react compiler to take advantage of some performance improvements
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [e13h]
|
||||
---
|
||||
|
||||
Remove auto-scrolling behavior when editing split transactions on mobile
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [karimkodera]
|
||||
---
|
||||
|
||||
Introduction of APIs to handle Schedule + a bit more. Refer to updated API documentation PR2811 on documentation.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Re-design mobile accounts page to better match the transactions and budget tables/list + add all accounts page
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [itsbekas]
|
||||
---
|
||||
|
||||
Fix range calculator on the MonthPicker component
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Add tools to migrate/un-migrate to/from UI automations
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Add issue types for bug reports and feature requests in GitHub issue templates.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Add error boundary for modal windows
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [misu-dev]
|
||||
---
|
||||
|
||||
Add BRL, JMD, RSD, RUB, THB and UAH currencies
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Fix version bump logic to work if the month has rolled over
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [michaelsanford]
|
||||
---
|
||||
|
||||
Add NO_COLOR standard environment flag to sync-server logging (https://no-color.org/).
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Remove usage of a raw variable in Account component
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Remove usage of a raw variable in AccountAutocomplete component
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Remove usage of a raw variable in CategoryAutocomplete component
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Remove usage of a raw variable in PayeeAutocomplete component
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [biolan]
|
||||
---
|
||||
|
||||
Add Romanian and Moldovan Leu currencies
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Optimize usage of useScrollListener and useTransactionsSearch
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Fix local dockerfile build memory allocation
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [Nalin-Gupta]
|
||||
---
|
||||
|
||||
Fix issue where marking existing transactions as transfer switches the date and notes of the two transactions
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [xshalan]
|
||||
---
|
||||
|
||||
Added support for Egyptian Pound (EGP) and Saudi Riyal (SAR) currencies.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [mauroartizzu]
|
||||
---
|
||||
|
||||
Fixes detailedAccounts that might me null or undefined with some Italian Banks (Widiba, Poste, Intesa)
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Bump vite from 6.3.5 to 6.3.6
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [youngcw]
|
||||
---
|
||||
|
||||
Add bank sync option to only import account balance
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Update Electron to the latest stable version
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Remove `BANKS_WITH_LIMITED_HISTORY` override array for GoCardless
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Remove usage of raw variables in renders
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Add a setting to disable update notifications
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Mobile: open recurring schedule picker in modal
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [jgeneaguilar]
|
||||
---
|
||||
|
||||
Change account menu popover width to minWidth to accommodate different text lengths when switching languages
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [mgibson-scottlogic]
|
||||
---
|
||||
|
||||
Exclude hidden categories from "Copy last month's budget" when copying whole month
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [milanalexandre]
|
||||
---
|
||||
|
||||
Force the display of the 'Schedules transaction' badge on a single line
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [misu-dev]
|
||||
---
|
||||
|
||||
Fixes Spending Card crash
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [dirk-apers]
|
||||
---
|
||||
|
||||
Add BPER Italy bank parser (BPER_RETAIL_BPMOIT22)
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [StephenBrown2]
|
||||
---
|
||||
|
||||
Dynamically update currency display options
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Optimize scroll provider and replace usage of debounce package with lodash's debounce
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Fix scrolling inside modals on iOS 26
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [csenel]
|
||||
---
|
||||
|
||||
Extend report end date to the latest transaction date
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [amrawadk]
|
||||
---
|
||||
|
||||
Add 'dryRun' option to importTransactions API.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Fix timestamp error when shutting down API with no budget loaded
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Improve cleanup when opening multiple budgets through the API
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [thromer]
|
||||
---
|
||||
Optimise the way payee information is fetched.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Add quiet mode to API
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [milanalexandre]
|
||||
---
|
||||
|
||||
Translations of category labels in the TransactionEdit component (mobile)
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Mobile rules page - set min width for value editor
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Create a mobile payees list page with search and filtering capabilities.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
TypeScript: move ImportTransactionsModal to ts
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [youngcw]
|
||||
---
|
||||
|
||||
Add limit type template for use in future template GUI
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Mobile payees - clicking takes to appropriate pages
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [milanalexandre]
|
||||
---
|
||||
|
||||
Translated ‘(No payee)’ for a scheduled transaction
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Ensure file upload size limits are respected when syncing files
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [lelemm]
|
||||
---
|
||||
|
||||
Add Express-based CORS proxy with rate limiting, logging, and GitHub token authentication support.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Fix token expiration parsing to accept numeric strings and validate special string formats.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Add data-1p-ignore attribute to transaction amount inputs to prevent 1Password autofill.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Downgrade Ubuntu image in electron build runner for more compatibility with older distros
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Fix live report time ranges
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Fix deprecation warning in count-points script
|
||||
Reference in New Issue
Block a user