@actual-app/core 26.4.0 → 26.5.0-nightly.20260407

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actual-app/core",
3
- "version": "26.4.0",
3
+ "version": "26.5.0-nightly.20260407",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "typesVersions": {
@@ -90,11 +90,9 @@
90
90
  "date-fns": "^4.1.0",
91
91
  "handlebars": "^4.7.9",
92
92
  "lru-cache": "^11.2.6",
93
- "md5": "^2.3.0",
94
93
  "memoize-one": "^6.0.0",
95
94
  "mitt": "^3.0.1",
96
95
  "promise-retry": "^2.0.1",
97
- "slash": "5.1.0",
98
96
  "typescript-strict-plugin": "^2.4.4",
99
97
  "ua-parser-js": "^2.0.9",
100
98
  "uuid": "^13.0.0"
@@ -107,7 +105,6 @@
107
105
  "@types/emscripten": "^1.41.5",
108
106
  "@types/jlongster__sql.js": "npm:@types/sql.js@latest",
109
107
  "@types/node": "^22.19.15",
110
- "@types/pegjs": "^0.10.6",
111
108
  "@typescript/native-preview": "^7.0.0-dev.20260309.1",
112
109
  "assert": "^2.1.0",
113
110
  "browserify-zlib": "^0.2.0",
@@ -129,7 +126,7 @@
129
126
  "ts-node": "^10.9.2",
130
127
  "util": "^0.12.5",
131
128
  "vite": "^8.0.0",
132
- "vite-plugin-node-polyfills": "^0.25.0",
129
+ "vite-plugin-node-polyfills": "^0.26.0",
133
130
  "vite-plugin-peggy-loader": "^2.0.1",
134
131
  "vitest": "^4.1.0",
135
132
  "yargs": "^18.0.0"
@@ -40,6 +40,7 @@ function isWidgetType(type: string): type is DashboardWidgetEntity['type'] {
40
40
  'calendar-card',
41
41
  'formula-card',
42
42
  'custom-report',
43
+ 'sankey-card',
43
44
  ].includes(type);
44
45
  }
45
46
 
@@ -1,6 +1,5 @@
1
1
  // @ts-strict-ignore
2
2
  import AdmZip from 'adm-zip';
3
- import normalizePathSep from 'slash';
4
3
  import { v4 as uuidv4 } from 'uuid';
5
4
 
6
5
  import { logger } from '../../platform/server/log';
@@ -396,7 +395,7 @@ export async function doImport(data: YNAB4.YFull) {
396
395
  }
397
396
 
398
397
  export function getBudgetName(filepath) {
399
- let unixFilepath = normalizePathSep(filepath);
398
+ let unixFilepath = filepath.replace(/\\/g, '/');
400
399
 
401
400
  if (!/\.zip/.test(unixFilepath)) {
402
401
  return null;
@@ -1,5 +1,5 @@
1
1
  // @ts-strict-ignore
2
- import md5 from 'md5';
2
+ import { createHash } from 'node:crypto';
3
3
 
4
4
  import { makeViews, schema, schemaConfig } from './aql';
5
5
  import * as db from './db';
@@ -20,7 +20,7 @@ async function updateViews() {
20
20
  const { value: hash } = row || {};
21
21
 
22
22
  const views = makeViews(schema, schemaConfig);
23
- const currentHash = md5(views);
23
+ const currentHash = createHash('md5').update(views).digest('hex');
24
24
 
25
25
  if (hash !== currentHash) {
26
26
  db.execQuery(views);
@@ -28,6 +28,7 @@ export const currencies: Currency[] = [
28
28
  { code: 'BYN', name: 'Belarusian Ruble', symbol: 'Br', decimalPlaces: 2, numberFormat: 'space-comma', symbolFirst: false },
29
29
  { code: 'CAD', name: 'Canadian Dollar', symbol: 'CA$', decimalPlaces: 2, numberFormat: 'comma-dot', symbolFirst: true },
30
30
  { code: 'CHF', name: 'Swiss Franc', symbol: 'Fr.', decimalPlaces: 2, numberFormat: 'apostrophe-dot', symbolFirst: true },
31
+ { code: 'CLP', name: 'Chilean Peso', symbol: 'CLP$', decimalPlaces: 2, numberFormat: 'dot-comma', symbolFirst: true },
31
32
  { code: 'CNY', name: 'Yuan Renminbi', symbol: '¥', decimalPlaces: 2, numberFormat: 'comma-dot', symbolFirst: true },
32
33
  { code: 'COP', name: 'Colombian Peso', symbol: 'Col$', decimalPlaces: 2, numberFormat: 'dot-comma', symbolFirst: true },
33
34
  { code: 'CRC', name: 'Costa Rican Colón', symbol: '₡', decimalPlaces: 2, numberFormat: 'space-comma', symbolFirst: true },
@@ -201,6 +201,40 @@ describe('Transactions', () => {
201
201
  expect(data.length).toBe(5);
202
202
  });
203
203
 
204
+ test('partially updating a split parent preserves amount and does not set error', () => {
205
+ const transactions = [
206
+ makeTransaction({ amount: 2001 }),
207
+ ...makeSplitTransaction({ id: 't1', amount: 2500 }, [
208
+ { id: 't2', amount: 2000 },
209
+ { id: 't3', amount: 500 },
210
+ ]),
211
+ makeTransaction({ amount: 3002 }),
212
+ ];
213
+
214
+ // Simulate a partial update (only `notes`) on the parent — this is
215
+ // how `api.updateTransaction(id, { notes: '...' })` calls it in
216
+ // `api.ts`: `updateTransaction(transactions, { id, ...fields })`.
217
+ const { data, diff } = updateTransaction(transactions, {
218
+ id: 't1',
219
+ notes: 'updated note',
220
+ } as TransactionEntity);
221
+
222
+ // The parent should get the updated notes without an error
223
+ const parent = data.find(d => d.id === 't1');
224
+ expect(parent?.notes).toBe('updated note');
225
+ expect(parent?.amount).toBe(2500);
226
+ expect(parent?.error).toBeNull();
227
+
228
+ // Children should be unchanged
229
+ expect(data.filter(t => t.parent_id === 't1').length).toBe(2);
230
+
231
+ expect(diff).toEqual({
232
+ added: [],
233
+ deleted: [],
234
+ updated: [expect.objectContaining({ id: 't1', notes: 'updated note' })],
235
+ });
236
+ });
237
+
204
238
  test('deleting a split transaction works', () => {
205
239
  const transactions = [
206
240
  makeTransaction({ amount: 2001 }),
@@ -262,7 +262,8 @@ export function updateTransaction(
262
262
  ) {
263
263
  return replaceTransactions(transactions, transaction.id, trans => {
264
264
  if (trans.is_parent) {
265
- const parent = trans.id === transaction.id ? transaction : trans;
265
+ const parent =
266
+ trans.id === transaction.id ? { ...trans, ...transaction } : trans;
266
267
  const originalSubtransactions =
267
268
  parent.subtransactions ?? trans.subtransactions;
268
269
  const sub = originalSubtransactions?.map(t => {
@@ -114,13 +114,13 @@ type SpecializedWidget =
114
114
  | MarkdownWidget
115
115
  | SummaryWidget
116
116
  | CalendarWidget
117
- | FormulaWidget;
117
+ | FormulaWidget
118
+ | SankeyWidget;
118
119
  export type DashboardWidgetEntity = SpecializedWidget | CustomReportWidget;
119
120
  export type NewDashboardWidgetEntity = Omit<
120
121
  DashboardWidgetEntity,
121
122
  'id' | 'tombstone' | 'dashboard_page_id'
122
123
  >;
123
-
124
124
  // Exported/imported (json) widget definition
125
125
  export type ExportImportCustomReportWidget = Omit<
126
126
  CustomReportWidget,
@@ -197,3 +197,16 @@ export type FormulaWidget = AbstractWidget<
197
197
  >;
198
198
  } | null
199
199
  >;
200
+
201
+ export type SankeyWidget = AbstractWidget<
202
+ 'sankey-card',
203
+ {
204
+ name?: string;
205
+ conditions?: RuleConditionEntity[];
206
+ conditionsOp?: 'and' | 'or';
207
+ timeFrame?: TimeFrame;
208
+ mode?: 'budgeted' | 'spent';
209
+ topNcategories?: number;
210
+ categorySort?: 'per-group' | 'global' | 'budget-order';
211
+ } | null
212
+ >;
@@ -7,7 +7,8 @@ export type FeatureFlag =
7
7
  | 'crossoverReport'
8
8
  | 'customThemes'
9
9
  | 'budgetAnalysisReport'
10
- | 'payeeLocations';
10
+ | 'payeeLocations'
11
+ | 'sankeyReport';
11
12
 
12
13
  /**
13
14
  * Cross-device preferences. These sync across devices when they are changed.
package/vite.config.ts CHANGED
@@ -71,6 +71,7 @@ export default defineConfig(({ mode }) => {
71
71
  },
72
72
  plugins: [
73
73
  peggyLoader(),
74
+ // https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/142
74
75
  nodePolyfills({
75
76
  include: [
76
77
  'process',