mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-11 01:18:59 -05:00
Compare commits
10 Commits
matiss/crd
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
320d66444a | ||
|
|
17198863a4 | ||
|
|
a9f8ae0e21 | ||
|
|
3799b587ec | ||
|
|
8e1f27f316 | ||
|
|
fb95d4c92d | ||
|
|
2782d464ab | ||
|
|
b63f5dd303 | ||
|
|
35a01b0fa6 | ||
|
|
3104503a8a |
@@ -1,7 +1,7 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "Actual development",
|
||||
"name": "Actual Devcontainer",
|
||||
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
|
||||
1
.github/actions/docs-spelling/expect.txt
vendored
1
.github/actions/docs-spelling/expect.txt
vendored
@@ -44,6 +44,7 @@ CLP
|
||||
CMCIFRPAXXX
|
||||
COBADEFF
|
||||
CODEOWNERS
|
||||
Codespaces
|
||||
COEP
|
||||
commerzbank
|
||||
Copiar
|
||||
|
||||
9
.github/workflows/vrt-update-apply.yml
vendored
9
.github/workflows/vrt-update-apply.yml
vendored
@@ -75,9 +75,12 @@ jobs:
|
||||
|
||||
echo "Found patch file: $PATCH_FILE"
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
# Validate patch only contains PNG files. `git format-patch` emits a
|
||||
# `GIT binary patch` block for PNGs (no +++/--- lines), so check
|
||||
# `diff --git` headers — those are present for both text and binary.
|
||||
echo "Validating patch contains only PNG files..."
|
||||
if grep -E '^(\+\+\+|---) [ab]/' "$PATCH_FILE" | grep -v '\.png$'; then
|
||||
if grep -E '^diff --git ' "$PATCH_FILE" \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files! Rejecting for security."
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch validation failed: contains non-PNG files" >> "$GITHUB_OUTPUT"
|
||||
@@ -85,7 +88,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Extract file list for verification
|
||||
FILES_CHANGED=$(grep -E '^\+\+\+ b/' "$PATCH_FILE" | sed 's/^+++ b\///' | wc -l)
|
||||
FILES_CHANGED=$(grep -cE '^diff --git ' "$PATCH_FILE")
|
||||
echo "Patch modifies $FILES_CHANGED PNG file(s)"
|
||||
|
||||
# Configure git
|
||||
|
||||
244
.github/workflows/vrt-update-generate.yml
vendored
244
.github/workflows/vrt-update-generate.yml
vendored
@@ -36,15 +36,16 @@ jobs:
|
||||
content: 'eyes'
|
||||
});
|
||||
|
||||
generate-vrt-updates:
|
||||
name: Generate VRT Updates
|
||||
get-pr:
|
||||
name: Resolve PR details
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on PR comments containing /update-vrt
|
||||
if: >
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
outputs:
|
||||
head_sha: ${{ steps.pr.outputs.head_sha }}
|
||||
head_ref: ${{ steps.pr.outputs.head_ref }}
|
||||
head_repo: ${{ steps.pr.outputs.head_repo }}
|
||||
steps:
|
||||
- name: Get PR details
|
||||
id: pr
|
||||
@@ -60,9 +61,125 @@ jobs:
|
||||
core.setOutput('head_ref', pr.head.ref);
|
||||
core.setOutput('head_repo', pr.head.repo.full_name);
|
||||
|
||||
build-web:
|
||||
name: Build web bundle
|
||||
runs-on: ubuntu-latest
|
||||
needs: get-pr
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ steps.pr.outputs.head_sha }}
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trust workspace directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
shell: bash
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build browser bundle
|
||||
# REACT_APP_NETLIFY=true keeps the "Create test file" button in the
|
||||
# production bundle — every VRT test's beforeEach relies on it via
|
||||
# ConfigurationPage.createTestFile().
|
||||
env:
|
||||
REACT_APP_NETLIFY: 'true'
|
||||
run: |
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace @actual-app/crdt build
|
||||
yarn workspace @actual-app/core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: desktop-client-build
|
||||
path: packages/desktop-client/build/
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
browser-vrt:
|
||||
name: Browser VRT (shard ${{ matrix.shard }}/3)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-pr, build-web]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3]
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
env:
|
||||
E2E_USE_BUILD: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trust workspace directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
shell: bash
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Download web build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: desktop-client-build
|
||||
path: packages/desktop-client/build/
|
||||
- name: Run VRT Tests
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots --shard=${{ matrix.shard }}/3
|
||||
- name: Create shard patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add "**/*.png"
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes in this shard"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git commit -m "Update VRT screenshots (browser shard ${{ matrix.shard }})"
|
||||
git format-patch -1 HEAD --stdout > vrt-shard.patch
|
||||
|
||||
# Validate patch only contains PNG files. `git format-patch` emits a
|
||||
# `GIT binary patch` block for PNGs (no +++/--- lines), so check
|
||||
# `diff --git` headers — those are present for both text and binary.
|
||||
if grep -E '^diff --git ' vrt-shard.patch \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Shard patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload shard patch
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: vrt-shard-browser-${{ matrix.shard }}
|
||||
path: vrt-shard.patch
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
desktop-vrt:
|
||||
name: Desktop VRT
|
||||
runs-on: ubuntu-latest
|
||||
needs: get-pr
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trust workspace directory
|
||||
@@ -78,45 +195,120 @@ jobs:
|
||||
- name: Install build tools
|
||||
run: apt-get update && apt-get install -y build-essential python3
|
||||
|
||||
- name: Run VRT Tests on Desktop app
|
||||
- name: Run Desktop VRT Tests
|
||||
continue-on-error: true
|
||||
run: |
|
||||
yarn rebuild-electron
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
|
||||
- name: Run VRT Tests
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots
|
||||
|
||||
- name: Create patch with PNG changes only
|
||||
- name: Create shard patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Stage only PNG files
|
||||
git add "**/*.png"
|
||||
|
||||
# Check if there are any changes
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes to commit"
|
||||
echo "No VRT changes in desktop shard"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Create commit and patch
|
||||
git commit -m "Update VRT screenshots"
|
||||
git format-patch -1 HEAD --stdout > vrt-update.patch
|
||||
git commit -m "Update VRT screenshots (desktop)"
|
||||
git format-patch -1 HEAD --stdout > vrt-shard.patch
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
if grep -E '^(\+\+\+|---) [ab]/' vrt-update.patch | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files!"
|
||||
# See validation note in browser-vrt above.
|
||||
if grep -E '^diff --git ' vrt-shard.patch \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Desktop shard patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Patch created successfully with PNG changes only"
|
||||
- name: Upload shard patch
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: vrt-shard-desktop
|
||||
path: vrt-shard.patch
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
merge-patch:
|
||||
name: Merge VRT Patches
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-pr, browser-vrt, desktop-vrt]
|
||||
if: ${{ !cancelled() && needs.get-pr.result == 'success' }}
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download all shard patches
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/shard-patches
|
||||
pattern: vrt-shard-*
|
||||
|
||||
- name: Merge shard patches
|
||||
id: create-patch
|
||||
run: |
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
shopt -s nullglob
|
||||
patches=(/tmp/shard-patches/*/vrt-shard.patch)
|
||||
|
||||
if [ ${#patches[@]} -eq 0 ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No shard patches to merge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Defense in depth: re-validate every shard patch before applying.
|
||||
# See validation note in browser-vrt above for why we match
|
||||
# `diff --git` headers instead of +++/--- lines.
|
||||
for patch in "${patches[@]}"; do
|
||||
echo "Validating $patch"
|
||||
if grep -E '^diff --git ' "$patch" \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: $patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Apply each shard patch. Shards touch disjoint PNG files so
|
||||
# order does not matter. --index stages the applied changes.
|
||||
for patch in "${patches[@]}"; do
|
||||
echo "Applying $patch"
|
||||
git apply --index "$patch"
|
||||
done
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes after merge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git commit -m "Update VRT screenshots"
|
||||
git format-patch -1 HEAD --stdout > vrt-update.patch
|
||||
|
||||
# Final guard on the combined patch.
|
||||
if grep -E '^diff --git ' vrt-update.patch \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Merged patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Merged patch created successfully with PNG changes only"
|
||||
|
||||
- name: Upload patch artifact
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
@@ -131,11 +323,11 @@ jobs:
|
||||
run: |
|
||||
mkdir -p pr-metadata
|
||||
echo "${{ github.event.issue.number }}" > pr-metadata/pr-number.txt
|
||||
echo "${STEPS_PR_OUTPUTS_HEAD_REF}" > pr-metadata/head-ref.txt
|
||||
echo "${STEPS_PR_OUTPUTS_HEAD_REPO}" > pr-metadata/head-repo.txt
|
||||
echo "${NEEDS_GET_PR_OUTPUTS_HEAD_REF}" > pr-metadata/head-ref.txt
|
||||
echo "${NEEDS_GET_PR_OUTPUTS_HEAD_REPO}" > pr-metadata/head-repo.txt
|
||||
env:
|
||||
STEPS_PR_OUTPUTS_HEAD_REF: ${{ steps.pr.outputs.head_ref }}
|
||||
STEPS_PR_OUTPUTS_HEAD_REPO: ${{ steps.pr.outputs.head_repo }}
|
||||
NEEDS_GET_PR_OUTPUTS_HEAD_REF: ${{ needs.get-pr.outputs.head_ref }}
|
||||
NEEDS_GET_PR_OUTPUTS_HEAD_REPO: ${{ needs.get-pr.outputs.head_repo }}
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
|
||||
@@ -516,6 +516,29 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// apis: getNote, updateNote
|
||||
test('Notes: successfully get and update note', async () => {
|
||||
const categories = await api.getCategories();
|
||||
const categoryId = categories[0].id;
|
||||
|
||||
// No note exists initially
|
||||
const initial = await api.getNote(categoryId);
|
||||
expect(initial).toBeNull();
|
||||
|
||||
// Set a note
|
||||
await api.updateNote(categoryId, 'Test note content');
|
||||
const afterSet = await api.getNote(categoryId);
|
||||
expect(afterSet).toEqual({ id: categoryId, note: 'Test note content' });
|
||||
|
||||
// Update the note
|
||||
await api.updateNote(categoryId, 'Updated note content');
|
||||
const afterUpdate = await api.getNote(categoryId);
|
||||
expect(afterUpdate).toEqual({
|
||||
id: categoryId,
|
||||
note: 'Updated note content',
|
||||
});
|
||||
});
|
||||
|
||||
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
|
||||
test('Rules: successfully update rules', async () => {
|
||||
await api.createPayee({ name: 'test-payee' });
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { ImportTransactionsOpts } from '@actual-app/core/types/api-handlers
|
||||
import type { Handlers } from '@actual-app/core/types/handlers';
|
||||
import type {
|
||||
ImportTransactionEntity,
|
||||
NoteEntity,
|
||||
RuleEntity,
|
||||
TransactionEntity,
|
||||
} from '@actual-app/core/types/models';
|
||||
@@ -247,6 +248,14 @@ export function deleteCategory(
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getNote(id: NoteEntity['id']) {
|
||||
return send('api/note-get', { id });
|
||||
}
|
||||
|
||||
export function updateNote(id: NoteEntity['id'], note: NoteEntity['note']) {
|
||||
return send('api/note-update', { id, note });
|
||||
}
|
||||
|
||||
export function getCommonPayees() {
|
||||
return send('api/common-payees-get');
|
||||
}
|
||||
|
||||
@@ -590,6 +590,8 @@ export function useSyncAccountsMutation() {
|
||||
accountIdsToSync = accountIdsToSync.filter(
|
||||
id => !simpleFinAccounts.find(sfa => sfa.id === id),
|
||||
);
|
||||
|
||||
dispatch(setAccountsSyncing({ ids: accountIdsToSync }));
|
||||
}
|
||||
|
||||
// Loop through the accounts and perform sync operation.. one by one
|
||||
|
||||
@@ -81,7 +81,6 @@ export const Modal = ({
|
||||
inset: 0,
|
||||
zIndex: MODAL_Z_INDEX,
|
||||
fontSize: 14,
|
||||
willChange: 'transform',
|
||||
// on mobile, we disable the blurred background for performance reasons
|
||||
...(isNarrowWidth
|
||||
? {
|
||||
|
||||
@@ -466,6 +466,7 @@ const AccountList = forwardRef<HTMLDivElement, AccountListProps>(
|
||||
<ListBox
|
||||
aria-label={ariaLabel}
|
||||
items={accounts}
|
||||
dependencies={[syncingAccountIds, failedAccounts, updatedAccounts]}
|
||||
dragAndDropHooks={dragAndDropHooks}
|
||||
ref={ref}
|
||||
style={{
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { calculateSpendingReportTimeRange } from './reportRanges';
|
||||
import {
|
||||
calculateSpendingReportTimeRange,
|
||||
calculateTimeRange,
|
||||
} from './reportRanges';
|
||||
|
||||
// In test mode, monthUtils.currentMonth() returns '2017-01'
|
||||
describe('calculateTimeRange', () => {
|
||||
it('keeps last month as a live time range when restoring a saved widget', () => {
|
||||
const [start, end, mode] = calculateTimeRange({
|
||||
start: '2016-11',
|
||||
end: '2016-11',
|
||||
mode: 'lastMonth',
|
||||
});
|
||||
|
||||
expect(start).toBe('2016-12');
|
||||
expect(end).toBe('2016-12');
|
||||
expect(mode).toBe('lastMonth');
|
||||
});
|
||||
});
|
||||
|
||||
// In test mode, monthUtils.currentMonth() returns '2017-01'
|
||||
describe('calculateSpendingReportTimeRange', () => {
|
||||
|
||||
@@ -212,6 +212,10 @@ export function calculateTimeRange(
|
||||
|
||||
return getLatestRange(offset);
|
||||
}
|
||||
if (mode === 'lastMonth') {
|
||||
const lastMonth = monthUtils.subMonths(monthUtils.currentMonth(), 1);
|
||||
return [lastMonth, lastMonth, 'lastMonth'] as const;
|
||||
}
|
||||
if (mode === 'lastYear') {
|
||||
return [
|
||||
monthUtils.getYearStart(monthUtils.prevYear(monthUtils.currentMonth())),
|
||||
|
||||
@@ -87,6 +87,11 @@ import APIList from './APIList';
|
||||
"deleteSchedule"
|
||||
]} />
|
||||
|
||||
<APIList title="Notes" sections={[
|
||||
"getNote",
|
||||
"updateNote"
|
||||
]} />
|
||||
|
||||
<APIList title="Misc" sections={[
|
||||
"BudgetFile",
|
||||
"initConfig",
|
||||
@@ -730,6 +735,22 @@ Update fields of a rule. `fields` can specify any field described in [`Schedule`
|
||||
|
||||
<Method name="deleteSchedule" args={[{ name: 'id', type: 'id' }]} returns="Promise<null>" />
|
||||
|
||||
## Notes
|
||||
|
||||
Notes can be attached to any entity (categories, budget months, etc.) by ID. They are also used to define budget templates and savings goals (e.g. `#template 250`, `#goal 1000`).
|
||||
|
||||
#### `getNote`
|
||||
|
||||
<Method name="getNote" args={[{ name: 'id', type: 'id' }]} returns="Promise<Note | null>" />
|
||||
|
||||
Returns the note for the given entity ID, or `null` if no note has been set.
|
||||
|
||||
#### `updateNote`
|
||||
|
||||
<Method name="updateNote" args={[{ name: 'id', type: 'id' }, { name: 'note', type: 'string' }]} returns="Promise<void>" />
|
||||
|
||||
Sets the note on the entity with the given ID. Pass an empty string to clear the note.
|
||||
|
||||
## Misc
|
||||
|
||||
#### BudgetFile
|
||||
|
||||
@@ -6,6 +6,10 @@ This guide will help you set up your development environment for contributing to
|
||||
|
||||
## Prerequisites
|
||||
|
||||
:::tip
|
||||
If you prefer not to install Node and Yarn locally, you can use the [Dev Container](#dev-container) or run [Docker Compose](#docker-compose) directly.
|
||||
:::
|
||||
|
||||
Before you begin, ensure you have the following installed:
|
||||
|
||||
- **Node.js**: Version 22 or greater. You can download it from the [Node.js website](https://nodejs.org/en/download) (we recommend the LTS version).
|
||||
@@ -39,6 +43,34 @@ Before you begin, ensure you have the following installed:
|
||||
yarn typecheck
|
||||
```
|
||||
|
||||
## Dev Container
|
||||
|
||||
The repo includes a [`.devcontainer/`](https://github.com/actualbudget/actual/tree/master/.devcontainer) configuration that follows the [Dev Containers spec](https://containers.dev/). Any tool that supports the spec can use it — for example VS Code or Cursor (with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)), JetBrains IDEs (via Gateway), GitHub Codespaces, or the [`@devcontainers/cli`](https://github.com/devcontainers/cli).
|
||||
|
||||
In an editor that supports the spec, open the cloned repo and accept the **Reopen in Container** prompt (or run the equivalent command from your editor's command palette). The container will build, `yarn install` will run automatically via `postCreateCommand`, and you'll be dropped into a shell with the toolchain ready.
|
||||
|
||||
To start the dev server, open a terminal inside the container and run:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
The dev server will be available at `http://localhost:3001/`. Most editors automatically forward the port from the container to your host.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
For other editors, run from the repo root:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
This starts a container that runs `yarn start:browser` on port 3001. Open `http://localhost:3001/` in your browser.
|
||||
|
||||
:::note
|
||||
The container mounts your repo at `/app`. If you've already run `yarn install` on your host, the native modules (`better-sqlite3`, `bcrypt`, `electron`, `sharp`) will be compiled for your host OS and won't work inside the Linux container. Either delete `node_modules/` first and let the container reinstall, or run the dev container path above (which rebuilds them automatically).
|
||||
:::
|
||||
|
||||
## Essential Development Commands
|
||||
|
||||
All commands should be run from the **root directory** of the repository. Never run yarn commands from child workspace directories.
|
||||
|
||||
@@ -707,6 +707,16 @@ handlers['api/category-delete'] = withMutation(async function ({
|
||||
});
|
||||
});
|
||||
|
||||
handlers['api/note-get'] = async function ({ id }) {
|
||||
checkFileOpen();
|
||||
return handlers['notes-get']({ id });
|
||||
};
|
||||
|
||||
handlers['api/note-update'] = withMutation(async function ({ id, note }) {
|
||||
checkFileOpen();
|
||||
return handlers['notes-save']({ id, note });
|
||||
});
|
||||
|
||||
handlers['api/common-payees-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const payees = await handlers['common-payees-get']();
|
||||
|
||||
@@ -7,12 +7,20 @@ import type { NoteEntity } from '#types/models';
|
||||
export type NotesHandlers = {
|
||||
'notes-save': typeof updateNotes;
|
||||
'notes-save-undoable': typeof updateNotes;
|
||||
'notes-get': (arg: Pick<NoteEntity, 'id'>) => Promise<NoteEntity | null>;
|
||||
};
|
||||
|
||||
export const app = createApp<NotesHandlers>();
|
||||
app.method('notes-save', updateNotes);
|
||||
app.method('notes-save-undoable', mutator(undoable(updateNotes)));
|
||||
app.method('notes-get', getNote);
|
||||
|
||||
async function updateNotes({ id, note }: NoteEntity) {
|
||||
await db.update('notes', { id, note });
|
||||
}
|
||||
|
||||
async function getNote({
|
||||
id,
|
||||
}: Pick<NoteEntity, 'id'>): Promise<NoteEntity | null> {
|
||||
return db.first<NoteEntity>('SELECT id, note FROM notes WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ImportTransactionEntity,
|
||||
NearbyPayeeEntity,
|
||||
NewRuleEntity,
|
||||
NoteEntity,
|
||||
PayeeEntity,
|
||||
PayeeLocationEntity,
|
||||
RuleEntity,
|
||||
@@ -195,6 +196,10 @@ export type ApiHandlers = {
|
||||
transferCategoryId?: APICategoryEntity['id'];
|
||||
}) => Promise<void>;
|
||||
|
||||
'api/note-get': (arg: Pick<NoteEntity, 'id'>) => Promise<NoteEntity | null>;
|
||||
|
||||
'api/note-update': (arg: NoteEntity) => Promise<void>;
|
||||
|
||||
'api/payees-get': () => Promise<APIPayeeEntity[]>;
|
||||
|
||||
'api/common-payees-get': () => Promise<APIPayeeEntity[]>;
|
||||
|
||||
6
upcoming-release-notes/7641.md
Normal file
6
upcoming-release-notes/7641.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Refactor VRT workflow to enable parallel execution of browser and desktop tests.
|
||||
6
upcoming-release-notes/7729.md
Normal file
6
upcoming-release-notes/7729.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [nikhilweee]
|
||||
---
|
||||
|
||||
Document the Dev Container and Docker Compose options as alternatives to local Node and Yarn setup in the contributor development-setup guide.
|
||||
6
upcoming-release-notes/7760.md
Normal file
6
upcoming-release-notes/7760.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [totallynotjon]
|
||||
---
|
||||
|
||||
Fix sporadic text blur in modals by removing unnecessary `will-change: transform` on the modal overlay.
|
||||
6
upcoming-release-notes/7768.md
Normal file
6
upcoming-release-notes/7768.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [ADGJSD]
|
||||
---
|
||||
|
||||
Fix dashboard report widgets saved with the "Last month" live range restoring as static.
|
||||
6
upcoming-release-notes/7769.md
Normal file
6
upcoming-release-notes/7769.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [whlapinel]
|
||||
---
|
||||
|
||||
Add `getNote` and `updateNote` to the public `@actual-app/api`, enabling programmatic read/write of category notes (templates, goals, etc.) without internal API access.
|
||||
6
upcoming-release-notes/7784.md
Normal file
6
upcoming-release-notes/7784.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Fix mobile bank sync indicators not updating live during sync.
|
||||
27
yarn.lock
27
yarn.lock
@@ -791,7 +791,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3, @babel/helper-module-transforms@npm:^7.28.6":
|
||||
"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-module-transforms@npm:7.28.6"
|
||||
dependencies:
|
||||
@@ -820,6 +820,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-plugin-utils@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.28.6"
|
||||
checksum: 10/21c853bbc13dbdddf03309c9a0477270124ad48989e1ad6524b83e83a77524b333f92edd2caae645c5a7ecf264ec6d04a9ebe15aeb54c7f33c037b71ec521e4a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-remap-async-to-generator@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1"
|
||||
@@ -1352,16 +1359,16 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-modules-systemjs@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5"
|
||||
version: 7.29.4
|
||||
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.4"
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms": "npm:^7.28.3"
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
"@babel/helper-module-transforms": "npm:^7.28.6"
|
||||
"@babel/helper-plugin-utils": "npm:^7.28.6"
|
||||
"@babel/helper-validator-identifier": "npm:^7.28.5"
|
||||
"@babel/traverse": "npm:^7.28.5"
|
||||
"@babel/traverse": "npm:^7.29.0"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10/1b91b4848845eaf6e21663d97a2a6c896553b127deaf3c2e9a2a4f041249277d13ebf71fd42d0ecbc4385e9f76093eff592fe0da0dcf1401b3f38c1615d8c539
|
||||
checksum: 10/79269e6ec8ec831bb63bf1c7cc1a980e28da785e92b36d42612f0139e4044499b99aa109fca849e1a156c092aabf6c24d145f4cabf2ac9ea84ef468852fe4c03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -16279,9 +16286,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"fast-uri@npm:^3.0.1":
|
||||
version: 3.1.0
|
||||
resolution: "fast-uri@npm:3.1.0"
|
||||
checksum: 10/818b2c96dc913bcf8511d844c3d2420e2c70b325c0653633f51821e4e29013c2015387944435cd0ef5322c36c9beecc31e44f71b257aeb8e0b333c1d62bb17c2
|
||||
version: 3.1.2
|
||||
resolution: "fast-uri@npm:3.1.2"
|
||||
checksum: 10/1dff04865b2a38d3e0659deadfbf72efdf83a776bfbf9667e4aa9e5a3ec31bc341cda9622136b32b7652a857c8ba11896794186e8f876f8b2b72731fce8622f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user