Skip to content

Commit acf45cc

Browse files
authored
Merge branch 'master' into claude/budget-template-ui
2 parents e6dd84c + daab7f7 commit acf45cc

21 files changed

Lines changed: 1452 additions & 648 deletions

File tree

.github/workflows/nightly-theme-catalog-scan.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,21 @@ jobs:
2727
download-translations: 'false'
2828
- name: Validate themes
2929
run: yarn workspace @actual-app/web validate:theme-catalog
30+
31+
notify-failure:
32+
name: Notify Discord on failure
33+
needs: validate-theme-catalog
34+
if: failure() && github.repository == 'actualbudget/actual'
35+
runs-on: ubuntu-latest
36+
environment: nightly-alerts
37+
timeout-minutes: 5
38+
steps:
39+
- name: Notify Discord
40+
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
41+
with:
42+
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
43+
status: Failure
44+
title: Nightly theme catalog scan failed
45+
description: The nightly scan failed. One or more themes may be broken, or the scan itself did not complete.
46+
username: Actual Nightly
47+
nofail: true
Loading
Loading

packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// @ts-strict-ignore
2-
import React, { useCallback, useEffect, useEffectEvent, useState } from 'react';
32
import type {
43
ComponentProps,
54
Dispatch,
65
ReactNode,
76
SetStateAction,
87
} from 'react';
8+
import { useCallback, useEffect, useEffectEvent, useState } from 'react';
99
import { Trans, useTranslation } from 'react-i18next';
1010

1111
import { Button, ButtonWithLoading } from '@actual-app/components/button';
@@ -39,6 +39,7 @@ import { FieldMappings } from './FieldMappings';
3939
import { InOutOption } from './InOutOption';
4040
import { MultiplierOption } from './MultiplierOption';
4141
import { Transaction } from './Transaction';
42+
import type { DateFormat, FieldMapping, ImportTransaction } from './utils';
4243
import {
4344
applyFieldMappings,
4445
dateFormats,
@@ -48,7 +49,6 @@ import {
4849
parseDate,
4950
stripCsvImportTransaction,
5051
} from './utils';
51-
import type { DateFormat, FieldMapping, ImportTransaction } from './utils';
5252

5353
function CheckboxToggle({
5454
id,
@@ -203,7 +203,6 @@ export function ImportTransactionsModal({
203203
const [flipAmount, setFlipAmount] = useState(false);
204204
const [multiplierEnabled, setMultiplierEnabled] = useState(false);
205205
const [reconcile, setReconcile] = useState(true);
206-
const [reimportDeleted, setReimportDeleted] = useState(false);
207206
const [importNotes, setImportNotes] = useState(true);
208207

209208
// This cannot be set after parsing the file, because changing it
@@ -242,6 +241,9 @@ export function ImportTransactionsModal({
242241
const [camtSwapPayeeAndMemo, setCamtSwapPayeeAndMemo] = useState(
243242
String(prefs[`camt-swap-payee-memo-${accountId}`]) === 'true',
244243
);
244+
const [reimportDeleted, setReimportDeleted] = useState(
245+
String(prefs[`import-reimport-deleted-${accountId}`] || 'true') === 'true',
246+
);
245247

246248
const [parseDateFormat, setParseDateFormat] = useState<DateFormat | null>(
247249
null,
@@ -706,6 +708,10 @@ export function ImportTransactionsModal({
706708
});
707709
}
708710

711+
savePrefs({
712+
[`import-reimport-deleted-${accountId}`]: String(reimportDeleted),
713+
});
714+
709715
importTransactions.mutate(
710716
{
711717
accountId,

packages/desktop-client/src/components/reports/graphs/SankeyGraph.tsx

Lines changed: 21 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,22 @@ import {
1313
} from 'recharts';
1414
import type { SankeyData } from 'recharts/types/chart/Sankey';
1515

16-
import { getColorScale } from '#components/reports/chart-theme';
1716
import { Container } from '#components/reports/Container';
1817
import { useFormat } from '#hooks/useFormat';
1918
import { usePrivacyMode } from '#hooks/usePrivacyMode';
2019

2120
type SankeyGraphNode = SankeyData['nodes'][number] & {
22-
hasChildren?: boolean;
23-
isCollapsed?: boolean;
24-
toBudget?: number;
25-
isNegative?: boolean;
26-
actualValue?: number;
21+
value: number;
2722
percentageLabel?: string;
28-
targetLinks?: Array<Record<string, unknown>>;
29-
sourceLinks?: Array<Record<string, unknown>>;
23+
key: string;
24+
color?: string;
25+
};
26+
27+
type SankeyLinkPayload = {
28+
source: SankeyGraphNode;
29+
target: SankeyGraphNode;
30+
value: number;
31+
color?: string;
3032
};
3133

3234
type SankeyLinkProps = {
@@ -38,16 +40,10 @@ type SankeyLinkProps = {
3840
targetControlX: number;
3941
linkWidth: number;
4042
index: number;
41-
payload: {
42-
source: SankeyGraphNode;
43-
target: SankeyGraphNode;
44-
value: number;
45-
isNegative?: boolean;
46-
};
43+
payload: SankeyLinkPayload;
4744
isHovered: boolean;
4845
onMouseEnter: () => void;
4946
onMouseLeave: () => void;
50-
color: string;
5147
};
5248

5349
function SankeyLink({
@@ -62,9 +58,11 @@ function SankeyLink({
6258
isHovered,
6359
onMouseEnter,
6460
onMouseLeave,
65-
color,
6661
}: SankeyLinkProps) {
67-
const linkColor = payload.isNegative ? theme.errorText : color;
62+
if (payload.value <= 0) {
63+
return null;
64+
}
65+
const linkColor = payload.color ?? theme.reportsGray;
6866
const strokeWidth = linkWidth;
6967
const strokeOpacity = isHovered ? 1 : 0.6;
7068

@@ -91,8 +89,8 @@ type SankeyNodeProps = {
9189
index: number;
9290
payload: SankeyGraphNode;
9391
containerWidth: number;
94-
containerHeight: number;
9592
showPercentages?: boolean;
93+
color?: string;
9694
};
9795
function SankeyNode({
9896
x,
@@ -102,21 +100,17 @@ function SankeyNode({
102100
index: _index,
103101
payload,
104102
containerWidth,
105-
containerHeight,
106103
showPercentages,
107104
}: SankeyNodeProps) {
108105
const privacyMode = usePrivacyMode();
109106
const format = useFormat();
110-
const isOut = x + width + 6 > containerWidth;
111107

112-
const fillColor = payload.isNegative ? theme.errorText : theme.reportsBlue;
108+
if (payload.value <= 0) {
109+
return null;
110+
}
111+
const isOut = x + width + 6 > containerWidth;
113112

114-
const toBudget = payload.toBudget ?? 0;
115-
const availableBelow = Math.max(0, containerHeight - 25 - (y + height));
116-
const proportionalHeight =
117-
toBudget > 0 && payload.value ? height * (toBudget / payload.value) : 0;
118-
const isClamped = proportionalHeight > availableBelow;
119-
const toBudgetHeight = Math.min(proportionalHeight, availableBelow);
113+
const fillColor = payload.color ?? theme.reportsBlue;
120114

121115
const renderText = (
122116
text: string,
@@ -142,27 +136,6 @@ function SankeyNode({
142136
return (
143137
<Layer>
144138
<Rectangle x={x} y={y} width={width} height={height} fill={fillColor} />
145-
{toBudgetHeight > 0 &&
146-
(isClamped ? (
147-
<polygon
148-
points={`
149-
${x},${y + height}
150-
${x + width},${y + height}
151-
${x + width},${y + height + toBudgetHeight - 8}
152-
${x + width / 2},${y + height + toBudgetHeight}
153-
${x},${y + height + toBudgetHeight - 8}
154-
`}
155-
fill={theme.toBudgetPositive}
156-
/>
157-
) : (
158-
<Rectangle
159-
x={x}
160-
y={y + height}
161-
width={width}
162-
height={toBudgetHeight}
163-
fill={theme.toBudgetPositive}
164-
/>
165-
))}
166139
{renderText(payload.name || '', height / 2)}
167140
{renderText(
168141
showPercentages && payload.percentageLabel
@@ -173,24 +146,6 @@ function SankeyNode({
173146
0.5,
174147
privacyMode ? t('Redacted Script') : undefined,
175148
)}
176-
{toBudgetHeight > 0 &&
177-
renderText(
178-
format(toBudget, 'financial'),
179-
toBudgetHeight / 2 + 13,
180-
11,
181-
0.5,
182-
privacyMode ? t('Redacted Script') : undefined,
183-
y + height,
184-
)}
185-
{toBudgetHeight > 0 &&
186-
renderText(
187-
t('To budget'),
188-
toBudgetHeight / 2,
189-
13,
190-
1,
191-
undefined,
192-
y + height,
193-
)}
194149
</Layer>
195150
);
196151
}
@@ -199,7 +154,6 @@ type SankeyGraphProps = {
199154
style?: CSSProperties;
200155
data: SankeyData;
201156
showTooltip?: boolean;
202-
collapsedNodes?: string[];
203157
showPercentages?: boolean;
204158
};
205159
export function SankeyGraph({
@@ -212,19 +166,6 @@ export function SankeyGraph({
212166
const format = useFormat();
213167
const [hoveredLinkIndex, setHoveredLinkIndex] = useState<number | null>(null);
214168

215-
const colors = getColorScale('qualitative');
216-
const sourceColorMap = new Map(
217-
[
218-
...new Set(
219-
data.links
220-
.filter(l => (l.source as number) !== 0)
221-
.map(l => data.nodes[l.source as number]?.name),
222-
),
223-
]
224-
.filter(Boolean)
225-
.map((name, i) => [name, colors[i % colors.length]]),
226-
);
227-
228169
return (
229170
<Container style={style}>
230171
{(width, height) => (
@@ -235,7 +176,6 @@ export function SankeyGraph({
235176
<SankeyNode
236177
{...props}
237178
containerWidth={width}
238-
containerHeight={height}
239179
showPercentages={showPercentages}
240180
/>
241181
)}
@@ -245,10 +185,6 @@ export function SankeyGraph({
245185
isHovered={hoveredLinkIndex === props.index}
246186
onMouseEnter={() => setHoveredLinkIndex(props.index)}
247187
onMouseLeave={() => setHoveredLinkIndex(null)}
248-
color={
249-
sourceColorMap.get(props.payload.source.name) ??
250-
theme.reportsGray
251-
}
252188
/>
253189
)}
254190
sort={false}

0 commit comments

Comments
 (0)